CSCI 2330
Foundations of Computer Systems

Bowdoin College
Spring 2018
Instructor: Sean Barker

Lab 6 - The Bowdoin Shell

Assigned:Friday, April 27.
Due Date:Wednesday, May 9.
Collaboration Policy:Level 1 (refer to the official policy for details)
Group Policy:Pair-optional (you may work in a group of 2 if you wish)

This lab will help you understand the inner workings of processes in the context of the shell (a program that you have been using all semester). In particular, you will write your own simple shell program that supports Unix-style job control. Implementing your shell will teach you the core concepts of process control and signaling, as well as give you experience with low-level system programming in C.

Lab Overview

A shell is an interactive command-line interpreter that runs programs on behalf of the user. At a high level, a shell repeatedly prints a prompt, waits for a program name and command-line arguments on stdin (i.e., the terminal window), then carries out some action as directed by the input.

The shell program that you have been using all semester is bash (the Bourne Again Shell). Bash is only one of many shell programs, however - others include sh, tcsh, and csh. In this lab, you will implement your own shell, bsh (the Bowdoin Shell).

Unix Shell Basics

As we saw in Lab 2, a command-line string is a sequence of text words delimited by whitespace. The first word of the string is either the pathname of an executable file (i.e., a program) or a built-in command. The remaining words are command-line arguments. If the first word is a built-in command, the shell immediately executes the command within the current shell process. Otherwise, the shell forks a child process, then executes the specified program in the context of the child. The set of all child processes created as a result of interpreting a single command (there may be mulitple, if the program itself forks) are known as a job. A job can also contain multiple child processes connected by Unix pipes (denoted in a command by vertical bars, |), which allow for passing output from one program as input into another program (although your shell will not need to support pipes).

By default, a job runs in the foreground, which means that the shell waits for the job to terminate before prompting for the next command string. Thus, at any point in time, at most one job can be running in the foreground. However, if the command string ends with an ampersand (&), then the job runs in the background. A background job means that the shell does not wait for the job to terminate, but instead immediately prints another prompt and allows for another command string. As a result, an arbitrary number of background jobs can be running at a given time, in addition to at most one foreground job.

Typing the following command runs the program ls (located in the directory /bin) in the foreground with command line arguments -l -d:

bsh> /bin/ls -l -d

Note that more specifically, calling the above will execute the main function of /bin/ls with the following values of argc and argv:

Alternately, typing the same command with an ampersand will run ls in the background:

bsh> /bin/ls -l -d &

Normally, the shell allows you to just specify the command name without the enclosing directory (e.g., ls instead of /bin/ls) by automatically searching for the specified program within a list of known directories. This list of directories, called the PATH, normally includes /bin and several other system directories. However, since your shell will not support a PATH, you will need to specify the complete directory containing any program you wish to run. From a regular shell, you can locate any given program using the which program, e.g., which pwd, which will tell you how to specify the program from within bsh.

Job Control

Unix shells support the notion of job control, which allows users to move jobs back and forth between background and foreground, and to change the process state (running, stopped, or terminated) of all the processes in a job. Job states can be changed via signals: typing Ctrl-Z causes a SIGTSTP signal to be delivered to every process in the foreground job. The default action for SIGTSTP is to place the process in the stopped state, where it remains until it is awakened by the receipt of a SIGCONT signal. Typing Ctrl-C causes a SIGINT signal to be delivered to each process in the foreground job. The default action for SIGINT is to terminate the process.

Unix shells also provide various built-in commands that support job control. Key commands are listed below:

The bsh Specification

The bsh shell should have the following features:

Code Structure

To start, you have been provided with a functional skeleton of the shell. The starting code implements a number of less interesting functions (such as command line parsing and error reporting) that you should use while implementing the complete shell, allowing you to focus on the more interesting components.

You are responsible for implementing each of the empty functions listed below. To give you an idea of the complexity of each function, also listed below is the number of code lines implementing each function in my reference shell (including comments):

Note: While the function lengths given above are fairly modest, don't be lulled into a false sense of security! System programming involves writing dense, precise, and often error-prone code, and is likely to require significant debugging time.

You do not need to define any functions beyond those already specified in bsh.c, but you are welcome to do so if you wish.

The single file you should modify that contains the code of your shell is bsh.c. The included Makefile will compile the shell for you. To run your shell, simply execute it:

unix$ ./bsh
bsh> [type commands to your shell here]

Included Files

You have also been provided with a number of tools to help you check your work. All included files are described below:

Use the -h flag to see the usage string for sdriver.pl:

unix$ ./sdriver.pl -h
Usage: ./sdriver.pl [-hv] -t <trace> -s <shellprog> -a <args>
Options:
  -h            Print this message
  -v            Be more verbose
  -t <trace>    Trace file
  -s <shell>    Shell program to test
  -a <args>     Shell arguments

For example, you could run the shell driver on trace01.txt by typing the following:

unix$ ./sdriver.pl -t trace01.txt -s ./bsh

Similarly, you could run the trace driver on the reference shell by simply substituting bsh with bshref in the command above.

More simply, you can use the included Makefile to run the driver on the trace files. To pass trace01.txt through your shell, you can just run:

unix$ make test01

Similarly, to pass trace01.txt through the reference shell, you can run:

unix$ make rtest01

The output of your shell from the trace files is exactly the same as the output you would have gotten from running your shell interactively, except for an initial comment that identifies the trace.

Output Formatting and Logging

Since your shell output should exactly match that of the reference shell, your output messages should contain the same information in the same format as the reference shell. Particular messages that you should look out for include the following:

Note that the above messages should always be printed. You are welcome to add additional output when running in verbose mode, but your verbose output does not need to match that of the reference shell. Refer to the reference shell if you are unsure about any of the exact formatting of these messages. In particular, trace 14 exercises many of the error messages.

Trace File Format

Each trace file consist of a series of commands to test the functionality of your shell. The trace files are understood by the sdriver.pl driver program, which launches your shell, then executes a given trace file against the running shell process, capturing its output. In order to understand what the tests are doing, you should also be sure to understand the trace files. The format of each trace file is summarized below:

General and Function-Specific Advice

Here are some useful tips for working on your shell:

Tips for specific parts of the shell are given below.

Eval

Signal Handlers

Using GDB in a Multi-Process Program

To debug a multi-process program such as your shell using gdb, you will need a few extra commands:

(gdb) set detach-on-fork on/off

The above command sets whether child processes will be detached when fork is called. The default is on (i.e., the child runs without any interruption). If you turn this option off, the child is suspended as soon as it is forked. Then, you can use the inferior command to switch between the various processes started by the shell:

(gdb) info inferiors
... listing of processes ...
(gdb) inferior 1
[Switching to inferior 1 [process 0] (<noexec>)]

Another useful option is the following:

(gdb) set follow-fork-mode parent/child

The above sets which process gdb will automatically follow (either the parent or the child -- parent is the default) after fork is called.

Logistics

You are responsible for completing the contents of the bsh.c file. You should not modify any other file. You are responsible for ensuring that your program runs on the class server, regardless of where else you may be writing code. Since other systems may have the same system calls but with slightly different behavior, you are strongly urged to develop your code entirely on the class server.

As usual, your final submission will consist of your committed bsh.c file at the time of the due date. Each group need only make one submission.

Evaluation

Your simulator will be graded on program correctness (as determined by the 16 trace files), design, and style. Your shell will be tested on the class server, where it should product identical output to the reference shell on the trace files, with two exceptions:

You can (and should) consult the Coding Design & Style Guide for tips on design and style issues. Please ask if you have any questions about what constitutes good program design and/or style that are not covered by the guide.

Other specific things to watch out for: