A process is an instance of a program that is being executed, or ready to be executed. Each process has its own memory (code, heap, stack, etc.), registers (stack pointers, program counter), and other resources.
If each process has the entire memory address space to itself, how does an OS run multiple processes at the same time? Because they actually don’t. Operating systems are a protection system for processes.
Multiprocessing
A single processor can execute multiple processes “concurrently.” That’s in quotes because it actually switches between processes really, really, really, really fast. When it wants to switch to another process, the following occurs:
- It saves the current state of the registers to memory, specific for the current process.
- It schedules the next process for execution.
- It loads the saved register states for that process from memory into the actual registers, and then begins execution again.
Switching processes and address space is called context switching. This is handled by the kernel.
In a modern computer we have multiple cores in a CPU, which essentially allows multiple CPUs to execute multiple processes at once. They share the same memory. This is actual parallelism.
Process states and lifetime
A process can be in 1 of 6 states:
- Running (currently executing)
- Ready to run (waiting to be scheduled by the kernel)
- Blocked (e.g. the parent process after calling
wait()
) - Zombie (e.g. the completed child process before the parent calls
wait()
) - Terminated
- Stopped (via
SIGSTOP
, can be resumed withSIGCONT
)
As the OS switches between processes to execute them, a process can switch between ready and running multiple times before it’s done.
Zombie processes
Child processes that terminate before the parent process finishes blocking are known as zombie processes.
- They deallocate their address space and don’t run anymore
- They still exist and have a process control block so that the parent can still check the status and handle any clean up when it gets to the child
- This mechanism doesn’t “block” the parent. It basically sees that one of its children is a zombie, cleans it up, and continues running.
Creating new processes: fork()
Creates a new child process that is almost an exact clone of the current process, called the parent
- All the memory and registers are copied from the parent
- If
fork()
is called in the middle of the program, the two processes continue execution from that point onwards e.g. the child process doesn’t start from the very beginning. They both pick up execution from wherefork()
returns. - Child process gets its own virtual address space, separate from the parent
What’s unique about fork()
is that its return value differs between the two processes. This is the only difference between the two processes. The parent process gets the child PID, while the child process receives 0.
- Type of return value is
pid_t
, which is an integer
fork()
makes an identical copy of the parent’s File descriptor table for the child. Obviously, subsequent file openings and closings will not affect either process.
Waiting for updates on a process
pid_t wait(int *wstatus);
Calling process waits for any child process to change status. The usual change in status is to be terminated.
- Gets exit status of child process through the output parameter
wstatus
- Returns the PID of the child process that terminated, or
-1
on error. This is to help differentiate between possibly multiple child processes
pid_t waitpid(pid_t pid, int *wstatus, int options);
Calling process waits for a child process, this time specified by a pid
we pass in, to change status. The same as wait()
except we can specify the PID.
- More options to control which children we’re waiting for, and when it should return.
options
can be combined together via bitwise OR e.g.int options = WUNTRACED | WCONTINUED
;waitpid(-1, &status, 0)
is equivalent towait(&status)
, meaning, both functions only return when any child terminates.- Note that the
WNOHANG
option and a while loop checking thatpid != 0
would do the same thing, however this would take up actual CPU cycles and execution time. So make sure the work being done in the while loop is meaningful. Otherwise, we’re just busy waiting.
wstatus
can be checked using macros (e.g. to detect if a child was terminated by a certain signal or if it exited normally)
Execution blocking
When a process calls wait()
, and there is a process it needs to wait on, the calling process blocks or is blocked. This means it’s not scheduled for execution anymore.
Blocked processes will not be scheduled by the scheduler and thus will not use CPU. This means we waste less CPU cycles.
Note that while in this state, Signals are not blocked (unless we manually did using masks). A blocked process can still receive signals (otherwise, how would it receive SIGCONT
if it was stopped?)
Concurrent processes
Two processes are said to run concurrently if their execution is interleaved. They’re said to be sequential if one does not run until the other is finished.
Critical sections and signal interrupting
Critical sections are sections of code1 where if one or more resources are accessing it concurrently, then it can put the program in an invalid state. In particular, we are reading/writing memory that is dependent on each other and must happen as a unit.
While the OS usually handles this for us, it cannot protect us from Signal handlers executing at any point in time. This can be very bad if the signal handler interrupts during the middle of executing a number of lines that can’t be interrupted (e.g. setting next pointers for a linked list or any syscalls that update some internal shared state like malloc()
).
Kernel and context switching
The kernel is a shared chunk of memory-resident OS code that manages processes. It is not a separate process, but runs as part of an existing one.
How the kernel switches between processes is by taking over from the first process, context switches to the second, and then hands back control to the second process.
Assume the scheduler is nondeterministic and handled by the OS. Dun worry about it.
Footnotes
-
This refers to literal lines of code that we consider “critical.” ↩