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:
-
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, the shell will fork a child process to execute the command.
-
If it is a foreground process, the shell will wait for the child process to finish and cleanly exit.
-
Otherwise, the shell does not wait for the child to exit, but instead gets notified by an O.S. signal handler.
-
-
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 executesleeper
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 offork()
).
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 executesleeper
, 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 aSIGCHLD
handler that will callwaitpid()
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!
-
exit
: Terminate the shell program. You can print out a goodbye message, if you’d like. -
history
: Print a list of the user’sMAXHIST
most 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 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 andwaitpid
instead ofwait
. 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
orperror
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:-
Add a call to your library from part 1 to parse the input line into
argv
strings. -
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 testcs31shell> ./sleeper & cs31shell> ./sleeper 2 & cs31shell> ps w
-
-
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 theMAXLINE
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 towaitpid
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 theargv
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 toprintf
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.