1. Due Date

Due 11:59 PM, Tuesday, April 21

Your partner for this lab is: Lab 7 Partners

2. Lab Goals

  • Demystify how a Unix shell program works by writing one.

  • Executing commands in the shell that run in the foreground and the background.

  • Learn how to create and reap processes with the fork, execvp, waitpid system calls.

  • Interact with signals and write signal handler code.

3. Lab Description

You will implement a shell, which is the program you interact with on the command line of a terminal window. A shell operates by doing the following things:

  1. Print a prompt and wait for the user to type in a command.

  2. Read in the command string entered by the user.

  3. Parse the command line string into an argv list.

  4. If the command (first item in the parsed argv list) is a built-in shell command, the shell will handle it on its own (without forking a child process).

  5. Otherwise, if it’s not a built-in command, the shell will fork a child process to execute the command.

    1. If it is a foreground process, the shell will wait for the child process to finish and cleanly exit.

    2. Otherwise, the shell does not wait for the child to exit, but instead gets notified by an O.S. signal handler.

  6. These steps repeat until the user enters the built-in command exit to exit the shell program.

4. Handy References

5. Examples programs to get started

5.1. Signals and Signal Handlers

Let’s first look at the program signals.c. This program has some examples of registering a signal handler function on a signal, and of some examples of ways in which you can send signals to processes (for example, alarm can be used to send a SIGALRM to one’s self).

We will try running this and use the kill command to send the process signals:

kill -INT 1234  # sends a SIGINT signal to process 1234

Let’s try running the program and see what it is doing.

5.2. Forking a process and reaping children!

Now, let’s look at the program forky.c. This program gives an example of registering a SIGCHLD signal handler that helps us reap child processes after they have completed execution. This is going to be how your shell program should behave when it needs to reap background processes once they exit. Let’s walk through this program and the comments.

The man page for signal lists the signals on this system and describes the signal system call in more detail. Read through the following section on Signals in the textbook as well to help understand how SIGCHLD works.

6. Building a shell

Now that we can tokenize command line strings, let’s put together the rest of the pieces for executing user commands. Your shell should support the following features:

6.1. Running commands in the foreground

When a command is run in the foreground, for example:

cs31shell> ./sleeper 2
  • Your shell program should fork() a child process to execute sleeper and then wait until the child process exits before proceeding.

  • You can accomplish this by calling waitpid in the parent (your shell) by passing in the pid of the child process (the return value of fork()).

6.2. Running commands in the background.

When a command is run in the background, for example:

cs31shell> ./sleeper 3 &
  • Your shell program should fork() a child process to execute sleeper, but it should NOT wait for the child to exit.

  • Instead, after forking the child process, it should immediately return to step 1 (print out the prompt and read in the next command line). The child process will execute the command concurrently while the parent shell handles other command(s).

  • Your shell must still reap background processes after they exit, so you can’t just forget about them! When a child that was run in the background exits, your shell program will receive a SIGCHLD signal. You should install a SIGCHLD handler that will call waitpid() to reap the exited child process(es).

  • Please look through the weekly lab code forky.c and the Signals section in the textbook to help implement reaping background processes.

  • Your shell should be able to run any number of processes in the background, so if you type in quick succession:

cs31shell> ./sleeper &
cs31shell> ./sleeper &
cs31shell> ./sleeper &
cs31shell> ps

The ps program output should list all three sleeper child processes.

6.3. Built-in commands

Your shell should respond to the following three built-in commands on its own. It should not fork off a child to handle these!

  1. exit: Terminate the shell program. You can print out a goodbye message, if you’d like.

  2. history: Print a list of the user’s MAXHIST most recently entered command lines. (Note: blank lines should not be added to the history.)

  3. !num (where num is an actual number, e.g., !5): Re-execute a previous command from the history.

    • The previous command could be a run-in-the-foreground, run-in-the-background, or a built-in command that your shell should execute appropriately.

    • The command line retrieved from the history list should be added to the history list. That is, executing !5 should not put !5 in the history list; instead, a copy of the command line associated with command ID 5 from the history list should be added to the history list. See the sample output below for some examples of history and !num commands.

In all three cases, as long as the first argument matches the built-in command, you should run the built-in command. You do not need to check if there are extraneous arguments after the built-in command. For example, exit now will trigger the exit built-in command, and history -a will trigger the history built-in command.

6.4. Requirements

  • Your shell should support running programs in the foreground (e.g. ls -l)

  • Your shell should support running programs in the background (e.g. ./sleeper &)

  • Your shell should support the built-in command exit to terminate.

  • Use the execvp version of exec for this assignment and waitpid instead of wait. See the "Tips" section below for examples.

  • You need to add a signal handler for SIGCHLD signals so that you can reap exited child processes that are run in the background. You should not leave any long-term zombies!

  • Whenever your code calls a library or system call function that returns a value, you should have code that checks the return value and handles error values. You can call exit for unrecoverable errors, but print out an error message first (printf or perror for system call error return values).

The only global variables allowed are those associated with the history list and its state. All other program variables should be declared locally and passed to functions that use them.
  • For full credit, your shell should use good modular design, be well-commented and free of valgrind errors. The main function should probably not be much longer than that in the starting point code. Think about breaking your program’s functionality into distinct functions.

6.5. Example Output

It may be helpful for you to take a look at Tia’s sample output.

6.6. Tips

  • Implement and test incrementally (and run valgrind as you go). Here is one suggestion for an order to implement:

    1. Add a call to your library from part 1 to parse the input line into argv strings.

    2. Add support for the built-in command exit.

    3. Add support for running commands in the foreground (the parent process, the shell, waits for the child pid that it forks off to exec the command).

    4. Add support for running commands in the background (the parent process, the shell, does NOT wait for the child pid that it forks off to run the command).

      1. After forking off a child to run the command, the shell program should go back to its main loop of printing out a prompt and waiting for the user to enter the next command.

      2. You will need to add a signal handler on SIGCHLD so that when the process that is running in the background terminates, the shell reaps it.

      3. Use waitpid to reap all child processes. Use the sleeper program to test

        cs31shell> ./sleeper &
        cs31shell> ./sleeper 2 &
        cs31shell> ps w
    1. Add support for !num built-in command that will run command num from your history list. num is a command ID, which is increasing as your shell runs commands. It is NOT the bucket index into the history list.

  • The maximum length of a command line is defined in parsecmd.h. You can use the MAXLINE constant in your program.

  • Use the functions execvp(), waitpid(), and signal() for executing programs, waiting on children, and registering signal handlers. Note that the first argument to waitpid is the process id (PID) of the process you’d like to wait for, but if you pass in an argument of -1, it will wait for ANY reapable child process. This is useful inside your SIGCHLD handler, where you won’t know which child (or children) exited. You can pass it WNOHANG as the third parameter to prevent it from blocking when there are no children to reap.

  • Remember that if you dynamically allocate space for a string (using malloc), you need to allocate a space at the end for the terminating null character (\0), and that you need to explicitly free the space when you are done using it (call free). Since your parsing library is allocating memory for the argv list, it’s up to your shell to free that memory when it’s done with it.

  • You can call fflush(stdout); after any calls to printf to ensure the printf output is written immediately to the terminal. Otherwise, the C standard I/O library might buffer it for a short while.

  • When in doubt about what your shell should do, try running the command in the bash shell (a standard system terminal) and see what it does.

7. Submitting

Please remove any debugging output prior to submitting.

To submit your code, simply commit your changes locally using git add and git commit. Then run git push while in your lab directory. Only one partner needs to run the final push, but make sure both partners have pulled and merged each others changes. See the section on Using a shared repo on the git help page.