Chapter 3. Using Processes

Using Processes

Introduction

Processes are at the very heart of the operating system. As we have seen, all but a very few special processes are generated by the fork system call. If successful, the fork system call produces a child process that continues its execution at the point of its invocation in the parent process. In this chapter, we explore the generation and use of child processes in detail. In Chapter 11, “Threads,” the creation of threads will be discussed.

The fork System Call Revisited

The fork system call is unique in that while it is called once, it returns twice—to the child and to the parent processes. As noted in Chapter 1, “Programs and Processes,” if the fork system call is successful, it returns a value of 0 to the child process and the process ID of the child to the parent process. If the fork system call fails, it returns a −1 and sets the global variable errno. The failure of the system to generate a new process can be traced, by examination of the errno value, to either exceeding the limits on the number of processes (systemwide or for the specific user) or to the lack of available swap space for the new process. It is interesting to note that in theory the operating system is always supposed to leave room in the process table for at least one superuser process, which could be used to remove (kill) hung or runaway processes. Unfortunately, on many systems it is still relatively easy to write a program (sometimes euphemistically called a fork bomb) that will fill the system with dummy processes, effectively locking out system access by anyone, including the superuser.

After the fork system call, both the parent and child processes are running and continue their execution at the next statement after the fork. The return from the fork system call can be examined, and the process can make a decision as to what code is executed next. The process receiving a 0 from the fork system call knows it is the child, as 0 is not valid as a PID. Conversely, the parent process will receive the PID of the child. An example of a fork system call is shown in Program 3.1.

Example 3.1. Generating a child process.

File : p3.1.cxx
  |     /*
  |         Generating a child process
  |      */
  |     #include <iostream>
  +     #include <sys/types.h>
  |     #include <unistd.h>
  |     using namespace std;
  |     int
  |     main( ){
 10       if (fork(  ) == 0)
  |         cout << "In the CHILD process"  << endl;
  |       else
  |         cout << "In the PARENT process" << endl;
  |       return 0;
  +     }

There is no guarantee as to the output sequence that will be generated by this program. For example, if we issue the command-line sequence

linux$ p3.1 ; echo DONE ; p3.1 ; echo DONE ; p3.1

numerous times, sometimes the statement In the CHILD process will be displayed before the In the PARENT process, and other times it will not. The output sequence is dependent upon the scheduling algorithm used by the kernel. Keep in mind that commands separated by a semicolon on the command line are executed sequentially, with the shell waiting for each command to terminate before executing the next. The effects of process scheduling are further demonstrated by Program 3.2.

Example 3.2. Multiple activities parent/child processes.

File : p3.2.cxx
  |     /*
  |          Multiple activities PARENT -- CHILD processes
  |      */
  |     #include <iostream>
  +     #include <cstring>
  |     #include <sys/types.h>
  |     #include <unistd.h>
  |     using namespace std;
  |     int
 10     main( ) {
  |       static char buffer[10];
  |       if (fork(  ) == 0) {                 // In the child process
  |         strcpy(buffer, "CHILD...");
  |       } else {                             // In the parent process
  +         strcpy(buffer, "PARENT..");
  |       }
  |       for (int i=0; i < 3; ++i) {          // Both processes do this
  |         sleep(1);                          // 3 times each.
  |         write(1, buffer, sizeof(buffer));
 20       }
  |       return 0;
  |     }

Figure 3.1 shows the output of this program when run twice on a local system.

Example 3.1. Output of Program 3.2.

linux$ p3.2
PARENT..CHILD...CHILD...PARENT..PARENT..CHILD...linux$
linux$ p3.2
PARENT..CHILD...PARENT..CHILD...PARENT.. $ CHILD...

There are several interesting things to note about this program and its output. First, the write (line 19) system call, not the cout object, was used in the program. The cout object (an instance of the ostream class defined in <iostream>) is buffered and, if used, would have resulted in the three-message output from each process being displayed all at one time without any interleaving of messages. Second, the system call sleep (sleep a specified number of seconds) was used to prevent the process from running to completion within one time slice (which again would produce a homogenous output sequence). Third, one process will always end before the other. If there is sufficient intervening time before the second process ends, the system will redisplay the prompt, thus producing the last line of output where the output from the child process is appended to the prompt (i.e., linux$ CHILD...).

Keep in mind the system will flush an output stream (write its data to the physical media) in a variety of circumstances. This synchronization occurs when (a) a file is closed, (b) a buffer is full, (c) in C++ the flush or endl manipulators are placed in the output stream, or (d) a call is made to the sync system call.

exec's Minions

Processes generate child processes for a number of reasons. In a Linux environment, there are several long-lived processes, which run continuously in the background and provide system services upon demand. These processes, called daemon processes, frequently generate child processes to carry out the requested service. Some daemon processes commonly found in a Linux environment are lpd, the line printer daemon; xinetd, the extended Internet services daemon; and syslogd, the system logging daemon. Some problems (such as with databases) lend themselves to concurrent type solutions that can be effected via multiple child processes executing the same code. More commonly, such as when the shell processes a command, a process procreates a child process because it would like to transform the child process by changing the program code the child process is executing.

In Linux, any one of five library functions and one system call can be used to replace the current process image with a new image.[1] The library functions act as a front end to the system call. The library functions are discussed in the exec manual pages (Section 3), while the system call (execve) warrants its own manual page entry in Section 2. Any of these can be directly invoked by the programmer. For ease of comparison, the library functions and the system call are discussed as a group. The phrase exec call will reference this group.

It is important to remember that when a process issues any exec call, if the call is successful, the existing process is overlaid with a new set of program code. The text, data (initialized and uninitialized), and stack segment of the process are replaced and only the u (user) area of the process remains the same. The new program code (if a C/C++ binary) begins its execution at the function main. Since the system is now executing a different set of code for the same process, some things, by necessity, must change:

  • Signals that were specified as being caught by the process (i.e., associated with a signal-catching routine) are reset to their default action. This is necessary, as the addresses for the signal-catching routines are no longer valid.

  • In a similar vein, if the process was profiling (determining how much time is spent in individual routines), the profiling will be turned off in the overlaid process.

  • If the new program has its SUID bit set, the effective EUID and EGID are set accordingly.

