Add Book to My BookshelfPurchase This Book Online

Chapter 11 - Processes

UNIX Systems Programming for SVR4
David A. Curry
 Copyright © 1996 O'Reilly & Associates, Inc.

Advanced Program Execution
In this section, we will examine the procedures used to create new processes, execute other programs, and retrieve processes' termination statuses. All three of these procedures are used in the construction of the system function, described above; at the end of this section we will show how system can be written.
Creating a New Process
The first step in executing a program is to create a new process. The function to do this is called fork:
    #include <sys/types.h>
    #include <unistd.h>
UNIX Systems Programming for SVR4Chapter 11 - Processes
Add Book to My BookshelfPurchase This Book Online

Chapter 11 - Processes

UNIX Systems Programming for SVR4
David A. Curry
 Copyright © 1996 O'Reilly & Associates, Inc.

Advanced Program Execution
In this section, we will examine the procedures used to create new processes, execute other programs, and retrieve processes' termination statuses. All three of these procedures are used in the construction of the system function, described above; at the end of this section we will show how system can be written.
Creating a New Process
The first step in executing a program is to create a new process. The function to do this is called fork:
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
The fork function creates an exact copy of the calling process. This means that the child process inherits a number of characteristics from the parent process:
 •The real user ID, real group ID, effective user ID, and effective group ID of the parent process
 •The set-user-id and set-group-id mode bits of the parent process
 •The supplementary group ID list of the parent process
 •The saved user ID and saved group ID of the parent process
 •All of the parent process' environment variables (see Chapter 16, Miscellaneous Routines)
 •All of the parent process' open file descriptors and file offsets
 •Any file descriptor close-on-exec flags (see Chapter 6, Special-Purpose File Operations) set by the parent process
 •The file mode creation mask (umask) of the parent process
 •Any signal handling dispositions (SIG_DFL, SIG_IGN, SIG_HOLD, or a handler function address) set by the parent process
 •The session ID and process group ID of the parent process
 •The parent process' controlling terminal
 •The parent process' nice value (see above)
 •The current working directory of the parent process
 •The parent process' resource limits
The child process will differ from the parent process in the following ways:
 •The child process will have a unique process ID.
 •The child process will have a different parent process ID.
 •The child process will have its own copy of the parent's open file descriptors. It may close these file descriptors without affecting the parent. However, the parent and child will share the file offset for each descriptor; this means that if they both write to the file at the same time, the output will be intermixed. Likewise, if they both read from the file, they will each receive only part of the data.
 •The child process will not have any of the file locks its parent may have created.
 •The set of pending signals for the child process is initialized to the empty set.
