The Shell Lab in Computer Systems: A Programmer's Perspective (CSAPP) at CMU is one of the most interesting and rewarding labs in the course. It involves writing a Unix-like shell that can handle job control, including running jobs in the foreground or background, handling signals, and managing jobs (processes).
A shell is a command-line interpreter that allows users to interact with the operating system. It runs user commands, manages job control (foreground/background processes), and handles signals like SIGINT
(Ctrl-C) and SIGTSTP
(Ctrl-Z).
Key concepts to implement:
quit
, jobs
, bg
, and fg
.fork
and exec
.SIGCHLD
(child status change), SIGINT
, and SIGTSTP
.Start by copying the provided files and reviewing tsh_helper.h
and csapp.h
. These files provide useful functions and constants for job management and signal handling.
The first task is to parse user commands. The function parseline
provided in the lab splits the input command into tokens and returns whether it should be run in the background or foreground. Your shell will have to handle both built-in and external commands.
Here's a simple command-line parser (eval
) that decides whether to execute a built-in command (like quit
, jobs
, bg
, or fg
) or an external command:
void eval(const char *cmdline) {
parseline_return parse_result;
struct cmdline_tokens token;
// Parse command line
parse_result = parseline(cmdline, &token);
if (parse_result == PARSELINE_ERROR || parse_result == PARSELINE_EMPTY) {
return;
}
// Determine the type of command and handle it accordingly
if (token.builtin == BUILTIN_QUIT) {
exit(0); // Quit the shell
} else if (token.builtin == BUILTIN_NONE) {
run_external_command(cmdline, token, parse_result); // External command
} else if (token.builtin == BUILTIN_JOBS) {
handle_jobs(token); // List jobs
} else { // 'fg' or 'bg' commands
handle_fg_bg(token);
}
}
To run external commands, you need to fork a child process and then use execve
to run the command in the child. You also need to handle I/O redirection if the user specifies input/output files with >
or <
.
Here’s how to implement the function that runs external commands:
void run_external_command(const char *cmdline, struct cmdline_tokens token, parseline_return parse_result) {
pid_t pid;
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &prev_mask); // Block all signals
if ((pid = fork()) == 0) { // Child process
setpgid(0, 0); // Set process group ID to avoid terminal issues
// Handle I/O redirection
if (token.infile) {
int in_file = open(token.infile, O_RDONLY);
if (in_file < 0) {
perror(token.infile);
exit(1);
}
dup2(in_file, STDIN_FILENO);
close(in_file);
}
if (token.outfile) {
int out_file = open(token.outfile, O_WRONLY | O_CREAT | O_TRUNC, DEF_MODE);
if (out_file < 0) {
perror(token.outfile);
exit(1);
}
dup2(out_file, STDOUT_FILENO);
close(out_file);
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // Unblock signals
if (execve(token.argv[0], token.argv, environ) < 0) {
perror(token.argv[0]);
exit(1);
}
}
// Parent process: manage jobs based on foreground/background
if (parse_result == PARSELINE_FG) {
add_job(pid, FG, cmdline); // Add to foreground
jid_t jid = job_from_pid(pid);
while (job_exists(jid) && job_get_state(jid) == FG) {
sigsuspend(&prev_mask); // Wait for the job to finish
}
} else {
add_job(pid, BG, cmdline); // Add to background
jid_t jid = job_from_pid(pid);
printf("[%d] (%d) %s\\\\n", jid, pid, cmdline);
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // Unblock all signals
}
This function: