"[S]ignals are significantly more powerful and expressive when SA_SIGINFO
-style
handlers are used."
—Robert Love, Linux System Programming, 2nd Edition, Chapter 10, pp. 362
Signals are a simple yet powerful way for Linux processes to communicate in pre-defined ways. They are reasonably well suited for inter-process event handling during system operation, especially for parent-child process interactions. Signals can be sent to any process in the system, and can be recieved by defining special functions called signal handlers, which are executed upon receipt of the signal.
In this studio, you will:
Please complete the required exercises below, as well as any optional enrichment exercises that you wish to complete. We encourage you to please work in groups of 2 or 3 people on each studio (and the groups are allowed to change from studio to studio) though if you would prefer to complete any studio by yourself that is allowed.
As you work through these exercises, please record your answers, and when finished upload them along with the relevant source code to the appropriate spot on Canvas. If you work in a group with other people, only one of you should please upload the answers (and any other files requested) for the studio, and if you need to resubmit (e.g., if a studio were to be marked incomplete when we grade it) the same person who submitted the studio originally should please do that.
Make sure that the name of each person who worked on these exercises is listed in the first answer, and make sure you number each of your responses so it is easy to match your responses with each exercise.
The following exercises are intended to acquaint you with signal delivery in Linux.
On your Raspberry Pi 3, or on the shell.cec.wustl.edu
server,
download the program print500000.c
.
And easy way to download it is to issue the command:
wget https://classes.engineering.wustl.edu/cse422/studios/print500000.c
This is a very simple program that prints the numbers from 1 to 50000 in order.
Build and run it. While it runs, try to interrupt it by pressing CTRL+C
.
Only press CTRL+C
once.
As the answer to this exercise, please report what you observed. How did the program behave at the moment you pressed CTRL+C? Please copy the last few lines of program output as part of your answer.
There are many signals that can be sent or recieved in Linux. An overview
of the signal mechanism can be found at the manual page displayed when you issue
the terminal window command man 7 signal
. Some commonly used signals
are SIGINT
(keyboard interrupt), SIGQUIT
(telling a program to quit from keyboard), and SIGTERM
(terminate process).
Every signal in the system is associated with a default action (which typically
terminates the recieving process), but each of these can be overridden simply by defining
your own signal handling function, which is what we will now do.
The modern, portable way to define a signal handler is to use the function
sigaction()
, which is documented by man 2 sigaction
. Use
this function to define a new handler for SIGINT
within the
print500000.c
file. You will need to:
void function_name (int arg)
struct sigaction
variable in your main()
functionsa_handler
field of your struct sigaction
variablesa_flags
field of your struct sigaction
variable SA_RESTART
sigaction()
function
in your main()
function, with SIGINT
as the signum argument and a pointer to your struct sigaction
for the act argument.In the body of your signal handler, add the following code:
called[i] = 1;
As the answer to this exercise,
please describe the purpose of the called
array.
Build and run your program, and while it is running send it SIGINT
a few times by hitting CTRL+C
.
As the answer to this exercise, copy and paste the last several lines of program output.
NOTE: Overriding the SIGINT
handler may make it
difficult to terminate your program. You can alternately use CTRL+\ to send
the SIGQUIT
signal or the kill
program to terminate
your processes.
Now, declare a variable of type volatile sig_atomic_t
that acts as a flag to indicate that the program should terminate.
Initialize the variable to 0.
Modify your signal handler to also set this variable to 1.
In the main
function,
inside the loop that prints the numbers from 1 to 50000,
add a statement that checks the value of the flag.
If it has been set to 1, your program should break
out of the loop.
Build and run it. Interrupt it with SIGINT
,
this time only pressing CTRL+C
once.
As the answer to this exercise, please report what you observed. How did the program behave at the moment you pressed CTRL+C? Please copy the last few lines of program output as part of your answer.
One way that signal handlers differ from other functions of a user-space program is that they execute asynchronously with respect to the rest of a program. When called, they may interrupt the normal execution of the program at virtually any point. Consider what might happen, for example, if the same function is called from both the regular program flow as well as a signal handler. This could result in the function being called concurrently from two different contexts, even within a single-threaded program! This could cause data corruption or even deadlock if not handled appropriately.
Even library functions are not immune to problems with such asynchrony.
Additionally, add the following line of code to the handler function:
printf("Caught SIGINT\n");
Additionally, in your signal handler,
comment out the statement that updates the flag to 1.
This will once again allow your program to receive several SIGINT
signals.
Build your program, but this time run it using the following command:
./print | tee -i print500000.txt
The tee
utility allows a program's output to be sent
both to standard output and to a specified file.
The utility is documented in man 1 tee
While your program is running, send it SIGINT
a few times by hitting CTRL+C
.
Open the output file print500000.txt
in a text editor
that allows you to search for the signal handler's output, "Caught SIGINT."
Observe where this was printed, and what numbers were printed before and after it.
As the answer to this exercise, please describe what you see, and why you think you're observing this behavior. Please copy a few relevant lines of program output as part of your answer.
For the reasons outlined above, it may not be safe to make many common
system calls or library function calls from within a signal handler. Many
functions, such as printf()
are not guaranteed to execute
correctly when called from within a handler, since they may be called
concurrently from different contexts.
Fortunately, there is a list of functions that is guaranteed to be safe
when called from a handler. These functions are given in the signal safety documentation
at man 7 signal-safety
.
Update your signal handler so that instead of the printf()
function,
it uses the write()
function documented in the man 2 write
page,
to produce output from within the signal handler. In particular, you must:
char * message = "Caught SIGINT\n"
)strlen()
function)write()
function with argument fd equal to STDOUT_FILENO
> to write to standard output (e.g. write(STDOUT_FILENO, message, num_chars);
)#include
the appropriate header files as documented in man 2 write
and man 3
Repeat the previous run, interrupting the program several times,
and again inspect the print500000.txt
output file.
As the answer to this exercise,
explain briefly the differences you observed when using the
write()
function instead of printf()
.
The next few exercises will explore the differences between standard signals and real-time signals.
First, download the program receiver.c and build it.
Look at the code, and as the answer to this exercise, briefly explain what it does.
Now, write and compile a second program, sender.c
,
that takes a PID and a number of signals to send as arguments.
Your program should send SIGUSR1
to the specified PID a number of times equal to the argument provided.
It should then send SIGUSR2
once.
Now, run your receiver program, and note its PID.
Run your sender program in a new terminal window, providing the receiver's PID and a number of signals to send.
As the answer to this exercise, please report how many times the receiver received SIGUSR, and why you think this behavior makes sense.
Now, you will modify both programs to use real-time signals.
In receiver.c
, replace SIGUSR1
and SIGUSR2
with SIGRTMIN
and SIGRTMIN+1
respectively.
In your sender.c
program,
do the same thing, and replace calls to kill()
with appropriate calls to sigqueue()
.
The Linux kernel enforces resource limits to prevent userspace programs from hogging computational resources.
One such limit is RLIMIT_SIGPENDING
, which limits the number of queued real-time signals
a process may have pending.
In your sender.c
program, use a call to getrlimit
to determine the current limit (also called the soft limit) of this value,
and ensure that the provided argument for the number of signals to send does not exceed this value.
More information on getrlimit
can be found with the following command:
man 2 getrlimit
Now, run your receiver and sender again,
like in the previous exercise.
As the answer to this exercise, please report how many times the receiver received SIGRTMIN
,
and tell us the current limit on the number of pending real-time signals.
Now, we are going to explore sending signals with data. Instead of using two signals (one to increase a counter, and one to end the program), we will use a single signal type, but with accompanying data to indicate what action should occur.
First, modify your sender.c
program.
The sigqueue
function includes a union sigval
parameter.
This union contains an integer value, sival_int
Where the sender would have previously sent SIGRTMIN
,
have it again send SIGRTMIN
but with sival_int = 0
.
Where the sender would have previously sent SIGRTMIN + 1
,
have it now send SIGRTMIN
but with sival_int = 1
.
Now, modify receiver.c
so it only handles SIGRTMIN
.
To allow receiver.c
to access the integer it has been sent,
instead of setting ss.sa_handler
, you will need to set ss.sa_sigaction
.
You will also need to add (using bitwise OR) the SA_SIGINFO
flag to ss.sa_flags
.
Modify the signal handler function with a new signature:
void sig_handler( int signum, siginfo_t * si, void * ucontext )
You can access the sigval
union through si->si_value
Modify the signal handler appropriately so that it increments the count when sival_int == 0
and sets the done
flag when sival_int == 1
.
Now, run your receiver and sender again,
like in the previous exercise.
As the answer to this exercise, please report how many times the receiver received SIGRTMIN
with sival_int == 0
.
For this studio, please turn in the following:
print500000.c
that contains a signal handlersender.c
and modified receiver.c
programs that send and receive real-time signals with data