"Come on, we want steep learning curves here!" -Paul Eggert, 2014
Virtualization:
Layering:
OS + Virtualizable Processor
Combining an operating system with a virtualizable processor allows you to support something called a process. In Linux, we define a process as a program that's running in isolation on a virtual interpreter. To create a process, you use the fork function:
pid_t fork(void);
where pid_t stands for the ID of the process. This function returns 0 if the process is a child, and a positive number if the process is a parent. You cannot actually destroy a process. The only way for one to end is for the process itself to call: _Noreturn void exit(int exit_status);
There are two types of exit functions: a system call (_exit(int exit_status) and a library function call (exit(void)). The system call doesn't flush output before returning, while the library function does - the system call is a more immediate and severe way to end a process. So what do we do when we want to wait for a process to complete? There's a function called
pid_t waitpid(pid_t wait_for_process, int* loc_to_store_status, int flags);
This is sort of like a destructor for a process, since exit doesn't delete process table entries. When a process exits, but hasn't been deleted from the process table, we call it a zombie, because the data in the row is still valid even though the process has ended.
The Garden of Fork()ing Paths
Since each individual process runs on its own virtual interpreter, there needs to be some way to organize all the different processes executing at the same time. Suppose, for example, we only have one physical CPU and we want to run many processes at once. We would have to simulate multiple processors. Because we only have one set of registers and one ALU, we have to store old process data in something called a process table:
Each row in the table represents a single process, and contains information such as its registers, exit status, and process ID. When you fork a process, the new child copies the data from its parent's row, except for the %eax register (because the call to fork returns different values for parent and child). However, the fork function can also be used as an exploit, or a fork bomb. By infinitely forking, every new process contributes more fork calls to the attack. Eventually the process table fills up and the CPU crashes.
The process table can also be used to save a process' registers when the CPU switches processes. An example of this is a call to the read function. In this case, the CPU saves its current process' register data to the process table and resumes some other process, instead of simply waiting for read to return.