From a very high-level perspective, we can describe the life cycle of a process as a sequence of
events. The first event is the creation of the process as a new virtual memory
instance. The CPU will then begin executing the program code according to the
von Neumann instruction cycle until it reaches the halt
instruction indicating the
program is complete. At that point, the process is destroyed and any resources (such as physical
memory) associated with it are reclaimed by the kernel.
There are generally two system calls that are associated with creating a new process: fork()
and
exec()
. When a program calls fork()
, a system call is triggered that asks the kernel to
create a new virtual memory instance. In the immediate moment when this occurs, the contents of the
new virtual memory instance is an exact duplicate of the process that was running. We refer to the
two processes as the parent and child. While
handling the fork()
, the kernel will allocate new internal data structures for the child, assign
it a process identifier (PID), and assign a small set of initial resources to it. A PID is
an integer value that acts like a name that can be used to uniquely refer to a process.
C library functions – <unistd.h>
pid_t fork(void);
pid_t getpid(void);
pid_t getppid(void);
Code Listing 2.6 demonstrates how to use fork()
. Once both processes are running, the code can
distinguish between the parent and child by looking at the value returned from fork()
.The kernel
returns 0 to the child, and it returns the child’s PID to the parent. If the kernel failed or
refused to create a new process for some reason, it would return a negative value to the parent; the
global variable errno
(declared in the standard library file errno.h
) would be set to an
integer value indicating the error that occurred.
1 2 3 4 5 6 7 8 9 10 11 12 | /* Code Listing 2.6:
Using fork() to create a child process and access its PID
*/
pid_t child_pid = fork ();
if (child_pid < 0)
printf ("ERROR: No child process created\n");
else if (child_pid == 0)
printf ("Hi, I am the child!\n");
else
printf ("Parent just learned about child %d\n", child_pid);
|
The return values from fork()
make sense when you consider that the C library defines two
functions that allow the process to determine its own PID (getpid()
) and to find out its
parent’s PID (getppid()
). The child can use these two functions to identify itself and its
parent. However, the parent process may have already created dozens of child processes. The return
value from fork()
informs the parent of the PID for this new child process that was just created.
Code Listing 2.7 shows an example of what happens when the return code from fork()
is not
checked. On line 6, the original process creates a child. For clarity, let the original process of
PID 1000 and this child has PID 1001. Since both processes continue to execute with the same code,
both reach line 9 and create two additional processes (PIDs 1002 and 1003). As a result, there would
be four processes that execute line 12, printing the message four times (with different PIDs each time).
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* Code Listing 2.7:
Running the same code in multiple processes
*/
/* Start by creating a second process */
pid_t first_fork = fork ();
/* BOTH the original parent and child create new processes */
pid_t second_fork = fork ();
/* This line will print four times */
printf ("Hello world from %d!\n", getpid ());
|
It may seem odd that fork()
would create a new process running the exact same code as the
parent. However, there are several times where this makes sense. Consider a web browser, such as
Google Chrome. Whenever you open a new tab, Chrome will make a call to fork()
to create a new
process for that tab. Intuitively, all tabs in a web browser work the same way: they get a URL
request from the user and retrieve the corresponding web page or file from a server somewhere.
Consequently, it makes sense that all of the processes for Chrome would start with the same code
segment. Making the tabs run in separate processes provides some security and reliability: If a
plug-in crashes in one tab, only that tab is affected; the other tabs exist in other processes, so
they are isolated from the crash.
Bug Warning
Once a process uses fork()
to create a child process, the timing of the execution becomes
nondeterministic from the programmer’s point of view. The programmer cannot predict or control when
the child begins executing relative to the parent. In some systems, the child process may begin
executing immediately before the parent process returns from the call to fork()
. In others, the
child may simply be created and start running at some later time. Consequently, if the order of the
execution matters, the programmer must take extra steps to ensure correct behavior; one example of
this is to use wait()
as described below. We will examine more advanced techniques in later chapters.
In many cases, after making a call to fork()
, you want to switch to a different program. For
instance, consider the bash
terminal program. Within bash
, some commands are considered
built-in and are executed by bash
itself. One example of a built-in is the export command,
which can be used to define or change an environment variable, such as PATH
or
CLASSPATH
. Other commands are used to call a separate compiled program, such as ls
, gcc
,
or vim
. The code to execute these commands does not exist as a part of bash
. Rather, these
are distinct programs and the code needs to be loaded into the new process.
C library functions – <unistd.h>
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
int execle(const char *path, const char *arg0, ... /*, (char *)0, char* const envp[]*/);
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
PATH
.int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
Loading a new program is handled by the exec()
system call, which can be invoked by a family of
C functions. The functions differ by how you pass the parameters. The execl()
, execle()
, and
execlp()
functions take the command line arguments as additional function parameters, whereas
execv(), execve(), and execvp() require you to create a single array that contains all of the
arguments. The execl()
and execv()
functions expect that you provide the exact path to the
executable file, while execlp()
and execvp()
will look up the path for you. The execle()
and execve()
functions allow you to specify a custom set of environment variables for the new
program. Finally, fexecve()
allows you to pass an open file descriptor for the program instead
of the program’s name or path. Code Listings 2.8 and 2.9 show two different ways to
call the ls
program to list files, one using execl()
and the other using execvp()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /* Code Listing 2.8:
Using execl() lists all parameters in one list and requires an exact path
*/
/* Create the child and run 'ls -l' */
pid_t child_pid = fork ();
if (child_pid < 0)
exit (1); /* exit if fork() failed */
if (child_pid == 0)
{
int rc = execl ("/bin/ls", "ls", "-l", NULL);
exit (1); /* exit if exec fails */
}
/* Make the parent wait for the first child */
wait (NULL);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /* Code Listing 2.9:
Using execvp() puts command-line arguments in a vector and does a path lookup
*/
/* Set up all parameters for 'ls -l' in a single vector */
char *const parameters[] = { "ls", "-l", NULL};
child_pid = fork ();
if (child_pid < 0)
exit (1); /* exit if fork() failed */
if (child_pid == 0)
{
int rc = execvp ("ls", parameters);
exit (1); /* exit if exec fails */
}
/* Make the parent wait for the second child */
wait (NULL);
|
There are a couple of important points to note in this code. First, note that both calls to ls
will have the same three arguments (argv[0] = "ls"
, argv[1] = "-l"
, and argv[2] = NULL
).
That is, the first parameter passed to execl()
on line 12 of Code Listing 2.8 is not included in
the argument list as the child process receives it. The convention that argv[0]
is the name of
the program arises from how programs like bash specify the remaining parameters to the exec()
functions.
Second, observe that both versions use NULL
to indicate the end of the arguments. In the case of
execl()
, function takes a variable-length parameter list, so it relies on NULL
to know when
it should stop looking for arguments. In the case of execvp()
, the function expects exactly two
arguments, with the second being an array of unknown length.
Third, note that the first parameter of execl()
must be an exact path to the location of the
executable file. If you call execl()
with just "ls"
instead of "/bin/ls"
, then it will
look for an executable file in the current directory that is named ls
. If the file does not
exist, the call to execl()
will return a negative value to indicate an error. For the call to
execvp()
, bash will look through the directories specified by the PATH
environment variable
to try to find the executable file for you. If line 12 of Code Listing 2.8 used execlp()
instead
of execl()
, bash
would perform a PATH
lookup there, as well.
Finally, note that no code after a successful call to any of the exec()
functions will execute
in the current process. In this case, Code Listing 2.8 line 13 and Code Listing 2.9 line 14 should
never be reached, so neither of the calls to exit()
should happen. The reason is that exec()
replaces the current process’s code, data, stack, and heap segments. Assuming the call to
exec()
is successful, the child process will start over with a clean virtual memory instance;
none of the parent process’s information is retained within the child process.
Bug Warning
Always make sure to check the return values for both fork()
and exec()
. Most OS enforce a
limit on the number of processes that a single user can create; once you reach this limit,
fork()
will fail. Similarly, if you make a mistake in the name of the file that you want to run
or you do not have permissions, exec()
will fail and the child process will continue to execute
the current program’s code. Failing to check these return values can lead to unexpected behavior.
In recent years, the use of fork()
has been criticized for a variety of reasons. For instance,
the implementation requirements of it are considered too slow and require too much power for very
small embedded computing systems (e.g., think of the “computers” in a car that operate the cruise
control or anti-lock breaking). Another problem is that calling fork()
on a process with
multiple threads (see Chapter 7) can lead to inconsistent copies of the parent process; that is, the
thread that calls fork()
might not have a completely accurate memory image, particularly in
multicore architectures. Lastly, the semantics of fork()
have
become very complicated, as there are many special cases that must be handled, such as how to deal
with open files, timers, asynchronous I/O buffers, etc. [1]
To address some of these criticisms, POSIX includes a new function interface, posix_spawn()
.
This function takes multiple parameters, including the path to the executable and the argv
and
envp
arrays that would be passed into execv()
or execve()
functions. In addition,
posix_spawn()
takes other parameters that can specify additional operations. These operations
give the programmer explicit control over the special cases mentioned above.
C library functions – <spawn.h>
int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]);
Code Listing 2.10 shows how to use posix_spawn()
to run an external program. In this case, the
cksum
program is run on a CSV file, as specified in the path
and argv
arguments. The
child’s process ID is returned through the call-by-reference parameter &child
. The assert()
is used here to confirm that the process was successfully spawned; posix_spawn()
returns a
non-zero value if an error occurs at any point and the process was not successfully spawned.
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* Code Listing 2.10:
Using posix_spawn() to run a helper program instead of fork() and execv()
*/
pid_t child = -1;
char *path = "/usr/bin/cksum";
char *argv[] = { "cksum", "movies.csv", NULL };
/* Spawn and run an external program in one step */
assert (posix_spawn (&child, path, NULL, NULL, argv, NULL) == 0);
/* Make the parent wait for the child */
wait (NULL);
|
A process can be terminated in a number of ways. Some methods of process termination are voluntary
in nature, such as when the code makes a call to exit()
, abort()
, or returns from
main()
. Other methods are involuntary, such as when a process is killed after a segmentation
fault or similar exception. Regardless of what causes the process termination, the same set of
procedures are typically performed: open file handles are closed, the virtual memory space is
destroyed, the associated physical memory is reclaimed by the system, and any relevant data
structures in the kernel are cleaned up.
The preceding example also illustrates a key element about the timing of the destruction of
processes. Once a process calls fork()
and creates a child process, the relative scheduling
between the parent and child is nondeterministic from the programmer’s perspective. The two
processes are scheduled independently by the OS running on that particular machine, and the
scheduling will be influenced by other processes running, as well. However, the parent can
temporarily pause its own execution by making a call to wait()
or waitpid()
.
C library functions – <sys/wait.h>
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
The wait()
system call will block the current process until all of its children have been
terminated. A call to waitpid()
is similar, but it only waits for a single, identified child to
terminate. In the preceding example, the first child makes a call to execl()
to run the ls
program. At approximately the same time, the parent process calls wait(NULL)
. The
nondeterministic nature of scheduling means it is impossible to determine which happens first, but
the result is the same: the parent process will not proceed to the second call to fork()
until
the first child process runs the ls
program and terminates.
Note that there is no problem even if the child process terminates before the parent gets scheduled
to run. If all children have been terminated by the time a process calls wait()
, then the parent
process will simply continue on without pausing. The only requirement for wait()
is that the
parent cannot continue if there is at least one child process still trying to run.
The convention with C programs is to return an integer value from main()
to indicate whether the
program completed successfully. The standard practice is to return 0 to indicate that no error
occurred, whereas a non-zero value typically indicates an error occurred in some way. When working
on the command-line, this value can be retrieved by printing the bash
variable $?
. In C
programs, this value can be retrieved through the int *
parameter passed to wait()
as shown
in Code Listing 2.11.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /* Code Listing 2.11:
Using wait() and WEXITSTATUS() to retrieve a process's return code
*/
/* Create the first child and run 'ls -l' */
pid_t child_pid = fork ();
if (child_pid < 0)
exit (1); /* exit if fork() failed */
if (child_pid == 0)
exit (23); /* child process exits */
/* Make the parent wait to retrieve the return code*/
int status = 0;
pid_t exited = wait (&status);
/* Make the parent print the child's exit code of 23 */
printf ("PID %d exited with code %d\n", exited, WEXITSTATUS(status));
|
Bug Warning
The return value of the child process is shifted to the left when it is retrieved using wait()
,
as the lower eight bits are used for other purposes. The WEXITSTATUS()
macro corrects this bit
shifting to receive the actual return code from the child process.
[1] | For a full description of the problems with fork() , see the paper: A. Baumann, J.
Appavoo, O. Krieger, and T. Roscoe, “A fork() in the road.” In Workshop on Hot Topics in Operating
Systems (HotOS ‘19). New York, NY: ACM, 2019. |