Shell Lab


1. Features

2. Details

2.1 Command Execution

The shell prints a prompt and waits for a command line. The command line consists of command and 0 or more arguments separated by one or more spaces. The arguments are optionally followed by an ampersand &

Four commands are to be built-in in the shell. If command is a built-in command, the shell program handles it immediately, then prints a prompt and waits for the next command line.

Otherwise, the shell creates a child process to load and execute the program for command. This child process is made the group leader of a new process group in the session. The shell adds this process group to the job list. The process id (pid) for this job is the pid of the group leader (the new child process). The job id (jid) is the integer returned by the addjob function that is provided. Note that the job id is just the index in the array of jobs of the first free entry selected by addjobfor the new job.

Warning: There is a race condition between these two possible events:

The signal causes exceptional flow of control in the parent. A big problem occurs if this signal comes before the parent has added the new child to the job list. In that case, no job is removed from the job list. But after the signal, the parent will mistakenly add the terminated child to the job list!! This must be avoided.

2.2 Foreground Process Group

The foreground process group receives interrupt signals entered at the keyboard. E.g, ctrl-C will send the SIGINT (default action is to terminate) signal to the each process in the foreground process group and ctrl-Z will send the SIGTSTP (default action is to stop the receiving process - i.e., suspend but not terminate).

There can be only one foreground process group at a time, but which process group is the foreground can be changed.

2.3 Shell and Signals

Suppose the shell is currently the foreground process. If the shell has printed a prompt and is waiting for a command line, typing ctrl-C (or ctrl-Z) would send the SIGINT (or SIGTSTP) signal to the shell. These signals should be ignored by the shell. That is, the shell should not terminate because of SIGINT and not be suspended because of SIGTSTP.

However, during development of the shell there should be an easy way to terminate your shell itself in case it is misbehaving. There is another signal, SIGQUIT (ctrl-\) whose default action is to terminate the receiver. A real shell will also ignore the SIGQUIT (ctrl-\) signal, but your shell should print a message and exit if this signal is received. The provided code has a signal handler for SIGQUIT that does exactly this. (The default SIGQUIT action just terminates with no message.)

2.4 Quit and Other Built-in Shell Commands

The quit command should be built into your shell. In the function builtin_cmd you should check if the command from the command line is one of the commands your shell should handle directly - the built-in commands. If builtin_cmd discovers that the command entered is "quit", then just exit the shell.

Important: The builtin_cmd function should return 1 if the command is built-in and 0 otherwise. In the case of the "quit" command, builtin_cmd doesn't return at all. It exits. But for all the other built-in commands, make sure you return 1 and for any other commands, return 0.

2.5 The jobs Built-in Command

The jobs command should also be built into your shell. This command should list all the current jobs. The provided code has a function to do this: listjobs.

Of course, this will not work properly if you have not correctly added and/or deleted jobs from the array of jobs, but the implementation of the jobs command will only need to call the listjobs function.

The listing will display jobs like this example:

	    [1] (7923) Running myspin 10
	  

where the meaning is

	    [job id] (process group id) Job State command line
	  

The possible job states are

When the jobs command is executed, the shell is the foreground process. However, The shell itself is not entered in the job list. So the Foreground state will not show up in the listing - ony Running and/or Stopped.

2.6 The fg Built-in Command

The fg command is the third command to be built-in. The argument to the fg command should be the job id or the process id of the job. These are both integers, so to distinguish whether the integer is the jid or the pid, the jid should be preceded by a percent sign. For example,

 tsh> jobs
 [1] (7923) Running myspin 10 &	    
 [2] (7922) Stopped mysplit 4	    
 [3] (7925) Stopped myspin 8	    
 

The first jobs is running, but as a background process. The other two are suspended. To make the second job continue running and as the foreground process, you could use the fg command in either of these ways:


      tsh> fg %2
      tsh> fg 7922

	  

The fg command could also be applied to the first process. Even though it is already running, it is not the foreground process. The fg command can make it continue running, but as the foreground process.

Note that the shell was the foreground process, but this fg command makes process 7922 become the foreground process instead of the shell.

As noted above the provided code has an incomplete function, builtin_cmd, that checks if a command is one the shell should handle directly. The shell handles the "quit" command directly and this can be done in the builtin_cmd function. However, the "fg" (and "bg") commands are a bit more complex. So the provided code expects builtin_cmd to call another function, do_bgfg to handle "fg" and "bg".

2.7 The bg Built-in Command

The bg command is the final command to be built-in. This command is to be applied to a job that is stopped. Its action is to make the stopped job be running but as a background process. That is, it should continue the job, but not make it be the foreground process.

The argument to the bg command is just like the argument to fg.

2.8 All Other Commands

