Processes
What is a process?
A process is one of the most fundamental abstractions provided by an OS. It is simply an abstraction by the OS of a running program.
Really, processes are just a bunch of bytes on a disk and the OS gets them running.
CPU Virtualization
To run multiple processes at once, the OS creates the illusion of there being many CPUs. This way, each process gets its own virtual CPU.
The way this works is as follows: the OS runs one process, stops it, runs another process, and so on. This is called time sharing and is one method of virtualizing the CPU. It allows the OS to run many concurrent processes. Most modern operating systems implement some kind of time sharing.
Time sharing can have a performance cost; each process will run slower if the CPU(s) must be shared.
Operating systems have various scheduling policies to decide which processes run when.
Machine State
The machine state is the set of resources that a process can read or update when it is running.
There are three common resources that make up machine state:
Memory
In order for a process to run, its instructions and static data must first be loaded into memory. The address space of a process is the sections of memory that process can access during its run time,
Registers
Some CPU registers store important things like the program counter (also called the instruction pointer), the stack pointer, and the frame pointer. These are used to track the currently-executing instruction, the stack, function parameters, local variables, and return addresses.
Storage Devices
Processes will often utilize some resources on physical disk media. For example, most processes keep a list of files that they have open during runtime.
Process API
The following are some examples of what a basic OS should have regarding interactions with its processes:
Creation: Operating systems should have the ability to create new processes.
Destruction: Operating systems should have the ability to kill processes, especially runaway processes that are not cleaned up properly.
Wait: The ability to wait for a process to stop running.
Miscellaneous Control: Other process controls, such as suspending and resuming
Status Check: The ability to fetch process information such as how long it's been running, the current state, etc.
This begs the question: what interfaces should an OS present for process creation can control? On UNIX systems, those interfaces consist of the following:
The fork()
System Call
fork()
System CallThe fork()
system call is useful for creating new processes. The way it works is a follows:
Every process has a process identifier (PID), a name we can use when we want to control it. When fork()
is called, it creates a new process with its own PID. This new process also has its own copy of the process's address space, its own registers, etc. It is an almost exact copy of the calling process.
The new process is called the child, while the original process is called the parent. After the call to fork()
, the child process receives a return code of 0 and the parent receives a return code of the child's PID.
When the child process runs, it does not start at main()
. Instead, it starts at the same location where fork()
was called.
Either the child or parent can run after the fork()
is called. That is determined by the CPU scheduler.
TODO: Add example code that uses the fork()
system call.
The wait()
System Call
wait()
System CallThe wait()
system call is useful if you want to make a process wait for another process to finish. When called after a fork()
, it makes the parent process suspend execution until the child has finished running.
TODO: Add example code that uses the wait()
system call.
The exec()
System Call
exec()
System CallThe exec()
system call (and its variants) are useful if you want to run a program that is different from the calling program.
exec()
does not create a new process. Instead it takes the name of an executable, loads code and static data from it, and overwrites its current code segment with it; the stack, heap, and other parts of program memory are re-initialized. It essentially transforms a currently-running program into a different running program.
A successful call to exec()
never returns.
TODO: Add example code that uses the exec()
system call (or one of its variants).
Output Redirection, Pipes, and "The Why"
Why do we use such an odd interface for simple process creation?
The answer is that the separation of fork()
and exec()
are essential in building a UNIX shell. It allows the shell to run code after the call to fork()
but before the call to exec()
. This code could alter the environment of the program that is about to run.
Remember, the shell is just a user program. When you interact with it, it performs the following steps:
Display a prompt
Wait for user input
Call
fork()
to create a new process for running the given commandCall some variant of
exec()
to run the commandWait for the command to complete by using
wait()
The separation of fork()
and exec()
also allow the shell to do a number of useful things like output redirection. Take the following command as an example:
Here, the output of hello.c
is redirected to newfile.txt
. How does this work? When the child process is created, before calling exec()
, the shell closes standard output and opens newfile.txt
. By doing this, any output from wc
is sent to the file.
This works because of a particular behavior in UNIX systems. They start looking for free file descriptors starting at 0 (which is standard output). If you close 0, then it checks for 1, then 2, and so on until it finds an open one. In this case, the open one is our file.
Pipes are implemented in a similar way, except with the pipe()
system call. Output of one process is connected to an in-kernel pipe (basically, a queue) and the input of another process is connected to that same pipe. Thus, the output of one process is used as input to the next.
Process Creation
This is a list of steps performed that turns a program into a running process:
Program code and static data are loaded into memory
Memory is allocated for the stack
Memory is allocated for the heap
Any necessary file descriptors are opened
The OS jumps into the
main()
function of the program
Early and simple operating systems load their programs eagerly, meaning they do all of these steps at once before running the program. Modern OSes load their programs lazily, only loading pieces of the code or data as needed.
Process States
A process can be in one of three states:
Running: The process is running and executing instructions.
Ready: The process is ready to run but the OS has chosen to not run it at the moment
Blocked: The process has performed some kind of operation that makes it not ready to run until some other thing happens. Examples include waiting for a disk to write and waiting to receive a network packet. When the process is blocked, it stays that way until the other then happens, then it becomes ready again.
A process is said to be scheduled when it goes from being ready to actively running. The opposite referred to as the process being descheduled.
Data Structures
The OS is just a program, and thus keep some data structures around to keep track of things. One of these data structures is a process list that tracks all processes that are ready and some additional info about them. There's also structures to track blocked processes.
For a more concrete example, check out the proc
data structure in xv6 or Linux.
References
Last updated