Concurrent Process Creation and Execution: Multi-Processing
Concurrent Process Creation and Execution: Multi-Processing
Objective:
Some application programs may have several possible execution sequences, which may be independent,
or may depend on each other. Such programs can be made more responsive if the different sequences of
execution are written as separate units that can compete for the CPU together with other program units in
the system. Such units can be made as part of the same process, (i.e. have the same PCB and share same
address space), called Threads; or can have their own Processes (i.e. with different PCBs and different
address spaces).
Modern operating systems support multi-threading and multi-processing in user programs, through
numerous data structures and functions provided in high-level programming language libraries.
In this lab you will learn how a program in Linux can create, terminate, and control child processes, and
how it can replace its own code with that of another executable file. There are three distinct main
operations involved:
• Creating a new child process,
• Causing the new process to execute a program, and
• Coordinating the completion of the child process with the original program.
You can skip the discussions and go directly to the numbered instructions written in RED
Processes are organized hierarchically. Each process has a parent process, which explicitly created it.
The processes created by a given parent are called its child processes. A child process inherits many of
its attributes from the parent process.
After forking a child process, both the parent and child processes continue to execute normally. The new
child process continues to execute the same program as its parent process, starting at the point after the
fork system call. We can tell whether the program is running in the parent process or the child process by
examining the return value from the fork system call: The parent receives the child’s Process ID, while
the child receives a zero.
If a program needs to wait for a child process to finish execution, or terminate, before continuing, it must
specify this explicitly after the forking operation, by calling the wait() or waitpid() system calls.
When a child process terminates, its parent is informed through one of these two functions so that the
parent may take some appropriate action. The two wait functions give limited information about why the
child process has terminated by passing its returned exit status code.
The lifetime of a process ends when it terminates and its termination is reported to its parent process. At
that time, all of the process resources, including its process ID, will be freed. A terminating process that is
waiting for its parent to accept its return code is called a zombie process. If a parent terminates before
its child, the child is called an orphan process, and it is adopted automatically by the original (great
grandparent) “init” process whose PID is 1.
Process Monitoring:
To monitor the state of your processes under Linux, use the ps command.
ps [-option]
This command, if used without any options, produces a list of all the processes owned by the current user
and associated with the current user’s terminal. The information displayed by the ps command varies
according to which command option(s) used and the type of Linux under which it is used. These are some
of the column headings displayed by the different versions of this command:
PID SZ(size in Kb) TTY(controlling terminal) TIME(used by CPU) CMD(program name)
Examples:
1. To display a short list of the current user’s processes running in the current terminal, type:
$ ps
2. To display a long list of the current user’s processes running in the current terminal, type:
$ ps -ly
3. To display information about all processes running for user ‘mohammad’, type:
$ ps -u mohammad
Foreground Processes:
Foreground processes are processes created by the shell when you run a command (or a program)
and they do not give you access to the shell’s command prompt until they terminate or you exit out of
the process.
Example:
1) Run the following command from the shell:
The ping process will become a foreground job and will not return you to the command prompt until
you stop it or kill it.
You can exit out of the ping process (or kill it) by pressing ctrl+c. This signals the foreground
process to terminate by sending it the SIGINT (Signal Interrupt) signal. You will learn about signals
in a future lab.
Background Processes:
Background processes run exactly as if they were in the foreground except that they do not receive
user input from the shell. If a command produces output and is put in the background, it will still
produce the output.
There are a number of reasons why we may want to put a process in the background, and they will
become clear in the examples below.
Stopped Processes:
Stopped Processes are those processes that are neither in the foreground nor in the background, and
yet they are not terminated. They are technically in a third state, called stopped state.
Like background processes, stopped processes do not receive user input. Unlike background
processes, stopped processes do not produce output. As a matter of fact, they don’t do anything. They
have essentially been put on hold and won’t do any processing until they are resumed or killed.
Example:
2) While your ping process is still running in the foreground, do the following sequence of inputs in
your shell:
The key to stopping a running process is to press ctrl+z. This sends the SIGTSTP (Signal Tty Stop)
signal to the foreground process. When the running process receives the signal, it will be stopped and you
will be returned to the command prompt.
The number appearing in square brackets when the process was stopped (e.g., [1]+) is called a job
number, used to identify the stopped process(s) in case you have multiple jobs in stopped states. At any
time, you can run the jobs command to get a list of the current jobs. In our example we used the number
1 to get our ping job back to the foreground by the command fg %1.
After stopping a process you can continue running it in the background by the bg command followed by
its job number.
Example:
3) While your ping process is still running in the foreground, do the following sequence of inputs in
your shell:
ctrl+z You will see: [1]+ Stopped ping localhost > /dev/null
$ bg %1 You will see: [1]+ ping localhost > /dev/null &
$ Your ping command is now executing in the background. Shell is free.
When a process that is running in the background finishes, you will receive a message the next time your
command prompt refreshes. The prompt refreshes when you run another command or simply hit Enter to
get a new prompt.
$
[1]+ Done ping localhost > /dev/null
$
As with all the other job-related output, you will be notified of the job number and the command.
Example:
4) At the shell command prompt, enter the following command:
$ ping localhost > /dev/null & You will see something like: [2] 3353
$
Even though you’ve put a task in the background, you can still bring it to the foreground using the fg
command. In the previous example, you can pull the background process to the foreground by running:
$ fg %1.
Terminating Processes:
Sometimes a process goes rogue and either won’t respond to input, is consuming massive amounts of
resources, or both. At times like this, it’s very important to know how to terminate a process which will
cease the program’s execution and free up its used resources.
The command used to terminate processes is appropriately called kill. It can accept either process IDs
or job numbers (the job number must be preceded by a percent sign, “%”).
Example:
5) With the two ping processes still in the background, get their job numbers by entering:
$ jobs You may see: [1]- Running ping localhost > /dev/null &
$ [2]+ Running ping localhost > /dev/null &
6) To terminate the first job whose number is 1, enter the following command at the shell prompt:
$ kill %1 You will see: [1]- Terminated ping localhost > /dev/null
$
You can also run the kill command on a process ID rather than a job number. At this point you
may still have one ping process running in the background.
8) Enter the following kill command to terminate the ping process, with process ID 3353:
$ kill 3353 You will see: [2]+ Terminated ping localhost > /dev/null
$
By default, the kill command sends a SIGTERM (Signal Terminate) signal to the process; however,
sometimes this is not enough to stop the execution of an out of control process. To shut down these
processes, we need something more powerful such as:
$ kill -9 %1
This may not look much different, but the addition of the “-9” option tells the command that things are
serious now. This option causes a SIGKILL (Signal Kill) signal to be sent to the process. A kill signal
cannot be intercepted by a process and causes the process to immediately terminate without allowing it to
clean up after itself. It’s for this reason that you should only use SIGKILL after first sending the process a
SIGTERM.
When fork is called, it creates a new process. If the operation is successful, there are then both parent
and child processes and both see the fork return, but with different values:
• Returns a value of 0 in the child process, and
• Returns the child's process ID in the parent process.
If process creation failed, fork returns a value of -1 in the parent process and no child is created. When
the fork call is successful, the newly created process will be a copy of its parent process, except for the
following differences:
• The child process has its own unique process ID.
• The getppid()function call within the child process returns its parent process ID.
• The child process gets its own copy of the parent process's open file descriptors. Subsequent
changes of the file descriptors in the parent process won't affect the file descriptors in the child,
and vice versa. However, both processes share the file position associated with each descriptor.
• The elapsed processor times for the child process are set to zero.
• The child doesn't inherit file locks set by the parent process.
• The child doesn't inherit alarms set by the parent process.
• The set of pending signals for the child process is cleared.
Example 1:
/* Hello World */
#include <stdio.h>
#include <unistd.h> /* contains fork prototype */
int main(void) {
printf("Hello World!\n");
fork();
printf("I am after forking\n");
printf("\tI am process %d.\n", getpid());
}
Example 2:
/* Parent - Child */
#include <stdio.h>
#include <unistd.h> /* contains fork prototype */
int main(void)
{
pid_t pid;
printf("Hello World!\n");
printf("I am the parent process, my pid is: %d.\n", getpid());
printf("Here i am before forking\n");
pid = fork();
printf("Here I am just after forking\n");
Example 3:
/* Multiple Forks */
#include <stdio.h>
#include <unistd.h> /* contains fork prototype */
int main(void)
{
printf("Here I am just before first forking statement\n");
fork();
printf("Here I am just after first forking statement\n");
fork();
printf("Here I am just after second forking statement\n");
printf("\t\tHello World from process %d!\n", getpid());
}
10) Run the above two programs and observe their outputs. Why the second program repeats itself ?
The wait() function will force a parent process to wait for a child process to stop or terminate. It returns
the process ID of the child or -1 for an error. The exit status of the child is returned to status_ptr.
The exit() function terminates the process which calls it and returns the exit status value. Both Linux
and C (forked) programs can read the status value.
By convention, a status of 0 means normal termination. Any other value indicates an error or unusual
occurrence. Many standard library calls have errors defined in the ‘sys/stat.h’ header file. We can
easily derive our own conventions.
If the child process must be guaranteed to execute before the parent continues, the wait system call is
used. A call to this function causes the parent process to wait until one of its child processes exits. The
wait call returns the process ID of the child process, which gives the parent the ability to wait for a
particular child process to finish.
This sleep function suspends the process until the specified number of seconds has elapsed or a signal
arrives which is not ignored. It returns 0 if the requested time has elapsed, or the number of seconds left to
sleep, if the call was interrupted by a signal handler.
Example 4:
/* Guarantees the child process will print
* its message before the parent process
*/
#include <stdio.h>
#include <stdlib.h> /* contains prototype for exit */
#include <sys/wait.h> /* contains prototype for wait */
#include <unistd.h> /* contains prototype for fork */
#include <sys/types.h> /* contains pid_t declaration */
int main(void)
{
pid_t pid;
int status;
printf("Hello World!\n");
pid = fork();
if (pid == -1) { /* check for error in fork */
perror("bad fork");
exit(1);
}
if (pid == 0) /* child will print his message */
printf("I am the child process.\n");
else {
wait(&status); /* parent waits for child to finish */
printf("I am the parent process.\n");
}
}
Orphan processes:
When a parent dies before its child, the child is automatically adopted by the original “init” process
whose Process ID is 1. To illustrate this, insert a sleep statement into the child’s code. This ensures that
the parent process is terminated before its child.
#include <stdio.h>
#include <sys/wait.h> /* contains prototype for wait */
#include <unistd.h> /* contains prototype for fork */
#include <sys/types.h> /* contains pid_t declaration */
int main(void)
{
pid_t pid;
printf("I am the original process with PID %d and PPID %d.\n", getpid(), getppid());
Zombie processes:
A process that terminates cannot leave the system until its parent accepts its return code. If its parent
process is already dead, it’ll already have been adopted by the “init” process, which always accepts its
children’s return codes. However, if a process’s parent is alive but never executes a wait(), the
process’s return code will never be accepted and the process will remain a zombie.
The following program created a zombie process, which was indicated in the output from the ps utility.
When the parent process is killed, the child was adopted by “init” and allowed to rest in peace.
Example 6:
/* Produces a Zombie process */
#include <stdio.h>
#include <stdlib.h> /* contains prototype for exit */
#include <unistd.h> /* contains prototype for fork */
#include <sys/types.h> /* contains pid_t declaration */
int main(void) {
pid_t pid;
pid = fork(); /* Duplicate. Child and parent continue from here */
if (pid != 0) { /* pid is non-zero, so I must be the parent */
while(1) /* never terminates and never executes a wait() */
sleep(100); /* stop executing for 100 seconds */
}
else exit(42); /* pid is zero, so I must be the child, then exit */
}
13) Run the above program in the background and do the following commands shown in blue color.
Executing a file:
A child process can execute another program by using one of the exec functions. The program that the
process is executing is called its process image. Starting execution of a new program causes the process
to forget all about its previous process image; when the new program exits, the process exits too, instead
of returning to the previous process image. This section describes the exec family of functions, for
executing a file as a process image. You can use these functions to make a child process execute a new
program after it has been forked.
The functions in this family differ in how you specify the arguments, but otherwise they all do the same
thing. They are defined in the header file “unistd.h”.
The execv() function executes the file named by filename as a new process image. The filename
argument may include the full path of the program file to be executed. This new program may require its
own list of arguments, which we can specify as elements of argv. So the argv argument of execv() is
an array of pointers to null-terminated strings that represent the argument list available to the new
program. By convention, the first element of this array is the file name of the program. The last element
of this array must be a NULL pointer. The environment for the new process image is taken from the
environ variable of the current process image.
The execl() function is similar to execv(), but the argv strings are specified individually instead of
as an array. A null pointer must be passed as the last such argument.
The execvp() function is similar to execv(), except that it searches the directories listed in the PATH
environment variable to find an executable matching the specified file name if it does not contain a slash.
The execvp()function is useful for executing system utility programs, because it looks for them in the
places that the user has chosen. Shells use it to run the commands that user’s type.
The execlp()function is like execl(), except that it performs the same file name searching as the
execvp()function.
Executing a new process image completely changes the contents of memory, copying only the argument
and environment strings to new locations. But many other attributes of the process are unchanged:
• The process ID and the parent process ID.
• Session and process group membership.
• Real user ID and group ID, and supplementary group IDs.
• Current working directory and root directory.
• File mode creation mask.
• Process signal mask.
• Pending signals.
• Elapsed processor time associated with the process.
Examples:
The following programs exec the commands "ls -l -a" and "echo hello there" using the four
most-used forms of exec.
15) Enter each of the four programs below, compile, and run them. Observe the output.
/* Using execl */
#include <stdio.h>
#include <unistd.h>
main() {
execl("/bin/ls", /* program to run – we give the full path */
"ls", /* name of program sent to argv[0] */
"-l", /* first parameter (argv[1])*/
"-a", /* second parameter (argv[2]) */
NULL); /* terminate arg list */
/* Using execlp */
#include <stdio.h>
#include <unistd.h>
main() {
execlp("ls", /* program to run – PATH will be searched */
"ls", /* name of program sent to argv[0] */
"-l", /* first parameter (argv[1])*/
"-a", /* second parameter (argv[2]) */
NULL); /* terminate arg list */
/* Using execv */
/* Run with a message on the command line, this message will be written to the output */
#include <stdio.h>
#include <unistd.h>
main(int argc, char *argv[]) {
execv("/bin/echo", /* program to run – we give the full path */
&argv[0]); /* needs to pass name of the program in argv[0] */
#include <stdio.h>
#include <unistd.h>
main(int argc, char *argv[]) {
execvp("echo", /* program to run – PATH will be searched */
&argv[0]); /* needs to pass name of the program in argv[0] */
We can also have the program create a child process to execute a command. The following program
forks a child process to execute the ls command.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
main()
{
pid_t forkresult;
printf("%d: I am the parent. Remember my number!\n", getpid());
printf("%d: I am now going to fork ... \n", getpid());
forkresult = fork();
/* At this point, the child is still here! This means that the exec has failed */
printf("%d: AAAAH !! My EXEC failed !!!!\n", getpid());
exit(1);
}
/* From this point on, only the parent can execute the following statements */
printf("%d: I am the parent again.\n", getpid());
}
17) Observe the outputs of the different runs of the above program.
You should be able to get different ordering of the output lines (sometimes the parent finished
before the child, or vice versa). This means that after the fork, the two processes are no longer
synchronized. The following is a sample output:
4790: I am the parent. Remember my number!
4790: I am now going to fork ...
4790: My child's pid is 4791
4790: I am the parent again.
4791: Hi ! I am the child.
4791: I'm now going to exec ls!
$ a.out exec-child.c execl-ls.c execlp-ls.c execv-echo.c
exec-child execl-ls execlp-ls execv-echo
$
Example 1:
The following program forks, then the parent process waits for the child, while the child process asks
the user to type in a number from 0 to 255 then exits, returning that number as status.
/* wait-exit status example 1 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
main() {
int number=0, statval;
printf("%d: I'm the parent !\n", getpid());
printf("%d: number = %d\n", getpid(), number);
printf("%d: forking ! \n", getpid());
if(fork() == 0) { /* Child */
printf("%d: I'm the child !\n", getpid());
printf("%d: number = %d\n", getpid(), number);
printf("%d: Enter a number : ", getpid());
scanf("%d", &number);
printf("%d: number = %d\n", getpid(), number);
printf("%d: exiting with value %d\n", getpid(), number);
exit(number);
}
printf("%d: number = %d\n", getpid(), number); /* Parent */
printf("%d: waiting for my kid !\n", getpid());
wait(&statval);
if(WIFEXITED(statval))
printf("%d: my kid exited with status %d\n", getpid(), WEXITSTATUS(statval));
else
printf("%d: My kid was killed off !! \n", getpid());
}
18) Run the above program and observe its output, here is a sample:
Example 2:
The following program spawns two children, then waits for their completion and behaves differently
according to which one is finished.
/* wait-exit status example 2 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
main() {
pid_t whichone, first, second;
int howmany, status;
if((first = fork()) == 0) { /* Parent spawns 1st child */
printf("Hi, I am the first child, and my ID is %d\n", getpid());
sleep(10);
exit(0);
}
else if(first == -1) { /* 1st fork did not succeed */
perror("1st fork: something went wrong\n");
exit(1);
}
else if((second = fork()) == 0) { /* Parent spawns 2nd child */
printf("Hi, I am the second child, and my ID is %d\n", getpid());
sleep(15);
exit(0);
}
else if(second == -1) { /* 2nd fork did not succeed */
perror("2nd fork: something went wrong\n");
exit(1);
}
printf("This is parent\n"); /* Parent */
howmany = 0;
while(howmany < 2) { /* wait twice until a child terminates */
whichone = wait(&status); /* status lower 16-bits is status info. */
howmany++;
if(whichone == first)
printf("First child exited ");
else
printf("Second child exited ");
20) Try changing the argument passed to exit() in one of the children in the above program, to a
value other than zero, and run the program again. Observe the new output.
PPID(parent process ID) PID(process ID) PGID(process group ID) SID(session ID)
setpgid(pid, pgid);
If pgid == pid or pgid == 0, then this function call creates a new process group with process group
leader pid. Otherwise, this puts pid into the already existing process group pgid. A zero pid refers to
the current process. The call setpgrp() is equivalent to setpgid(0,0).
killpg(pgrp, sig);
Also, one can wait for children in his own process group by:
For more about this topic, see the manual pages for both functions.
Example:
The next program forks, the child process changes group and exits, the parent waits for it then exits.
21) Compile and Run the following program to understand the process of process groups creation.
22) Observe the output of the program. A sample output follows:
Important Notes:
1. The Shell acts as the parent process. All processes started by the user are the children of his shell.
2. The status of a Linux process is shown as a column of the process table when viewed by the
execution of the ps command. States may be: R: running, O: orphan, S: sleeping, Z: zombie.
3. The child process is given the time slice before the parent process. This is quite logical. For
example, we do not want the process started by us to wait until its parent, the Linux shell finishes.
This will explain the order in which the print statement is executed by the parent and the children.
4. The call to the wait() function results in a number of actions. A check is first made to see if the
parent process has any children. If it does not, a -1 is returned by wait(). If the parent process
has a zombie child, that child's PID is returned and it is removed from the process table. However
if the parent process has a child that is not terminated, the parent is suspended till it receives a
signal. The signal is received as soon as a child terminates.