Homework — Signals

All the sources are available in the part-3 tarball.

The C files are located in directory part-3/homework-signals/material/.

The general specifications of the POSIX functions are available at OpenGroup website

This homework is reused in the lab about virtual memory.

DO NOT COMPILE WITH -std=c11 - signal.h is broken on Linux

Overview

In this homework, we study signals. A signal is a form of inter-process communication used by Unix systems. It is an asynchronous notification sent to a process to alert it to the occurrence of an event.

After a short course and few exercises to learn how to handle signals, we implement an incomplete but original exception handling mechanism for the C language. We shall handle an out-of-bounds access to an array or a division by zero, just as we would do with exceptions in python, java, c++, ada, etc.

The sources for the labs are in the archive you downloaded at the beginning of the course.

Course on signal management

This section is a short course about UNIX signals.

Signals are notifications sent to a process to inform it of "important" events like errors. A process receiving a signal interrupts what it was doing to handle the signal "immediately" (handling can consist in ignoring them, though). Each signal has an integer to represent it (1, 2, etc.), as well as a symbolic name which is usually defined in the /usr/include/signal.h file. These events may be intended for another process. Signals have various origins, and can be:

  • forwarded by the kernel (from hardware): division by zero, overflow, forbidden instruction,
  • sent from the keyboard by the user (keystrokes: CTRL-Z, CTRL-C, ...)
  • sent by the kill command from the shell or by the kill() system call

Each signal can have a signal processing function, which is a function called when the process receives that signal. The function is called in "asynchronous mode", which means that no program code directly calls this function. Instead, when the signal is sent to the process, the operating system suspends the execution of the process and "forces" it to call the signal handling function. When the signal handling function returns, the process resumes execution where it was before the signal was received, as if the interruption had never occurred.

Sending a signal

The kill() system call allows to send a signal to a process.

  • In C:

    int kill(pid_t pid, int sig);
    
    • pid designates the id of the targeted process.
      • Note that a pid of 0, -1 or negative value are not incorrect, but designate a group of processes.
    • The returned value is zero except when sig has an incorrect value, or the user has no right to send a signal to the process.
      • Note: the sender has no way of knowing whether or not the recipient has received the signal.
  • In Shell:

    $ kill -SIGNAME pid
    

In these two cases, the signals originate from user requests, but signals can also be sent by the kernel itself.

Receiving a signal

The sigaction() function allows the receiving process to specify the desired handling of a specific signal.

Function sigaction()

int sigaction(int sig, struct sigaction *act, struct sigaction *oact);
  • The sigaction() system call assigns an action for a signal sig.
  • If act is non-zero, it specifies:
    • a signal handler or action (SIG_DFL, SIG_IGN, or a user-defined subprogram) and
    • a mask or a set of signals that are blocked when executing the signal handler and
    • flags or options to change default parameters or behaviors.
  • If oact is non-zero, it returns to the user the previous handling information for the signal.

act (or action) and oact (or old action) are parameters of type struct sigaction.

Structure sigaction

The type struct sigaction helps specifying the signal subprogram to call when receiving the signal.

struct  sigaction {
    void    (*__sa_handler)(int sig);
    void    (*__sa_sigaction)(int sig, siginfo_t *info, void *context);
    sigset_t sa_mask;                                                       /* signal mask to apply */
    int     sa_flags;                                                       /* signal options  */
};
  • sa_handler and sa_sigaction attributes specify the signal subprogram to call when receiving the signal.
  • sa_mask designates the set of signals that are blocked while executing the signal handler.
  • sa_flags is a set of flags which allows to change the default behaviour of sigaction.

Only one of the sa_handler and sa_sigaction attributes is used. By default, the sa_handler is the only one considered. The last two attributes are intended for extended use. We shall go into more detail later about them, but let's first focus on sa_handler attributes and ignore the three other ones.

Function sa_handler

The sa_handler attribute may have the following values:

  • SIG_IGN specifies that the signal should be ignored, but some signals cannot be ignored, for instance SIG_KILL.
  • SIG_DFL specifies that when the signal is received, the processing must be the default one. For most signals, the default action is to terminate (or "kill") the process, explaining the (bad) naming of system call kill().
  • A user-defined function with the following signature:
    void my_handler(int sig);
    
  • The sig parameter specifies the received signal.

