Chapter 11. Project: Cracking Shells

By developing a shell from the bottom up, this chapter explores the intricacies of process creation, termination, identification and the correct handling of signals. Example programs handle foreground and background processes, pipelines, process groups, sessions and controlling terminals. The chapter also looks at job control and terminal I/O. The closing project integrates these concepts by incorporating job control into a shell.

Building a Simple Shell

A shell is a process that does command-line interpretation. In other words, a shell reads a command line from standard input and executes the command corresponding to the input line. In the simplest case, the shell reads a command and forks a child to execute the command. The parent then waits for the child to complete before reading in another command. A real shell handles process pipelines and redirection, as well as foreground process groups, background process groups and signals.

This section starts with the simplest of shells. Later sections add features piece by piece. The shells use the makeargv function of Program 2.2 on page 37 to parse the command-line arguments. Section 11.2 adds redirection, and Section 11.3 adds pipelines. Section 11.4 explains how a shell handles signals for a foreground process. The programs for each of these phases are given, along with a series of exercises that point out the important issues. Work through these exercises before going on to the main part of the project. The heart of this project is signal handling and job control. Section 11.5 introduces the machinery needed for job control. Section 11.6 describes how background processes are handled without job control, and Section 11.7 introduces job control at the user level. Finally, Section 11.8 specifies the implementation of a complete shell with job control.

Program 11.1 shows Version 1 of ush (ultrasimple shell). The shell process forks a child that builds an argv type array and calls execvp to execute commands entered from standard input.

Example 11.1. ush1.c

Version 1 of ush has no error checking or prompts.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_BUFFER 256
#define QUIT_STRING "q"

int makeargv(const char *s, const char *delimiters, char ***argvp);

int main (void) {
    char **chargv;
    char inbuf[MAX_BUFFER];

    for( ; ; ) {
        gets(inbuf);
        if (strcmp(inbuf, QUIT_STRING) == 0)
            return 0;
        if ((fork() == 0) && (makeargv(inbuf, " ", &chargv) > 0))
            execvp(chargv[0], chargv);
        wait(NULL);
    }
}

Example 11.1. 

Run Program 11.1 with a variety of commands such as ls, grep and sort. Does ush1 behave as expected?

Answer:

No. Program 11.1 does not display a prompt or expand filenames containing wildcards such as * and ?. The ush1 shell also does not handle quotation marks in the same way as standard shells do. A normal shell allows quotation marks to guarantee that a particular argument is passed to the exec in its entirety and is not interpreted by the shell as something else. You may also notice that certain commands such as cd do not behave in the expected way.

Example 11.2. 

What happens if Program 11.1 doesn’t call wait?

Answer:

If a user enters a command before the previous one completes, the commands execute concurrently.

Another problem is that Version 1 of ush does not trap errors on execvp. This omission has some interesting consequences if you enter an invalid command. When execvp succeeds, control never comes back from the child. However, when it fails, the child falls through and tries to get a command line too!

Example 11.3. 

Run Program 11.1 with several invalid commands. Execute ps and observe the number of shells that are running. Try to quit. What happens?

Answer:

Each time you enter an invalid command, ush1 creates a new process that behaves like an additional shell. You must enter q once for each process.

Example 11.4. 

Only the child parses the command line in Program 11.1. What happens if the parent parses the command line before forking? What are the memory allocation and deallocation issues involved in moving the makeargv call before fork in these programs?

Answer:

When the child exits, all memory allocated by the child is freed. If the parent calls makeargv before fork, the shell has to later free the memory allocated by makeargv.

Version 1 of ush is susceptible to buffer overflows because it uses gets rather than fgets. A long command can exceed the space allocated for input. Program 11.2 shows an improved version of ush that prompts for user input and handles an unsuccessful execvp call. The system-defined constant MAX_CANON replaces the user-defined MAX_BUFFER, and fgets replaces gets.

The shell in Program 11.2 does not exit if there is an error on fork. In general, the shell should be impervious to errors—and bullet-proofing takes a lot of effort. The function executecmd replaces the makeargv and execvp calls. Control should never return from this function.

Example 11.2. ush2.c

Version 2 of ush handles simple command lines.

#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define PROMPT_STRING "ush2>>"
#define QUIT_STRING "q"

void executecmd(char *incmd);

