nvysh : The Navy Shell
Table of Contents
- Project Preliminaries
- Project Description
- Managing Terminal Signals
- Waiting/Killing on all processes in a process group
- Waiting on state changes other than termination
- Prompting with
readlinelibrary to make your terminal nicer
- More Tips
Project Learning Goals
The goal of this project are:
- To write a pipeline shell with background/foreground control
- Parse pipelines with
- Manage process groups
- Manage terminal control
- waiting/killing processes within process groups
- Using sprintf()/readline() for prompting
Project Grading and Due Date
This project is graded out of 100 points and is due on Mon. 3 Apr at 2359. Late submissions will not be allowed.
We will apply the following grading rubric to this project
- 70% Complete a
nvyshsuch that it can parse and execute an arbitrary command without pipes but does not place the process in its own process group
- 85% Complete a
nvyshsuch that it can parse and execute arbitrary pipeline commands but does not place the process in its own process group.
- 92% Complete a
nvyshsuch that it can parse and execute arbitrary pipeline commands and places the pipeline in its own process group.
- 100% Complete a
nvyshsuch that it can parse and execute arbitrary pipeline commands and places the pipeline in its own process group with terminal management, allowing for just a single background process.
- You must provide a Makefile to compile your program
- You must ensure that there are no memory leaks
- You must provide a
READMEfile for your program describing tasks completed and processes used. This is also the place to provide additional details to your grader.
- Run the following command in your terminal
- Then change into the following directory
- You will find all the material you need to complete this lab in that directory.
- During the course of this lab, we will refer to the
ic221/proj/02as the project directory
To submit this lab you will place all relevant content into your lab directory:
Then issue the submission script
Select the option for
confirm. If you see
SUCCESS at the
end. You may submit multiple times up until the submission deadline.
Only your final submission will be considered for grading.
In this project, you will implement a pipeline capable shell with
minimal job control, the Navy Shell or
nvysh. Your shell should be
able to perform the following tasks:
- Parse a command line user input using string tokenizer and separate individual commands along a pipeline
- Unroll the pipeline using
- Establish each of the child process in a process group
- Wait on the process group to fully complete
- Allow for terminal signaling to the foreground process group
- Allow for one backgrounded/stopped command that can be moved to the foreground.
As there are many moving parts here, we strongly recommend you build simpler, smaller versions of the shell building up to a full complete model.
Here's some demo of a working version of the shell. In this example we can put together arbitrary pipelines.
aviv@saddleback: sol $ ./nvysh nvysh (-1) $ ls / bin boot cdrom courses devetc home initrd.img initrd.img.old lib lib64 lost+found media mnt optproc root run sbin snap srv sys tmp usr var vmlinuz vmlinuz.old xtras nvysh (-1) $ ls / | head -2 bin boot nvysh (-1) $ ls / | head -2 | tail -1 boot nvysh (-1) $ ls / | head -2 | tail -1 | cat | cat | cat | cat | cat boot nvysh (-1) $ ^C aviv@saddleback: sol $
Further, as you are required to wait until all processes in the group complete, the following command should run for 3 seconds:
nvysh (-1): sol $ sleep 1 | sleep 3 | sleep 1
There is some additional details below on how to do this with
With job control, all process in the pipeline should be in the same
process group, which you can see based on the
ps output. When the
^Z, all the processes in the foreground process group
should stop and the process group leader's (or the pgid) should be
printed in the parenthesis within the prompt, indicating there is a
background process. If there is not a background process, then -1 is
used in the prompt.
After a process group has been stopped, the shell should return and prompt the user for more input.
aviv@saddleback: sol $ ./nvysh nvysh (-1) $ cat | cat | head ^Z nvysh (10008) $ ps -o pid,pgid,cmd PID PGID CMD 1900 1900 -bash 9978 9978 ./nvysh 10008 10008 cat 10009 10008 cat 10010 10008 head 10073 10073 ps -o pid,pgid,cmd nvysh (10008) $
If the user issues a
fg command and there is a background process
group, the background process group is moved to the foreground via a
kill() command by sending the
SIGCONT signal. The shell then
waits for that process group to finish before proceeding, just like
it does with any normal command.
nvysh (10008): sol $ fg hello hello world world ^D nvysh (-1) $ ps -o pid,pgid,cmd PID PGID CMD 1900 1900 -bash 9978 9978 ./nvysh 10156 10156 ps -o pid,pgid,cmd
It's important that there cannot be more than one background process
at a time. So, if a process stops while there is already a
background process, the foreground process group should be continued
kill() command and not allowed to be stopped. Meanwhile,
the shell continues to wait for the foreground process group to
finish, like normal.
nvysh (-1) $ cat ^Z nvysh (10360): sol $ cat ^Z^Z^Z^Z^Z^Z^Z^Z^C nvysh (10360): sol $ fg ^C nvysh (-1) $
When there is no background process, as indicated with a
-1 in the
parenthesis, an error should be reported when the user issues a
nvysh (-1) $ fg ERROR: no background process
Managing Terminal Signals
A big part of this project that differs from the labs is that you
will need to manage terminal signals in addition to parsing and
unrolling a pipeline. This requires specifying a process group as
the foreground process group such that it can receive the terminal
signals generated from
There is a new system call that does that:
tcsetpgrp(). The "tc"
tcsetpgrp() stands for "terminal control" and the system call
takes two options: a file descriptor for the terminal
is usually 0 for standard input, and a
pgrp for the foreground
process group identifier (a pid).
int tcsetpgrp(int fd, pid_t pgrp);
Transferring control the terminal should solely be the domain of
your shell. The child should never call
tcsetpgrp(). The shell
tcsetpgrp() in the following situations
- When control of terminal is going from the shell to the child process, i.e., the child is becoming the foreground process.
- When control of the terminal is going from the child process to the shell, i.e., the shell is becoming the foreground process.
There is one more property of
tcsetpgrp() that you must consider
in regards to what happens when a background process tries to
reclaim the foreground, as your shell will do when a child process
group completes. Here's an excerpt from the man page:
If tcsetpgrp() is called by a member of a background process group in its session, and the calling process is not blocking or ignoring SIGTTOU, a SIGTTOU signal is sent to all members of this background process group.
To prevent your shell from stopping, the default action of
signal, whenever it tries to reclaim the foreground, you will need
Waiting/Killing on all processes in a process group
Another important part of this project is the ability to write a
wait routine to wait on all process in a process group. To do
that, you will use the
waitpid() system call, which is a slightly
more powerful form of the
wait() system call.
waitpid() if you were to negate a pid, as in,
waitpid(-2020, &status) then you will wait on a status change from
any process in the process group identified by -2020. Of course, you
could also pass a variable as the pid value, and negate it in the
same way, and you can also use the return value to check which child
//wait on all children in the process group pgid //the return value is the pid of the child whose status changed cpid = waitpid(-pgid,&status,NULL);
The final step is determining if any of the children in the process
group are still running, or have they all been waited upon? That can
be done easily by checking the return value of
wait(), returns a negative error value if their are no
children to wait upon.
cpid = waitpdi(-pgid, &status,NULL); if(cpid < 0) printf("All children in the process group have terminated\n"); else printf("Child %d in process group %d just changed state\n", cpid,pgid);
Similar to the
waitpid() system call syntax for process groups,
kill() also uses negative
pid's to indicate that you wish to
send a signal to all process in the process group.
//send SIGCONT to all processes in the process group pgid kill(-pgid, SIGCONT);
Waiting on state changes other than termination
Normally, when we call
wait() we only care about one state change
of the child: did the child terminate yet? But this project is going
to require you to also consider state changes from running to
stopped, that is, when the user types
^Z to send
You can also wait on that event using
waitpid() by adding
WUNTRACED option. For example, if you were waiting on a state
change from running to stopped of any process in a process group,
you might end up with the following code:
cpid = waitpid(-pgid,&status, WUNTRACED);
where, like before, the return value is which process changed state
to stopped. However, if it is the case that they were stopped via
^Z and you managed your terminal signaling properly, then all
processes in the foreground process group should have received the
readline library to make your terminal nicer
As with the
mini-sh lab, you're more than welcome to use the
readline library to make your shell more user friendly. This will
allow you to use the arrow keys left and right to edit input as well
as provide a command history by using arrow up and down.
readline, first include the appropriate headers:
#include <readline/readline.h> #include <readline/history.h>
In your main function, you can prompt and get user input in one step:
char * input; input = readline("Enter input> "); //... free(input);
Whatever is passed to the
readline() function is prompted to the
screen, and whatever the user entered, is returned as a
which you can assign to a variable of your choosing.
Importantly, the string that is returned by
readline() is created
malloc(), so you'll need to
free() it at some point to
avoid a memory leak.
The last piece of the prompting is to include the background process
pgid in the prompt. Normally, you'd do this by using a
printf(), but the
readline() command does not take a format, it
takes a string. You'll need to do the formatting differently.
The right tool for this job is
sprintf(), which like
will format print a string, but instead of printing it to
will store the result in another string. For example, here's how I
produced the prompt in the examples above:
char prompt[64+PATH_MAX]; //PATH_MAX is a pre-defined length of the path char cwd[PATH_MAX]; getcwd(cwd,path_max); //get the current working directory sprintf(prompt, //store result of formating in string prompt "nvysh (%d): %s $ ", //the format string bg_pgid, //the process group id of the background process basename(cwd)); //the basename of the current working directory input = readline(prompt); //prompt with the prompt
- Much of this project is figuring out how to interact between the previous labs. Take a close look at the key parts of those labs before proceeding.
- You may find it useful to use a linked list for your parsing. This way you can parse all the commands and then perform the pipeline unrolling. But you could also use the the parsing methods from the lab.
- You can assume that all command lines have no more than 1024
arguments. This will make setting up the
argvarray more straightforward.
- You may want look into
strtok_r(), which allows you to tokenize the same string multiple times, but there are other ways to do your parsing.
valgrindto find memory errors and violations as these may cause strange errors down the road.
- The man pages are your best friend. Read them carefully, and scroll all the way down. There may be some sample code in there.