The fork function is interesting in that it returns twice—once in the parent, and once in the child. In the parent process, fork returns the process ID of the child process (it returns -1 if a child process could not be created). In the child process, however, fork returns 0. In this way, the parent and child can distinguish themselves from one another.
As soon as fork returns, there are two nearly identical copies of the program running. There is no guarantee that the child will run before the parent or vice-versa; this must be taken into account to avoid a deadlock condition in which each process is waiting on the other to do something.
Example 11-2 is a program that creates a child process. The child process writes out the lowercase letters in alphabetical order ten times; the parent process writes out the uppercase letters in alphabetical order ten times. Notice that running the program multiple times may not produce the same output each time; this is because two processes are performing the task, and the order in which they execute is dependent on the system scheduler, how many other processes are running on the system, and other parameters outside of the program's control.
Example 11-2:  fork
#include <sys/types.h>
#include <unistd.h>
int
main(void)
{
    int i;
    char c;
    pid_t pid;
    /*
     * Create a child process.
     */
    if ((pid = fork()) < 0) {
        perror("fork");
< UNIX Systems Programming for SVR4Chapter 11 - Processes
Add Book to My BookshelfPurchase This Book Online

Chapter 11 - Processes

UNIX Systems Programming for SVR4
David A. Curry
 Copyright © 1996 O'Reilly & Associates, Inc.

Advanced Program Execution
In this section, we will examine the procedures used to create new processes, execute other programs, and retrieve processes' termination statuses. All three of these procedures are used in the construction of the system function, described above; at the end of this section we will show how system can be written.
Creating a New Process
The first step in executing a program is to create a new process. The function to do this is called fork:
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
The fork function creates an exact copy of the calling process. This means that the child process inherits a number of characteristics from the parent process:
 •The real user ID, real group ID, effective user ID, and effective group ID of the parent process
 •The set-user-id and set-group-id mode bits of the parent process
 •The supplementary group ID list of the parent process
 •The saved user ID and saved group ID of the parent process
 •All of the parent process' environment variables (see Chapter 16, Miscellaneous Routines)
 •All of the parent process' open file descriptors and file offsets
 •Any file descriptor close-on-exec flags (see Chapter 6, Special-Purpose File Operations) set by the parent process
 •The file mode creation mask (umask) of the parent process
 •Any signal handling dispositions (SIG_DFL, SIG_IGN, SIG_HOLD, or a handler function address) set by the parent process
 •The session ID and process group ID of the parent process
 •The parent process' controlling terminal
 •The parent process' nice value (see above)
 •The current working directory of the parent process
 •The parent process' resource limits
The child process will differ from the parent process in the following ways:
 •The child process will have a unique process ID.
 •The child process will have a different parent process ID.
 •The child process will have its own copy of the parent's open file descriptors. It may close these file descriptors without affecting the parent. However, the parent and child will share the file offset for each descriptor; this means that if they both write to the file at the same time, the output will be intermixed. Likewise, if they both read from the file, they will each receive only part of the data.
 •The child process will not have any of the file locks its parent may have created.
 •The set of pending signals for the child process is initialized to the empty set.
The fork function is interesting in that it returns twice—once in the parent, and once in the child. In the parent process, fork returns the process ID of the child process (it returns -1 if a child process could not be created). In the child process, however, fork returns 0. In this way, the parent and child can distinguish themselves from one another.
As soon as fork returns, there are two nearly identical copies of the program running. There is no guarantee that the child will run before the parent or vice-versa; this must be taken into account to avoid a deadlock condition in which each process is waiting on the other to do something.
Example 11-2 is a program that creates a child process. The child process writes out the lowercase letters in alphabetical order ten times; the parent process writes out the uppercase letters in alphabetical order ten times. Notice that running the program multiple times may not produce the same output each time; this is because two processes are performing the task, and the order in which they execute is dependent on the system scheduler, how many other processes are running on the system, and other parameters outside of the program's control.
Example 11-2:  fork
#include <sys/types.h>
#include <unistd.h>
int
main(void)
{
    int i;
    char c;
    pid_t pid;
    /*
     * Create a child process.
     */
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        /*
         * This code executes in the child process
         * (fork returned zero).
         */
        for (i=0; i < 10; i++) {
            for (c = 'a'; c <= 'z'; c++)
                write(1, &c, 1);
        }
    }
    else {
        /*
         * This code executes in the parent process.
         */
        for (i=0; i < 10; i++) {
            for (c = 'A'; c <= 'Z'; c++)
                write(1, &c, 1);
        }
    }
    /*
     * This code executes in both processes (i.e.,
     * it gets executed twice).
     */
    write(1, "\n", 1);
    exit(0);
}
% fork
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnABCDEFG
HIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB
CDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVW
XYZABCDEFopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu
vwxyzabcdefghijklmnopqGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDE
FGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
% fork
abcdefghijklmnopqrABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABC
DEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX
YZABCDEFGHIJKLMNstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw
xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr
stuvwxyzabcOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCdefghijklmnopqrstuvwx
yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkDEFGHIJK
LMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ
Executing a Program
The second step in executing a program is to bring the program into memory and begin executing its instructions. To do this, use any of several routines, all generically referred to as the exec functions:
    #include <unistd.h>
    int execl(const char *path, const char *arg0, ..., const char *argn,
            char * /*NULL*/);
    int execv(const char *path, const char *argv[]);
    int execle(const char *path, const char *arg0, ..., const char *argn,
            char * /*NULL*/, const char *envp[]);
    int execve(const char *path, const char *argv[], const char *envp[]);
    int execlp(const char *file, const char *arg0, ..., const char *argn,
            char * /*NULL*/);
    int execvp(const char *file, const char *argv[], const char *envp[]);
In all its forms, exec overlays the image of the calling process with the image of a new program. The new process image is constructed from an ordinary executable file, either an object file as produced by a compiler, or a file of data for an interpreter, such as the shell. If exec succeeds, it never returns, because the calling process is overlaid by the new process image (and thus no longer exists).
On most modern UNIX systems, shell scripts and other files of interpreted commands may begin with a line of the form
    #!pathname [argument]
Where pathname is the full pathname to the interpreter, and argument is an optional argument. For example, “#!/bin/sh” is common in shell scripts. When one of these files is the target of an exec, the interpreter is invoked with its zeroth argument equal to pathname, and if present, its first argument equal to argument. The remaining arguments to the interpreter are the arguments specified in the call to exec. Most UNIX systems limit the length of this line to about 32 characters.
When an object file is executed, it is called as follows:
    int main(int argc, char *argv[], char *envp[]);
where argc is the argument count, argv is an array of character pointers to the arguments themselves, and envp is an array of character pointers to the environment strings (see Chapter 16, Miscellaneous Routines). The argc parameter is always at least 1, and the first element of argv points to the name of the executable file.
The execl and execle functions execute the file named by the pathname in path, with the strings pointed to by arg0 through argn as arguments. The argument following argn should be a null pointer, to indicate the end of the argument list. By convention, arg0 should always be present; it will become the name of the process as displayed by the ps command. Usually, arg0 is given as the pathname of the executable file, or the last component of the pathname. A program executed by execl will inherit the calling process' environment strings; execle allows the calling process to provide a new set of environment strings in envp.
The execv and execve functions execute the file named by the pathname in path, with the strings pointed to by the array of pointers in argv as arguments. By convention, argv should always contain at least one member, which will become the name of the process as displayed by the ps command. Usually, argv[0] is given as the pathname of the executable file, or the last component of the pathname. A program executed by execv will inherit the calling process' environment strings; execve allows the calling process to provide a new set of environment strings in envp.
The execlp and execvp functions are identical to execl and execv, except that instead of requiring a path name to the executable file, you supply only the file's name. These functions then search the directories in the calling process' search path (as defined by the PATH environment variable), looking for an executable file of the same name. The first such file encountered is then executed. If the target file is not an object file or executable interpreter script as described above, the contents of the file are used as input to the Bourne shell (/bin/sh).
An exec causes the new process to inherit the open file descriptors of the calling process, except those with the close-on-exec flag set (see Chapter 6, Special-Purpose File Operations). For those file descriptors that remain open, the file offset is unchanged. Signals that are being caught by the calling process are reset to their default dispositions in the new process; all other signal dispositions remain the same. If a call to exec fails, it returns -1 and places the reason for failure in errno.
Example 11-3 shows a program that creates a child process, after which both the child and parent processes execute other commands.
Example 11-3:  forkexec
#include <sys/types.h>
#include <unistd.h>
int
main(void)
{
    pid_t pid;
    char *args[4];
    /*
     * Create a child process.
     */
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        /*
         * This code executes in the child process
         * (fork returned zero).
         */
        execl("/bin/echo", "echo", "Today's", "date", "is:", 0);
        /*
         * If the exec succeeds, we'll never get here.
         */
        perror("exec");
        exit(1);
    }
    /*
     * This code executes in the parent process.
     */
    args[0] = "date";
    args[1] = "+%A, %B %d, %Y";
    args[2] = NULL;
    execv("/bin/date", args);
    /*
     * If the exec succeeds, we'll never get here.
     */
    perror("exec");
    exit(1);
}
    % forkexec
    Today's date is:
    Wednesday, November 30, 1994
    % forkexec
    Wednesday, November 30, 1994
    Today's date is:
Note that this program suffers from the same plight that our last example did—because there is no guarantee that the child process will execute before the parent process, the output can come out in the wrong order (you may have to run the program several times to see this behavior). To get around this, we could place a call to sleep in the parent right before the call to exec. However, if we use a small sleep value, there is no guarantee, on a heavily loaded system, that the child will get to execute in that amount of time. But if we use anything much larger than one or two seconds, the program will have an uncomfortable delay between printing “Today's date is:” and actually printing the date. In the next section, we will see how to solve this problem.
Collecting the Process Termination Status
The last step in executing a program is to wait for it to complete, and collect the termination status of the process. This is an optional step; if it is not performed, the child process will become a zombie while the parent process still exists, and if the parent process exits, the child process will be inherited by init.
The basic function used to wait for a child process to complete, and retrieve its termination status, is called wait:
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
The wait function suspends the calling process until one of its immediate child processes terminates. (It also returns if a child process that is being traced is stopped due to the receipt of a signal, but that is beyond the scope of this book.) The termination status of the child process is stored in the integer pointed to by status. If the calling process does not care about the termination status, and is only interested in waiting until the child process terminates, status may be given as the null pointer. If a child process has terminated prior to the call to wait, wait returns immediately with the status for that process. The process ID of the process that terminated is returned by wait; if there are no unwaited-for child processes, wait returns -1.
The following macros, defined in the include file sys/wait.h, assist in decoding the termination status returned by wait. Each of them takes a single argument, the integer containing the termination status.
WIFEXITED
Evaluates to a non-zero value if the process terminated normally.
WEXITSTATUS
Evalutes to the exit code the process passed to exit or returned from main if WIFEXITED evaluates to a non-zero value (indicating normal termination).
WIFSIGNALED
Evaluates to a non-zero value if the process terminated due to the receipt of a signal.
WTERMSIG
Evaluates to the number of the signal that caused the process to terminate if WIFSIGNALED evaluates to a non-zero value (indicating termination due to a signal).
WIFSTOPPED
Evaluates to a non-zero value if the process is currently stopped.
WSTOPSIG
Evalutes to the number of the signal that caused the process to stop if WIFSTOPPED evaluates to a non-zero value (indicating the process is stopped).
WIFCONTINUED
Evaluates to a non-zero value if the process has been continued from a stopped state. This macro is not defined in HP-UX 10.x.
WCOREDUMP
Evaluates to a non-zero value if a core image of the process was created and if WIFSIGNALED evaluates to a non-zero value (indicating termination due to a signal).
Example 11-4 shows how to modify the program from Example 11-3 so that it always prints things in the right order. The only difference is the addition of the call to wait in the parent.
Example 11-4:  forkexecwait
#include <sys/types.h>
#include <unistd.h>
int
main(void)
{
    pid_t pid;
    char *args[4];
    /*
     * Create a child process.
     */
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        /*
         * This code executes in the child process
         * (fork returned zero).
         */
        execl("/bin/echo", "echo", "Today's", "date", "is:", 0);
        /*
         * If the exec succeeds, we'll never get here.
         */
        perror("exec");
        exit(1);
    }
    /*
     * Wait for the child process to complete.  We
     * don't care about the termination status.
     */
    while (wait((int *) 0) != pid)
        continue;
    /*
     * This code executes in the parent process.
     */
    args[0] = "date";
    args[1] = "+%A, %B %d, %Y";
    args[2] = NULL;
    execv("/bin/date", args);
    /*
     * If the exec succeeds, we'll never get here.
     */
    perror("exec");
    exit(1);
}
    % forkexecwait
    Today's date is:
    Wednesday, November 30, 1994
    % forkexecwait
    Today's date is:
    Wednesday, November 30, 1994
