CMPSC 311, Introduction to Systems Programming

Process Control and Signals, Projects 6 and 7



This sequence of two projects shows how to create and test (6) a child process and signal handlers, and (7) a simple command interpreter, or shell.  The first project asked you to manage (in a simple way) the coordination between two processes.  The second project extends the first with interactive features and multiple processes.  The first project was individual and took one week, while the second is for two people and will take two weeks.

Note that the second midterm exam will be given during the time you are working on Project 7, and a solution to Project 6 has been posted before the in-class exam review on Mar. 27.

Background information for the projects, with a complete example program, is provided.  You should work through the background information first; we covered this in class on Feb. 1.  Starting points for Projects 6 and 7 are also provided, which use some of the background information.

Here is what you should turn in for these projects:
  1. a printed copy of the source code, with comments, your name, and the date;
  2. a printed copy of the output from the program (if it compiles and runs successfully);
  3. a printed copy of the error messages from the compiler or runtime system (otherwise);
  4. any additional write-up required for the project;
  5. a brief statement of how you allocated your time working on the project (planning, reading the manuals, coding, debugging, cursing the prof, etc.).
  6. An electronic version of your program should be submitted through ANGEL.  Specific instructions will be included with each project.  Be sure to attach all parts of your program's source code.  Do not attach an executable file.
See the Project 6 description for more information, which still applies.

The test cases to demonstrate the output for Project 7 are your choice.  Be sure to try all the major features of the program; fewer than 5 test cases would not be enough, more than 15 would probably be too many.  There are some examples in this description.

It is possible that some parts of this project are incompletely specified.  Some of that is intentional.  If you are unsure how to resolve an ambiguity in the description, please ask.

There is a lot of code provided here.  You could derive a reasonably simple but lengthy program with everything in one file and everything in main().  You could follow an "object-oriented" design and have a somewhat more complex program with some better properties.  Or, you could just take this as general guidance, and follow your own design.  The idea is to learn how to put together a reasonably powerful program from the basic parts provided by the Unix and C libraries.



CMPSC 311, Project 7

Posted Monday, Mar. 25, 2013.  Due Tuesday, Apr. 9, 2013, 11:55 pm (electronic version, to ANGEL), and Wednesday, Apr. 10, in class (paper version).  A solution will be posted on ANGEL at 5 pm on Apr. 10, so no late projects will be accepted after that time.   60 points (+ 10 points possible as extra credit).

This is a project for two people.  If you need a partner, check the list on the course Projects web page.
This project is based on CS:APP Homework Problem 8.26.  The source code from the book is available at http://csapp.cs.cmu.edu/public/code.html under the code/ecf/shellex.c heading; this appears in the book as Fig. 8.22, 8.23 and 8.24.  The code provided here is similar but rewritten.

Reading, CS:APP
Reading, APUE
Reading, CP:AMA
Reading, C:ARM
The following files are linked here:

pr7.1.c  (initial version)
pr7.2.c  (starter version)
pipe.c  (a demo program)
pr7.h  (described later)
try_pr7.h.c  (test program)
try_pr7.h.sh  (test program)



A simple interactive Unix command shell works as follows:
  1. shell:  read a command line
  2. shell:  create a child process with fork()
  3. child (new process, also running the shell program):  use exec() to run the program specified in the command line
  4. child (specified program):  terminate by calling exit()
  5. parent (original shell):  use wait() to wait for the child to call exit()
  6. shell:  print a prompt to standard output and go back to step 1
There are also a number of special commands that should be done by the shell itself; some of these will be described later.  The child process could be run in the foreground, as indicated above, or in the background, where the parent does not wait.  The actual command shells add a large number of features that are not required for this project, though some of them are feasible for extra credit.

The explanation will first assume everything goes into main(), and then will show how to rearrange the code, along with providing a starter kit for the final version.  You should be able to assemble a working program from the basic information, and test it before going to a full rewrite.

One of the design concepts not used here is to write the command interpreter as a function that could be used later to start a thread using the POSIX Pthreads library.  This suggests that functions written for this project should be reentrant, or as close as you can get to it.



Let's start with the absolute simplest shell program, which has essentially no desirable features other than brevity.  Even then, we left out a few things, like the header files and declarations and the rest of main().

