Homework #7¶
Due: Friday, December 1st at 11:59pm
In this assignment, we will wrap up our shell implementation by implementing signals to handle job control and a few built-in shell commands.
CS Linux Machine¶
You will need access to an Linux based machine when working on your homework assignments. You should not test your programs on macOS or Windows Linux because these operating systems do not provide all utility commands necessary for completing this and possibly future assignments. Additionally, if they do provide a command then it may not contain all options that a Unix-like system provides. We will use and grade all assignments on the CS Linux machines and all programming assignments must work correctly on these machines. However, you can work locally on a Unix or Unix-like machine but ensure that you test your final solutions on a CS Linux machine.
Please follow the instructions provided here
Creating Your Private Repository¶
There is no new repository to create for this assignment. You will continue to use the repository you created in homework #5.
Short Answer Questions¶
We decided not to assign short answer questions for this assignment to give you more time to complete the coding portion.
Task 1: History Module (history.h
and history.c
)¶
You will implement the history
command/feature included in most shells. To get started, create and copy the following code into the include/history.h
file:
#ifndef _HISTORY_H_
#define _HISTORY_H_
extern const char *HISTORY_FILE_PATH;
//Represents the state of the history of the shell
typedef struct history {
char **lines;
int max_history;
int next;
}history_t;
history_t *alloc_history(int max_history);
void add_line_history(history_t *history, const char *cmd_line);
void print_history(history_t *history);
char *find_line_history(history_t *history, int index);
void free_history(history_t *history);
#endif
The header must contain the following definitions where you can add new fields but cannot delete or modify the function prototypes and types. Make sure to include a documentation header for each function prototype. To help you get started with the implementation file, create and copy the following code into the src/history.c
file:
#include "history.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *HISTORY_FILE_PATH = "../data/.msh_history";
The constant variable HISTORY_FILE_PATH
contains the path to where you will store the history after the shell exits. You must use this variable in your implementation.
alloc_history
function¶
Most shells store the users history when they logout in a file under their home directory (e.g., your bash history is located here: ~/.bash_history
). For msh
, you will create and store the history here: data/.msh_history
(i.e., inside the data
directory of your repository in a file named msh_history
). The alloc_history
function creates a new history_t
state by
Allocating memory for a new
history_t
value.Allocating memory for array of command line strings (
char **lines
). Make the size of this array equal to themax_history
argument. The history must be ordered such that the oldest command line history starts at index zero to the most recent command line history being at the last empty index.We recommend you make the
next
field of thehistory_t
state point to the next available slot.The function must also open the
HISTORY_FILE_PATH
file and load all prior history intochar **lines
up untilmax_history
is reached. This means ifdata/.msh_history
has 14 entries but nowmax_history=5
then only the 5 oldest lines are loaded intochar **lines
.
The function returns the allocated history_t
state.
add_line_history
function¶
The add_line_history
adds the parameter cmd_line
to the history of lines. If cmd_line
is empty then do not add it to the history. If the history of lines is full (i.e., all elements in char **lines
is taken) then the function always deletes the oldest command line history (i.e., always at index 0). The function should shift all elements over by one and then insert the cmd_line
at the last element in char **lines
.
Do not add the ``exit`` command to your history. The function must return if given exit
and not add it to the history lines.
print_history
function¶
We provide the implementation of print_history
below:
void print_history(history_t *history) {
for(int i = 1; i <= history->next; i++) {
printf("%5d\t%s\n",i,history->lines[i-1]);
}
}
Please use this implementation in your code. History listing always starts with 1-indexing similar to bash.
find_line_history
function¶
The find_line_history
returns the command line at the specified index
. If index
is not within the range of [1,max_history]
inclusive then the function returns NULL
. History uses indexing starting at one but the char **lines
start at zero-indexing so be mindful of this difference.
free_history
function¶
The free_history
functions frees the history_t
and all allocated memory associated with the state. However, before freeing the memory, the function must save all history inside the char **lines
array to the HISTORY_FILE_PATH
file. The oldest command line history starts at line one in the file until the most recent command line history at the end of the file.
Testing the history
module¶
Professor Samuels has given you test_history.c
file inside the Ed Post for homework #7. Place the test_history.c
under the tests
directory and compile and run it as follows:
$ gcc -I../include/ -o test_history test_history.c ../src/history.c
$ ./test_history
Update the evaluate
function¶
Make sure to update the evaluate
function to include the command lines coming into the evaluate function. Remember a command line as such: ls -la . ; sort bigfile & cd ..
counts as one history line and not three; although, there are three jobs on the line.
Task 2: Signal Handlers Module (signal_handlers.h
and signal_handlers.c
)¶
Currently, the way we handle background jobs is naive; therefore, we will update this code to use signals, which is a better mechanism. To help you get started, we have provided the skeleton code for signal handling. Create and copy the following code into the include/signal_handlers.h
file:
#ifndef _SIGNAL_HANDLERS_H_
#define _SIGNAL_HANDLERS_H_
void initialize_signal_handlers();
#endif
You shouldn’t need to add any additional functions here but you can if you wish. Create and copy the starter implementation into src/signal_handlers.c
:
#include "signal_handlers.h"
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
* Citation: Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition
*/
void sigchld_handler(int sig)
{
}
/*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
* Citation: Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition
*/
void sigint_handler(int sig)
{
}
/*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
* Citation: Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition
*/
void sigtstp_handler(int sig)
{
}
/*
* setup_handler - wrapper for the sigaction function
*
* Citation: Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition
*/
typedef void handler_t(int);
handler_t *setup_handler(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0) {
perror("Signal error");
exit(1);
}
return (old_action.sa_handler);
}
void initialize_signal_handlers() {
// sigint handler: Catches SIGINT (ctrl-c) signals.
setup_handler(SIGINT, sigint_handler); /* ctrl-c */
// sigtstp handler: Catches SIGTSTP (ctrl-z) signals.
setup_handler(SIGTSTP, sigtstp_handler); /* ctrl-z */
// sigchld handler: Catches SIGCHILD signals.
setup_handler(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
}
Update your alloc_shell
function to call initialize_signal_handlers
to setup the signal handlers. Read over the documentation above each signal handler code to make sure you understand when those functions will be called. Additionally, make sure to watch the pre-recorded videos on signals because they provide insight about how to think about implementing these functions.
You can remove the code in evaluate
that checks for background jobs (also in exit_shell
). The signal handlers will now handle the updating of job list based on when a child process exits. Shell must not exit
until all background jobs complete. Make sure to still include this requirement in your code.
Task 3: Built-in Commands¶
Your msh
must support a series of built-in shell commands. We recommend that you implement a function inside shell.c
called char *builtin_cmd(char **argv)
that takes in your argv
array to handle processing any built-in commands. It retunrs char *
because the !N
may want to return the chosen comamnd line history. In all other cases you can return NULL
to indicate nothing additional should happen.
You must implement the following 6 built-in commands:
jobs
command - Thejobs
command lists all the jobs currently active. It must look similar to Bash’sjobs
command but with one slight difference. Each line must have the format:[JOB_ID] PID STATE CMD_LINE
, whereJOB_ID
is theint jid;
from thejob_t
entry,PID
is the process id for the job,STATE
is eitherRUNNING
if the job is currently running orStopped
if the job is currently suspended, andCMD_LINE
is the full command line string for the job. For example, if the user enters in/usr/bin/sort -n LARGE_NUM_FILE.txt &
and its the first job entered with apid=2343
then its output would be:[1] 2343 RUNNING /usr/bin/sort -n LARGE_NUM_FILE.txt
. In ourmsh
, the only jobs you will see here are jobs that are background jobs or jobs that were suspended that could either be background or foreground jobs.history
command - Callsprint_history
to print the current shell history.!N
command - This command is the history expansion command whereN
is a line number from thehistory
command. The command reruns theN
command from the user’s history list. Similiar to ``exit``, Do not add !N command to the history of the user. This command must display to the user the command that was chosen to be ran. The command line chosen must be executed again by theevaluate
function.bg <job>
command - restarts<job>
by sending it aSIGCONT
signal, and then runs it in the background. The<job>
argument can be either a PID or a JOB_ID (i.e., you specify it as%JOB_ID
).fg<job>
command - restarts<job>
by sending it aSIGCONT
signal, and then runs it in the foreground. The<job>
argument can be either a PID or a JOB_ID (i.e., you specify it as%JOB_ID
).kill SIG_NUM PID
command - Calls thekill
function, withSIG_NUM
equal to either2``(``SIGINT
),9
(SIGKILL
),18``(``SIGCONT
), or19``(``SIGSTOP
). Any otherSIG_NUM
number must produce the error message:error: invalid signal number
to the user.
Hints & Tips¶
When you implement your signal handlers, be sure to send
SIGINT
andSIGTSTP
signals to the entire foreground process group, using-pid
instead ofpid
in the argument to the kill function.- You now may want to define a function
waitfg
that waits for the foreground process to complete. One of the tricky parts of the assignment is deciding on the allocation of work between thewaitfg
and sigchld handler functions. We recommend the following approach: In
waitfg
, use a whilte(1) loop around the sleep function.In
sigchldhandler
, use exactly one call towaitpid
.
While other solutions are possible, such as calling
waitpid
in bothwaitfg
andsigchld
handler, these can be very confusing. It is simpler to do all reaping in the handler.- You now may want to define a function
In
evaluate
, the parent must usesigprocmask
to blockSIGCHLD
signals before it forks the child, and then unblock these signals, again usingsigprocmask
after it adds the child to the job list by callingadd_job
. Since children inherit the blocked vectors of their parents, the child must be sure to then unblockSIGCHLD
signals before it execs the new program.The parent needs to block the
SIGCHLD
signals in this way in order to avoid the race condition where the child is reaped bysigchld
handler (and thus removed from the job list) before the parent callsadd_job
.Programs such as
more
,less
,vi
, andemacs
do strange things with the terminal settings. Don’t run these programs from your shell. Stick with simple text-based programs such as/bin/ls
,/bin/ps
, and/bin/echo
for testing purposes.When you run your shell from the standard Unix shell, your shell is running in the foreground process group. If your shell then creates a child process, by default that child will also be a member of the foreground process group. Since typing ctrl-c sends a
SIGINT
to every process in the foreground group, typing ctrl-c will send aSIGINT
to your shell, as well as to every process that your shell created, which obviously isn’t correct.Here is the workaround: After the fork, but before the
execve
, the child process should callsetpgid(0, 0)
, which puts the child in a new process group whose group ID is identical to the child’s PID. This ensures that there will be only one process, your shell, in the foreground process group. When you type ctrl-c, the shell should catch the resultingSIGINT
and then forward it to the appropriate foreground job (or more precisely, the process group that contains the foreground job).
Testing¶
We provide test code for testing the history
feature of the shell but we cannot provide other test cases easily for the remaining features. We will have to test this by hand since the new features require user interaction. However, please make sure test_msh.sh
and other test_***.c
files still work.
You will need to do your own testing of the built-in commands since many require user interaction.
Grading¶
Programming assignments will be graded according to a general rubric. Specifically, we will assign points for completeness, correctness, design, and style. (For more details on the categories, see our Assignment Rubric page.)
The exact weights for each category will vary from one assignment to another. For this assignment, the weights will be:
Completeness: 60%
Correctness: 25%
Design/Style: 15%
Submission¶
Before submitting, make sure you’ve added, committed, and pushed all your code to GitHub. You must submit your final work through Gradescope (linked from our Canvas site) in the “Homework #7” assignment page via two ways,
Uploading from Github directly (recommended way): You can link your Github account to your Gradescope account and upload the correct repository based on the homework assignment. When you submit your homework, a pop window will appear. Click on “Github” and then “Connect to Github” to connect your Github account to Gradescope. Once you connect (you will only need to do this once), then you can select the repository you wish to upload and the branch (which should always be “main” or “master”) for this course.
Uploading via a Zip file: You can also upload a zip file of the homework directory. Please make sure you upload the entire directory and keep the initial structure the same as the starter code; otherwise, you run the risk of not passing the automated tests.
Note
For either option, you must upload the entire directory structure; otherwise, your automated test grade will not run correctly and you will be penalized if we have to manually run the tests. Going with the first option will do this automatically for you. You can always add additional directories and files (and even files/directories inside the stater directories) but the default directory/file structure must not change.
Depending on the assignment, once you submit your work, an “autograder” will run. This autograder should produce the same test results as when you run the code yourself; if it doesn’t, please let us know so we can look into it. A few other notes:
You are allowed to make as many submissions as you want before the deadline.
Please make sure you have read and understood our Late Submission Policy.
Your completeness score is determined solely based on the automated tests, but we may adjust your score if you attempt to pass tests by rote (e.g., by writing code that hard-codes the expected output for each possible test input).
Gradescope will report the test score it obtains when running your code. If there is a discrepancy between the score you get when running our grader script, and the score reported by Gradescope, please let us know so we can take a look at it.