OS Processes
So far in 3410, we have been operating under the ridiculous notion that a computer only runs one program at a time. A given program gets to own the computer’s entire memory, there is only a single program counter (PC) keeping track of a single stream of instructions to execute.
You know from your everyday computing life that this is not how “real” computers work. They can simultaneously run multiple programs with their own instructions, heap, and stack. The operating system (OS) is, among other responsibilities, the thing that makes it possible to run multiple programs on the same hardware at the same time. The next part of the course will focus on this mechanism: how the OS and hardware work together to work on multiple things concurrently.
Executable vs. Process
When you compile the C code you have written this semester, an executable file is produced. This is a file that contains the instructions (i.e., machine code) and data for your program. An executable is inert: it’s not doing anything; it’s just sitting there on your disk. You can copy an executable, rename it, attach it to an email, print it out, put it on a USB drive and send it through the US mail—anything you can do with any other file.
When you run an executable, that creates a process. A process is a currently running instance of a program. You can run the same executable multiple times and get multiple, concurrently executing processes of the same program. The different processes will share the same instructions and constant data, but they will have different heaps and different stacks (so different values of all their variables, potentially). It’s not just a file—you can’t print out a process or burn it to a CD. A process is something that occurs on a specific computer at a specific time.
Part of an operating system’s job is to provide processes with the illusion that they own the entire computer. That means that a process gets to use all of the machine’s registers without worrying about other processes using them at the same time. The OS manages the CPU’s program counter so it appears, to each process, to proceed normally through a given program’s instructions—without jumping willy-nilly to other programs’ instructions. Through a mechanism called virtual memory, every process gets the illusion of owning the entire 264-byte memory address space. (Virtual memory will be covered after spring break.)
The Process Lifecycle
What happens when you type ./myprog
in your shell to launch an executable?
(Assume you already compiled an executable, myprog
.)
The OS first must create a new process with the instructions and data from myprog
.
The OS keeps track of all the processes on the system (running or not) in a
process list.
Each process gets an entry in this list called a process control block (PCB).
The PCB includes metadata like the process id (pid),
information about the user who owns the process,
the current state of the process (running, waiting, ready, etc.),
and so on.
To create a new myprog
process, the OS allocates a new PCB and adds it to its
process list.
Next, the OS sets up the memory for the process. Recall that programs expect to have access to regions of memory for their stack, heap, global data, and instructions. So at the very least, the OS needs to take the instructions from the executable and put them into the text segment in memory. This per-process view of memory is called an address space — we will cover more about how to set up the memory address space for a process when we talk about virtual memory. Once completed, the OS updates the process’s state as ready in the PCB.
Finally, it’s time to run the process. The OS transfers control of the processor to the program’s first instruction by setting the program counter to that instruction’s address. At this point the process is running.
It can be helpful to think about a process’s state (as tracked by its PCB) as a state machine.
Process states include initializing, ready, running, waiting, and finished.
While setting up the PCB and the process’s memory, the OS places a new process in the initializing state.
Eventually, when this is all set up, the process becomes ready.
Then, when the OS decides to finally start a process, it sets the PCB’s state to running.
The OS uses the waiting state for processes that are waiting for the OS to complete some task on its behalf (such as I/O).
Finally, after main
eventually returns, the process enters the finished state.
Context Switching
Many processes may be active at the same time, i.e., they may all have PCBs that are all ready. However, only one process can actually be running at a time. To give the illusion that multiple programs are running on your computer at the same time, the OS chooses some process to run for a short span of time, and then it pauses that process to allow another process run for some time. While the length of these time windows varies by OS and according to how busy the computer is, you can think of them happening every 1–5 ms if it helps contextualize the idea. The OS aims to give a “fair” amount of time to each process. This process (pun intended) is called time-sharing.
The act of changing from running one process to running another is called a context switch. Here’s what the OS needs to do to perform a context switch:
- Save the current process state. That means recording the current CPU registers (including the program counter) somewhere in memory.
- Update the current process’s PCB (to exit the running state).
- Select another process. (Picking which one to run is an interesting problem, and it’s the responsibility of the OS scheduler.)
- Update that PCB to indicate that the process is now in the running state.
- Restore that process’s state: read the previously-saved register values back from memory.
- Resume execution by jumping to the new process’s current instruction.
Context switches are not cheap. Again as a very rough estimate, you can imagine them taking about a microsecond, or something like a thousand clock cycles. The OS tries to minimize the total number of context switches while still achieving a “fair” division of time between processes.
Kernel Space & User Space
The kernel is a special piece of software that forms the central part of the operating system. You can think of it as being sort of like a process, except that it is the first one to run when the computer boots and it has the special privilege of managing all the actual processes. The kernel has its own instructions, stack, and heap.
Systems hackers will often refer to a separation between kernel space and user space.
OS stuff happens in kernel space: maintaining the PCBs, choosing which processes to run, and so on.
All the stuff that the processes do (every single line of code in myprog
above, for instance) happen in user space.
This is a cute way to refer to the separation of code and responsibilities between the two kinds of code.
However, there is also an important difference in privileges:
kernel-space code has unrestricted access to all of the computer’s memory and to I/O peripherals.
It can read and write the memory of any process.
User-space code, because of kernel-space machinations, gets that aforementioned illusion of running in a sandbox where it does not have to worry about other processes.
In user space, each process receives a limited number of privileges from the kernel and must ask the kernel nicely to perform things like I/O or to communicate with other processes.
Processor ISAs provide mechanisms to enforce this distinction in privileges. For example, RISC-V has a special set of privileged instructions and registers that only kernel-space code is allowed to use. The CPU starts in a state where these instructions are allowed; when the OS starts a user-space process, it instructs the CPU to take away access to these instructions. When control eventually transfers back into kernel space, the CPU re-enables access to these privileged instructions.