Lab 7 Goals:
- Demystify how a Unix shell program works by writing one, including the
following features:
- Executing commands that run in the foreground and the background.
- Executing previous commands by interacting with the shell history (using
the !num syntax.)
- Learn how to create and reap processes with the fork, execvp,
waitpid system calls.
- Interact with signals and write signal handler code.
- Gain more expertise with gdb and valgrind for debugging C programs.
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:
- Print a prompt and wait for the user to type in a command.
- Read in the command string entered by the user.
- Parse the command line string into an argv list.
- 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).
- Otherwise, if it's not a built-in command, fork a child process to execute
the command and wait for it to finish (unless the command is to be run in the
background, then the shell doesn't wait for the child to exit).
- Repeat until the user enters the built-in command exit to exit the
shell program.
You will be implementing steps 1, 2, 4, 5, and 6 above.
Step 3 will be accomplished by a call to the parse_cmd library.
Using the parse_cmd library.
In parsecmd.h is the function prototype for the parse_cmd function that you can use to construct the argv array of strings from the command line string.
The argv array is passed into execvp.
The function comment describes how to call the function.
There are also constant definitions that you can use in your shell.
(Note: parsecmd.h is already #included at the top of cs31shell.c, so your code can use anything it defines.)
Note: The parse_cmd function is already implemented for you.
It is compiled into the parsecmd.o binary that is linked into your cs31shell executable when you type make:
you do not need to implement the parse_cmd function, but just call it in your code when you want to use it
(much like you can call printf in your code without implementing the printf function).
Building a shell.
Your shell should support the following features:
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()).
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).
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.
- 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!
- exit: Terminate the shell program. You can print out a goodbye
message, if you'd like.
- history: Print a list of the user's 10 more recently entered
command lines. (Note: blank lines should not be added to the history.)
- !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 should be added into
the history list (i.e. 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.
History.
Your shell program should keep a list of the 10 most recently entered
command lines by the user (use a #define constant named MAXHIST whose value is
10). The built-in history command should print out the history list in
order from first (oldest) to last (most recently entered) command. For each
element in the list, it should print the command ID and its corresponding
command line string. The command ID is an ever-increasing number, and each
command should increment the command ID by one. I suggest using an unsigned
int to store this value (don't worry, I'm not going to try executing 4 billion
commands in an attempt to overflow this value).
Your history list should be implemented as circular queue of strings: a circular array of MAXHIST buckets where new commands are added
to one end and old ones removed from the other end.
Users can request the execution of a previous command, as stored in the
history list, by executing the built-in !num, where "num" is a number
corresponding to a history command ID. Upon receiving such a command:
- Check whether your history list contains a command with a matching command ID.
Remember that the command ID is not the position in the history list, it is
the unique number of the command in your shell's execution history (i.e. !5
is the 5th command run by your shell, !34 is the 34th command run by your
shell). You will probably need an additional global variable to track this.
- If a matching command ID is not in the history queue, print out an error message.
- Otherwise, use the command line from the matching history command ID, and
re-execute it (this command now also becomes the most recent command to add
to your history list).
If the command from the history list is not a built-in command, then your
shell should run it just like it does any foreground or background program
(parse its command line into argv list, fork-execvp and waitpid or not).
If the command from the history list is a built-in command (which could
only be the history command), then it should execute it directly just
like any built-in command.
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.
- Your shell should support the built-in command history that keeps
a list of the MAXHIST most recently entered command lines entered by the user.
Use a constant definition for MAXHIST, and submit your solution with it set to
10, but try your shell out with other sizes too.
- Your shell should support running commands from the history using
!num syntax, where num is the command ID of a command from
your command history (e.g. !33 should execute the command with command
ID 33 from your history list). If a matching command num is not found the
current history list, then your shell should print out and error message
(command not found), otherwise the command line from the matching command on
the history list should be executed (again).
- 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.
Example Output
It may be helpful for you to take a look at Tia's sample output, particularly to see how the built-in history features work.
Tips
- Implement and test incrementally (and run valgrind as you go). Here
is one suggestion for an order to implement:
- Figure out how to call parse_cmd() to split up the command line input.
- Add support for the built-in command exit.
- 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).
- 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). 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. 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. Use waitpid to
reap all child processes. Use the sleeper program to test:
cs31shell> ./sleeper &
cs31shell> ./sleeper 2 &
cs31shell> ps w
- Add support for the history list (implemented as a circular queue). The
operations on your circular queue are slightly different when the queue is
not yet full from when it is full and you need to replace the oldest entry
each time a new command is entered. Think about all state you will need to
keep to keep track of the first element in the list (for printing out), the
next insertion spot, and the end of the list.
- 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.
- 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.
- 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.
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.