The program to be executed can be a script. In this case, the script should have its execute bit set and start with the line #! interpreter [arg(s)], where interpreter is a valid executable (but not another script). If successful, the exec calls do not return, as the initial calling image is lost when overlaid with a new image.

Before we delve into these calls, we should take a quick look at what normally transpires when a valid command is issued at the system (shell) level, as this process will reflect the functionality available in a program. If the command issued is

linux$ cat file.txt > file2.txt

the shell parses the command line and divides it into valid tokens (e.g., cat, file.txt, etc.). The shell (via a call to fork) then generates a child process. After the fork, the shell closes standard output and opens the file file2.txt, mapping it to standard output in the child process. Next, by calling execve, the shell overlays the current program code with the program code for the command (in this case, the code for cat). When the command is finished, the shell redisplays its prompt. Figure 3.2 shows the process creation and command execution sequence.

Process creation and command execution at the shell level.

Figure 3.2. Process creation and command execution at the shell level.

While the command is executing, the shell, by default, waits in the background. As we will see, there is a wait system call that allows the shell or any other process to wait. Should the user place an & at the end of the command (to indicate to the shell that the command be placed in background), the shell will not wait and will return immediately with its prompt. When the command is finished, it may perform a call to exit or return when in the function main. The integer value passed to these calls is made available to the parent process via an argument to the wait system call. When on the command line, the returned value is stored in the system variable named status. If in the Bourne or BASH shell you issue the command

linux$ echo $?

the system will display the value returned by the last command executed. As the mapping of standard output to the file file2.txt was done in the child process and not in the shell, the I/O redirection has no further impact on ensuing command sequences.

We should note that it is possible for a user at the command line to issue an exec call. The syntax would be

linux$ exec command [arguments]

However, most users would not do this. The current process (the shell) would be overlaid with the program code for the command. Once the command was finished, the user would be logged out, as the original shell process would no longer exist!

In a programming environment, the exec calls can be used to execute another program. The prototypes for the exec calls are listed in Table 3.1.

Table 3.1. The exec Call Prototypes.

#include <unistd.h>

extern char **environ;

int execl (const char *path, const char *arg, ...);
int execv (const char *path, char *const argv[]);

int execle(const char *path, const char *arg , ...
The exec Call Prototypes., char * const envp[]);
int execve(const char *path, char *const argv[],  
The exec Call Prototypes.  char * const envp[]);

int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

The naming convention for these system calls reflects their functionality. Each call starts with the letters exec. The next letter in the call name indicates if the call takes its arguments in a list format (i.e., literally specified as a series of arguments) or as a pointer to an array of arguments (analogous to the argv structure discussed earlier). The presence of the letter l indicates a list arrangement (a variable argument list—see the manual page on stdarg for details); v indicates the array or vector arrangement. The next letter of the call name (if present) is either an e or a p. The presence of an e indicates the programmers will construct (in the array/vector format) and pass their own environment variable list. The passed environment variable list will become the third argument to the function main (i.e., envp). As noted in the section on environment variables, envp is of limited practical value. When the programmer is responsible for the environment, the current environment variable list is not passed. The presence of a p indicates the current environment PATH variable should be used when searching for a file whose name does not contain a slash.[2] In the four calls, where the PATH string is not used (execl, execv, execle and execve), the path to the program to be executed must be fully specified.

The functionality of the exec system calls is best summarized by Table 3.2.

Table 3.2. exec Call Functionality.

Library Call Name

Argument Format

Pass Current Set of Environment Variables?

Search of PATH Automatic?

execl

list

yes

no

execv

array

yes

no

execle

list

no

no

execve

array

no

no

execlp

list

yes

yes

execvp

array

yes

yes

Of the six variations, execlp and execvp calls are used most frequently (as automatic environment passing and path searching are usually desirable) and will be explained in detail.

execlp

The execlp library function (Table 3.3) is used when the number of arguments to be passed to the program to be executed is known in advance.

When using execlp, the initial argument, file, is a pointer to the file that contains the program code to be executed. If this file reference begins with a /, it is assumed that the reference is an absolute path to the file. In this circumstance, it would appear that the p specification (execlp) is superfluous; however, the PATH string is still used if other arguments are file names or if the code to be executed contains file references. If no / is found, each of the directories specified in the PATH variable will be, in turn, preappended to the file name specified, and the first valid program reference found will be the one executed. It is a good practice to fully specify the program to be executed in all situations to prevent a program with the same name, found in a prior PATH string directory, from being inadvertently executed. For the execlp call to be successful, the file referenced must be found and be marked as executable. If the call fails, it returns a −1 and sets errno to indicate the error. As the overlaying of one process image with another is very complex, the possibilities for failure are numerous (as shown in Table 3.4).

Table 3.3. Summary of the execlp Library Function.

Include File(s)

<unistd.h>
extern char **environ;

Manual Section

3

Summary

int execlp(const char *file,const char *arg, . . .);

Return

Success

Failure

Sets errno

Does not return

−1

Yes

Table 3.4. exec Error Messages.

#

Constant

perror Message

Explanation

1

EPERM

Operation not permitted

  • The process is being traced, the user is not the superuser, and the file has an SUID or SGID bit set.

  • The file system is mounted nosuid, the user is not the superuser, and the file has an SUID or SGID bit set.

2

ENOENT

No such file or directory

One or more parts of path to new process file does not exist (or is NULL).

4

EINTR

Interrupted system call

Signal was caught during the system call.

5

EIO

Input/output error

 

7

E2BIG

Argument list too long

New process argument list plus exported shell variables exceed the system limits.

8

ENOEXEC

Exec format error

New process file is not in a recognized format.

11

EAGAIN

Resource temporarily unavailable

Total system memory while reading raw I/O is temporarily insufficient.

12

ENOMEM

Cannot allocate memory

New process memory requirements exceed system limits.

13

EACCES

Permission denied

  • Search permission denied on part of file path.

  • The new file to process is not an ordinary file.

  • No execute permission on the new file to process.

14

EFAULT

Bad address

path references an illegal address.

20

ENOTDIR

Not a directory

Part of the specified path is not a directory.

21

EISDIR

Is a directory

An ELF interpreter was a directory.

22

EINVAL

Invalid argument

