Lab 10: Signal Handling
Table of Contents
1 Preliminaries
In this lab you will complete a set of C programs that will expose you to the set of system calls that are used in shell pipelines, such as process grouping, pipes, and filed descriptor duplication. There are 2 tasks, and you will likely complete neither of the tasks in lab, You will need to finish the remaining asks outside of the lab period.
1.1 Lab Learning Goals
In this lab, you will learn the following topics and practice C programming skills.
- Using signal handlers,
signal()
andsigaction()
- Scheduling alarm signals with
alarm()
- Using
siginfo_t
when handlingSIGCHLD
1.2 Lab Setup
Run the following command
~aviv/bin/ic221-up
Change into the lab directory
cd ~/ic221/labs/10
All the material you need to complete the lab can be found in the lab directory. All material you will submit, you should place within the lab directory. Throughout this lab, we refer to the lab directory, which you should interpret as the above path.
1.3 Submission Folder
For this lab, all ubmission should be placed in the following folder:
~/ic221/labs/10
1.4 Compiling your programs with clang
and make
You are not required to provide your own Makefiles for this lab.
1.5 README
In the top level of the lab directory, you will find a README
file. You must fill out the README file with your name and alpha.
Please include a short summary of each of the tasks and any other
information you want to provide to the instructor.
1.6 Test Script
You are provided a test script which prints pass/fail information
for a set of tests for your programs. Note that passing all the
tests does not mean you will receive a perfect score: other tests
will be performed on your submission. To run the test script,
execute test.sh
from the lab directory.
./test.sh
You can comment out individual tests while working on different parts of the lab. Open up the test script and place comments at the bottom where appropriate.
2 PART 1: Shredder: Alarm Management
In this part of the lab you will develop a program that will time
the execution of a child process using alarm()
and signal
handling. Your program will execute a program specified on the
command line and schedule SIGALRM
to occur ever 1
seconds. Dependent on how many alarms are set off, different
information will print to the screen, and if too many alarms are set
off, the program will terminate.
Change into the shredder
directory in the lab
directory. In there, you will find the source file shredder.c
.
which you will complete. Your shredder
program most meet the
following specification:
- It must use
alarm()
andSIGALARM
signal handling to count the number of seconds the child process runs. - If the child runs for more than 5 seconds, it must terminate it
with a
SIGKILL
signal. - It must print out different information about the termination state of the child dependent on how long the child ran for.
- It must print "tick-tock" for each
SIGALRM
If the child ran for < 3 seconds, your program should print:
Blast that grotesque ganglion! You let them get away!
If the child ran for >= 3 seconds, your program should print:
Sayonara you shell-backed simpletons. I'll get you next time!
If the child was terminated because it ran >= 5 seconds:
Tonight I dine on turtle soup! Muhaha!
Below, is some sample runs of the program to compare against.
#> ./shredder
Tonight I dine on turtle soup! Muhaha!
#> ./shredder ls
Makefile shredder shredder.c
Blast that grotesque ganglion! You let them get away!
#> ./shredder sleep 2
1: tick-tock
2: tick-tock
Blast that grotesque ganglion! You let them get away!
#> ./shredder sleep 4
1: tick-tock
2: tick-tock
3: tick-tock
4: tick-tock
Sayonara you shell-backed simpletons. I'll get you next time!
#> ./shredder sleep 5
1: tick-tock
2: tick-tock
3: tick-tock
4: tick-tock
5: tick-tock
Tonight I dine on turtle soup! Muhaha!
#> ./shredder cat
1: tick-tock
2: tick-tock
3: tick-tock
4: tick-tock
5: tick-tock
Tonight I dine on turtle soup! Muhaha!
#> ./shredder BAD_FILE
execvp: No such file or directory
Blast that grotesque ganglion! You let them get away!
3 PART 2: A minimalist job control shell: fg-shell
In the last task of this lab, you will adopt your old mini-sh
and
add basic of job control. This will include the ability to
stop a foreground process and then bring the background process back
to the foreground. Essentially, just two of the job control
commands:
- Ctrl-Z : stop a process
- fg : bring a proces to the foreground.
To do this, you'll need your shell to wait on process state changes of children, other than termination. You will also need to manage the terminal device driver so that the signals are properly delivered. And finally, you'll need to be able to signal a process that was stopped to be continued. Fortunately, all of these commands are relatively easy, using them in the right order, that's the hard part.
3.1 Waiting on State Changes with waitpid()
UNTRACED
Parents can wait()
on children to terminate, or for any other
kind of state change. This includes process that are stopped or
continued. Unfortunately, the basic wait()
system call won't cut
it, and instead, we need to use the waitpid()
system call, a
slight more advanced form of wait()
.
Here is an example of how waitpid()
is used in the fg-shell
program:
if ( (pid = waitpid(-1, &status, WUNTRACED)) < 0){ perror("wait failed"); _exit(2); }
The first argument, -1
, indicates to wait for all children. Like
before, the status is written to status
, and the option
WUNTRACED
says to also return when children are either stopped or
continued.
You can test why a child process changed state, that is, why a
SIGCHLD
was delivered using the following macro:
WIFSTOPPED(status);
which returns true when waitpid()
returned because a child was
stopped. And similarly, there is a macro to test when a child is
continued:
WIFCONTINUED(status);
These may come in handy when managing children.
3.2 Terminal Signaling with tcsetpgrp()
One thing you must do when you have processes running in the
foreground other than the shell is tell the terminal device driver
to which process (or group of processes) to deliver terminal
signals, such as those generated from Ctrl-C
and Ctrl-Z
.
For example, if we are running a process in the foreground and we
want terminate the process with Ctrl-C
, we don't want the
foreground process and the shell to terminate due to delivering
the SIGINT
signal. To ensure this doesn't happen, we need to
inform the terminal device driver which process is in the
foreground, and then only the right process will receive the
signal.
The system call that does this is tcsetpgrp()
. The "tc" stands
for "terminal control." The function takes two options, a file
descriptor for the terminal tty
, which is usually 0 for standard
input, and a pid
for the foreground process group.
int tcsetpgrp(int fd, pid_t pgrp);
The return value os positive if succesful and negative on
error. Use perror()
to report erros.
3.3 Who and When should call tcsetpgrp()
Transferring control the terminal should solely be the domain of
the shell. The child should never call tcsetpgrp()
. The shell
will call tcsetpgrp()
in the following situations
- When control of terminal is going from the shell to the child process, i.e., the child is becoming the foreground process.
- When control of the terminal is going from the child process to the shell, i.e., the shell is becoming the foreground process.
3.4 tcsetpgrp()
and SIGTTOU
There is one more property of =tcsetpgrp() that you must control for. Looking at the main page closely, the following information is provided:
If tcsetpgrp() is called by a member of a background process group in its session, and the calling process is not blocking or ignoring SIGTTOU, a SIGTTOU signal is sent to all members of this background process group. #+ENDEXAMPLE
If you consider the situation above where terminal control is
transferring from the child to the shell, the shell will need to
call tcsetpgrp()
to itself. However, the shell is no longer in the
foreground process group, the child is since it was terminating. As
a result, the shell will be sent a SIGTTOU
signal whose default
action is to stop. This will cause your shell to hang.
To prevent these situations, you need to make sure the shell is
blocking/ignoring SIGTTOU
:
signal(SIGTTOU, SIG_IGN);
3.5 Continuing a process usig kill()
and SIGCONT
To continue a process that was previously stopped, you must send
that process a signal that says, "hey, you, wake up and continue
what you were doing." The signal that does that is SIGCONT
, and
you can deliver that signal using the kill()
system call.
We'll discuss kill()
in more detail when covering inter-process
communication, but for this lab, you'll exclusively use it to
continue a stopped process allowing it to run in the
foreground. Here is an example usage:
kill(last_pid, SIGCONT);
Here, last_pid
, refers to the child process pid
that was most
recently stopped with Ctrl-z
.
3.6 fg-shell
Change into the fg-shell
directory in the lab directory, in which
you'll find the source code fg-shell.c
. You should provide a
Makefile to compile the source code to fg-shell
. This compiltion
requires the readline library, so you will need to compile like so:
clang -g -Wall -lreadline fg-shell.c -o fg-shell
Opening the fg-shell.c
source file, you'll find that you need to
make edits in two locations or the program to function properly.
my_wait()
: function that executes the primary wait logic. This functions should check if the last program was stopped, and if so, save thepid
tolast_pid
. If there as a previously stopped program, i.e.,last_pid > 0
, do not let the current process stop.fg
logic : In themain()
function you will need to complete the logic for when a user entersfg
. Generally, you should try and continue the last stopped process, if there is one, and then callmy_wait()
In both situations, whichever is the foreground process must have
control of the terminal, as indicated via a call to tcsetpgrp()
.
Here is some sample output, note that the last_pid
is displayed in
the fgshell prompt for convenience.
aviv@saddleback: fg-shell $ ./fg-shell
fg-shell (-1) #> ls
fg-shell fg-shell.c Makefile
fg-shell (-1) #> cat
^Z
fg-shell (13097) #> ls
fg-shell fg-shell.c Makefile
fg-shell (13097) #> fg
^Cfg-shell (-1) #> cat
^Z
fg-shell (13104) #> cat
^Z
^Z
^Z
^Z
^Z
^Cfg-shell (13104) #> fg
^Cfg-shell (-1) #> BAD_COMMAND
fg-shell: No such file or directory
Of note: while cat
is in the background, other commands can
run. The second cat, in the foreground, could not be stopped because
there was already a cat
saved as the background process. Finally,
neither Ctrl-z
or Ctrl-c
affected the shell program while
another process was in the foreground.