If the command is not built-in a new process will be created to execute the command. If the command line ends does not end with an ampersand, &, the new process should become the foreground process. If the command does end with &, the shell should remain the foreground process and the new process will therefore be running as a background process. You will need to call the parseline function to get the the command line strings into an array.

The parseline function reads the command line and puts the command and the command line arguments into an array. Importantly, it returns 1 if the command line ended with an & and 0 otherwise.

2.9 Shell Handling of its Child Processes

The shell waits for commands it executes as foreground processes, but not for those executed as background processes.

This means that many zombie processes can accumulate. The shell must clean up these zombie processes (when they terminate). Also if some process terminates because it gets a signal that it doesn't catch, the shell needs to print a message to the terminal with the job's pid and a description of the signal that terminated the process.

When a child terminates or is stopped (e.g. by ctrl-z), the shell should takes an appropriate action. In either case a SIGCHLD signal is sent to the parent. The provided code has a stub signal handler function, sigchld_handler for the shell to catch the SIGCHLD signal. In this function is where you will need to put the code to handle zombies and terminated or stopped jobs.

3. Implementation Notes

The shell code that is provided has some functions that are fully implemented and some that are only stubs. However, even the functions that are stubs have comments that specify what the function should do. You should read the comments for both kinds of functions.

The functions you will need to modify or complete are:

The main function takes care of printing the prompt and reading the command line from the user. Then the eval function is called with the command line passed as eval's single parameter.

3.1 Implementing eval - execvp

Call the parseline function to fill an array with the command line strings. This function also determines if the command line ends with an ampersand, returning 1 if so and 0 otherwise.

You will need to use the execvp function for a forked child process to execute the command line. The execvp function takes two arguments, the command name and an array of the strings typed on the command line. The array should have one more entry that is just the NULL value. The parseline function fills in an array that is suitable to pass to execvp.

3.2 Implementing eval - sigprocmask

The race condition that can occur when the shell is trying to add a child process to the job list can be avoided by temporarily blocking the SIGCHLD signal that the shell will receive when the child terminates or stops. After the child has been added to the job list, the shell should unblock this signal so that it can handle child termination or stoppage.

The sigprocmask function can be used to block or unblock sets of signals. You only need to block one signal, SIGCHLD. So you need to specify a set of signals that contains only SIGCHLD. One way to do this is to create an empty sigset_t with no signals, then add SIGCHLD. The sigemptyset function does the first step and the sigaddset can do the second.

A child does not receive pending signals sent to the parent, but it does inherit the blocked signal mask of its parent. So if the shell blocks SIGCHLD and then forks a child, the child needs to unblock SIGCHLD before calling execvp.

3.3 Implementing eval - setpgid

When the shell creates a child process, that child needs to create a new process group separate from from the shell. The setpgid function can be called by the child (after the return from fork, but before the call to execvp) to create a new process group with the child as the group leader

 pid_t id;
 ...
 id = getpid();
 setpgid(id, id);
      

This is necessary since the shell and its children need to handle signals differently. E.g., if the shell is currently the foreground process a SIGINT signal should be sent to the shell and not to the shell's children. This will be the the case provided the children are in a different process group.

3.4 Implementing fg

The fg command specifies either the job number or the pid of a background job to become the foreground job. The background job will either be running or stopped. If it is stopped, a SIGCONT signal should be sent to it. For this you need the pid. If the job number is given, then a function should first be called to get the pid.

Once the pid is determined, this process the SIGCONT signal can be sent if it is stopped. The process can then be made the foreground process by calling the function

int tcsetpgrp(int fd, pid_t pgid);	
      

where fd can be 0 (or 1 or 2) since each of these file descriptors is associated with the controlling terminal of the session and pgid is the pid of the process that is to become the foreground process. The return value is 0 for success or -1 for error.

What should the shell do after making the selected process the foreground process? It should wait by calling the function

	waitfg()
      

This function should wait by sleeping for 1 second repeatedly until the specified process is no longer the foreground process (state = FG).

It would be possible to call waitpid to wait for the foreground process, but the shell's signal handler for the SIGCHLD signal already does that and it makes the logic simpler if calls to waitpid occur in only that one place.

3.5 Implementing bg

The bg command specifies a stopped background process to continue. Send a SIGCONT signal to the process and change its state from ST (stopped) to BG (running in background).

3.6 Implementing sigchld_handler

This is the shell's handler for the SIGCHLD signal. The waitpid function should be called with options so that it returns when a child has either terminated or stopped. Macros can be used to determine which case occurred and whether a process terminated normally or by receipt of a signal. The state of the process may need to be changed. E.g., if a child has stopped, the job's state should be changed to ST. The function

struct job_t *getjobpid(struct job *jobs, pid_t pid);
      

returns a pointer to the job struct for the specified process. Using this you can change the state.