previous | start

Tiny Shell (tsh) Implementation Overview

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).
   


previous | start