tsh (tiny shell with job control): Implementation Notes Note: The comments for the DECLARATIONS for functions sigint_handler and sigtstp_handler are incorrect. You will need to make some changes for each of these functions. The comments before each function DEFINITION are correct and indicate what change is to be made. A. Concepts 1. Signals sent to a process a. Representing by a single bit in an integer signal "mask" b. signals can be pending in the signal mask (i.e. received while the process is waiting to run again) c. blocked signal mask (A pending signal is not "delivered" until the corresponding bit in the block mask is cleared). d. pending signals are represented by a single bit, not a list or queue. So multiple instances of the same signal can be lost. 2. Register signal handlers a. When a process is (re)scheduled, if there are is a pending signal that is not also blocked, the registered signal handler for the signal executes instead of the current program counter instruction. b. Some signal handlers cannot be changed; that is, a user cannot catch these signals: E.g, SIGKILL. The default signal handler for SIGKILL terminates the process and cannot be replaced. The default signal handler for most signals terminates the receiving process. The SIGCHLD signal is sent to the parent when a child terminates, is stopped, or is continued. The default signal handler for SIGCHLD does nothing - so the default action is to ignore this signal. c. The default signal handler for other signals can be replaced by the user by calling the signal function with a new handler: sighandler_t signal(int signum, sighandler_t handler) where the type, sighandler_t, must be a function with void return and a single integer argument to receive the signal number. 2. Signal handler desired semantics a. When a signal handler begins, the bit in the signal mask for that signal is cleared. b. While in a signal handler for a signal executes, that signal is blocked until the signal handler returns. So 1 or more signals of the same kind are sent to a process while it is in the signal handler, only 1 will be observed. c. If a "slow" system call such as a file read is interrupted by a signal, we would want the read to resume. 3. Process groups a. A process belongs to a process group. So it has a process id and a process group id. b. Generally the process group id is inherited from its parent, but the process group can be changed. c. When a shell creates a process to execute a command, it would ordinarily have the process group id being the same as that of the shell. But the child can change its process group id. The that child and any of its children will have process group id the same as the child, not the shell. 5. Process groups and signals a. kill(pid_t pid, int sig) sends signal sig to process with id, pid. b. B. Implementation 1. Suggested start of implementation: - builtin_cmd - quit - jobs - fg and bg (call do_bgfg, but don't implement do_bgfg yet) - eval - parse command line into args array - just return for blank line: args[0] == (char *) 0 - call builtin_cmd to check for and handle builtin commands and return if cmd was builtin. Check: a. Enter blank lines (should print prompt again) b. quit command (should terminate the shell) c. jobs command (should print nothing at this point) 2. Compile with -g option to allow source code to be available in the debugger, gdb. a. Change CFLAGS in the Makefile from CFLAGS = -Wall -O2 to CFLAGS = -Wall -g Then delete executables, etc, and rebuild them make clean make OR b. Instead of changing the Makefile, at the prompt type: $ export CFLAGS="-Wall -g" Version b allows you to easily rebuild choosing either debugging or optimization. To get debugging information: make clean make -e To get optimized version without debugging: make clean make 3. Use etags a. $ etags tsh.c (creates file named TAGS) b. Use emacs editor alt + "." key combination can be bound to the command find-tag tags are: - function names - typedef names - define names - global variables 4. Continue implementation - sigint_handler - sigtstp_handler Get the pid of the foreground process. If there is a foreground process, send the signal to its whole process group. Otherwise, just return. 5. waitfg(pid_t pid) Get the pointer p to the job_t struct for job with process id = pid. If p is NULL, just return. This foreground process has terminated and been removed from the job table. Then loop as long as p->state == FG (foreground). Sleep for 1 second between loop iterations. 6. sigchld_handler - Loop as long as there are any child processes that have changed state by terminating normally or by getting a signal or by being stopped. Use waitpid with WNOHANG and WUNTRACED options: WNOHANG | WUNTRACED With WUNTRACED option, each call to waitpid will return information about a process that has changed state by being terminated or being stopped. With only the WUNTRACED option, waitpid returns -1 (error) and sets errno if no process has changed state. With both WUNTRACED and WNOHANG, waitpid returns 0 (no error) if no process has changed state. The sigchld_handler should call waitpid for all the processes that have already changed state, but not for any additional children that have not yet changed state. So use both options: WUNTRACED | WNOHANG and loop as long as waitpid returns a value > 0. - For each pid returned by waitpid a. Did it terminate normally? If so just delete it from the job list. b. Else did it terminate by getting a signal that it didn't catch? If so, print a termination message including the job id, pid and the signal number. c. Else did the process become stopped? If so change its state in the job table to be ST (stopped) 7. eval (continued) Builtin commands will be taken care of and return to main Continue with code for a command that is not builtin. - Block SIGCHLD temporarily. Function: sigprocmask - fork child - child must change to a new process group different from the shell. Function: setpgid - child unblocks SIGCHLD (in case the command also creates its own children) - child execvp's to replace its copy of shell with the program for the command. - child prints error and exits in case execvp fails. - print panic error if fork fails and return - Otherwise, add the child to the job table with state FG or BG depending on the absense or presence of & on the command line. - Now after safely adding the child to the job table, shell should unblock the SIGCHLD signal. - If state FG, then call waitfg (not waitpid) to wait for the foreground child to finish. Then return to main. Check: a. Simple non-builtin commands with command line arguments such as: ls -l echo hello ps -f which perl b. Try sending SIGINT to a program. The myspin program loops for n seconds where n is given as a command line argument. Run that program and then type ctrl-c to send a SIGINT signal to terminate it. The message you have in the sigchlld_handler should print. ./myspin 20 ctrl-c 8. do_bgfg(char *argv[]) The builtin_cmd function should call do_bgfg if the command is either fg or bg. In do_bgfg, you need to check: a. If argv[1] is 0, the argument for the command is missing. Print an error message and return. b. Otherwise argv[1] should be a pid (an integer) or a percent sign followed immediately by a job id. You can use sscanf to easily check if argv[1] is one of these 2 cases. If it is neither, print an error message and return. If argv[1] is just an integer (pid), use the getjobpid to get a pointer to the job table entry for the process with that pid. If there is no process in the job table with the pointer returned by getjobpid will be NULL. Print an error msg and return. Else if argv[1] is a percent sign followed by an integer, use getjobjid to get a pointer to the job table for the process with that job id. Again if the pointer returned is NULL, print an error msg and return. If no returns so far, the newstate should become FG or BG depending on whether argv[0] is "fg" or "bg". The oldstate should be either ST (stopped) or BG(running in background); if not, some programming error has probably already occurred, so print a message and return. If oldstate is ST (stopped), send the SIGCONT signal to ALL processes in the process group for this process so they will become READY and can continue. Now you can change the state in the job table to the newstate (as already determined from argv[0]). Finally, if the newstate is FG (foreground), the shell needs to wait for this process. Call waitfg with the pid for this process. (Don't call waitpid).