IC221: Systems Programming (SP15)


Home Policy Calendar

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.

  1. Using signal handlers, signal() and sigaction()
  2. Scheduling alarm signals with alarm()
  3. Using siginfo_t when handling SIGCHLD

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:

  1. It must use alarm() and SIGALARM signal handling to count the number of seconds the child process runs.
  2. If the child runs for more than 5 seconds, it must terminate it with a SIGKILL signal.
  3. It must print out different information about the termination state of the child dependent on how long the child ran for.
  4. 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 the pid to last_pid. If there as a previously stopped program, i.e., last_pid > 0, do not let the current process stop.
  • fg logic : In the main() function you will need to complete the logic for when a user enters fg. Generally, you should try and continue the last stopped process, if there is one, and then call my_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.