Processes are created using the fork() primitive, they may be destroyed using the kill() primitive, they may destroy themselves using the exit() primitive. A process executing a fork() primitive is said to be the ``parent'' of the newly created process. A parent may wait on its children using the wait() primitive.
Please note that the examples in this section are quite simple. Real-world applications using these primitives might need to manipulate signals, file descriptors, shared memory segments, and any number of other resources. In addition, some applications need to take specific actions if a given child terminates, and might also need to be concerned with the reason that the child terminated. These concerns can of course add substantial complexity to the code. For more information, see any of a number of textbooks on the subject [Ste92].
If fork() succeeds, it returns twice, once for the parent
and again for the child.
The value returned from fork() allows the caller to tell
the difference, as shown in
Figure
(forkjoin.c).
Line 1 executes the fork() primitive, and saves its return value
in local variable pid.
Line 2 checks to see if pid is zero, in which case, this is the
child, which continues on to execute line 3.
As noted earlier, the child may terminate via the exit() primitive.
Otherwise, this is the parent, which checks for an error return from
the fork() primitive on line 4, and prints an error and exits
on lines 5-7 if so.
Otherwise, the fork() has executed successfully, and the parent
therefore executes line 9 with the variable pid containing the
process ID of the child.
The parent process may use the wait() primitive to wait for its children
to complete.
However, use of this primitive is a bit more complicated than its shell-script
counterpart, as each invocation of wait() waits for but one child
process.
It is therefore customary to wrap wait() into a function similar
to the waitall() function shown in
Figure
(api-pthread.h),
this waitall() function having semantics similar to the
shell-script wait command.
Each pass through the loop spanning lines 6-15 waits on one child process.
Line 7 invokes the wait() primitive, which blocks until a child process
exits, and returns that child's process ID.
If the process ID is instead -1, this indicates that the wait()
primitive was unable to wait on a child.
If so, line 9 checks for the ECHILD errno, which indicates that there
are no more child processes, so that line 10 exits the loop.
Otherwise, lines 11 and 12 print an error and exit.
Quick Quiz 5.4: Why does this wait() primitive need to be so complicated? Why not just make it work like the shell-script wait does? End Quick Quiz
It is critically important to note that the parent and child do not
share memory.
This is illustrated by the program shown in
Figure
(forkjoinvar.c),
in which the child sets a global variable x to 1 on line 6,
prints a message on line 7, and exits on line 8.
The parent continues at line 14, where it waits on the child,
and on line 15 finds that its copy of the variable x is still zero.
The output is thus as follows:
Child process set x=1 Parent process sees x=0 |
Quick Quiz 5.5: Isn't there a lot more to fork() and wait() than discussed here? End Quick Quiz
The finest-grained parallelism requires shared memory, and
this is covered in
Section .
That said, shared-memory parallelism can be significantly more complex
than fork-join parallelism.
Paul E. McKenney 2011-12-16