An ELF executable had more than one interpreter.

24

EMFILE

Too many open files

Process has exceeded the maximum number of files open.

26

ETXTBSY

Text file busy

More than one process has the executable open for writing.

36

ENAMETOOLONG

File name too long

The path value exceeds system path/file name length.

40

ELOOP

Too many levels of symbolic links

The perror message says it all.

67

ENOLINK

Link has been severed

The path value references a remote system that is no longer active.

72

EMULTIHOP

Multihop attempted

The path value requires multiple hops to remote systems, but file system does not allow it.

80

ELIBBAD

Accessing a corrupted shared library

An ELF interpreter was not in a recognized format.

The ellipses in the execlp function prototype can be thought of as argument 0 (arg0) through argument n (argn). These arguments are pointers to the null-terminated strings that would be normally passed by the system to the program if it were invoked on the command line. That is, argument 0, by convention, should be the name of the program that is executing. This is usually the same as the value in file, although the program referenced by file may include an absolute path, while the value in argument 0 most often would not. Argument 1 would be the first parameter to be passed to the program (which, using argv notation, would be argv[1]), argument 2 would be the second, and so on. The last argument to the execlp library call must be a NULL that is, for portability reasons, cast to a character pointer. Program 3.3, which invokes the cat utility program, demonstrates the use of the execlp library call.

Example 3.3. Using the execlp system call.

File : p3.3.cxx
  |     /*
  |          Running the cat utility via an exec system call
  |      */
  |     #include <iostream>
  +     #include <cstdio>
  |     #include <unistd.h>
  |     using namespace std;
  |     int
  |     main(int argc, char *argv[ ]){
 10       if (argc > 1) {
  |         execlp("/bin/cat", "cat", argv[1], (char *) NULL);
  |         perror("exec failure ");
  |         return 1;
  |       }
  +       cerr <<  "Usage: " << *argv << " text_file" << endl;
  |       return 2;
  |     }

When passed a text file name on the command line, this program displays the contents of the file to the screen. The program accomplishes this by overlaying its own process image with the program code for the cat utility program. The program passes the cat utility program the name (referenced by argv[1]) of the file to display. If the execlp system call fails, the call to perror is made and the program exits and returns the value 1 to the system. If the call is successful, the perror and return statements are never reached, as they are replaced with the program code for the cat utility.

A sample run of the program is shown in Figure 3.3.

Example 3.3. Output of Program 3.3.

linux$ p3.3 test.txt
This is a sample text
file for the program to
display!

execvp

If the number of arguments for the program to be executed is dynamic, then the execvp call can be used (Table 3.5). As with the execlp call, the initial argument to execvp is a pointer to the file that contains the program code to be executed. However, unlike execlp, there is only one additional argument that execvp requires. This second argument, defined as

char *const argv[ ]

specifies that a reference to an array of pointers to character strings should be passed. The format of this array parallels that of argv and, in many cases, is argv. If the reference is not the argv values for the current program, the programmer is responsible for constructing and initializing a new argv-like array. If this second approach is taken, the last element of the new argv array should contain a NULL address value. If execvp fails, it returns a value of −1 and sets the value in errno to indicate the source of the error (see Table 3.5).

Table 3.5. Summary of the execvp System Call.

Include File(s)

<unistd.h>
<extern char **environ;

Manual Section

3

Summary

Int execvp(const char *file, char *const argv[]);

Return

Success

Failure

Sets errno

Does not return

−1

Yes

Program 3.4 makes use of the argv values for the current program.

Example 3.4. Using execvp with argv values.

File : p3.4.cxx
  |     /*
  |          Using execvp to execute the contents of argv
  |      */
  |     #include <iostream>
  +     #include <cstdio>
  |     #include <unistd.h>
  |     using namespace std;
  |     int
  |     main(int argc, char *argv[ ]) {
 10       if ( argc > 1 ) {
  |         execvp(argv[1], &argv[1]);
  |         perror("exec failure");
  |         return 1;
  |       }
  +       cerr << "Usage: " << *argv << " exe [arg(s)]" << endl;
  |       return 2;
  |     }

The program will execute, via execvp, the program passed to it on the command line. The first argument to execvp, argv[1], is the reference to the program to execute.

The second argument, &argv[1], is the reference to the remainder of the command-line argv array. Notice that both of these references began with the second element of argv (that is, argv[1]), as argv[0] is the name of the current program (e.g., p3.4). The output in Figure 3.4 shows that the program does work as expected.

Example 3.4. Output of Program 3.4 when passed the cat command.

linux$ p3.4 cat test.txt
This is a sample text
file for a program to
display!

If we place additional information on the command line when running Program 3.4, we find the program will pass the information on, as demonstrated in Figure 3.5.

Example 3.5. Output of Program 3.4 when passed the cat command with the -n option.

linux$ p3.4 cat -n test.txt
     1  This is a sample text
     2  file for a program to
     3  display!

If command-line argv values of the current program are not used with execvp, then the programmer must construct a new argv to be passed. An example of how this can be done is shown in Program 3.5.

Example 3.5. Using execvp with a programmer-generated argument list.

File : p3.5.cxx
  |     /*
  |          Generating our own argv type list for execvp
  |      */
  |     #include <iostream>
  +     #include <cstdio>
  |     #include <unistd.h>
  |     using namespace std;
  |     int
  |     main( ){
 10       char    *new_argv[ ] = {"cat",
  |                               "test.txt",
  |                               (char *) 0
  |                              };
  |       execvp("/bin/cat", new_argv );
  +       perror("exec failure ");
  |       return 1;
  |     }

When compiled and run as p3.5, the output of this program will be the same as the output from the first run of Program 3.4.

Using fork and exec Together

In most programs, the fork and exec calls are used in conjunction with one another (in some operating systems, the fork and exec calls are packaged as a single spawn system call). The parent process generates a child process, which it then overlays by a call to exec, as in Program 3.6.

Example 3.6. Using fork with execlp.