Two variants of the wait function provide additional functionality:
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *status, int options);
    pid_t waitid(idtype_t idtype, id_t id, singinfo_t *info,
            int options);
The waitpid function is specified by the POSIX standard. It allows the programmer greater control over waiting for processes, by assigning several meanings to the values in the pid argument:
 •If pid is equal to -1, the status is requested for any child process (in this case, waitpid is equivalent to wait).
 •If pid is greater than 0, the status is requested for the process whose process ID is equal to pid. The process identified by pid must be a child of the calling process.
 •If pid is 0, the status is requested for any process in the same process group as the calling process.
 •If pid is less than -1, the status is requested for any process whose process group ID is equal to the absolute value of pid. The processes in that process group must be children of the calling process.
The waitid function, which is not specified by the POSIX standard, allows the list of processes to be waited for to be specified in much the same way as for the sigsend and sigsendset functions described in the last chapter. The idtype and id parameters specify which processes waitid should wait for:
 •If idtype is P_PID, waitid waits for the child with process ID id.
 •If idtype is P_PGID, waitid waits for any child process with process group ID id.
 •If idtype is P_ALL, waitid waits for any child process, and id is ignored.
The waitid function is not available in HP-UX 10.x.
Both waitpid and waitid use the options parameter to allow the programmer to specify the state changes that are of interest. The value of the options parameter is constructed from the logical or of the following values:
WCONTINUED
Returns the status of any specified process that has continued, and whose status has not been reported since it continued (waitid only).
WEXITED
Wait for processes to exit (waitid only).
WNOHANG
Do not cause the calling process to block. If no status is immediately available, -1 is returned with errno set to ECHILD. This allows a process to poll for status information periodically while otherwise performing other tasks.
WNOWAIT
Keep the process whose status is returned in a waitable state. The process may be waited for again with identical results. This option is not available in IRIX 5.x.
WSTOPPED
Wait for and return the status of any process that has been stopped due to a signal (waitid only).
WTRAPPED
Wait for traced processes to become trapped or reach a breakpoint (waitid only).
WUNTRACED
Report the status of any specified child processes that are stopped, and whose status has not yet been reported since they stopped (waitpid only).
If we put all three of these steps together, we can construct a function much like system. Example 11-5 shows our function, called shellcmd, and also demonstrates the use of the macros described above.
Example 11-5:  shellcmd
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
int     shellcmd(char *);
void    prstat(int);
int
main(void)
{
    int status;
    char command[BUFSIZ];
    /*
     * Forever...
     */
    for (;;) {
        /*
         * Prompt for a command.
         */
        printf("Enter a command: ");
        /*
         * Read a command.  If NULL is returned, the
         * user typed CTRL-D, so exit.
         */
        if (fgets(command, sizeof(command), stdin) == NULL) {
            putchar('\n');
            exit(0);
        }
        /*
         * Strip off the trailing newline character
         * left by fgets.
         */
        command[strlen(command)-1] = '\0';
        /*
         * Execute the command and print the termination
         * status.
         */
        status = shellcmd(command);
        prstat(status);
        putchar('\n');
    }
}
/*
* shellcmd - start a child process, and pass command to the shell.
*/
int
shellcmd(char *command)
{
    int status;
    pid_t p, pid;
    extern int errno;
    sigset_t mask, savemask;
    struct sigaction ignore, saveint, savequit;
    /*
     * Set up a sigaction structure to ignore signals.
     */
    sigemptyset(&ignore.sa_mask);
    ignore.sa_handler = SIG_IGN;
    ignore.sa_flags = 0;
    /*
     * Ignore keyboard signals; save old dispositions.
     */
    sigaction(SIGINT, &ignore, &saveint);
    sigaction(SIGQUIT, &ignore, &savequit);
    /*
     * Block SIGCHLD.
     */
    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);
    sigprocmask(SIG_BLOCK, &mask, &savemask);
    /*
     * Start a child process.
     */
    if ((pid = fork()) < 0)
        status = -1;
    /*
     * This code executes in the child process.
     */
    if (pid == 0) {
        /*
         * Restore signals to their original dispositions,
         * and restore the signal mask.
         */
        sigaction(SIGINT, &saveint, (struct sigaction *) 0);
        sigaction(SIGQUIT, &savequit, (struct sigaction *) 0);
        sigprocmask(SIG_SETMASK, &savemask, (sigset_t *) 0);
        /*
         * Execute a shell with the command as argument.
         */
        execl("/bin/sh", "sh", "-c", command, 0);
        _exit(127);
    }
    /*
     * Wait for the child process to finish.
     */
    while (waitpid(pid, &status, 0) < 0) {
        /*
         * EINTR (interrupted system call) is okay; otherwise,
         * we got some error that we need to report back.
         */
        if (errno != EINTR) {
            status = -1;
            break;
        }
    }
    /*
     * Restore signals to their original dispositions,
     * and restore the signal mask.
     */
    sigaction(SIGINT, &saveint, (struct sigaction *) 0);
    sigaction(SIGQUIT, &savequit, (struct sigaction *) 0);
    sigprocmask(SIG_SETMASK, &savemask, (sigset_t *) 0);
    /*
     * Return the child process' termination status.
     */
    return(status);
}
/*
* prstat - decode the termination status.
*/
void
prstat(int status)
{
    if (WIFEXITED(status)) {
        printf("Process terminated normally, exit status = %d.\n",
               WEXITSTATUS(status));
    }
    else if (WIFSIGNALED(status)) {
        printf("Process terminated abnormally, signal = %d (%s)",
               WTERMSIG(status), strsignal(WTERMSIG(status)));
        if (WCOREDUMP(status))
            printf(" -- core file generated.\n");
        else
            printf(".\n");
    }
    else if (WIFSTOPPED(status)) {
        printf("Process stopped, signal = %d (%s).\n",
               WSTOPSIG(status), strsignal(WSTOPSIG(status)));
    }
    else if (WIFCONTINUED(status)) {
        printf("Process continued.\n");
    }
}
We then run the program.
    % shellcmd
    Enter a command: date
    Wed Nov 30 17:15:24 EST 1994
    Process terminated normally, exit status = 0.
    Enter a command: date | grep Wed
    Wed Nov 30 17:15:42 EST 1994
    Process terminated normally, exit status = 0.
    Enter a command: date | grep Thu
    Process terminated normally, exit status = 1.
    Enter a command: sleep 5
    ^CProcess terminated normally, exit status = 130.
    Enter a command: sleep 5
    ^\Quit - core dumped
    Process terminated normally, exit status = 131.
    Enter a command: exec sleep 5
    ^CProcess terminated abnormally, signal = 2 (Interrupt).
    Enter a command: exec sleep 5
    ^\Process terminated abnormally, signal = 3 (Quit)--core file generated.
    Enter a command: ^D