int main (void) {
    pid_t childpid;
    char inbuf[MAX_CANON];
    int len;

    for( ; ; ) {
        if (fputs(PROMPT_STRING, stdout) == EOF)
            continue;
        if (fgets(inbuf, MAX_CANON, stdin) == NULL)
            continue;
        len = strlen(inbuf);
        if (inbuf[len - 1] == '
')
            inbuf[len - 1] = 0;
        if (strcmp(inbuf, QUIT_STRING) == 0)
            break;
        if ((childpid = fork()) == -1)
            perror("Failed to fork child");
        else if (childpid == 0) {
            executecmd(inbuf);
            return 1;
        } else
            wait(NULL);
    }
    return 0;
}

Program 11.3 shows a simple version of executecmd for Program 11.2. We will augment this function as we improve the shell. The executecmdsimple.c version simply constructs an argument array and calls execvp.

Example 11.3. executecmdsimple.c

A simplified version of executecmd for Program 11.2.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BLANK_STRING " "

int makeargv(const char *s, const char *delimiters, char ***argvp);

void executecmd(char *incmd) {
    char **chargv;
    if (makeargv(incmd, BLANK_STRING, &chargv) <= 0) {
        fprintf(stderr, "Failed to parse command line
");
        exit(1);
    }
    execvp(chargv[0], chargv);
    perror("Failed to execute command");
    exit(1);
}

Example 11.5. 

Why does Program 11.3 treat a makeargv return value of 0 as an error?

Answer:

The makeargv returns the number of items in the command argument array. Technically, an empty command is not an error, and a real shell would ignore it without printing a warning message. For more complicated command lines that include redirection and pipelines, an empty command portion is considered to be an error. You may want to consider adding additional checks and not count it as an error in some circumstances.

Example 11.6. 

Try the cd command as input to Program 11.2. What happens? Why? Hint: Read the man page on cd for an explanation.

Answer:

The cd command changes the user’s environment, so it must be internal to the shell. External commands are executed by children of the shell process, and a process cannot change the environment of its parent. Most shells implement cd as an internal command or a built-in command.

Example 11.7. 

What happens when Program 11.2 encounters commands such as ls -l and q with leading and interspersed extra blanks?

Answer:

Program 11.2 correctly handles commands such as ls -l because makeargv handles leading and interspersed blanks. The q command does not work because this command is handled directly by ush2, which has no provision for handling interspersed blanks.

Example 11.8. 

Execute the command stty -a under your regular shell and record the current settings of the terminal control characters. The following is a possible example of what might appear.

intr = ^c; quit = ^|; erase = ^?; kill = ^u;
eof = ^d; eol = <undef>; eol2 = <undef>; swtch = <undef>;
start = ^q; stop = ^s; susp = ^z; dsusp = ^y;
rprnt = ^r; flush = ^o; werase = ^w; lnext = ^v;

Try each of the control characters under ush2 and under a regular shell and compare the results.

In Exercise 11.8 the erase and werase continue to work even though there is no explicit code to handle them in ush2 because ush2 does not receive characters directly from the keyboard. Instead, the terminal device driver processes input from the keyboard and passes the input through additional modules to the program. As described in Section 6.5.1, terminals can operate in either canonical (line-buffered) or noncanonical mode. Canonical mode is the default.

In canonical mode, the terminal device driver returns one line of input at a time. Thus, a program does not receive any input until the user enters a newline character, even if the program just reads in a single character. The terminal device driver also does some processing of the line while the line is being gathered. If the terminal line driver encounters the erase or werase characters, it adjusts the input buffer appropriately.

Noncanonical mode allows flexibility in the handling of I/O. For example, an editing application might display the message "entering cbreak mode" to report that it is entering noncanonical mode with echo disabled and one-character-at-a-time input. In noncanonical mode, input is made available to the program after a user-specified number of characters have been entered or after a specified time has elapsed. The canonical mode editing features are not available. Programs such as editors usually operate with the terminal in noncanonical mode, whereas user programs generally operate with the terminal in canonical mode.

Redirection

POSIX handles I/O in a device-independent way through file descriptors. After obtaining an open file descriptor through a call such as open or pipe, the program can execute read or write, using the handle returned from the call. Redirection allows a program to reassign a handle that has been opened for one file to designate another file. (See Section 4.7 for a review of redirection.)

Most shells allow redirection of standard input, standard output and possibly standard error from the command line. Filters are programs that read from standard input and write to standard output. Redirection on the command line allows filters to operate on other files without recompilation.

Example 11.9. 

The following cat command redirects its standard input to my.input and its standard output to my.output.

cat < my.input > my.output

Recall that open file descriptors are inherited on exec calls (unless specifically prevented). For shells this means that the child must redirect its I/O before calling execvp. (After the execvp, the process no longer has access to the variables holding the destination descriptors.)

Program 11.4 shows a version of executecmd that redirects standard input and standard output as designated by the input command line incmd. It calls parseandredirectin and parseandredirectout, which are shown in Program 11.5.

Example 11.4. executecmdredirect.c

A version of executecmd that handles redirection.

#include <errno.h>
#include <stdio.h>
#include <unistd.h>

int makeargv(const char *s, const char *delimiters, char ***argvp);
int parseandredirectin(char *s);
int parseandredirectout(char *s);

void executecmd(char *incmd) {
    char **chargv;
    if (parseandredirectout(incmd) == -1)
        perror("Failed to redirect output");
    else if (parseandredirectin(incmd) == -1)
        perror("Failed to redirect input");
    else if (makeargv(incmd, " 	", &chargv) <= 0)
        fprintf(stderr, "Failed to parse command line
");
    else {
        execvp(chargv[0], chargv);
        perror("Failed to execute command");
    }
    exit(1);
}

The parseandredirectin function looks for the standard input redirection symbol <. If the symbol is found, the program replaces it with a string terminator. This removes it from the command. The program then uses strtok to remove leading and trailing blanks and tabs. What is left is the name of the file to use for redirection. The parseandredirectout function works similarly.

Since the version of executecmd in Program 11.4 calls parseandredirectout before parseandredirectin, it assumes that the output redirection appears on the command line after the input redirection.

Example 11.10. 

How does Program 11.2 handle the following command? How would you fix it?

sort > t.2 < t.1

Answer:

After the call to parseandredirectout, the > is replaced by a string terminator so the command is just sort. The redirection of standard input is ignored. One way to fix this problem is to use strchr to find the positions of both redirection symbols before handling redirection. If both symbols are present, the redirection corresponding to the one that appears last should be done first.

Link ush2 with executecmdredirect and parseandredirect to obtain a shell that handles simple redirection.

Example 11.11. 

How would ush2 handle redirection from an invalid file?

Answer:

If parseandredirectin or parseandredirectout fails to open the file, the function returns –1 and executecmdredirect does not attempt to execute the command.

Pipelines

Pipelines, introduced in Section 6.2, connect filters in an assembly line to perform more complicated functions.

Example 11.12. 

The following command redirects the output of ls -l to the standard input of sort and the standard output of sort to the file temp.

ls -l | sort -n +4 > temp

The ls and the sort commands are distinct processes connected in a pipeline. The connection does not imply that the processes share file descriptors, but rather that the shell creates an intervening pipe to act as a buffer between them.

Example 11.5. parseandredirect.c

Functions to handle redirection of standard input and standard output. These functions must be called in a particular order. The redirection that occurs last must be handled first.

#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#define FFLAG (O_WRONLY | O_CREAT | O_TRUNC)
#define FMODE (S_IRUSR | S_IWUSR)

int parseandredirectin(char *cmd) {    /* redirect standard input if '<' */
    int error;
    int infd;
    char *infile;

    if ((infile = strchr(cmd, '<')) == NULL)
        return 0;
    *infile = 0;                  /* take everything after '<' out of cmd */
    infile = strtok(infile + 1, " 	");
    if (infile == NULL)
        return 0;
    if ((infd = open(infile, O_RDONLY)) == -1)
        return -1;
    if (dup2(infd, STDIN_FILENO) == -1) {
        error = errno;                       /* make sure errno is correct */
        close(infd);
        errno = error;
        return -1;
    }
    return close(infd);
}

int parseandredirectout(char *cmd) {  /* redirect standard output if '>' */
    int error;
    int outfd;
    char *outfile;

    if ((outfile = strchr(cmd, '>')) == NULL)
        return 0;
    *outfile = 0;                  /* take everything after '>' out of cmd */
    outfile = strtok(outfile + 1, " 	");
    if (outfile == NULL)
        return 0;
    if ((outfd = open(outfile, FFLAG, FMODE)) == -1)
        return -1;
    if (dup2(outfd, STDOUT_FILENO) == -1) {
        error = errno;                        /* make sure errno is correct */
        close(outfd);
        errno = error;
        return -1;
    }
    return close(outfd);
}

Program 11.6 contains a version of executecmd that handles a pipeline of arbitrary length. The implementation uses makeargv with the pipeline symbol as a delimiter to make an array of commands for the pipeline. For each command (except the last), executecmd creates a pipe and a child process. The executecmd redirects the standard output of each command, except the last through a pipe, to the standard input of the next one. The parent redirects its standard output to the pipe and executes the command by calling executeredirect of Program 11.7. The child redirects its standard input to come from the pipe and goes back to the loop to create a child to handle the next command in the pipeline. For the last command in the list, executecmd does not create a child or pipe but directly calls executeredirect.

Errors need to be handled very carefully. Program 11.6 creates multiple child processes. This version of executecmd never returns. An error in any of the processes results in a call to perror_exit, which prints an appropriate message to standard error and exits.

The executeredirect function takes three parameters: the command string and two flags. If the first flag is nonzero, executeredirect allows standard input to be redirected. If the second flag is nonzero, executeredirect allows standard output to be redirected. The pipeline can redirect standard input only for the first command in the pipeline and can redirect standard output only for the last one.

The executecmd function only sets the first flag parameter of executeredirect for the call with i equals 0. The executecmd only sets the second flag after the last loop iteration completes. If the pipeline contains only one command (no pipeline symbol on the command line), executecmd does not execute the loop body and calls executeredirect with both flags set. In this case, executeredirect behaves similarly to the executecmd in executecmdredirect (Program 11.4).

The first if in executeredirect handles the case of the output redirection occurring before the input redirection, as discussed in Exercise 11.10.

Example 11.13. 

What would this shell do with the following command.

ls -l > temp1 | sort -n +4 > temp

Answer:

The redirection of standard output to temp1 would be ignored. The shell would treat > and temp1 as names of files to list. Most real shells would detect this as an error.

Example 11.14. 

How are the processes in the following pipeline related when they are executed by executecmdpipe?

ls -l | sort -n +4 | more

Answer:

The first command, ls -l, is a child of the shell. The second command, sort -n +4, is a child of ls. The third command, more, is a child of sort.

Example 11.6. executecmdpipe.c

The executecmd function that handles pipelines.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void executeredirect(char *s, int in, int out);
int makeargv(const char *s, const char *delimiters, char ***argvp);

static void perror_exit(char *s) {
    perror(s);
    exit(1);
}

void executecmd(char *cmds) {
    int child;
    int count;
    int fds[2];
    int i;
    char **pipelist;

    count = makeargv(cmds, "|", &pipelist);
    if (count <= 0) {
        fprintf(stderr, "Failed to find any commands
");
        exit(1);
    }
    for (i = 0; i < count - 1; i++) {              /* handle all but last one */
        if (pipe(fds) == -1)
            perror_exit("Failed to create pipes");
        else if ((child = fork()) == -1)
            perror_exit("Failed to create process to run command");
        else if (child) {                                       /* parent code */
            if (dup2(fds[1], STDOUT_FILENO) == -1)
                perror_exit("Failed to connect pipeline");
            if (close(fds[0]) || close(fds[1]))
                perror_exit("Failed to close needed files");
            executeredirect(pipelist[i], i==0, 0);
            exit(1);
        }
        if (dup2(fds[0], STDIN_FILENO) == -1)                    /* child code */
            perror_exit("Failed to connect last component");
        if (close(fds[0]) || close(fds[1]))
            perror_exit("Failed to do final close");
    }
    executeredirect(pipelist[i], i==0, 1);             /* handle the last one */
    exit(1);
}

Example 11.7. executeredirect.c

A function to handle a single command with possible redirection.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int makeargv(const char *s, const char *delimiters, char ***argvp);
int parseandredirectin(char *s);
int parseandredirectout(char *s);

void executeredirect(char *s, int in, int out) {
    char **chargv;
    char *pin;
    char *pout;

    if (in && ((pin = strchr(s, '<')) != NULL) &&
          out && ((pout = strchr(s, '>')) != NULL) && (pin > pout) ) {
        if (parseandredirectin(s) == -1) { /* redirect input is last on line */
            perror("Failed to redirect input");
            return;
        }
        in = 0;
    }
    if (out && (parseandredirectout(s) == -1))
        perror("Failed to redirect output");
    else if (in && (parseandredirectin(s) == -1))
        perror("Failed to redirect input");
    else if (makeargv(s, " 	", &chargv) <= 0)
        fprintf(stderr,"Failed to parse command line
");
    else {
        execvp(chargv[0], chargv);
        perror("Failed to execute command");
    }
    exit(1);
}

Signal Handling in the Foreground

Most shells support job control that allows users to terminate running processes and move processes between the foreground and the background. The ordinary user may not be explicitly aware that signals control these actions.

Suppose a user enters Ctrl-C to terminate a running process. The terminal device driver buffers and interprets characters as they are typed from the keyboard. If the driver encounters the intr character (usually Ctrl-C), it sends a SIGINT signal. In normal shell operation, Ctrl-C causes the executing command to be terminated but does not cause the shell to exit.

Example 11.8. ush3.c

A shell that does not exit on SIGINT or SIGQUIT.

#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define PROMPT_STRING "ush3>>"
#define QUIT_STRING "q"

void executecmd(char *incmd);
int signalsetup(struct sigaction *def, sigset_t *mask, void (*handler)(int));

int main (void) {
    sigset_t blockmask;
    pid_t childpid;
    struct sigaction defaction;
    char inbuf[MAX_CANON];
    int len;

    if (signalsetup(&defaction, &blockmask, SIG_IGN) == -1) {
        perror("Failed to set up shell signal handling");
        return 1;
    }
    if (sigprocmask(SIG_BLOCK, &blockmask, NULL) == -1) {
        perror("Failed to block signals");
        return 1;
    }

    for( ; ; ) {
        if (fputs(PROMPT_STRING, stdout) == EOF)
            continue;
        if (fgets(inbuf, MAX_CANON, stdin) == NULL)
            continue;
        len = strlen(inbuf);
        if (inbuf[len - 1] == '
')
            inbuf[len - 1] = 0;
        if (strcmp(inbuf, QUIT_STRING) == 0)
            break;
        if ((childpid = fork()) == -1) {
            perror("Failed to fork child to execute command");
        } else if (childpid == 0) {
            if ((sigaction(SIGINT, &defaction, NULL) == -1) ||
                  (sigaction(SIGQUIT, &defaction, NULL) == -1) ||
                  (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)) {
                  perror("Failed to set signal handling for command ");
                  return 1;
            }
            executecmd(inbuf);
            return 1;
        }
        wait(NULL);
    }
    return 0;
}

If a user enters Ctrl-C with ush2 in Program 11.2, the shell takes the default action, which is to terminate the shell. The shell should not exit under these circumstances. Program 11.8 shows a modification of ush2 that ignores SIGINT and SIGQUIT.

After setting up various signal handling structures by calling signalsetup, ush3 ignores and blocks SIGINT and SIGQUIT. The ush3 shell forks a child as before. The key implementation point here is that the child must restore the handlers for SIGINT and SIGQUIT to their defaults before executing the command. Program 11.9 shows the signalsetup function that initializes various signal structures to block SIGINT and SIGQUIT.

Example 11.9. signalsetup.c

A function for setting up signal structures for ush3.

#include <signal.h>
#include <stdio.h>

int signalsetup(struct sigaction *def, sigset_t *mask, void (*handler)(int)) {
    struct sigaction catch;

    catch.sa_handler = handler;  /* Set up signal structures  */
    def->sa_handler = SIG_DFL;
    catch.sa_flags = 0;
    def->sa_flags = 0;
    if ((sigemptyset(&(def->sa_mask)) == -1) ||
          (sigemptyset(&(catch.sa_mask)) == -1) ||
          (sigaddset(&(catch.sa_mask), SIGINT) == -1) ||
          (sigaddset(&(catch.sa_mask), SIGQUIT) == -1) ||
          (sigaction(SIGINT, &catch, NULL) == -1) ||
          (sigaction(SIGQUIT, &catch, NULL) == -1) ||
          (sigemptyset(mask) == -1) ||
          (sigaddset(mask, SIGINT) == -1) ||
          (sigaddset(mask, SIGQUIT) == -1))
        return -1;
    return 0;
}

Example 11.15. 

If a user enters Ctrl-C while ush3 in Program 11.8 is executing fgets, nothing appears until the return key is pressed. What happens if the user enters Ctrl-C in the middle of a command line?

Answer:

When the user enters Ctrl-C in the middle of a command line, some systems display the symbols ^C. All the characters on the line before entry of Ctrl-C are ignored because the terminal driver empties the input buffer when Ctrl-C is entered (canonical input mode). These characters still appear on the current input line because ush3 does not redisplay the prompt.

Example 11.16. 

The parent process of ush3 ignores and blocks SIGINT and SIGQUIT. The child unblocks these signals after resetting their handlers to the default. Why is this necessary?

Answer:

Suppose the parent does not block SIGINT and the operating system delivers a SIGINT signal before ush3 restores the SIGINT handler to the default. Since the ush3 child ignores SIGINT, the child continues to execute the command after the user enters Ctrl-C.

The ush3 implementation isn’t the final answer to correct shell signal handling. In fact, the shell should catch SIGINT rather than ignore it. Also, the parent in ush3 has SIGINT and SIGQUIT blocked at all times. In fact, the parent should have them unblocked and block them only during certain critical time periods. Remember that ignoring is different from blocking. Ignore a signal by setting the signal handler to be SIG_IGN, and block a signal by setting a flag in the signal mask. Blocked signals are not delivered to the process but are held for later delivery.

In ush4, the parent shell and the child command handle the SIGINT in different ways. The parent shell clears the input line and goes back to the prompt, which the shell accomplishes with calls to sigsetjmp and siglongjmp.

The strategy for the child is different. When the child is forked, it inherits the signal mask and has a copy of the signal handler from the parent. The child should not go to the prompt if a signal occurs. Instead, the child should take the default action, which is to exit. To accomplish this, the parent blocks the signal before the fork. The child then installs the default action before unblocking the signal. When the child executes execvp, the default action is automatically installed since execvp restores any signals being caught to have their default actions. The program cannot afford to wait until execvp automatically installs the default action. The reason is that the child needs to unblock the signal before it executes execvp and a signal may come in between unblocking the signal and the execvp.

The parent shell in Program 11.10 uses sigsetjmp, discussed in Section 8.7, to return to the prompt when it receives Ctrl-C. The sigsetjmp function stores the signal mask and current environment in a designated jump buffer. When the signal handler calls siglongjmp with that jump buffer, the environment is restored and control is transferred to the point of the sigsetjmp call. Program 11.10 sets the jumptoprompt point just above the shell prompt. When called directly, sigsetjmp returns 0. When called through siglongjmp, sigsetjmp returns a nonzero value. This distinction allows the shell to output a newline when a signal has occurred. The siglongjmp call pops the stack and restores the register values to those at the point from which the sigsetjmp was originally called.

In the shells discussed in this chapter we do not need to worry about function calls that are interrupted by a signal. No signal handler in any of these shells returns. Instead, the shells call siglongjmp, so no function has an opportunity to set errno to EINTR. Notice also that ush4 executes the command, even if it could not successfully block SIGINT and SIGQUIT.

Example 11.10. ush4.c

A shell that uses siglongjmp to handle Ctrl-C.

#include <limits.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define PROMPT_STRING "ush4>>"
#define QUIT_STRING "q"

void executecmd(char *incmd);
int signalsetup(struct sigaction *def, sigset_t *mask, void (*handler)(int));
static sigjmp_buf jumptoprompt;
static volatile sig_atomic_t okaytojump = 0;

/* ARGSUSED */
static void jumphd(int signalnum) {
    if (!okaytojump) return;
    okaytojump = 0;
    siglongjmp(jumptoprompt, 1);
}

int main (void) {
    sigset_t blockmask;
    pid_t childpid;
    struct sigaction defhandler;
    int len;
    char inbuf[MAX_CANON];

    if (signalsetup(&defhandler, &blockmask, jumphd) == -1) {
        perror("Failed to set up shell signal handling");
        return 1;
    }

    for( ; ; ) {
        if ((sigsetjmp(jumptoprompt, 1)) &&   /* if return from signal, 
 */
              (fputs("
", stdout) == EOF) )
            continue;
        wait(NULL);
        okaytojump = 1;
        if (fputs(PROMPT_STRING, stdout) == EOF)
            continue;
        if (fgets(inbuf, MAX_CANON, stdin) == NULL)
            continue;
        len = strlen(inbuf);
        if (inbuf[len - 1] == '
')
            inbuf[len - 1] = 0;
        if (strcmp(inbuf, QUIT_STRING) == 0)
            break;
        if (sigprocmask(SIG_BLOCK, &blockmask, NULL) == -1)
            perror("Failed to block signals");
        if ((childpid = fork()) == -1)
            perror("Failed to fork");
        else if (childpid == 0) {
            if ((sigaction(SIGINT, &defhandler, NULL) == -1) ||
                  (sigaction(SIGQUIT, &defhandler, NULL) == -1) ||
                  (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)) {
                perror("Failed to set signal handling for command ");
                return 1;
            }
            executecmd(inbuf);
            return 1;
        }
        if (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)
            perror("Failed to unblock signals");
    }
    return 0;
}

Compilers sometimes allocate local variables in registers for efficiency. It is important that variables that should not be changed when siglongjmp is executed are not stored in registers. Use the volatile qualifier from ISO C to suppress this type of assignment.

Program 11.10 uses the same signal handler for both SIGINT and SIGQUIT. Therefore, signalsetup sets the signals to block both of them when they are caught. It wasn’t necessary to block these signals in ush3, but it did not hurt to do so. The child of Program 11.10 installs the default action before unblocking the signal after fork. The parent shell only blocks SIGINT and SIGQUIT when it is creating a child to run the command.

Example 11.17. 

Why did we move wait in ush4 from the bottom of the loop to the top of the loop?

Answer:

If wait is at the bottom of the loop and you kill a child with Ctrl-C, the shell jumps back to the start of the loop without waiting for the child. When a new command is entered, the shell will wait for the child that was killed instead of waiting for the new command to complete.

Example 11.18. 

Why can’t we fix the problem described in Exercise 11.17 by restarting wait when errno is EINTR?

Answer:

When a function like wait is interrupted by the signal, it returns only when the signal handler returns. In this case, the signal handler is executing a siglongjmp, so wait does not return when the signal is caught.

Process Groups, Sessions and Controlling Terminals

The previous section implemented signal handling for ush with simple commands. Signal handling for pipelines and background processes requires additional machinery. Pipelines need process groups, and background processes need sessions and controlling terminals.

Process Groups

A process group is a collection of processes established for purposes such as signal delivery. Each process has a process group ID that identifies the process group to which it belongs. Both the kill command and the kill function treat a negative process ID value as a process group ID and send a signal to each member of the corresponding process group.

Example 11.19. 

The following command sends SIGINT to the process group 3245.

kill -INT -3245

In contrast, the following command sends SIGINT just to the process 3245.

kill -INT 3245

The process group leader is a process whose process ID has the same value as the process group ID. A process group persists as long as any process is in the group. Thus, a process group may not have a leader if the leader dies or joins another group.

A process can change its process group with setpgid. The setpgid function sets the process group ID of process pid to have process group ID pgid. It uses the process ID of the calling process if pid is 0. If pgid is 0, the process specified by pid becomes a group leader.

SYNOPSIS

  #include <unistd.h>

  int setpgid(pid_t pid, pid_t pgid);
                                         POSIX

The setpgid function returns 0 if successful. If unsuccessful, setpgid returns –1 and sets errno. The following table lists the mandatory errors for setpgid.

errno

cause

EACCES

pid corresponds to a child that has already called exec

EINVAL

pgid is negative or has an unsupported value

EPERM

pid is the process ID of a session leader, or pid is the process ID of a child process not in the caller’s session, or pgid does not match pid and there is no process with a process ID matching pgid in the caller’s session

ESRCH

pid does not match the caller’s process ID or that of any of its children

When a child is created with fork, it gets a new process ID but it inherits the process group ID of its parent. The parent can use setpgid to change the group ID of a child as long as the child has not yet called exec. A child process can also give itself a new process group ID by setting its process group ID equal to its process ID.

Example 11.20. 

The following code segment forks a child that calls executecmd. The child places itself in a new process group.

pid = fork();
if ((pid == 0) && (setpgid(getpid(), getpid()) != -1))) {
   executecmd(cmd);
   return 1;
}

Either or both of the calls to getpid could be replaced with 0.

Example 11.21. 

What can go wrong with the following alternative to the code of Example 11.20?

pid = fork();
if ((pid > 0) && (setpgid(pid, pid) == -1)) {
   perror("Failed to set child's process group");
else if (pid == 0) {
    executecmd(cmd);
    return 1;
}

Answer:

The alternative code has a race condition. If the child performs execvp in executecmd before the parent calls setpgid, the code fails.

The getpgrp function returns the process group ID of the caller.

SYNOPSIS

  #include <unistd.h>

  pid_t getpgrp(void);
                               POSIX

No errors are defined for getpgrp.

The POSIX:XSI Extension also defines a setpgrp function that is similar to setpgid. However, setpgrp allows greater flexibility than is required for job control and may present a security risk.

Sessions

To make signal delivery transparent, POSIX uses sessions and controlling terminals. A session is a collection of process groups established for job control purposes. The creator of a session is called the session leader. We identify sessions by the process IDs of their leaders. Every process belongs to a session, which it inherits from its parent.

Each session may have a controlling terminal associated with it. A shell uses the controlling terminal of its session to interact with the user. A particular controlling terminal is associated with exactly one session. A session may have several process groups, but at any given time only one of these process groups can receive input from and send output to the controlling terminal. The designated process group is called the foreground process group or the foreground job. The other process groups in the session are called background process groups or background jobs. The main purpose of job control is to change which process group is in the foreground. The background process groups are not affected by keyboard input from the controlling terminal of the session.

Use the ctermid function to obtain the name of the controlling terminal. The ctermid function returns a pointer to a string that corresponds to the pathname of the controlling terminal for the current process. This string may be in a statically generated area if s is a NULL pointer. If s is not NULL, it should point to a character array of at least L_ctermid bytes. The ctermid function copies a string representing the controlling terminal into that array.

SYNOPSIS

  #include <stdio.h>

  char *ctermid(char *s);
                               POSIX:CX

The ctermid function returns an empty string if it is unsuccessful.

Example 11.22. 

What happens if you enter Ctrl-C while executing the following command string in ush4?

ls -l | sort -n +4 | more

Answer:

The SIGINT signal is delivered to the three child processes executing the three filters as well as to the parent shell process because all of these processes are in the foreground process group. The parent catches SIGINT with jumphd; the three children take the default action and terminate.

Section 3.6 introduced background processes. The & character at the end of the command line designates a command or pipeline to be run as a background process group in most shells.

Example 11.23. 

What happens if you enter Ctrl-C while the following command is executing in the C shell?

ls -l | sort -n +4 | more &

Answer:

None of the processes in the pipeline receive the SIGINT signal, since the pipeline is in the background and has no connection to the controlling terminal.

A process can create a new session with itself as the leader by calling setsid. The setsid function also creates a new process group with the process group ID equal to the process ID of the caller. The calling process is the only one in the new process group and the new session. The session has no controlling terminal.

SYNOPSIS

  #include <unistd.h>

  pid_t setsid(void);
                                POSIX

If successful, setsid returns the new value of the process group ID. If unsuccessful, setsid returns (pid_t)–1 and sets errno. The setsid function sets errno to EPERM if the caller is already a process group leader.

A process can discover session IDs by calling getsid. The getsid function takes a process group ID parameter, pid, and returns the process group ID of the process that is the session leader of the process specified by pid. If 0, pid specifies the calling process.

SYNOPSIS

  #include <unistd.h>

  pid_t getsid(pid_t pid);
                                 POSIX:XSI

If successful, getsid returns a process group ID. If unsuccessful, getsid returns –1 and sets errno. The following table lists the mandatory errors for getsid.

errno

cause

EPERM

process specified by pid is not in the same session as the calling process and the implementation does not allow access to the process group ID of that session leader

ESRCH

no process corresponds to pid

Figure 11.1 shows a shell with several process groups. Each solid rectangle represents a process with its process ID, process group ID and the session ID. All of the processes have session ID 1357, the process ID and session ID of the shell. The process group ID is the same as the process ID of one of its members, the process group leader.

Five process groups for session 1357.

Figure 11.1. Five process groups for session 1357.

Example 11.24. 

The following sequence of commands might give rise to the process group structure of Figure 11.1.

ls -l | sort -n +4 | grep testfile > testfile.out &
grep process | sort > process.out &
du . > du.out &
cat /etc/passwd | grep users | sort | head > users.out &

Example 11.25. 

Write a short program called showid that takes one command-line argument. The showid program outputs to standard error a single line with its command-line argument, its process ID, parent process ID, process group ID and session ID. After the display, showid starts an infinite loop that does nothing. Execute the following commands to verify how your login shell handles process groups and sessions for pipelines.

showid 1 | showid 2 | showid 3

Which process in the pipeline is the process group leader? Is the shell in the same process group as the pipeline? Which processes in the pipeline are children of the shell and which are grandchildren? How does this change if the pipeline is started in the background?

Answer:

The results vary depending on the shell that is used. Some shells make all the processes children of the shell. Others have only the first or last process in the pipeline as a child of the shell and the rest are grandchildren. Either the first or the last process may be the process group leader. If a shell does not support job control, it is possible for the shell to be the process group leader of the pipeline unless the pipeline is started in the background.

Summary:

  • The shell is a session leader.

  • All processes created by the shell are in this session.

  • All processes created on a single command line are in the same process group.

  • If the shell supports job control or the command line is started in the background, a new process group is formed for these processes.

  • One of the process groups of the shell is the foreground process group and can interact with the controlling terminal.

Background Processes in ush

The main operational properties of a background process are that the shell does not wait for it to complete and that it is not terminated by a SIGINT sent from the keyboard. A background process appears to run independently of the terminal. This section explores handling of signals for background processes. A correctly working shell must prevent terminal-generated signals and input from being delivered to a background process and must handle the problem of having a child divorced from its controlling terminal.

Program 11.11 shows a modification of ush4 that allows a command to be executed in the background. An ampersand (&) at the end of a command line specifies that ush5 should run the command in the background. The program assumes that there is at most one & on the line and that, if present, it is at the end. The shell determines whether the command is to be executed in the background before forking the child, since both parent and child both must know this information. If the command is executed in the background, the child calls setpgid so that it is no longer in the foreground process group of its session. The parent shell does not wait for background children.

Example 11.11. ush5.c

A shell that attempts to handle background processes by changing their process groups.

#include <limits.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BACK_SYMBOL '&'
#define PROMPT_STRING "ush5>>"
#define QUIT_STRING "q"

void executecmd(char *incmd);
int signalsetup(struct sigaction *def, sigset_t *mask, void (*handler)(int));

static sigjmp_buf jumptoprompt;
static volatile sig_atomic_t okaytojump = 0;

/* ARGSUSED */
static void jumphd(int signalnum) {
    if (!okaytojump) return;
    okaytojump = 0;
    siglongjmp(jumptoprompt, 1);
}

int main (void) {
    char *backp;
    sigset_t blockmask;
    pid_t childpid;
    struct sigaction defhandler;
    int inbackground;
    char inbuf[MAX_CANON];
    int len;

    if (signalsetup(&defhandler, &blockmask, jumphd) == -1) {
        perror("Failed to set up shell signal handling");
        return 1;
    }

    for( ; ; ) {
        if ((sigsetjmp(jumptoprompt, 1)) &&   /* if return from signal, 
 */
              (fputs("
", stdout) == EOF) )
            continue;
        okaytojump = 1;
        printf("%d",(int)getpid());
        if (fputs(PROMPT_STRING, stdout) == EOF)
            continue;
        if (fgets(inbuf, MAX_CANON, stdin) == NULL)
            continue;
        len = strlen(inbuf);
        if (inbuf[len - 1] == '
')
            inbuf[len - 1] = 0;
        if (strcmp(inbuf, QUIT_STRING) == 0)
            break;
        if ((backp = strchr(inbuf, BACK_SYMBOL)) == NULL)
            inbackground = 0;
        else {
            inbackground = 1;
            *backp = 0;
        }
        if (sigprocmask(SIG_BLOCK, &blockmask, NULL) == -1)
            perror("Failed to block signals");
        if ((childpid = fork()) == -1)
            perror("Failed to fork");
        else if (childpid == 0) {
            if (inbackground && (setpgid(0, 0) == -1))
                return 1;
            if ((sigaction(SIGINT, &defhandler, NULL) == -1) ||
                  (sigaction(SIGQUIT, &defhandler, NULL) == -1) ||
                  (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)) {
                perror("Failed to set signal handling for command ");
                return 1;
            }
            executecmd(inbuf);
            return 1;
        }
        if (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)
            perror("Failed to unblock signals");
        if (!inbackground)    /* only wait for child not in background */
            wait(NULL);
    }
    return 0;
}

Example 11.26. 

Execute the command ls & several times under ush5. Then, execute ps -a (still under this shell). Observe that the previous ls processes still appear as <defunct>. Exit from the shell and execute ps -a again. Explain the status of these processes before and after the shell exits.

Answer:

Since no process has waited for them, the background processes become zombie processes. They stay in this state until the shell exits. At that time, init becomes the parent of these processes, and since init periodically waits for its children, the zombies eventually die.

The shell in Program 11.12 fixes the problem of zombie or defunct processes. When a command is to be run in the background, the shell does an extra call to fork. The first child exits immediately, leaving the background process as an orphan that can then be adopted by init. The shell now waits for all children, including background processes, since the background children exit immediately and the grandchildren are adopted by init.

Example 11.12. ush6.c

A shell that cleans up zombie background processes.

#include <errno.h>
#include <limits.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BACK_SYMBOL '&'
#define PROMPT_STRING ">>"
#define QUIT_STRING "q"

void executecmd(char *incmd);
int signalsetup(struct sigaction *def, struct sigaction *catch,
                sigset_t *mask, void (*handler)(int));

static sigjmp_buf jumptoprompt;
static volatile sig_atomic_t okaytojump = 0;

/* ARGSUSED */
static void jumphd(int signalnum) {
    if (!okaytojump) return;
    okaytojump = 0;
    siglongjmp(jumptoprompt, 1);
}

int main (void) {
    char *backp;
    sigset_t blockmask;
    pid_t childpid;
    struct sigaction defhandler, handler;
    int inbackground;
    char inbuf[MAX_CANON+1];

    if (signalsetup(&defhandler, &handler, &blockmask, jumphd) == -1) {
        perror("Failed to set up shell signal handling");
        return 1;
    }

    for( ; ; ) {
        if ((sigsetjmp(jumptoprompt, 1)) &&   /* if return from signal, 
 */
              (fputs("
", stdout) == EOF) )
            continue;
        if (fputs(PROMPT_STRING, stdout) == EOF)
            continue;
        if (fgets(inbuf, MAX_CANON, stdin) == NULL)
            continue;
        if (*(inbuf + strlen(inbuf) - 1) == '
')
            *(inbuf + strlen(inbuf) - 1) = 0;
        if (strcmp(inbuf, QUIT_STRING) == 0)
            break;
        if ((backp = strchr(inbuf, BACK_SYMBOL)) == NULL)
            inbackground = 0;
        else {
            inbackground = 1;
            *backp = 0;
            if (sigprocmask(SIG_BLOCK, &blockmask, NULL) == -1)
                perror("Failed to block signals");
            if ((childpid = fork()) == -1) {
                perror("Failed to fork child to execute command");
                return 1;
            } else if (childpid == 0) {
                if (inbackground && (fork() != 0) && (setpgid(0, 0) == -1))
                    return 1;
                if ((sigaction(SIGINT, &defhandler, NULL) == -1) ||
                      (sigaction(SIGQUIT, &defhandler, NULL) == -1) ||
                      (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)) {
                    perror("Failed to set signal handling for command ");
                    return 1;
                }
                executecmd(inbuf);
                perror("Failed to execute command");
                return 1;
            }
            if (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)
                perror("Failed to unblock signals");
            wait(NULL);
        }
        return 0;
    }

Example 11.27. 

Execute a long-running background process such as rusers & under the shell given in Program 11.12. What happens when you enter Ctrl-C?

Answer:

The background process is not interrupted because it is not part of the foreground process group. The parent shell catches SIGINT and jumps back to the main prompt.

Example 11.28. 

Use the showid function from Exercise 11.25 to determine which of three processes in a pipeline becomes the process group leader and which are children of the shell in ush6. Do this for pipelines started both in the foreground and background.

Answer:

If the parent starts the pipeline in the foreground, all the processes have the same process group as the shell and the shell is the process group leader. The first process in the pipeline is a child of the shell and the others are grandchildren. If the shell starts the pipeline in the background, the first process in the pipeline is the process group leader. Its parent will eventually be init. The other processes are children or grandchildren of the first process in the pipeline.

The zombie child problem is more complicated if the shell does job control. In this case, the shell must be able to detect whether the background process is stopped because of a signal (e.g., SIGSTOP). The waitpid function has an option for detecting children stopped by signals, but not for detecting grandchildren. The background process of Program 11.12 is a grandchild because of the extra fork call, so ush6 cannot detect it.

Program 11.13 shows a direct approach, using waitpid, for handling zombies. To detect whether background processes are stopped for a signal, ush7 uses waitpid with the WNOHANG for background processes rather than forking an extra child. The –1 for the first argument to waitpid means to wait for any process. If the command is not a background command, ush7 explicitly waits for the corresponding child to complete.

Example 11.13. ush7.c

A shell that handles zombie background processes by using waitpid.

#include <limits.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BACK_SYMBOL '&'
#define PROMPT_STRING "ush7>>"
#define QUIT_STRING "q"

void executecmd(char *incmd);
int signalsetup(struct sigaction *def, sigset_t *mask, void (*handler)(int));
static sigjmp_buf jumptoprompt;
static volatile sig_atomic_t okaytojump = 0;

/* ARGSUSED */
static void jumphd(int signalnum) {
    if (!okaytojump) return;
    okaytojump = 0;
    siglongjmp(jumptoprompt, 1);
}

int main (void) {
    char *backp;
    sigset_t blockmask;
    pid_t childpid;
    struct sigaction defhandler;
    int inbackground;
    char inbuf[MAX_CANON];
    int len;

    if (signalsetup(&defhandler, &blockmask, jumphd) == -1) {
        perror("Failed to set up shell signal handling");
        return 1;
    }

    for( ; ; ) {
        if ((sigsetjmp(jumptoprompt, 1)) &&   /* if return from signal, newline */
              (fputs("
", stdout) == EOF) )
            continue;
        okaytojump = 1;
        printf("%d",(int)getpid());
        if (fputs(PROMPT_STRING, stdout) == EOF)
            continue;
        if (fgets(inbuf, MAX_CANON, stdin) == NULL)
            continue;
        len = strlen(inbuf);
        if (inbuf[len - 1] == '
')
            inbuf[len - 1] = 0;
        if (strcmp(inbuf, QUIT_STRING) == 0)
            break;
        if ((backp = strchr(inbuf, BACK_SYMBOL)) == NULL)
            inbackground = 0;
        else {
            inbackground = 1;
            *backp = 0;
        }
        if (sigprocmask(SIG_BLOCK, &blockmask, NULL) == -1)
            perror("Failed to block signals");
        if ((childpid = fork()) == -1)
            perror("Failed to fork");
        else if (childpid == 0) {
            if (inbackground && (setpgid(0, 0) == -1))
                return 1;
            if ((sigaction(SIGINT, &defhandler, NULL) == -1) ||
                  (sigaction(SIGQUIT, &defhandler, NULL) == -1) ||
                  (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)) {
                perror("Failed to set signal handling for command ");
                return 1;
            }
            executecmd(inbuf);
            return 1;
        }
        if (sigprocmask(SIG_UNBLOCK, &blockmask, NULL) == -1)
            perror("Failed to unblock signals");
        if (!inbackground)        /* wait explicitly for the foreground process */
            waitpid(childpid, NULL, 0);
        while (waitpid(-1, NULL, WNOHANG) > 0);    /* wait for background procs */
    }
    return 0;
}

Example 11.29. 

Repeat Exercise 11.28 for Program 11.13.

Answer:

The results are the same as for Exercise 11.28 except that when started in the background, the first process in the pipeline is a child of the shell.

Example 11.30. 

Compare the behavior of ush6 and ush7 under the following scenario. Start a foreground process that ignores SIGINT. While that process is executing, enter Ctrl-C.

Answer:

The shell of ush6 jumps back to the main loop before waiting for the process. If this shell executes another long-running command and the first command terminates, the shell waits for the wrong command and returns to the prompt before the second command completes. This difficulty does not arise in ush7 since the ush7 shell waits for a specific foreground process.

Job Control

A shell is said to have job control if it allows a user to move the foreground process group into the background and to move a process group from the background to the foreground. Job control involves changing the foreground process group of a controlling terminal.

The tcgetpgrp function returns the process group ID of the foreground process group of a particular controlling terminal. To obtain an open file descriptor for the controlling terminal, open the pathname obtained from the ctermid function described in Section 11.5.

SYNOPSIS

  #include <unistd.h>

  pid_t tcgetpgrp(int fildes);
                                      POSIX

If successful, the tcgetpgrp function returns the process group ID of the foreground process group associated with the terminal. If the terminal has no foreground process group, tcgetpgrp returns a value greater than 1 that doesn’t match any existing process group ID. If unsuccessful, the tcgetpgrp function returns –1 and sets errno. The following table lists the mandatory errors for tcgetpgrp.

errno

cause

EBADF

fildes is invalid

ENOTTY

caller does not have a controlling terminal, or fildes does not correspond to a controlling terminal

The tcsetpgrp function sets the foreground process group of the controlling terminal associated with fildes to pgid_id. If a background process calls tcsetpgrp on a fildes associated with its controlling terminal, its process group receives a SIGTTOU signal, provided that this process is not blocking or ignoring SIGTTOU.

SYNOPSIS

  #include <unistd.h>

  int tcsetpgrp(int fildes, pid_t pgid_id);
                                               POSIX

If successful, tcsetpgrp returns 0. If unsuccessful, tcsetpgrp returns –1 and sets errno. The following table lists the mandatory errors for tcsetpgrp.

errno

cause

EBADF

fildes is invalid

EINVAL

implementation does not support the value of pgid_id

ENOTTY

caller does not have a controlling terminal, or fildes does not correspond to a controlling terminal, or controlling terminal is no longer associated with the session of the caller

EPERM

value of pgid_id is supported but does not match the process group ID of any process in the session of the caller

In addition to running processes in the foreground and background, job control allows users to selectively stop processes and resume their execution later. For example, you may want to run a long job in the background but periodically halt it to examine its status or provide input. The C shell and the KornShell allow job control, as do most shells under Linux, but the Bourne shell does not. This section describes job control in the C shell. The Linux shells and the KornShell are almost identical with respect to job control.

A job consists of the processes needed to run a single command line. When a shell starts a job in the background, it assigns a job number and displays the job number and process IDs of the processes in the job. If a pipeline is started in the background, all processes in the pipeline have the same job number. The job number is typically a small integer. If there are no other jobs in the background, the shell assigns the command job number 1. Generally, shells assign a background job a number that is one greater than the current largest background job number.

The jobs command displays the jobs running under a shell.

Example 11.31. 

The following commands illustrate job control for the C shell. The shell displays the prompt ospmt%. The commands appear after this prompt. The shell produces the other messages shown.

ospmt% du . | sort -n > duout &
[1] 23145 23146
ospmt% grep mybook *.tex > mybook.out &
[2] 23147
ospmt% rusers | grep myboss > myboss.out &
[3] 23148 23149
ospmt% jobs
[1]  + Running         du . | sort -n > duout
[2]  - Running         grep mybook *.tex > mybook.out
[3]    Running         rusers | grep myboss > myboss.out

The jobs command shows three running background jobs. The job number is at the start of the line in square brackets. If the second job finishes first, the shell displays the following line when the user presses the return.

[2]    Done            grep mybook *.tex > mybook.out

If at that time the user executes another jobs command, the following output appears.

[1]  + Running         du . | sort -n > duout
[3]  - Running         rusers | grep myboss > myboss.out

You may refer to job n by %n in various shell commands. Example 11.31 shows a + after the job number of job [1], meaning that it is the current job and is the default for the fg and bg commands. The - represents the previous job.

Example 11.32. 

The following command kills job 2 without referring to process IDs.

kill -KILL %2

A background job can be either running or stopped. To stop a running job, use the stop command. The stopped job becomes the current job and is suspended.

Example 11.33. 

The following command stops job two.

stop %2

To start a stopped job running in the background, use the bg command. In this case, bg or bg % or bg %2 all work, since job 2 is the current job.

Use the fg command to move a background job (either running or stopped) into the foreground, and the SIGSTOP character (typically Ctrl-Z) to move the foreground job into the background in the stopped state. The combination Ctrl-Z and bg makes the foreground job a running background job.

Since fg, bg and jobs are built into the shell, these commands may not have their own man pages. To get information on these commands in the C shell, execute man csh.

Example 11.34. 

Experiment with job control (assuming that it is available). Move processes in and out of the foreground.

A shell that supports job control must keep track of all foreground and background process groups in its session. When the terminal generates a SIGSTOP interrupt (usually in response to Ctrl-Z), the foreground process group is placed in the stopped state. How should the shell get back in control? Fortunately, waitpid blocks the parent shell until the state of one of its children changes. Thus, an appropriate call to waitpid by the parent shell allows the shell to regain control after the foreground process group is suspended. The shell can start a suspended process group by sending it the SIGCONT signal. If the shell wants to restart that group in the foreground, it must use tcsetpgrp to tell the controlling terminal what the foreground process group is. Since a given process or process group can run in the foreground or the background at different times during its execution, each child command must start a new process group regardless of whether it is started in the background or foreground.

One job control problem not yet addressed in this discussion is how a process obtains input from standard input. If the process is in the foreground, there is no problem. If there is no job control and the process is started in the background, its standard input is redirected to /dev/null to prevent it from grabbing characters from the foreground process. This simple redirection does not work with job control. Once a process redirects standard input, it cannot use standard input to read from the original controlling terminal when brought to the foreground. The solution specified by POSIX is for the kernel to generate a SIGTTIN signal when a background process attempts to read from the controlling terminal. The default action for SIGTTIN stops the job. The shell detects a change in the status of the child when it executes waitpid and then displays a message. The user can then choose to move the process to the foreground so it can receive input.

Background jobs can write to standard error. If a background process attempts to write to standard output while standard output is still directed to the controlling terminal, the terminal device driver generates a SIGTTOU for the process. In this case, the c_lflag member of the struct termios structure for the terminal has the TOSTOP flag set. A user then has the option of moving the job to the foreground so that it can send output to the controlling terminal. If the process has redirected standard input and standard output, it does I/O from the redirected sources.

Example 11.35. 

Write a simple program that writes to standard output. Start the program in the background under your regular shell and see if it can write to standard output without generating a SIGTTOU signal.

Job Control for ush

This section describes an implementation of job control for ush. Start by testing ush7 in the following cases to make sure that it correctly handles the SIGINT and SIGQUIT.

  1. Simple commands.

  2. Incorrect commands.

  3. Commands with standard input and output redirected.

  4. Pipelines.

  5. Background processes.

  6. All of the above interrupted by Ctrl-C.

A job list object

To do job control, ush must keep track of its children. Use a list object similar to the one used in Program 2.9 to keep a program history. The nodes in the list should have the following structure.

typedef enum jstatus
       {FOREGROUND, BACKGROUND, STOPPED, DONE, TERMINATED}
   job_status_t;

typedef struct job_struct {
    char *cmdstring;
    pid_t pgid;
    int job;
    job_status_t jobstat;
    struct job_struct *next;
} joblist_t;

static joblist_t *jobhead = NULL;
static joblist_t *jobtail = NULL;

Place the list structure in a separate file along with the following functions to manipulate the job list.

int add(pid_t pgid, char *cmd, job_status_t status);

Add the specified job to the list. The pgid is the process group ID, and cmd is the command string for the job. The status value can be either FOREGROUND or BACKGROUND. If successful, add returns the job number. If unsuccessful, add returns –1 and sets errno. It uses getlargest to determine the largest job number and uses a job number that is one greater than this.

int delete(int job);

Remove the node corresponding to the specified job from the list. If successful, delete returns the job number. If unsuccessful, delete returns –1 and sets errno. Be sure to free all space associated with the deleted node.

showjobs(void);

Output a list of jobs and each one’s status. Use the following format.

[job]  status  pgid  cmd
int setstatus(int job, job_status_t status);

Set the status value of the node of the corresponding job. If successful, setstatus returns 0. If unsuccessful, setstatus returns –1 and sets errno.

int getstatus(int job, job_status_t *pstatus);

Return the status value associated with the specified job in *pstatus. If successful, getstatus returns 0. If unsuccessful, getstatus returns –1 and sets errno.

pid_t getprocess(int job);

Return the process group ID of the specified job. If job doesn’t exist, getprocess returns 0.

int getlargest(void);

Scan the job list for the largest job number currently in the list. The getlargest function returns the largest job number if any nodes are on the list or 0 if the list is empty.

Write a driver program to thoroughly test the list functions independently of ush.

The job list in ush

After the job list functions are working, add the job list object to ush as follows.

  1. Each time ush forks a child to run a background process, it adds a node to the job list. It sets the pgid member of the joblist_t node to the value returned from fork. The process status is BACKGROUND.

  2. If the command is executed in the background, ush outputs a message of the following form.

    [job]  pid1  pid2 ....
    

    job is the job number and pid1, pid2 and so on are the process IDs of the children in the process group for the command. The parent ush knows only the process ID of the initial child, so the child that calls executecmd must produce this message.

  3. The ush calls showjobs when a user enters the jobs command.

  4. Replace the waitpid call in ush with a more sophisticated strategy by using waitpid in a loop with the WUNTRACED option. The WUNTRACED option specifies that waitpid should report the status of any stopped child whose status has not yet been reported. This report is necessary for implementing job control in the next stage.

Test ush with the job list. Do not add job control in this step. Execute the jobs command frequently to see the status of the background processes. Carefully experiment with an existing shell that has job control. Make sure that ush handles background and foreground processes similarly.

Job control in ush

Incorporate job control into ush by adding the following commands to ush in addition to the jobs command of the previous section.

stop

stop the current job

bg

start the current job running in the background

bg %n

start job n running in the background

fg %n

start job n running in the foreground

mykill -NUM %n

send the signal SIGNUM to job n

Some of these commands refer to the current job. When there are several jobs, one is the current job. The current job starts out as the first background job to be started. A user can make another job the current job by bringing it to the foreground with fg.

The ush shell now must handle SIGCONT, SIGTSTP, SIGTTIN and SIGTTOU in addition to SIGINT and SIGQUIT. When ush detects that a child has stopped because of a SIGTTIN or a SIGTTOU, it writes an informative message to standard error to notify the user that the child is waiting for input or output, respectively. The user can move that job to the foreground to read from or write to the controlling terminal.

Test the program thoroughly. Pay particular attention to how your regular shell does job control and adjust ush to look as similar as possible.

Process behavior in waiting for a pipeline

What happens when a shell starts a pipeline in the foreground and one of the processes in the pipeline terminates? The result depends on which process in the pipeline is the child of the shell.

Example 11.36. 

Make a new version of showid from Exercise 11.28 on page 395 that sleeps for one minute after displaying the IDs. Call the new program showidsleep. Run ush7 with each of the following command lines. What happens?

showidsleep first | showid second
showid first | showidsleep second

Answer:

For the first command line, the shell displays the prompt after one minute since the first command in the pipeline is the child of the shell. For the second command line, the shell displays the prompt immediately since it waits only for the first command in the pipeline. This is probably not the desired behavior. Typically, a pipeline consists of a sequence of filters, and the last one in the sequence is the last to finish.

Example 11.37. 

How would you solve the problem described in Exercise 11.36?

Answer:

One solution would be to rewrite executecmdpipe so that the last command of the pipeline was executed by the first process created. A better solution would be to have all of the processes in the pipeline be children of the shell and have the shell wait for all of them.

Additional Reading

Books on C shell programming include UNIX Shell Programming by Arthur [6], UNIX Shell Programming, Revised Edition by Kochan and Wood [64] and Portable Shell Programming by Blinn [13]. Learning the Korn Shell, 2nd ed. by Rosenblatt [101] is a clear reference on the KornShell. Another book on the KornShell is The New KornShell Command and Programming Language, 2nd ed. by Bolsky and Korn [15]. Using csh and tsch by DuBois [33] is another general shell reference. Linux Application Development by Johnson and Troan [60] develops a shell called ladsh over several chapters to illustrate application programming concepts.

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

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