while (true) {
  write_prompt();                           /* output */
  read_command(command, parameters);        /* input */
  if (fork() == 0)                          /* work */
    {
      /* child, does the work */
      execve(command, parameters, environ);
    }
  else
    {
      /* parent, waits for the child to finish the work */
      waitpid(-1, &status, 0);
    }
}

Try not to lose track of what's going on here - the parent (the shell) forks a child to do the work (the command entered by the user).  The child process replaces its program with a new one.  When the child has finished the work, its process terminates, the parent notices that, and the parent goes back for another command.  If the command is to be executed in the background, then the parent does not call waitpid().  This is actually the easier case, and should be the first example, except that it isn't very practical.



Here is the simplest almost-realistic version of the program, in the file pr7.1.c.  You could start by reviewing the textbooks and man pages for the small number of functions required; there is only one new one since Project 6, execlp().  The explanation to follow will review some of this.
/* CMPSC 311 Project 7 starter kit, version 1
 *
 * Usage:
 *   cc -v -o pr7 pr7.1.c            [Sun compiler]
 *   gcc -Wall -Wextra -o pr7 pr7.1.c    [GNU compiler]
 *
 *   pr7
 *   pr7%      [type a command and then return]
 *   pr7%      [type control-D to indicate end-of-input and thus exit the shell]
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAXLINE 128 /* maximum input line length */

int main(int argc, char *argv[])
{
  char buf[MAXLINE];                            /* command-line information */

  pid_t child_pid;                              /* child process information */
  int child_status;

  printf("%s - control-D to exit\n", argv[0]);  /* simple instructions */

  printf("%s%% ", argv[0]);                     /* prompt */

  while (fgets(buf, MAXLINE, stdin) != NULL)    /* input */
    {
      buf[strlen(buf) - 1] = '\0';              /* remove input newline */

                                        /* run the command in a child process */
      if ((child_pid = fork()) == -1)
        {
          fprintf(stderr, "fork failed\n"); break;         /* parent gives up */
        }
      else if (child_pid == 0)
        {
          execlp(buf, buf, NULL);               /* replace the child program */
          fprintf(stderr, "exec failed\n"); break;         /* child gives up */
        }

                                                /* parent waits for the child */
      if (waitpid(child_pid, &child_status, 0) == -1)
        {
          fprintf(stderr, "waitpid failed\n"); break;      /* parent gives up */
        }

      printf("%s%% ", argv[0]);                 /* prompt */
    }

  return EXIT_SUCCESS;
}
What is wrong or missing here? Try these examples and some others:  (input is highlighted)
% ls
pr7.1.c
% cc -v -o pr7 pr7.1.c
% pr7
pr7 - type control-D to exit
pr7% ls
pr7               pr7.1.c
pr7% ls pr7
exec failed
pr7% [control-D]
%
Note that pr7.1.c has some additional code that is not shown here.  Before proceeding, you should understand this version of the shell program completely.  There will soon be a better version, which improves the code structure and behavior.



Suppose that the name of the program implementing the command shell is pr7.

The program should have at least these options:

% pr7 -h
Usage:  pr7 [-h] [-v] [-i] [-e] [-s f] [file]
  -h     help
  -v     verbose mode
  -i     interactive mode
  -e     echo commands before execution
  -s f   use startup file f, default pr7.init
Shell commands:
  help

The default options are non-verbose, non-interactive, non-echoing.

The verbose mode should provide additional output that helps explain what the shell is doing.  This will be useful for reassuring yourself that the program is behaving correctly.  You could also add a -d option for debug mode, which would provide more detailed output than verbose mode.  The simplest distinction between verbose mode and debug mode might be that a curious user wants verbose mode, while only the program developer wants debug mode.  Some examples are given below, using

char *program_name;         /* global */
int self_pid;               /* global */
program_name = argv[0];     /* in the parent */
self_pid = (int) getpid();  /* in the parent and child */

Be sure to assign (or reassign) self_pid after fork() so the child's output gives the correct information.  self_pid is an int just for the convenience of printf() - it really should be a pid_t.  For example, the verbose output at the beginning of the program could be

/* say hello */
if (verbose)
  { printf("%s %d: hello, world\n", program_name, self_pid); }