File : p3.6.cxx
  |     /*
  |          Overlaying a child process via an exec
  |      */
  |     #include <iostream>
  +     #include <sstream>
  |     #include <cstdio>
  |     #include <unistd.h>
  |     using namespace std;
  |     int
 10     main( ){
  |       char  *mesg[ ] = {"Fie", "Foh", "Fum"};
  |       int   display_msg(char *);
  |       for (int i=0; i < 3; ++i)
  |          display_msg(mesg[i]);
  +       return 0;
  |     }
  |     int
  |     display_msg(char *m){
  |       ostringstream oss(ostringstream::out);
 20       switch (fork( )) {
  |       case 0:
  |         sleep(1);
  |         execlp("/bin/echo", "echo", m, (char *) NULL);
  |         oss << m << " exec failure";       // build error msg string
  +         perror(oss.str().c_str());
  |         return 1;
  |       case -1:
  |         perror("Fork failure");
  |         return 2;
 30       default:
  |         return 0;
  |       }
  |     }

Program 3.6 displays three messages (based on the contents of the array mesg). This action is accomplished by calling the display_msg function three times. Once in the display_msg function, the program forks a child process and then overlays the child process code with the program code for the echo command. The output of the program is shown in Figure 3.6.

Example 3.6. Output of Program 3.6.

linux$ p3.6
Foh
Fie
Fum

Due to scheduling, the order of the messages may change when run multiple times.

It is interesting to observe what happens if the execlp call in display_msg fails (line 23). If we purposely sabotage the execlp system call by changing it to

execlp("/bin/no_echo", "echo", m , (char *) NULL );

and assuming there is not an executable file called no_echo to be found in /bin, the output[3] of the program becomes that shown in Figure 3.7.

Example 3.7. Output of Program 3.6 when execlp fails.

linux$ p3.6
Foh exec failure: No such file or directory
Fie exec failure: No such file or directory
Fum exec failure: No such file or directory
Fum exec failure: No such file or directory
Foh exec failure: No such file or directory
Fum exec failure: No such file or directory
Fum exec failure: No such file or directory

Surprisingly, when the execlp call fails, we end up with a total of eight processes—the initial process and its seven children. Most likely this was not the intent of the original programmer. One way to correct this is within the display_msg function: In the case 0: branch of the switch statement, replace the return statement in line 26 with a call to exit.

Combining what we have learned so far, we can produce, in relatively few lines of code, a shell program that restricts the user to a few basic commands (in this example, ls, ps, and df). The code for our shell program[4] is shown in Program 3.7.

This program could be considered a very stripped-down version of a restricted[5] shell. The main thrust of the program is pedagogical, and improvements and expansions (of which there can be many) will be addressed in ensuing sections of the text and in a number of exercises.

Example 3.7. The huh shell.