Exercises on signal management

Ignore signals

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
    int sig;
    struct sigaction sa;
    for (sig = 1; sig < NSIG; sig++) {
        /* For now, we do not use any option. By default, the signal handler sa_handler is used */
        sa.sa_flags = 0;

        /* sa.sa_sigaction is the pointer to a signal-catching function */
        sa.sa_handler = SIG_IGN;

        /* For now, we do not use any mask. By default, the signal received is included in mask */
        sigemptyset(&sa.sa_mask);
    }
    /* Manage signal processing for 30 sec whatever happens */
    int delay = 30;
    while (delay != 0) delay = sleep(delay);
}
  1. Write a program sig-ignore.c that ignores (almost) all signals. The code to be completed is given above.
  2. Test the return value of the signal function to note the (rare) signals that cannot be ignored.
  3. The command /bin/kill -l gives a list of signals and their mnemonics.
  4. Which signals cannot be ignored by the process?
  5. Use Ctrl-C or Ctrl-\ in the terminal where the program is running. For MacOS users, Ctrl-\ is Ctrl-`.
  6. Also send signals to this program using kill command from another terminal.
  7. Find a way to end the program without waiting for 60 seconds the end of the call to sleep(30). Hint: note that SIGKILL (signal number 9) is not ignored by the program (why?) and still allows to terminate it.

About sleep()

unsigned sleep(unsigned s)

The sleep() function shall cause the calling thread to be suspended from execution until either a delay of s seconds has elapsed or a signal is delivered to the calling thread ("waking" it up) and its action is invoked. If the delay has elapsed, sleep() returns 0. If sleep() returns due to delivery of a signal, the return value is the "unslept" amount (the requested time minus the time actually slept) in seconds.

Handle signals with sa_handler

Let's now detail how to call a user-defined subprogram when receiving a signal. Write a program sig-user-def.c that handles signals with a user-defined function, called my_handler. You can make a copy of sig-ignore.c.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
    int sig;
    struct sigaction sa;
    for (sig = 1; sig < NSIG; sig++) {
        /* For now, we do not use any option. By default, the signal handler sa_handler is used */
        sa.sa_flags = 0;

        /* For now, we do not use any mask. By default, the signal received is included in mask */
        sigemptyset(&sa.sa_mask);
    }
    /* Manage signal processing for 30 sec whatever happens */
    int delay = 30;
    while (delay != 0) delay = sleep(delay);
}

void my_handler(int sig) {
  printf("Enter signal handler %d\n", sig);
  sleep(3);
  printf("Leave signal handler %d\n", sig);
}
  1. Write a program sig-user-def.c that handles signals with a user-defined function, called my_handler. The code to be completed is given above.
  2. Test the return value of signal() to identify the signals for which no user-defined processing can be defined.
  3. Do Ctrl-C or Ctrl-\ in the terminal where the program is running.
  4. Send signals to this program using the kill command from another terminal.
  5. Do Ctrl-C twice in a raw. What do you observe ?
  6. Modify sa.sa_flags to add SA_NODEFER. The second call to the signal handler will no longer be deferred after the execution of the first one.
  7. Do Ctrl-C twice in a raw. What do you observe ?
  8. Do Ctrl-C and Ctrl-\ in a raw. What do you observe ?

More about struct sigaction

  • sa_handler and sa_sigaction specifies the signal handler to call when receiving the signal. Only one of these two attributes is used. By default, the sa_handler is considered.
  • sa_mask designates the set of signals that are blocked while executing the signal handler.
    • By default, the signal being delivered is included in the mask.
    • It means that receiving the same signal twice in a raw will cause the second occurence to be deferred until the first one is handled.
  • sa_flags is a set of flags which allows to change the default behaviour of sigaction.
    • By default, the interface sa_handler is used. If flag SA_SIGINFO is set, then sa_sigaction is used instead.
    • By default, the signal being delivered is included in the mask. If flag SA_NODEFER is set, then the signal is no longer included in the mask.

As described above, there are two possible ways to call a user-defined subprogram when receiving a signal: sa_handler (default) and sa_sigaction.

Function sa_sigaction

When SA_SIGINFO is specified in sa_flags, sa_sigaction specifies to use the sa_sigaction to call when receiving the signal. As a consequence, the system ignores the sa_handler attribute. sa_sigaction has to be an user-defined function with the following signature:

void my_sigaction(int sig, siginfo_t *info, void *context);
  • The sig parameter specifies the received signal.
  • The info parameter of type siginfo_t explains the reason why the signal was generated. The structure siginfo_t includes several attributes among which we have:
    • int si_pid designates the sending pid,
    • *void si_address designates the address of faulting instruction,
  • The context parameter of type ucontext_t refers to the receiving thread's context that was interrupted when the signal was delivered. The structure ucontext_t includes several attributes among which we have:
    • sigset_t uc_sigmask specifies the set of signals that are currently blocked.

Handle signals with sa_sigaction

Let's now detail how to call a user-defined subprogram with sa_sigaction interface. Write a program sig-user-def-ext.c that handles signals with a user-defined function, called my_sigaction. You can make a copy of sig-user-def.c.

  1. Configure sigaction() to use the sa_sigaction interface.
  2. Display the pid of the current process.
  3. Do Ctrl-C or Ctrl-\ in the terminal where the program is running. Which process did send the signal ? Explain
  4. Send signals to this program using kill command from another terminal. Which process did send the signal ? Explain

Course on stack context management

To implement our exception like mechanism, we have to introduce two system calls related to stack context management.

The system call sigsetjmp() allows to save the execution context in order to restore it later on with a call to siglongjmp(). When a call is made to the siglongjmp() function with a given sigjmp buffer, execution resumes at the point where sigsetjmp() did saved the sigjmp buffer. Somehow, it is equivalent to a goto.

int sigsetjmp(sigjmp_buf env, int savemask); 
void siglongjmp(sigjmp_buf env, int val); [Option End]

If sigsetjmp() returns from a siglongjmp() call, it returns the value passed to siglongjmp() call. If sigsetjmp() does not return from a siglongjmp() call, because it is the first call to sigsetjmp(), it returns 0.

Exercises on stack context management

We consider the following code :

#include <setjmp.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    sigjmp_buf env;
    int val = sigsetjmp(env, 1);
    printf("val is %d\n", val);
    printf("new val (0 to exit): ");
    scanf("%d", &val);
    if (val != 0)
        siglongjmp(env, val);
}
  1. Compile and run it.
  2. Check that at runtime the program loops until you answer 0, even though its code does not include a for or while loop.

This behavior comes from the siglongjmp() system call, which causes execution to resume at the sigsetjmp() system call.

Note that to restore the stack context, siglongjmp() should not be called once sigsetjmp() has left the function in which it was invoked (the stackframe). Somehow, the sigjmp buffer does not exist anymore and the behaviour is undefined.

Problem on implementing an exception like mechanism in C

In this problem, we implement an exception mechanism in C. When we access an element in an array that is too far from its bounds, the system raises an interrupt and then a signal SIGBUS. When we access the content of a NULL pointer, the system raises an interrupt and then a signal SIGSEGV. In other languages such as python, these errors are translated into exceptions. The user can also define his/her own exceptions. This is what we are going to implement.

Reminder of python exceptions

In the Python code below, similarly to other languages with exceptions, the runtime first executes the code located in the try section. If an error occurs during this execution (for example, array_index is out of range), the runtime raises an exception corresponding to the error. If an except clause matches this exception, the code corresponding to the clause is executed. Otherwise, the exception is propagated to the upper block containing the try section. It is also possible to raise (or "throw") an exception explicitly, in particular an user-defined one in alternative to predefined exceptions.

## enter try block
try:
    ## main code of try block
    element = array[array_index]

## catch block for IndexError
except IndexError:
    element = array[0]

## leave try block

Overview of the code

As the C language does not propose an exception mechanism, we do not have the help of the compiler and therefore we have to use functions from an API to mimic the mechanism. In this lab, we define the following API:

#define OUT_OF_RANGE_EXCEPTION 1001
#define NULL_POINTER_EXCEPTION 1002
#define USER_DEFINED_EXCEPTION 1003

typedef struct _exception_t {
    int id;
    struct sigaction sa;
    jmp_buf block;
} exception_t;

/* Function declarations */
void on_error(int sig);
#define exception_enter_try_block(e) (e)->id = sigsetjmp((e)->block, 1);
void exception_add_catch_block(exception_t *e, int exception_id);
void exception_leave_try_block(exception_t *e);
void exception_raise_exception(int exception_id);
  • exception_t is a structure that includes:

    • an exception id and we consider three possible exceptions: OUT_OF_RANGE_EXCEPTION raised when we access an array with an index out of bounds, NULL_POINTER_EXCEPTION raised when we access a null pointer, USER_DEFINED_EXCEPTION raised by the user for implementation purpose.

    • It also includes a sigaction structure used to catch a potential system signal: SIGBUS when a process is trying to access memory that the CPU cannot physically address (OUT_OF_RANGE_EXCEPTION) and SIGSEGV when a process refers to an invalid memory area (NULL_POINTER_EXCEPTION).

    Note that a process can access an array with an index out of bounds without causing a SIGBUS interrupt. For instance, the address may be out of bounds, but not out of bounds of the process physical memory.

    • The attribute block of type jmp_buf is used to save the current execution context with function sigsetjmp(). This attribute can later be used to restore the current execution context with siglongjmp(). That is, when a call to siglongjmp() function is made with the attribute block, the execution continues at the particular call site that constructed the jmp_buf attribute block. In that case sigsetjmp() returns the value passed to siglongjmp(). If sigsetjmp() does not return from a siglongjmp() call, because it is the first call to sigsetjmp(), it returns 0.
  • exception_enter_try_block() is a "function" to declare the beginning of the try section.

    • Actually, it is a macro but the rationale for that is out of the scope of this lab. For those who are interested, exception_enter_try_block() calls sigsetjmp() and if it was a function, the jmpbuf execution context would point to a context no longer available once we leave the function call. exception_enter_try_block() is a macro to force inlining.
  • exception_add_catch_block() is a function to register an exception clause for a given exception passed as an exception id. It should be done right after entering the try section.

  • exception_leave_try_block() is a function to declare the end of the try section.

  • exception_raise_exception() is a function to raise an exception, in particular an user-defined exception, as would do a raise exc python instruction.

The result is the code provided in sig-try-catch.c, which mimics the Python code described above:

/* enter "try:" section -- initialise the exception mechanism */
exception_enter_try_block(&exception);

/* declare the "except:" clauses in advance -- no help from compiler */
exception_add_catch_block(&exception, OUT_OF_RANGE_EXCEPTION);

switch (exception.id) {
/* No exception: execute the main code of the try: section */
default:
    element = array[array_index];
    break;

/* OUT_OF_RANGE_EXCEPTION exception: execute the code of the OUT_OF_RANGE_EXCEPTION clause */
case OUT_OF_RANGE_EXCEPTION:
    element = array[0];
    break;
}

/* leave try: section -- clean the exception mechanism */
exception_leave_try_block(&exception);

Implementation of the exception mechanism step by step

  1. First build the sig-try-catch application and execute it.
    • Run option 1. The program just fails when accessing to content of array[1000000] because the SIGSEGV signal is not handled.
  2. Fix this issue in function exception_add_catch_block() for the exception OUT_OF_RANGE_EXCEPTION associated to the SIGSEGV signal.
    • Run option 1. The program keeps failing forever. Why ? It does not get back to exception_enter_try_block() either.
  3. Fix this last issue in function on_error().
    • Run option 1. Call the following command and check that the test with an exception handler works (almost):
    ./sig-try-catch |& less
    
    • Why does it not work without an exception handler ? What should be the behaviour when there is no exception handler ?
  4. Fix this issue in function exception_leave_try_block().
    • Run option 1. This should be fine this time.
  5. Implement the same mechanism for the exception NULL_POINTER_EXCEPTION associated to the SIGBUS signal.
    • Run option 2. This should be fine as well.
  6. Implement the same mechanism for the exception USER_DEFINED_EXCEPTION associated to the SIGUSR1 signal.
    • Run option 3. Analyse the function exception_test_main() to understand the issue.
  7. Implement the appropriate function.
    • Run options 1, 2 and 3. All should be fine now.