Process Creation
As you might (hopefully) already know, it is possible for a user to run a
process in the system, suspend it (Ctrl-Z), and move it to the background (using
the 'bg' command).
A D V E R T I S E M E N T
If you're not familiar with this, you would do best to read
the 'Job Control' section of the 'csh' manual page (or of 'bash', if that is the
shell you normally use). However, we are interested in learning how to create
new processes from within a C program.
The fork() System Call
The fork() system call is the basic way to create a new process.
It is also a very unique system call, since it returns twice(!) to the caller.
Sounds confusing? good. This confusion stems from the attempt to define as few
systems calls as possible, it seems. OK, lets see:
- fork()
- This system call causes the current process to be split into two
processes - a parent process, and a child process. All of the memory pages
used by the original process get duplicated during the fork()
call, so both parent and child process see the exact same image. The only
distinction is when the call returns. When it returns in the parent process,
its return value is the process ID (PID) of the child process. When it
returns inside the child process, its return value is '0'. If for some
reason this call failed (not enough memory, too many processes, etc.), no
new process is created, and the return value of the call is '-1'. In case
the process was created successfully, both child process and parent process
continue from the same place in the code where the fork() call
was used.
To make things clearer, lets see an example of a code that uses this system
call to create a child process that prints (you guessed it) "hello world" to the
screen, and exits.
#include <unistd.h> /* defines fork(), and pid_t. */
#include <sys/wait.h> /* defines the wait() system call. */
/* storage place for the pid of the child process, and its exit status. */
pid_t child_pid;
int child_status;
/* lets fork off a child process... */
child_pid = fork();
/* check what the fork() call actually did */
switch (child_pid) {
case -1: /* fork() failed */
perror("fork"); /* print a system-defined error message */
exit(1);
case 0: /* fork() succeeded, we're inside the child process */
printf("hello world\n");
exit(0); /* here the CHILD process exits, not the parent. */
default: /* fork() succeeded, we're inside the parent process */
wait(&child_status); /* wait till the child process exits */
}
/* parent's process code may continue here... */
Notes:
- The perror() function prints an error message based on the
value of the errno variable, to stderr.
- The wait() system call waits until any child process exits,
and stores its exit status in the variable supplied. There are a set of
macros to check this status, that will be explained in the next section.
Note: fork() copies also a memory area known as the 'U Area'
(or User Area). This area contains, amongst other things, the file descriptor
table of the process. This means that after returning from the fork()
call, the child process inherits all files that were open in the parent process.
If one of them reads from such an open file, the read/write pointer is advanced
for both of them. On the other hand, files opened after the call to fork()
are not shared by both processes. Further more, if one process closes a shared
file, it is still kept open in the other process
Child Process Termination
Once we have created a child process, there are two possibilities. Either the
parent process exits before the child, or the child exits before the parent.
Now, Unix's semantics regarding parent-child process relations state something
like this:
- When a child process exits, it is not immediately cleared off the
process table. Instead, a signal is sent to its parent process, which needs
to acknowledge it's child's death, and only then the child process is
completely removed from the system. In the duration before the parent's
acknowledgment and after the child's exit, the child process is in a state
called "zombie".
- When a process exits (terminates), if it had any child processes, they
become orphans. An orphan process is automatically inherited by the 'init'
process (process number 1 on normal Unix systems), and becomes a child of
this 'init' process. This is done to ensure that when the process
terminates, it does not turn into a zombie, because 'init' is written to
properly acknowledge the death of its child processes.
When the parent process is not properly coded, the child remains in the
zombie state forever. Such processes can be noticed by running the 'ps' command
(shows the process list), and seeing processes having the string "<defunct>" as
their command name.
The wait() System Call
The simple way of a process to acknowledge the death of a child process is by
using the wait() system call. As we mentioned earlier, When
wait() is called, the process is suspended until one of its child
processes exits, and then the call returns with the exit status of the child
process. If it has a zombie child process, the call returns immediately, with
the exit status of that process.
Asynchronous Child Death Notification
The problem with calling wait() directly, is that usually you
want the parent process to do other things, while its child process executes its
code. Otherwise, you're not really enjoying multi-processes, do you? That
problem has a solution by using signals. When a child process dies, a signal,
SIGCHLD (or SIGCLD) is sent to its parent process. Thus, using a proper signal
handler, the parent will get an asynchronous notification, and then when it'll
call wait(), the system assures that the call will return
immediately, since there is already a zombie child. Here is an example of our
"hello world" program, using a signal handler this time.
#include <stdio.h> /* basic I/O routines. */
#include <unistd.h> /* define fork(), etc. */
#include <sys/types.h> /* define pid_t, etc. */
#include <sys/wait.h> /* define wait(), etc. */
#include <signal.h> /* define signal(), etc. */
/* first, here is the code for the signal handler */
void catch_child(int sig_num)
{
/* when we get here, we know there's a zombie child waiting */
int child_status;
wait(&child_status);
printf("child exited.\n");
}
.
.
/* and somewhere in the main() function ... */
.
.
/* define the signal handler for the CHLD signal */
signal(SIGCHLD, catch_child);
/* and the child process forking code... */
{
int child_pid;
int i;
child_pid = fork();
switch (child_pid) {
case -1: /* fork() failed */
perror("fork");
exit(1);
case 0: /* inside child process */
printf("hello world\n");
sleep(5); /* sleep a little, so we'll have */
/* time to see what is going on */
exit(0);
default: /* inside parent process */
break;
}
/* parent process goes on, minding its own business... */
/* for example, some output... */
for (i=0; i<10; i++) {
printf("%d\n", i);
sleep(1); /* sleep for a second, so we'll have time to see the mix */
}
}
Lets examine the flow of this program a little:
- A signal handler is defined, so whenever we receive a SIGCHLD,
catch_child will be called.
- We call fork() to spawn a child process.
- The parent process continues its control flow, while the child process
is doing its own chores.
- When the child calls exit(), a CHLD signal is sent by the
system to the parent.
- The parent process' execution is interrupted, and its CHLD signal
handler, catch_child, is invoked.
- The wait() call in the parent causes the child to be
completely removed off the system.
- finally, the signal handler returns, and the parent process continues
execution at the same place it was interrupted in.
|