We can perform various operations on a process: spawning child processes, terminate the process, set up inter-process communication channels, change the process priority, and many more. All of these operations require a system call (switching to Kernel Mode). In this example, we use the C API to make the system call.
Process Creation
We can create new processes using fork()
system call.
- The process creator is called a parent process, the new processes are called the children of that process.
- Each of these new processes may in turn create more child processes, forming a tree of processes.
Process Tree
We can illustrate multiple process creation as a process tree:
In the example above, there are 5 processes in total. Process 2, 3, and 4 are direct children of Process 1. Process 5 is created by Process 2.
Process id
Each process is identified by an integer called the process id (pid
). Pid is unique in the system.
You can type the command ps [options]
to observe all running processes in your system, along with the pid
of each process. For instance,
Child Process vs Parent Process
The new process consists of the entire copy of the address space (code, stack, process of execution, etc) of the original parent process at the point of fork()
.
In other words, the child process inherits the parent process’ state at the point of
fork()
.
Parent and child processes operate in different address space (isolation). Since they are different processes, parent and children processes execute concurrently.
Practically, a parent process waits for its children to terminate (using wait()
system call) to read the child process’ exit status and only then its PCB entry in the process table can be removed.
Child processes cannot wait
for their parents to terminate. Since children processes are a duplicate of their parents (inherits the whole address space), they can either
- Execute the same instructions as their parents concurrently, or
- Load a new program into its address space
Program: How fork works
It is best to explain how fork()
process creation works by example.
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
printf("pid: %d\n", pid);
if (pid < 0)
{
fprintf(stderr, "Fork has failed. Exiting now");
return 1; // exit error
}
else if (pid == 0)
{
execlp("/bin/ls", "ls", NULL);
}
else
{
wait(NULL);
printf("Child has exited.\n");
}
return 0;
}
The simple C program above is executed and when the execution system call fork()
returns, two processes are present.
Both have the same copy of the text (code) and resources (any opened files, etc). The parent process is cloned, resulting in the child process. They’re at a different address space, executed concurrently by the system.
fork return value
fork()
returns 0 in the child process while in the parent process it returns the pid of the child (>0).
We can write just one instruction for both parent and child process but each will take a different branch of the if
clause. In the example code above:
- The child executes the line if-clause:
execlp
- The parent process executes the
else
clause where itwait
for the child process toexit
execlp
execlp
is a system call that loads a new program called ls
onto the child process’ address space, effectively replacing its text (code), data, and stack content.
wait
Concurrently, the parent process executes wait(NULL)
, which is a system call that suspends the parents’ execution until this child process that is executing ls
has returned.
Program: The fork tree
Compile and run the C program below.
How many processes are created in total? (excluding the parent process). Can you draw the process tree?
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int level = 3;
pid_t pid[level];
for (int i = 0; i < level; i++)
{
pid[i] = fork();
if (pid[i] < 0)
{
fprintf(stderr, "Fork has failed. Exiting now");
return 1; // exit error
}
else if (pid[i] == 0)
{
printf("Hello from child %d \n", i);
}
}
return 0;
}
Process Termination
A process needs certain resources (CPU time, memory, files, I/O devices) to run and accomplish its task. These resources are limited.
A process can terminate itself using exit()
system call. A process can also terminate other processes using the kill(pid, SIGKILL)
system call.
Once a process is terminated, these resources are freed by the kernel for other processes. Parent processes may terminate or abort its children as it knows the pid
of its children.
Orphaned processes
If a parent process with live children is terminated, the children processes become orphaned processes:
- Some operating system is designed to either abort all of its orphaned children (cascading termination) or
- Adopt the orphaned children processes (
init
process usually will adopt orphaned processes)
About init
In UNIX-like OS, init
is the first process started by the kernel during booting of the computer system. Init
is a daemon process that continues running until the system is shut down (see Appendix). It is the direct or indirect ancestor of all other processes, and automatically adopts all orphaned processes.
Init
is a user process like any other processes, and hence it is using virtual memory. The only special thing about init
is that it is one of the two processes that the kernel started initially. When init
is started by the kernel, it goes into user mode. When init
calls system call fork(),
it traps into the kernel mode, and the kernel does certain things to create the new process, and the new process will be scheduled in the future. When the fork()
returns, the original process is back to user mode. The equivalent of init
in macOS is launchd
.
Zombie Processes
Zombie processes are processes that are ALREADY TERMINATED, and memory as well as other resources are freed, but its exit status is not read by their parents, hence its entry (PCB) in the process table remains.
A parent process must call wait
or waitpid
to read their children’s exit status. A call to wait
or waitpid
blocks the calling process until one of its child processes exits or a signal is received. Otherwise, their child process becomes a zombie process.
- Children processes can terminate themselves after they have finished executing their tasks using
exit(int status)
system call. - The kernel will free the memory and other resources from this process, but not the PCB entry.
- Parent processes are supposed to call
wait
orwaitpid
to obtain the exit status of a child. - Only after
wait
orwaitpid
in the parent process returns, the kernel can remove the child PCB entry from the system wide process table. - If the parents didn’t call
wait
orwaitpid
and instead continue execution of other things, then children’s entry in the pcb remains; a zombie process remains.- A zombie process generally takes up very little memory space, but
pid
of the child remains - Recall that pid is unique, hence in a 32-bit system, there’re only 32768 available pids.
- A zombie process generally takes up very little memory space, but
Having too many zombie processes might result in inability to create new processes in the system, simply because we may run out of pid.
Note: if a parent process has died as well, then all the zombie children will be cleared by the kernel. To observe zombie children, you need to artifically suspend the parent process after the children have terminated.
All processes transition to this zombie state when they terminate, but generally they exist as zombies only briefly. Once the parent calls wait, waitpid
the pid
of the zombie process and its entry in the process table are released.
Program: Zombie making
Compile and run the C program below. It will suspend itself at scanf
, waiting for input at stdin
. Do not type anything, leave it hanging there.
What’s the (possible) maximum number of zombies created by this process?
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int level = 5;
pid_t pid[level];
for (int i = 0; i < level; i++)
{
pid[i] = fork();
if (pid[i] < 0)
{
fprintf(stderr, "Fork has failed. Exiting now");
return 1; // exit error
}
else if (pid[i] == 0)
{
printf("Hello from child %d \n", i);
return 0;
}
}
int testInteger;
printf("Enter an integer: "); // artificially blocking parent
scanf("%d", &testInteger);
return 0;
}
We can enter the ps aux | grep 'Z'
command to list all zombie processes in the system caused by running the program above.