on the theory that a "say goodbye" message will also be useful.  Since output can come from either the parent or child, the process ID helps to distinguish them.

The input for the shell should be taken from a file if one is specified on the command line, otherwise input should come from stdin.  It is useful to specify on the command line whether or not the shell is going to be used interactively, since Unix input redirection makes it difficult to discover this.  The shell should be equally useful (and produce similar results) when it is used as any one of these:

pr7 -i
pr7 < inputfile
cat inputfile | pr7
pr7 inputfile

The first use should be interactive, with a prompt (as indicated by the -i option), taking commands from stdin (as indicated by lack of a file name), which is connected to the terminal or console.  The second and third uses also take their input from stdin, but are not interactive, so the prompt should not be given.  The fourth use must open the file inputfile for reading.  When a program starts, stdin is already opened for reading, and is usually connected to the terminal.  Thus the first three uses treat their input in the same way, but only the first case should issue a prompt.  If the use is non-interactive, the user might want to echo the commands before attempting to execute them.  If the use is interactive, each input line will be echoed by the terminal driver, so the -e option would be unnecessary (but it's still acceptable, and you simply get extra output).

The shell will need to determine when it has reached the end of its input, or whether some error condition has occured.  When reading from a file, the system function used to read the next line will give an error indication.  An example is given below.

If the input file cannot be opened, or if the command input contains a command that cannot be executed, then issue an appropriate error message to stderr.  The shell program itself should not be allowed to fail if one of the commands given to it fails, or if there is something wrong with the command and it can't be run at all.  It is not specified what should happen if there is more than one file given, and it's ok to refuse to accept more than one.

Output from the shell normally goes to stdout unless it's an error message.  printf() sends output to stdout, and you can use fprintf() with stderr, as shown previously.

Most shells provide a "startup" file which is quietly executed before any other input.  The default name of this file should be pr7.init .  If this file is not present, no complaint should be issued.   If the -s option is used to specify a different startup file, and that one is missing, then a complaint should be issued.   If commands in the startup file fail, it is your choice whether or not to issue an error message (remember, the shell should not fail).

Most Unix programs allow the special file name - (a single hyphen) to represent standard input.  Your shell should accept this, but it is not specified what should happen if - also appears as the startup file.

Important:  When using the shell interactively (so stdin is connected to the terminal) end-of-file is indicated by typing the control-D character.  Later, you should add a special command exit that will cause the shell to terminate.  If the program is seriously misbehaving, use control-C to kill it.  If you suspect that some of the child processes have gotten out of control, use the ps(1) command to see what is running, and the kill(1) command to terminate a process.  When you are certain the program works correctly, you should add a signal handler to treat control-C in a less brutal fashion.

The C standard I/O functions that are useful for dealing with files are described in stdio(3C), and include
The string manipulation functions in C that might be useful here are
The memory allocation functions in C are
Note that strdup() calls malloc() so its result can be given to free().



You will need to open an input file, or just use stdin.  This will be easiest if you use a file pointer as provided with stdio.h.  Here is an example using fopen(3C) assuming there is at most one file given on the command line (not including the -s option):

  FILE *infile;    /* FILE is defined in stdio.h - see stdio(3C) */
  char *infile_name;
  if ( ... should take input from stdin ... )
    { infile = stdin; infile_name = "[stdin]"; }
  else
    { infile_name = argv[ ... something ... ];  /* also use strdup()? */
      infile = fopen(infile_name, "r");  /* read-only */
      if (infile == NULL)
        {
          fprintf(stderr, "%s: cannot open file %s: %s\n",
            program_name, infile_name, strerror(errno));
          exit(EXIT_FAILURE);  /* or something less brutal */
        }
    }

Now it does not matter where the input is actually coming from, all accesses are through the file pointer infile.  At the end of the input from the file, close the file:

  if (fclose(infile) != 0)
    {
      fprintf(stderr, "%s: cannot close file %s: %s\n",
        program_name, infile_name, strerror(errno));
      exit(EXIT_FAILURE);  /* or something less brutal */
    }

Since all open files are closed upon exit(), it is not absolutely necessary to use fclose() in all programs, but you should use it here, especially if infile is not stdin.

Important:  Every use of a system or library function must check for error conditions involving that function.  The simplest response to an error is to print a message and exit, but some errors might require other actions.  For example, the shell should just print an error message and prompt for new input, if the problem is not severe.  errno is used to indicate the specific problem, if there was one, and strerror(3C) translates that numeric code into a printable character string.  Be sure to include the files errno.h and string.h.



To read command lines from the input, you need a buffer to hold one line, a loop that ends on end-of-file, and some system or library function to do the reading.  To make the program easier, you can assume that no input line is longer than some known maximum length; the examples will use 128 characters, including the newline and null characters at the end of the input line.  The actual limit is system-dependent, and there should be a constant MAX_INPUT defined in the include file limits.h.  It is up to you to decide what to do if the line length is too long for the buffer, but avoiding that case is the easiest approach for now.  The functions feof(3C) and ferror(3C) are used to distinguish the two cases where fgets(3C) indicates that it cannot go any farther.

  char buffer[128];

  while (fgets(buffer, sizeof(buffer), infile) != NULL)
  {
    /* Note that buffer has a newline at the end if the input line
     *   was short enough to fit.
     * NULL indicates end-of-file or error.
     * Is line-too-long an error?
     */
    if (verbose) printf("%s: have: %s", program_name, buffer);

    /* do the work */
  }

  /* No more input.  Was it an input error or just end-of-file? */

  if (feof(infile))
    { printf("%s: end of input\n", program_name); }

  if (ferror(infile))
    { printf("%s: error reading input\n", program_name); }

Important:
  Do not use gets(3C), and be sure you understand why; the man page will explain this.  The loop structure will need to be revised in the next development stage, but this is enough for now.

If you are feeling ambitious, try using the GNU function readline(3) instead of fgets().  It would be better to get the simpler version working correctly before changing to readline().  One problem this will cause is that readline() only takes its input from stdin, and it uses malloc() to dynamically allocate memory for the input line, so you need to free() the memory later.

If you want to see how far you have gotten in reading the file, maintain a line counter and use ftell(3C) or ftello(3C) (see the Stdio notes or APUE Sec. 5.10).  The line counter would be useful for the prompt, and the ftell() information for the debug output.

If you need to clear an error condition associated with the input stream, use clearerr(3C).  Of course, end-of-file is a condition that cannot really be fixed.



Now the input command must be broken apart and reconstructed in the style of an argv[] array.  The simplest approach is to scan buffer looking for "whitespace", which is defined as the characters space, tab, newline, and a few others (see isspace(3C)).  We also need to rewrite the loop using fgets() so that a prompt can be issued more cleanly.  One simple technique is shown in the second version of the shell program, pr7.2.c, in the parse() function.  It uses the functions strspn() and strpbrk() to skip over whitespace and to stop at the next whitespace character.

Important.  Take the time to study and understand CS:APP Sec. 8.4 and pr7.2.c.  It is a good basis for the final version of the program.  Of course, there is a lot to add and some to change before you are done.



A second but less-flexible parsing technique is shown here, mainly to illustrate again the difference between reentrant and non-reentrant functions, and to indicate placement of some more code.  If the command line is allowed to use quoted strings (not a requirement here, but something to try for extra credit), this parsing method is not appropriate (the code in pr7.2.c is a better choice for extension).   int buf_argc;
  char *buf_argv[128];  /* how many arguments could there be? */
  char *buf_p;          /* for strtok() and strtok_r()*/
  char *buf_pp;         /* for strtok_r() */

  int interactive = 0;  /* -i, give a prompt? */
  int echo = 0;         /* -e, echo the command? */

  while (1)
  {
    ... maybe do something else useful here ...

    if (interactive)
      { ... print a prompt ... }

    if (fgets(buffer, sizeof(buffer), infile) == NULL)
      { break; }

    /* construct argc and argv[] for the child */
    buf_argc = 0;
    buf_p = strtok(buffer, " \t\n");      /* replace this, later */
    while (buf_p != NULL) {
      buf_argv[buf_argc++] = buf_p;
      buf_p = strtok(NULL, " \t\n");      /* replace this, later */
    }
    buf_argv[buf_argc] = NULL;  /* see exec(2) for an explanation */
        /* should also check that buf_argc is not too large */

    if (buf_argc == 0)   /* nothing to do */
      { continue; }

    if (echo)
      {
        printf("%s", buf_argv[0]);
        for (i = 1; i < buf_argc; i++)
          { printf(" %s", buf_argv[i]); }
        printf("\n");
      }

    ... run special commands in this process ...
    ... run other commands in a child process ...
    ... wait if foreground job ...
  }

The non-reentrant function strtok() retains some position information between calls, so it can continue to scan buffer correctly.  The reentrant version strtok_r() uses an extra argument to hold this position information, as follows:

    buf_p = strtok_r(buffer, " \t\n", &buf_pp);
      buf_p = strtok_r(NULL, " \t\n", &buf_pp);

Review question.  Why is the newline character required here?  Try running the program without it.  (In pr7.2.c, which uses a different method, see the array whsp[] in parse().)



The program that is to be run must be run in a child process so the parent process (the shell) does not fail if the command fails.  The basic structure is the same as in Project 6, but you also need to start the command using one of the exec(2) functions.  It is easiest to begin with execvp(2), although we previously used execlp(2) in pr7.1.c and execve(2) in pr7.2.c.  The exec(2) functions differ according to how the command line is communicated, whether the environment variables are communicated explicitly or implicitly, and how the command search path is used.  The execve() function is generally preferred, but execvp() causes fewer problems with the search path.  There is an extra complication in the use of exit() which will be explained next.

/* for fork(2), wait(2), execvp(2), _exit(2) */
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

  pid_t child_pid;          /* from fork() */
  int child_status;         /* from wait() */

    /* run the command in a child process */
    child_pid = fork();

    if (child_pid == (pid_t)(-1))
      {
        printf("%s %d: could not create new process: %s\n",
          program_name, self_pid, strerror(errno));
        exit(EXIT_FAILURE);   /* or something less brutal */
      }

    if (child_pid == 0)
      { /* child */
        self_pid = (int) getpid();  /* is this necessary? */

        /* run the requested program by replacing this one
         *   (in the same process)
         */
        execvp(buf_argv[0], buf_argv);

        /* execvp() only returns if there was an error */
        printf("%s %d: could not run program '%s': %s\n",
          program_name, self_pid, buf_argv[0], strerror(errno));

        /* This is important! - do not use exit() here.
         * Do not omit this line.
         */
        _exit(EXIT_FAILURE);
      }
    else
      { /* parent */

        /* wait for foreground job to finish */
        wait_child(child_pid, &child_status);  /* from Project 6 */
      }

Very Important:
  If execvp() fails in the child, then use _exit() and not exit().  The reason is that fork() causes the parent and child to share open file pointers, including stdin, stdout and stderrexit() closes all open files, while _exit() does not.  If you use exit() in the child after an execvp() failure, then the file pointers used by the parent are modified too often, which can cause some very baffling behavior.  See the man pages for exit(2) and fork(2) (especially the NOTES section) for more information.  This is essentially the only place where _exit() should be used.  The C99 standard implements this function as _Exit().



At this point you should have a shell that works properly even if it does not have many features.  Some good test cases for commands to execute would include

  whoami
  date
  pwd
  bgi -a -x 3
  echo foo bar
  cat something
  exit

The program bgi is version 5 of the code in the Background Information.  The others are ordinary Unix or shell commands.

Very Important:
  Test the program with a variety of commands (especially some that do not exist or do not have execute permission).  It should work equally well in interactive and non-interactive versions.  Note that the given programs pr7.1.c and pr7.2.c do not meet the project requirements in this regard.  The program can provide output that is not usually given explicitly by the Unix shells, and the verbose mode should be designed to help see what the program is actually doing or attempting.

Here is some sample output using the commands above interactively, and a more complete version of the shell program.  Yours should be similar, but does not need to be identical.  In particular, we added a call print_msg_2("...", child_pid, child_status) after wait_child().  The command echo is executed by the shell, so no child process information appears.  At prompt 8, the wrong command (Exit) was entered.  If exit has not been implemented as a special command, then control-D should be typed to mark the end of input.  Some additional blank lines have been added to make it easier to see what is going on.  The parts entered at the keyboard have been highlighted.

eru 21% cat pr7.init
echo greetings from pr7.init
whoami
hostname
echo ---------

eru 22% pr7 -i
greetings from pr7.init
dheller
 24798: Sun Mar 24 17:19:03 2013 command finished, pid, status =  24799 0x00000000
eru
 24798: Sun Mar 24 17:19:03 2013 command finished, pid, status =  24800 0x00000000
---------

pr7 0% whoami
dheller
 24798: Sun Mar 24 17:19:27 2013 command finished, pid, status =  24802 0x00000000

pr7 1% date
Sun Mar 24 17:19:32 EDT 2013
 24798: Sun Mar 24 17:19:32 2013 command finished, pid, status =  24803 0x00000000

pr7 2% pwd
/tmp/project-7
 24798: Sun Mar 24 17:19:39 2013 command finished, pid, status =  24804 0x00000000

pr7 3% bgi -a -x 3
bgi: failed: No such file or directory
 24798: Sun Mar 24 17:20:02 2013 command finished, pid, status =  24805 0x00000100

pr7 4% cc -o bgi bgi.5.c
 24798: Sun Mar 24 17:20:19 2013 command finished, pid, status =  24806 0x00000000

pr7 5% bgi -a -x 3
argc = 4
bgi
-a
-x
3
 24798: Sun Mar 24 17:20:31 2013 command finished, pid, status =  24809 0x00000300

pr7 6% echo foo bar
foo bar

pr7 7% cat something
cat: cannot open something
 24798: Sun Mar 24 17:20:52 2013 command finished, pid, status =  24810 0x00000200

pr7 8% Exit
Exit: failed: No such file or directory
 24798: Sun Mar 24 17:20:58 2013 command finished, pid, status =  24811 0x00000100

pr7 9% exit

eru 23%


Here is some sample output using the commands above but taken from a file.  Again, some additional blank lines have been added.

eru 26% cat pr7.test
echo greetings from pr7.test
whoami
date
pwd
bgi -a -x 3
echo foo bar
cat something
exit
echo foo

eru 27% pr7 pr7.test
greetings from pr7.init
dheller
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24822 0x00000000
eru
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24823 0x00000000
---------
greetings from pr7.test
dheller
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24825 0x00000000
Sun Mar 24 17:27:20 EDT 2013
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24826 0x00000000
/tmp/project-7
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24827 0x00000000
argc = 4
bgi
-a
-x
3
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24828 0x00000300
foo bar
cat: cannot open something
 24821: Sun Mar 24 17:27:20 2013 command finished, pid, status =  24829 0x00000200

eru 28%




Note that the child_status variable holds more information than was communicated with exit() or _exit(), and the argument to exit() now appears in a different byte position.  The right way to interpret this value uses the wait status macros in the following style, in the parent after returning from wait() with a valid result:

    if (WIFEXITED(child_status))
      {
        printf("%s %d: process %d, normal exit status %d\n",
          program_name, self_pid, (int)wait_pid, WEXITSTATUS(child_status));
      }
    else ...

The full description is in the man page for wait.h or wait(2).  CS:APP Sec. 8.4.3 and APUE Sec. 8.6 have more examples.  The WIFCONTINUED() macro is not part of the POSIX standard, so you can skip it.

This method of interpreting the status value goes into the function print_wait_status() mentioned later.



The following features can be implemented relatively easily once you have the simple parent/child structure working correctly, using some of the tools from Project 6 and pr7.2.c.  These features are required.  They are listed in increasing order of (probable) implementation difficulty.
  1. Execute some operations in pr7 itself instead of creating a child process.  There are eight essential special commands:
  2. command file execution
  3. Catch control-C signals.
  4. background command execution
  5. background job control
Here are some additional design questions to consider (to think about and perhaps to use in the program, but not to turn in):
  1. Should you write the pr7 program in C, C++ or Java?  Unix commands are typically written in C, and that is a requirement for this project, but you might decide that it should have been done in another language.  If so, what advantages and disadvantages go along with the decision?
  2. What would be a simple test program to use with pr7 as a command?  as a command file?  as a background command?
  3. What is the exit status of pr7 itself?
  4. Should pr7 accept more command-line options?  Obviously it needs to accept at least one command-line argument, a file name.
  5. What should be used as the command prompt?
  6. What should you do if a child process fails to terminate?  Try to avoid this situation, since you do not actually need to deal with the problem for this assignment, except with control-C.
  7. Should the background job list include background jobs started by command files?
  8. As background commands finish, what should pr7 print in the completion message?  Should the message be printed as soon as possible, or some time later?
  9. etc., etc.
You do not need to implement the following features for this assignment, but you are welcome to try if you have enough time and have all the previous features working correctly (this will be treated as extra credit, and you need to make it clear in your writeup which of these you have attempted):



Here are some more examples of interactive and noninteractive use.  The print_msg_2() call has been removed.  Extra blank lines have been added.  The parts entered from the keyboard have been highlighted.  At prompt 6 of the interactive test, only the return key was entered, and then the exit command.  eru is the name of the Solaris server.

eru 45% cat pr7.init
echo greetings from pr7.init
whoami
hostname
echo ---------

eru 46% cat pr7.test
echo greetings from pr7.test
# whoami
# date
# pwd
bgi -a -x 3
echo foo bar
cat something
exit
echo foo

eru 47% pr7 pr7.test
greetings from pr7.init
dheller
eru
---------
greetings from pr7.test
argc = 4
bgi
-a
-x
3
foo bar
cat: cannot open something

eru 48% pr7 -v pr7.test
pr7: reading pr7.init
greetings from pr7.init
dheller
process 24912, completed normally, status 0
eru
process 24913, completed normally, status 0
---------
pr7: reading pr7.test
greetings from pr7.test
argc = 4
bgi
-a
-x
3
process 24915, completed normally, status 3
foo bar
cat: cannot open something
process 24916, completed normally, status 2

eru 49% pr7 -i
greetings from pr7.init
dheller
eru
---------

pr7 0% help
Commands:
  help
  exit [n]
  echo
  dir
  cdir [directory]
  penv
  penv variable_name
  senv variable_name=value
  unsenv variable_name
  pjobs
  limits
  set
  set debug [on|off]
  set exec [lp|vp|ve]
  set verbose [on|off]

pr7 1% sleep 40 &

pr7 2% pjobs
process table, printed by pjobs
       pid         state        status    program
     24921       running    0x00000000    sleep

pr7 3% quit
quit: failed: No such file or directory

pr7 4% exit
There is one background job running.

pr7 5% exit
There is one background job running.

pr7 6%
pr7 6% exit
There is one background job running.

pr7 7% exit
There is one background job running.

pr7 8% exit
There is one background job running.
process 24921, completed normally, status 0

pr7 9% exit

eru 50%




It is possible that your program has gotten to be messy because of adding code at various points, and you should certainly consider writing some functions that will make the program easier to read.  In any case, take the time to write clean code.

The file pr7.h just defines a few constants based on the standard include file limits.h.  You can replace it or eliminate it, or use it as is.  Note that we used the compile-time constant _POSIX_C_SOURCE to force the Posix version of the limits.  Here is output from a test program try_pr7.h.c and shell script on Solaris, Linux and Mac OS X.



The process table functions should be modified from Project 6.  The compile-time table should be replaced by a run-time table allocated dynamically with malloc(), but this should eventually be replaced with a linked list to avoid limiting the number of background processes, and to economize on storage.  For example,

/* interface information for a process table */

typedef struct child_process {
  pid_t pid;            /* process ID, supplied from fork() */
                        /* if 0, this entry is not currently in use */
  int   state;          /* process state, your own definition */
  int   exit_status;    /* supplied from wait() if process has finished */
    /* Later... add more information */

  // struct child_process *next;
    /* Later... replace array with linked list */
} child_process_t;

typedef struct process_table {
  int children;
  child_process_t *ptab;
    /* formerly   child_process_t ptab[MAX_CHILDREN]; */
    /* Later... replace dynamic array with linked list */
} process_table_t;

/*--------------------------------------------------------------------------------*/

/* process table for background processes */

extern int number_of_children(process_table_t *pt);

/* return NULL if not successful */
extern process_table_t *allocate_process_table(void);

/* ignore possible errors */
extern void deallocate_process_table(process_table_t *pt);

/* return 0 if successful, -1 if not */
extern int print_process_table(process_table_t *pt);
extern int insert_new_process(process_table_t *pt, pid_t pid);
extern int update_existing_process(process_table_t *pt, pid_t pid, int exit_status);
extern int remove_old_process(process_table_t *pt, pid_t pid);

/*--------------------------------------------------------------------------------*/



The following function is new since Project 6, but it uses now-familiar system functions.  You may find it useful for collecting the terminated background processes without relying on a SIGCHLD signal handler.  See CS:APP Sec. 8.4.3 or APUE Sec. 8.6 for an explanation of the WNOHANG option to waitpid().

/* Find all the child processes that have terminated, without waiting.
 *
 * This code is adapted from the GNU info page on waitpid() and the Solaris
 * man page for waitpid(2).
 */

int cleanup_terminated_children(void)
{
  pid_t pid;
  int status;
  int count = 0;

  while (1)
    {
      pid = waitpid(-1, &status, WNOHANG);

      if (pid == 0)             /* returns 0 if no child process to wait for */
        { break; }

      if (pid == -1)            /* returns -1 if there was an error */
        {
          /* errno will have been set by waitpid() */
          if (errno == ECHILD)  /* no children */
            { break; }
          if (errno == EINTR)   /* waitpid() was interrupted by a signal */
            { continue; }       /* try again */
          else
            {
              printf("unexpected error in cleanup_terminated_children(): %s\n",
                strerror(errno));
              break;
            }
        }

      print_wait_status(pid, status);      /* supply this yourself */
      update_process_table(pid, status);
      remove_process_table(pid);
      count++;
    }

  return count;
}



The shell should install a signal handler for SIGINT.  The idea is that control-C should not terminate the shell or its background processes, but should terminate a foreground process.  The solution given here is imperfect (OK, it's a hack), but at least it's short.  The command interpreter sets a global variable foreground_pid while it is waiting for a foreground job to finish, and clears foreground_pid afterward.  The signal handler can then check to see if there is a child process that needs to be signaled with SIGINT.

static pid_t foreground_pid = 0;

void SIGINT_handler(int sig)
{
  if (foreground_pid == 0)
    {
      fprintf(stderr, "SIGINT ignored\n");
    }
  else
    {
      kill(foreground_pid, SIGINT);
      foreground_pid = 0;
    }
}

Actually, you might find that the "real" shell in which you started pr7 will forward SIGINT to all of pr7's children automatically, and the kill() might not be necessary at all.  Some experimentation will be required.   Read about process groups in CS:APP Sec. 8.5.2.



If you want to see more examples from a working solution, send email to dheller@cse.psu.edu and they will be added to the end of this description.



Some more suggestions



Some additional notes



To turn in your project electronically on ANGEL,
  1. Login to Solaris, using eru.cse.psu.edu., or Linux, using ladon.cse.psu.edu, or your own Linux or Mac OS X system.
  2. cd to the directory that contains your source code, for example, with the files README, pr7.3.c, pr7_wait.c, pr7_wait.h
  3. Run these commands, making sure to list all your source and include files.
    1. tar cvf pr7.tar README pr7.3.c pr7_wait.c pr7_wait.h
        (this will create the file pr7.tar)
    2. gzip pr7.tar
        (this will create the file pr7.tar.gz)
  4. Login to ANGEL, and put the file pr7.tar.gz in the Project 7 Dropbox.
The file name pr7.tar.gz is important - don't change it.

Your program will be compiled by unpacking the gzip'ed tar file and running a command like "cc -v *.c".  Don't include any extra source or include files, and don't leave any out.

Depending on the files you have, this might be easier for step 3.1:
  tar cvf pr7.tar README *.[ch]
But, you should verify this first by running
  cc -v *.c
and then running the program
  a.out -i
to make sure everything will work.

Of course, the actual compile command on Solaris would be more like one of these:
c99 -v -D_POSIX_C_SOURCE=200112L -D_XOPEN_SOURCE=600 -o pr7 *.c
gcc -std=c99 -Wall -Wextra -D_POSIX_C_SOURCE=200112L -D_XOPEN_SOURCE=600 -o pr7 *.c

On Linux, use
gcc -std=c99 -Wall -Wextra -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -o pr7 *.c
 


Last revised, 25 Mar. 2013