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