File : p3.7.cxx
  |     /*
  |         A _very_ limited shell program
  |     */
  |     #include <iostream>
  +     #include <cstdio>
  |     #include <cstring>
  |     #include <cstdlib>
  |     #include <unistd.h>
  |     using namespace std;
 10
  |     const int MAX    =256;
  |     const int CMD_MAX=10;
  |     char *valid_cmds = " ls  ps  df ";
  |     int
  +     main( ){
  |       char  line_input[MAX], the_cmd[CMD_MAX];
  |       char  *new_args[CMD_MAX], *cp;
  |       int   i;
  |       while (1) {
 20         cout << "cmd> ";
  |         if (cin.getline(line_input, MAX, '
') != NULL) {
  |           cp = line_input;
  |           i  = 0;
  |           if ((new_args[i] = strtok(cp, " ")) != NULL) {
  +             strcpy(the_cmd, new_args[i]);
  |             strcat(the_cmd, " ");
  |             if ((strstr(valid_cmds, the_cmd)—valid_cmds) % 4 == 1) {
  |               do {
  |                 cp = NULL;
 30                 new_args[++i] = strtok(cp, " ");
  |               } while (i < CMD_MAX && new_args[i] != NULL);
  |               new_args[i] = NULL;
  |               switch (fork( )) {
  |               case 0:
  +                 execvp(new_args[0], new_args);
  |                 perror("exec failure");
  |                 exit(1);
  |               case -1:
  |                 perror("fork failure");
 40                 exit(2);
  |               default:
  |                 // In the parent we should be waiting for
  |                 // the child to finish
  |                 ;
  +               }
  |             } else
  |               cout << "huh?" << endl;
  |           }
  |         }
 50       }
  |     }

The commands the user is permitted to issue when running our shell are found in the global character string called valid_cmds. In the valid_cmds string, each two-letter command is preceded and followed by a space. By delimiting the commands in this manner, a predefined C string searching function strstr can be used to determine if a user has entered a valid command. While this technique is simplistic, it is effective when a limited number of commands need to be checked. The program then issues a shell-like prompt, cmd>, and uses the C++ input function getline to store user input in a character array buffer called line_input. The getline function will read a line of input, including intervening whitespace that is terminated by a newline. If the getline function fails (such as when the user just presses return), the program loops back around and reprompts the user for additional input. Upon entry of input, the program uses the C string function strtok to obtain the first valid token from the line_input array. The strtok function, which will divide a referenced character string into tokens, requires a pointer to the array it is to parse and a list of delimiting characters that delimit tokens (in this case only a blank “ ” has been indicated). The strtok function is a wonderful example of the idiosyncratic nature of some functions in C/C++. When strtok is called successive times and passed a reference to NULL, it will continue to parse the initial input line starting each time where it left off previously. The strcat function is used to add a trailing blank to this first token (assumed to the command), and the resulting sequence is stored in a character array called the_cmd.

The next line of the program checks for the presence of the command in the valid_cmds string at a modulus-4-based offset (see Figure 3.8).

Character offsets in the valid_cmds string.

Figure 3.8. Character offsets in the valid_cmds string.

If the command is found, a do-while loop is used to obtain the remaining tokens (up to the limit CMD_MAX). These tokens are stored in successive elements of the previously declared new_args array. Upon exiting the loop, we assure that the last element of the new_args array contains the requisite NULL value. A switch statement, in concert with fork and execvp system calls, is used to execute the command.

Ending a Process

Eventually all things must come to an end. Now that we have generated processes, we should take a closer look at how to end a process. Under its own power (assuming the process does not receive a terminating signal and the system has not crashed) a process normally terminates in one of three ways.[6] In order of preference, these are

  1. It issues (at any point in its code) a call to either exit or _exit.

  2. It issues a return in the function main.

  3. It falls off the end of the function main ending implicitly.

Programmers routinely make use of the library function exit to terminate programs. This function, which does not return a value, is defined as shown in Table 3.6.

Table 3.6. Summary of the exit Library Function.

Include File(s)

<stdlib.h>

Manual Section

3

Summary

void exit(int status);

Return

Success

Failure

Sets errno

Does not return

No return

 

In earlier versions of C the inclusion of a specific header file was not required when using exit. More recent versions of C (and C++) require the inclusion of the file <stdlib.h> (or <cstdlib> if going the full ANSI-C++ route) that contains the exit function prototype. The exit function accepts a single parameter, an integer status value that will be returned to the parent process.[7] By convention, a 0 value is returned if the program has terminated normally; other wise, a nonzero value is returned.[8] For those who wish to standardize the value returned by exit when terminating, the header file <stdlib.h> contains two defined constants, EXIT_SUCCESS and EXIT_FAILURE, which can be used to indicate program success and failure respectively. If we somehow are able to slip by the compiler a call to exit without passing an exit status value (i.e., exit( );) or issue a return; in main without specifying a value, then what is returned to the parent process is technically undefined.

Upon invocation, the exit function performs several actions. Figure 3.9 shows the relationship of the actions taken.

Actions taken by library function exit.

Figure 3.9. Actions taken by library function exit.

First, exit will call, in reverse order, all functions that have been registered using the atexit library function. The atexit function is relatively new. Some older BSD-based versions of C (as well as some version of GNU) supported a library function called on_exit that offered a similar functionality. As future support for on_exit looks to be a bit sketchy; it might be best to stay clear of it. The atexit function should provide similar functionality.

A brief description of the atexit function is in order. The definition of atexit, shown in Table 3.7, indicates that functions to be called (when the process terminates normally)[9] are registered by passing the atexit function the address of the function. The registered functions should not have any parameters. If atexit is successful in registering the function, atexit returns a 0; otherwise, it returns a −1 but will not set errno.[10]

Program 3.8 demonstrates the use of atexit.

When run, the output of the program shows that the registered functions are called in inverse order (Figure 3.10).

In older versions of C, once all atexit functions were called, the standard I/O library function _cleanup would be called. Newer versions of GNU C/C++ do not support the _cleanup function. Now when all atexit functions have been processed, exit calls the system call _exit (passing on to it the value of status). Programmers may call _exit directly if they wish to circumvent the invocation of atexit registered functions and the flushing of I/O buffers. See Table 3.8.

Table 3.7. Summary of the atexit Library Function.

Include File(s)

<stdlib.h>

Manual Section

3

Summary

int atexit(void (*function)(void));

Return

Success

Failure

Sets errno

0

−1

No

Example 3.8. Using the atexit library function.

File : p3.8.cxx
  |     #include <iostream>
  |     #include <cstdlib>
  |     using namespace std;
  |     int
  +     main( ){
  |       void       f1( ), f2( ), f3( );
  |       atexit(f1);
  |       atexit(f2);
  |       atexit(f3);
 10       cout << "Getting ready to exit" << endl;
  |       exit(0);
  |     }
  |     void
  |     f1( ){
  +       cout << "Doing F1" << endl;
  |     }
  |     void
  |     f2( ){
  |       cout << "Doing F2" << endl;
 20     }
  |     void
  |     f3( ){
  |       cout << "Doing F3" << endl;
  |     }

Example 3.10. Output of Program 3.8.

linux$ p3.8
Getting ready to exit
Doing F3
Doing F2
Doing F1

Table 3.8. Summary of the _exit System Call.

Include File(s)

<unistd.h>

Manual Section

2

Summary

void _exit(int status);

Return

Success

Failure

Sets errno

Does not return

Does not return

 

The _exit system call, like its relative, exit, does not return. This call also accepts an integer status value, which will be made available to the parent process. When terminating a process, the system performs a number of housekeeping operations:

  • All open file descriptors are closed.

  • The parent of the process is notified (via a SIGCHLD signal) that the process is terminating.

  • Status information is returned to the parent process (if it is waiting for it). If the parent process is not waiting, the system stores the status information until a wait by the parent process is affected.

  • All child processes of the terminating process have their parent process ID (PPID) set to 1—they are inherited by init.

  • If the process was a group leader, process group members will be sent SIGHUP/ SIGCONT signals.

  • Shared memory segments and semaphore references are readjusted.

  • If the process was running accounting, the accounting record is written out to the accounting file.

Waiting on Processes

 

They also serve who only stand and wait.

 
 --John Milton 1608–1674 On His Blindness [1652]

More often than not, a parent process needs to synchronize its actions by waiting until a child process has either stopped or terminated its actions. The wait system call allows the parent process to suspend its activity until one of these actions has occurred (Table 3.9).

Table 3.9. Summary of the wait System Call.

Include File(s)

<sys/types.h>
<sys/wait.h>

Manual Section

2

Summary

pid_t wait(int *status);

Return

Success

Failure

Sets errno

Child process ID or 0

−1

Yes

The activities of wait are summarized in Figure 3.11.

Summary of wait activities.

Figure 3.11. Summary of wait activities.

The wait system call accepts a single argument, which is a pointer to an integer, and returns a value defined as type pid_t. Data type pid_t is found in the header file <sys/types.h> and is most commonly a long int. If the calling process does not have any child processes associated with it, wait will return immediately with a value of −1 and errno will be set to ECHILD (10). However, if any child processes are still active, the calling process will block (suspend its activity) until a child process terminates. When a waited-for child process terminates, the status information for the child and its process ID (PID) are returned to the parent. The status information is stored as an integer value at the location referenced by the pointer status. The low-order 16 bits of the location contain the actual status information, and the high-order bits (assuming a 32-bit machine) are set to zero. The low-order bit information can be further subdivided into a low- and high-order byte. This information is interpreted in one of two ways:

  1. If the child process terminated normally, the low-order byte will be 0 and the high-order byte will contain the exit code (0–255):

    byte 3

    byte 2

    byte 1

    byte 0

      

    exit code

    0

  2. If the child process terminated due to an uncaught signal, the low-order byte will contain the signal number and the high-order byte will be 0:

    byte 3

    byte 2

    byte 1

    byte 0

      

    0

    signal #

In this second situation, if a core file has been produced, the leftmost bit of byte 0 will be a 1. If a NULL argument is specified for wait, the child status information is not returned to the parent process, the parent is only notified of the child's termination.

Here are two programs, a parent (Program 3.9) and child (Program 3.10), that demonstrate the use of wait.

Example 3.9. The parent process.

File : p3.9.cxx
  |     /*
  |         A parent process that waits for a child to finish
  |      */
  |     #include <iostream>
  +     #include <cstdlib>
  |     #include <iomanip>
  |     #include <unistd.h>
  |     #include <sys/types.h>
  |     #include <sys/wait.h>
 10     using namespace std;
  |     int
  |     main(int argc, char *argv[] ){
  |       pid_t  pid, w;
  |       int    status;
  +       if ( argc < 4 ) {
  |         cerr << "Usage " << *argv << " value_1 value_2 value_3
";
  |         return 1;
  |       }
  |       for (int i = 1; i < 4; ++i)          // generate 3 child processes
 20         if ((pid = fork( )) == 0)
  |           execl("./child", "child", argv[i], (char *) 0);
  |         else                               // assuming no failures here
  |           cout << "Forked child " << pid << endl;
  |     /*
  +          Wait for the children
  |      */
  |       while ((w=wait(&status)) && w != -1)
  |           cout << "Wait on PID: " << dec <<  w << "  returns status of  "
  |                << setw(4) << setfill('0') << hex
 30                << setiosflags(ios::uppercase) << status << endl;
  |       return 0;
  |     }

The parent program forks three child processes. Each child process is overlaid with the executable code for the child (found in Program 3.10). The parent process passes to each child, from the parent's command line, a numeric value. As each child process is produced, the parent process displays the child process ID. After all three processes have been generated; the parent process initiates a loop to wait for the child processes to finish their execution. As each child process terminates, the value returned to the parent process is displayed.

Example 3.10. The child process.

File : p3.10.cxx
  |     /*
  |        The child process
  |     */
  |     #define _GNU_SOURCE
  +     #include <iostream>
  |     #include <cstdlib>
  |     #include <iomanip>
  |     #include <sys/types.h>
  |     #include <unistd.h>
 10     #include <signal.h>
  |     using namespace std;
  |     int
  |     main(int argc, char *argv[ ]){
  |       pid_t     pid = getpid( );
  +       int       ret_value;
  |       srand((unsigned) pid);
  |       ret_value = int(rand( ) % 256);      // generate a return value
  |       sleep(rand( ) % 3);                  // sleep a bit
  |       if (atoi(*(argv + 1)) % 2) {         // assuming argv[1] exists!
 20         cout << "Child " << pid << " is terminating with signal 0009" << endl;
  |         kill(pid, 9);                      // commit hara-kiri
  |       } else {
  |         cout << "Child " << pid << " is terminating with   exit("
  |              << setw(4) << setfill('0') << setiosflags(ios::uppercase)
  +              << hex  << ret_value << ")" << endl;
  |         exit(ret_value);
  |       }
  |     }

In the child program, the child process obtains its own PID using the getpid call. The PID value is used as a seed value to initialize the srand function. A call to rand is used to generate a unique value to be returned when the process exits. The child process then sleeps a random number of seconds (0–3). After sleeping, if the argument passed to the child process on the command line is odd (i.e., not evenly divisible by 2), the child process kills itself by sending a signal 9 (SIGKILL) to its own PID. If the argument on the command line is even, the child process exits normally, returning the previously calculated return value. In both cases, the child process displays a message indicating what it will do before it actually executes the statements.

The source programs are compiled and the executables named parent and child respectively. They are run by calling the parent program. Two sample output sequences are shown in Figure 3.12.

Example 3.12. Two runs of Programs 3.9 and 3.10.

linux$ parent 2 1 2        <-- 1
Forked child 8975
Forked child 8976
Child 8976 is terminating with signal 0009
Forked child 8977
Wait on PID: 8976  returns status of  0009
Child 8977 is terminating with   exit(008F)
Wait on PID: 8977  returns status of  8F00
Child 8975 is terminating with   exit(0062)
Wait on PID: 8975  returns status of  6200

linux$ parent 2 2 1        <-- 2
Forked child 8980
Forked child 8981
Forked child 8982
Child 8982 is terminating with signal 0009
Wait on PID: 8982  returns status of  0009
Child 8980 is terminating with   exit(00B0)
Wait on PID: 8980  returns status of  B000
Child 8981 is terminating with   exit(00D3)
Wait on PID: 8981  returns status of  D300
  • (1)Two even values and one odd

  • (2)Two even values and one odd but in a different order.

There are several things of interest to note in this output. In the first output sequence, one child processes (PID 8976) has terminated before the parent has finished its process generation. Processes that have terminated but have not been waited upon by their parent process are called zombie processes. Zombie processes occupy a slot in the process table, consume no other system resources, and will be marked with the letter Z when a process status command is issued (e.g., ps -alx or ps -el). A zombie process cannot be killed[11] even with the standard Teflon bullet (e.g., at a system level: kill -9 process_id_number). Zombies are put to rest when their parent process performs a wait to obtain their process status information. When this occurs, any remaining system resources allocated for the process are recovered by the kernel. Should the child process become an orphan before its parent issues the wait, the process will be inherited by init, which, by design, will issue a wait for the process. On some very rare occasions, even this will not cause the zombie process to “die.” In these cases, a system reboot may be needed to clear the process table of the entry.

Both sets of output clearly show that when the child process terminates normally, the exit value returned by the child is stored in the second byte of the integer value referenced by argument to the wait call in the parent process. Likewise, if the child terminates due to an uncaught signal, the signal value is stored in the first byte of the same referenced location. It is also apparent that wait will return with the information for the first child process that terminates, which may or may not be the first child process generated.

It is easy to see that the interpretation of the status information can be cumbersome, to say the least. At one time, programmers wrote their own macros to interrogate the contents of status. Now most use one of the predefined status macros. These macros are shown in Table 3.10.

Table 3.10. The wstat Macros.

Macro

Description

WIFEXITED(status)

Returns a true if the child process exited normally.

WEXITSTATUS(status)

Returns the exit code or return value from main. Should be called only if WIFEXITED(status)has returned a true.

WIFSIGNALED(status)

Returns a true if the child exited due to uncaught signal.

WTERMSIG(status)

Returns the signal that terminated the child. Should be called only if WIFSIGNALED(status) has returned a true.

WIFSTOPPED(status)

Returns a true if the child process is stopped.

WSTOPSIG(status)

Returns the signal that stopped the child. Should be called only if WIFSTOPPED(status)has returned a true.

The argument to each of these macros is the integer status value (not the pointer to the value) that is returned to the wait call. The macros are most often used in pairs. The WIF macros are used as a test for a given condition. If the condition is true, the second macro of the pair is used to return the specified value. As shown below, these macros could be incorporated in the wait loop in the parent Program 3.9 to obtain the child status information:

...
while ((w = wait(&status)) && w != -1)
  if (WIFEXITED(status))                           // test with macro
    cout << "Wait on PID: " << dec << w << " returns a value of "
         << hex << WEXITSTATUS(status) << endl;    // obtain value
  else if (WIFSIGNALED(status))                    // test with macro
    cout << "Wait on PID: " << dec << w << " returns a signal of "
         << hex << WTERMSIG(status) << endl;       // obtain value
...

While the wait system call is helpful, it does have some limitations. It will always return the status of the first child process that terminates or stops. Thus, if the status information returned by wait is not from the child process we want, the information may need to be stored on a temporary basis for possible future reference and additional calls to wait made. Another limitation of wait is that it will always block if status information is not available. Fortunately, another system call, waitpid, which is more flexible (and thus more complex), addresses these shortcomings. In most invocations, the waitpid call will block the calling process until one of the specified child processes changes state. The waitpid system call summary is shown in Table 3.11.

Table 3.11. Summary of the waitpid System Call.

Include File(s)

<sys/types.h>
<sys/wait.h>

Manual Section

2

Summary

pid_t waitpid(pid_t pid, int *status, int options);

Return

Success

Failure

Sets errno

Child PID or 0

−1

Yes

The first argument of the waitpid system call, pid, is used to stipulate the set of child process identification numbers that should be waited for (Table 3.12).

Table 3.12. Interpretation of pid Values by waitpid.

pid Value

Wait for

< -1

Any child process whose process group ID equals the absolute value of pid.

-1

Any child process—in a manner similar to wait.

0

Any child process whose process group ID equals the caller's process group ID.

> 0

The child process with this process ID.

The second argument, *status, as with the wait call, references an integer status location where the status information of the child process will be stored if the waitpid call is successful. This location can be examined directly or with the previously presented wstat macros.

The third argument, options, may be 0 (don't care), or it can be formed by a bitwise OR of one or more of the flags listed in Table 3.13 (these flags are usually defined in the <sys/wait.h> header file). The flags are applicable to the specified child process set discussed previously.

Table 3.13. Flag Values for waitpid.

FLAG Value

Specifies

WNOHANG

Return immediately if no child has exited—do not block if the status cannot be obtained; return a value of 0, not the PID.

WUNTRACED

Return immediately if child is blocked.

If the value given for pid is -1 and the option flag is set to 0, the waitpid and wait system call act in a similar fashion. If waitpid fails, it returns a value of –1 and sets errno to indicate the source of the error (Table 3.14).

Table 3.14. waitpid Error Messages.

#

Constant

perror Message

Explanation

4

EINTR

Interrupted system call

Signal was caught during the system call.

10

ECHILD

No child process

Process specified by pid does not exist, or child process has set action of SIGCHILD to be SIG_IGN (ignore signal).

22

EINVAL

Invalid argument

Invalid value for options.

85

ERESTART

Interrupted system call should be restarted

WNOHANG not specified, and unblocked signal or SIGCHILD was caught.

We can modify a few lines in our current version of the parent process (Program 3.9) to save the generated child PIDs in an array. This information can be used with the waitpid system call to coerce the parent process into displaying status information from child processes in the order of child process generation instead of their termination order. Program 3.11 shows how this can be done.

Example 3.11. A parent program using waitpid.

File : p3.11.cxx
  |     #include <iostream>
  |     #include <cstdlib>
  |     #include <iomanip>
  |     #include <unistd.h>
  +     #include <sys/types.h>
  |     #include <sys/wait.h>
  |     using namespace std;
  |     int
  |     main(int argc, char *argv[] ){
 10       pid_t  pid[3], w;
  |       int    status;
  |       if ( argc < 4 ) {
  |         cerr << "Usage " << *argv << " value_1 value_2 value_3
";
  |         return 1;
  +       }
  |       for (int i=1; i < 4; ++i)          // generate 3 child processes
  |         if ((pid[i-1] = fork( )) == 0)
  |           execl("./child", "child", argv[i], (char *) 0);
  |         else                               // assuming no failures here
 20           cout << "Forked child " << pid[i-1] << endl;
  |     /*
  |          Wait for the children
  |     */
  |       for (int i=0;(w=waitpid(pid[i], &status,0)) && w != -1; ++i){
  +         cout << "Wait on PID " << dec << w << " returns ";
  |         if (WIFEXITED(status))                        // test with macro
  |           cout << " a value of  " << setw(4) << setfill('0') << hex
  |                << setiosflags(ios::uppercase) << WEXITSTATUS(status) << endl;
  |         else if (WIFSIGNALED(status))                 // test with macro
 30           cout << " a signal of " << setw(4) << setfill('0') << hex
  |                << setiosflags(ios::uppercase) << WTERMSIG(status) << endl;
  |         else
  |           cout << " unexpectedly!" << endl;
  |       }
  +       return 0;
  |     }

A run of this program (using the same child process—Program 3.10) confirms that the status information returned to the parent is indeed ordered based on the sequence of child processes generation, not the order in which the processes terminated. Also, note that the status macros are used to evaluate the return from waitpid system call (Figure 3.13).

Example 3.13. Output of Program 3.11.

linux$ p3.11  2  2  1
Forked child 9772
Forked child 9773        <-- 1
Child 9773 is terminating with   exit(008B)        <-- 2
Forked child 9774
Child 9772 is terminating with   exit(00CD)
Wait on PID 9772 returns  a value of  00CD        <-- 3
Wait on PID 9773 returns  a value of  008B
Child 9774 is terminating with signal 0009
Wait on PID 9774 returns  a signal of 0009
  • (1)Order of creation: 9772, 9773, 9774

  • (2)Order of termination: 9773, 9772, 9774

  • (3)Order of wait: 9772, 9773, 9774

On some occasions, the information returned from wait or waitpid may be insufficient. Additional information on resource usage by a child process may be sought. There are two BSD compatibility library functions, wait3 and wait4,[12] that can be used to provide this information (Table 3.15).

Table 3.15. Summary of the wait3/wait4 Library Functions.

Include File(s)

#define _USE_BSD
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/wait.h>

Manual Section

3

Summary

pid_t wait3(int *status, int options,
            struct rusage *rusage);
pid_t wait4(pid_t pid, int *status,
            int options, struct rusage *rusage);

Return

Success

Failure

Sets errno

Child PID or 0

−1

Yes

The wait3 and wait4 functions parallel the wait and waitpid functions respectively. The wait3 function waits for the first child process to terminate or stop. The wait4 function waits for the specified PID (pid). In addition, should the pid value passed to the wait4 function be set to 0, wait4 will wait on the first child process in a manner similar to wait3. Both functions accept option flags to indicate whether or not they should block and/or report on stopped child processes. These option flags are shown in Table 3.16.

Table 3.16. Option Flag Values for wait3/wait4.

FLAG Value

Specifies

WNOHANG

Return immediately if no child has exited—do not block if the status cannot be obtained; return a value of 0 not the PID.

WUNTRACED

Return immediately if child is blocked.

Both functions contain an argument that is a reference to a rusage structure. This structure is defined in the header file <sys/resource.h>.[13]

struct rusage {
                 struct timeval ru_utime; /* user time used */
                 struct timeval ru_stime; /* system time used */
                 long ru_maxrss;          /* maximum resident set size */
                 long ru_ixrss;           /* integral shared memory size */
                 long ru_idrss;           /* integral unshared data size */
                 long ru_isrss;           /* integral unshared stack size */
                 long ru_minflt;          /* page reclaims */
                 long ru_majflt;          /* page faults */
                 long ru_nswap;           /* swaps */
                 long ru_inblock;         /* block input operations */
                 long ru_oublock;         /* block output operations */
                 long ru_msgsnd;          /* messages sent */
                 long ru_msgrcv;          /* messages received */
                 long ru_nsignals;        /* signals received */
                 long ru_nvcsw;           /* voluntary context switches */
                 long ru_nivcsw;          /* involuntary context switches */
            };

If the rusage argument is non-null, the system populates the rusage structure with the current information from the specified child process. See the getrusage system call in Section 2 of the manual pages for additional information. The status macros (see previous section on wait and waitpid) can be used with the status information returned by wait3 and wait4. See Table 3.17.

Table 3.17. wait3/wait4 Error Messages.

#

Constant

perror Message

Explanation

4

EINTR

Interrupted system call

Signal was caught during the system call.

10

ECHILD

No child process

Process specified by pid does not exist, or child process has set action of SIGCHILD to be SIG_IGN (ignore signal).

22

EINVAL

Invalid argument

Invalid value for options.

85

ERESTART

Interrupted system call should be restarted

WNOHANG not specified, and unblocked signal or SIGCHILD was caught.

Summary

Processes are generated by the fork system call. The process that issues the fork system call is known as the parent, and the new process as the child. Child processes may have their executable code overlaid with other executable code via an exec system call. When a process finishes executing its code, performs a return in the function main, or makes an exit system call, the process terminates. Parent processes may wait for their child processes to terminate. Terminating child processes return status information that can be examined by the parent process.

Key Terms and Concepts

_exit system call

atexit system call

daemon

exec

execl library function

execle function call

execlp library function

execv library function

execve system call

execvp library function

exit code

exit library function

flush

rand library function

restricted shell

rusage structure

srand library function

status information

strstr library function

strtok library function

wait system call

wait3 library function

wait4 library function

waitpid system call

WEXITSTATUS macro

WIFEXITED macro

WIFSIGNALED macro

WIFSTOPPED macro

wstat macros

WSTOPSIG macro

WTERMSIG macro

zombie



[1] In some versions of UNIX, such as Solaris, all the exec calls are system calls and are grouped together as library functions and discussed in one section of the manual. Linux has a more historic approach to things.

[2] If the executable file is a script, the Bourne shell (/bin/sh) is invoked to execute the script. The shell is then passed the specified argument information.

[3] The program uses a common programming trick to create a message string on-the-fly to pass to the perror routine.

[4] For reasons that become obvious when the program is run, this is nicknamed the huh shell.

[5] Many UNIX environments come with a predefined restricted shell (which is different from the remote shell /bin/rsh). A restricted shell is sometimes specified as a login shell for users (such as ftp) that require a more controlled environment. Linux does not come with a specific restricted shell for users, but some of the standard shells (such as bash and ksh) can be passed a command-line option (–r) that will run the shell in restricted mode. Linux does come with a restricted shell for sendmail (smrsh).

[6] Of course, the library function abort can also be used to end a process, but its call will result in an abnormal termination of the process.

[7] I know, I know—what if the parent is no longer around? Remember that init inherits processes whose parents are gone. The handling of status values is discussed further in Section 3.6.

[8] Only the low-order eight bits are returned, thus values range from 0 to 255. (Hmm, I wonder ... would exit(-1) actually return a 255?)

[9] A normal termination is considered a call to exit or a return in main. On our system, atexit registered functions will be called even if the program ends implicitly (without a return in main).

[10] This is one of the rare cases where no explanation of errno values is provided by system designers.

[11] This miraculous ability is the source of the name zombie.

[12] It is not clear if these functions will be supported in subsequent versions of the GNU compiler, and they may limit the portability of programs that incorporate them. As these are BSD-based functions, _USE_BSD must be defined in the program code or defined on the command line when the source code is compiled.

[13] On some systems, you may need the header file <sys/rusage.h> instead of <sys/resource.h>, and you may need to explicitly link in the BSD library that contains the object code for the wait3/wait4 functions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.103.74