First, we execute the command date, which terminates normally with an exit status of 0. Then, we execute the date command and send the output into grep, searching for the string “Wed.” The grep command finds the string, prints the line on which it occurs, and exits with status 0, indicating a match was found. In the third case, we repeat this experiment, but search for the string “Thu.” This time, grep exits with status 1, meaning no matches were found.
The next two runs demonstrate what happens when we press the interrupt (CTRL-C) and quit (CTRL-\) keys on the keyboard. We would expect the command to terminate abnormally, and we should learn what signal terminated it. But this doesn't happen. Instead, we find out that the command terminated normally ! The problem here is that our shellcmd function is using the Bourne shell to execute our command, rather than executing it directly. The shell is waiting for our command to complete, catching the fact that it terminated abnormally (that's where the “Quit—core dumped” message comes from), and then the shell is exiting normally. But the shell indicates in its exit status that the command terminated abnormally, and with what signal it terminated, by adding the signal's number to a base value of 128.
In the last two runs, we accomplish what we wanted to do in the previous two. All UNIX shells have a built-in command called exec that tells them to execute the following command without starting a child process. This overlays the shell with the new command, and when the new command exits, the shell is just gone. By using the exec command here, we can eliminate the shell's checking of our command's termination status, allowing us to obtain it directly.
Now let's look at the program itself, specifically the shellcmd function.
The first thing the function does is set the disposition of the two keyboard interrupt signals, SIGINT and SIGQUIT to be ignored. Recall that the keyboard-generated signals are delivered to all foreground processes—that means that both the child process (which we meant to interrupt) and the parent process (which we didn't mean to interrupt) will receive the signal. As an experiment, try commenting out the first two calls to sigaction and see what happens when you press CTRL-C or CTRL-\.
The next thing shellcmd does is set up a signal mask to block SIGCHLD. This is not really necessary in our example here, but it is necessary in the real system function. If system did not block SIGCHLD from delivery, and the calling process was catching SIGCHLD for its own purposes, its signal handler would be called when the child process started by system terminates. But since the parent process is presumably catching SIGCHLD because it is interested in processes it started itself, it might get confused if it received the signal for a process that system started instead.
After setting up the signal handling, shellcmd creates a child process with fork. The first thing the child process does is restore the two keyboard signals to their original dispositions (we want them to interrupt the child process), and reset the signal mask to its original value. We reset the signal mask so that if the command we execute needs SIGCHLD, it will be available. Then the child process executes the shell, passing the command string as an argument. The last thing we do in the child is call _exit; if the exec succeeds, this will never happen. But if the exec fails, the child process still needs to exit, or the parent will block indefinitely waiting for it to terminate. We call _exit instead of exit so that we don't call any exit handlers that may have been registered with atexit.
While the child process is doing all that, the parent is patiently sitting in the call to waitpid, waiting until the child process is done. The advantage to using waitpid here is that we are guaranteed that we will only receive the termination status of the process we started ourselves. If we used wait instead, we might receive the status of some process started by our caller; this would then make that status unavailable to the caller when it tries to get it later. If our call to waitpid is interrupted by a signal, we continue to wait. Finally, we restore our signal dispositions to their original values, restore the signal mask, and then return the child process' termination status.
The vfork Function
Most versions of UNIX that implement virtual memory also provide a function called vfork. This function creates a child process but, unlike fork, does not copy the entire address space of the calling process. Rather, the child process executes using the parent's address space, and thus the parent's memory and thread of control.
The purpose of vfork is to provide a more efficient method of creating a child process when the purpose is to execute another program via exec. Since the call to exec will overwrite the calling process' address space anyway, there is little point in copying everything first. Needless to say, great havoc can result if vfork is used to create a process that does not immediately call exec.
The need for vfork has diminished as more recent versions of UNIX implement copy-on-write in fork. That is, the address space of the parent is not copied for the child unless and until the child tries to modify that address space. The use of vfork in new programs is discouraged since it is non-standard, but it may crop up from time to time when porting older software.
The vfork function is not available in IRIX 5.x.

Previous SectionNext Section
Books24x7.com, Inc © 2000 –  Feedback