cppannotations/yo/concrete/fork.yo
2007-07-25 09:03:21 +00:00

167 lines
9.3 KiB
Text

From the bf(C) programming language, the ti(fork()) i(system call) is well
known. When a program needs to start a new process, ti(system()) can be used,
but this requires the program to wait for the emi(child process) to
terminate. The more general way to spawn subprocesses is to call tt(fork()).
In this section we will see how bf(C++) can be used to wrap classes around a
complex system call like tt(fork()). Much of what follows in this section
directly applies to the i(Unix) i(operating system), and the discussion will
therefore focus on that operating system. However, other systems usually
provide comparable facilities. The following discussion is based heavily on
the notion of emi(design patterns), as published by em(Gamma et al.) (1995)
hi(Gamma, E.)
When tt(fork()) is called, the current program is duplicated in memory,
thus creating a new process, and both processes continue their execution just
below the tt(fork()) system call. The two processes may, however, inspect the
return value of tt(fork()): the return value in the original process (called
the emi(parent process)) differs from the return value in the newly created
process (called the emi(child process)):
itemization(
it() In the em(parent process) tt(fork()) returns the emi(process ID) of
the child process created by the tt(fork()) system call. This is a positive
integer value.
it() In the em(child process) tt(fork()) returns 0.
it() If tt(fork()) fails, -1 is returned.
)
A basic tt(Fork) class should hide all bookkeeping details of a system
call like tt(fork()) from its users. The class tt(Fork) developed here will do
just that. The class itself only needs to take care of the proper execution of
the tt(fork()) system call. Normally, tt(fork()) is called to start a child
process, usually boiling down to the execution of a separate process. This
child process may expect input at its standard input stream and/or may
generate output to its standard output and/or standard error streams. tt(Fork)
does not know all this, and does not have to know what the child process will
do. However, tt(Fork) objects should be able to activate their child
processes.
Unfortunately, tt(Fork)'s constructor cannot know what actions its child
process should perform. Similarly, it cannot know what actions the parent
process should perform. For this particular situation, the
emi(template method design pattern)
hi(design pattern: template method)
was developed. According to Gamma c.s., the em(template method design
pattern)
quote(
``Define(s) the skeleton of an algorithm in an operation, deferring some
steps to subclasses. (The) Template Method (design pattern) lets
subclasses redefine certain steps of an algorithm, without changing
the algorithm's structure.''
)
This design pattern allows us to define an emi(abstract base class)
hi(base class)
already providing the essential steps related to the tt(fork()) system
call and deferring the implementation of certain normally used parts of the
tt(fork()) system call to subclasses.
The tt(Fork) abstract base class itself has the following characteristics:
itemization(
it() It defines a data member tt(d_pid). This data member will contain
the child's emi(process id) (in the parent process) and the value 0 in the
child process. Its public interface declares but two members:
itemization(
it() a ti(fork()) member function, performing the actual forking
(i.e., it will create the (new) child process);
it() an em(empty) tt(virtual) destructor tt(~Fork()), which will be
overridden by derived classes defining their own destructors.
verbinsert(DES)(concrete/examples/fork.h)
)
Here is tt(Fork)'s interface:
verbinsert(CLASS)(concrete/examples/fork.h)
it() All remaining member functions are declared in the class's
tt(protected) section and can thus em(only) be used by derived classes. They
are:
itemization(
it() The member function tt(pid()), allowing derived classes to
access the system tt(fork())'s return value:
verbinsert(PID)(concrete/examples/fork.h)
it() A member tt(int waitForChild()), which can be called by parent
processes to wait for the completion of their child processes (as discussed
below). This member is declared in the class interface. Its implementation is:
verbinclude(concrete/examples/waitforchild.cc)
This simple implementation returns the child's emi(exit status) to
the parent. The called system function ti(waitpid()) em(blocks) until the
child terminates.
it() When tt(fork()) system calls are used,
em(parent processes) hi(parent process) and
emi(child processes) hi(child process) must always be distinguished. The
main distinction between these processes is that tt(d_pid) will be equal to
the child's process-id in the parent process, while tt(d_pid) will be equal to
0 in the child process itself. Since these two processes must always be
distinguished (and present), their implementation by classes derived from
tt(Fork) is enforced by tt(Fork)'s interface: the members tt(childProcess()),
defining the child process' actions and tt(parentProcess()), defining the
parent process' actions were defined as pure virtual functions.
it() In addition, communication between parent- and child processes
may use standard streams or other facilities, like em(pipes) (cf. section
ref(PIPE)). To facilitate this inter-process communication, derived classes
em(may) implement:
itemization(
itt(childRedirections()): this member should be implemented if any
standard stream (tt(cin, cout)) or tt(cerr) must be redirected in the
em(child) process (cf. section ref(REDIRECTION));
itt(parentRedirections()): this member should be implemented if any
standard stream (tt(cin, cout)) or tt(cerr) must be redirected in the
em(parent) process.
)
Redirection of the standard streams will be necessary if parent- and
child processes should communicate with each other via the standard streams.
Here are their default definitions provided by the class's interface:
verbinsert(REDIRECT)(concrete/examples/fork.h)
)
)
The member function tt(fork()) calls the system function tt(fork())
(Caution: since the system function tt(fork()) is called by a member
function having the same name, the tt(::) scope resolution operator must be
used to prevent a recursive call of the member function itself). After calling
tt(::fork()), depending on its return value, either tt(parentProcess())
or tt(childProcess()) is called. Maybe redirection is
necessary. tt(Fork::fork())'s implementation calls tt(childRedirections())
just before calling tt(childProcess()), and tt(parentRedirections()) just
before calling tt(parentProcess()):
verbinclude(concrete/examples/fork.cc)
In tt(fork.cc) the class's emi(internal header file) tt(fork.ih) is
included. This header file takes care of the inclusion of the necessary system
header files, as well as the inclusion of tt(fork.h) itself. Its
implementation is:
verbinclude(concrete/examples/fork.ih)
Child processes should not return: once they have completed their tasks,
they should terminate. This happens automatically when the child process
performs a call to a member of the ti(exec...()) family, but if the child
itself remains active, then it must make sure that it terminates properly. A
child process normally uses ti(exit()) to terminate itself, but note that
tt(exit()) prevents the activation of destructors of objects
hi(destructor: called at exit())
hi(exit(): calling destructors)
defined at the same or more superficial nesting levels than the level at
which tt(exit()) is called. Destructors of globally defined objects em(are)
activated when tt(exit()) is used. When using tt(exit()) to terminate
tt(childProcess()), it should either itself call a support member function
defining all nested objects it needs, or it should define all its objects in a
compound statement (e.g., using a tt(throw) block) calling tt(exit()) beyond
the compound statement.
Parent processes should normally wait for their children to complete. The
terminating child processes inform their parent that they are about to
terminate by sending out a emi(signal) which should be caught by their
parents. If child processes terminate and their parent processes do not catch
those signal then such child processes remain visible as so-called emi(zombie)
processes.
If parent processes must wait for their children to complete, they may
call the member tt(waitForChild()). This member returns the exit status of a
child process to its parent.
There exists a situation where the em(child) process em(continues) to
live, but the em(parent) dies. In nature this happens all the time: parents
tend to die before their children do. In our context (i.e. bf(C++)), this is
called a emi(daemon) program: the parent process dies and the child program
continues to run as a child of the basic ti(init) process. Again, when the
child eventually dies a signal is sent to its `step-parent' hi(step-parent)
tt(init). No zombie is created here, as tt(init) catches the termination
signals of all its hi(step-child) (step-) children. The construction of a
daemon process is very simple, given the availability of the class tt(Fork)
(cf. section ref(DAEMON)).