Previous Up Next

 4  Signals

Signals, or software interrupts, are external, asynchronous events used to alter the course of a program. These may occur at any time during the execution of a program. Because of this, they differ from other methods of inter-process communication, where two processes must be explicitly directed to wait for external messages; for example, by calling read on a pipe (see chapter 5).

The amount of information transmitted via a signal is minimal, just the type of signal, and although they were not originally intended for communication between processes, they do make it possible to transmit atomic information about the state of an external entity (e.g. the state of the system or another process).

4.1  Default behavior

When a process receives a signal, there are four possible outcomes:

There are several types of signals, each associated with a particular event. Table 4 lists some of them with their default behaviors.

NameEventDefault Behavior
sighupHang-up (end of connection)Termination
sigintInterruption (ctrl-C)Termination
sigquitStrong interruption (ctrl-\)Term. & core dump
sigfpeArithmetic error (division by zero)Term. & core dump
sigkillVery strong interruption (cannot be ignored)Termination
sigsegvMemory protection violationTerm. & core dump
sigpipeWriting to a pipe without readersTermination
sigalrmTimer interruptIgnored
sigtstpTemporary halt (ctrl-Z)Suspension
sigcontResuming a stopped processIgnored
sigchldA child process died or was stoppedIgnored
Table 4 — Some signals and their default behaviors

The signals received by a process come from several possible sources:

4.2  Using signals

The system call kill makes it possible to send a signal to a process.

val kill : int -> int -> unit

The first parameter is the process id of the destination process and the second the signal number to send. An error occurs if we attempt to send a signal to a process not owned by the user. A process may send signals to itself. When the kill system call returns, it is guaranteed that the signal was delivered to the destination process. If a process receives the same signal in rapid succession it will execute the code associated with the signal only once. Therefore a program cannot count the number of times it receives a signal, rather only the number of times it responds to it.

The system call alarm makes it possible to schedule interruptions based on the system clock.

val alarm : int -> int

The call alarm s returns immediately but causes the sigalrm signal to be sent to the calling process at least s seconds later (note that there is no guarantee on the maximum wait time). The call returns the number of seconds remaining to an alarm scheduled by a previous call. If s is 0, the effect is simply to cancel an earlier alarm.

4.3  Changing the effect of a signal

The system call signal makes it possible to modify the behavior of a process when it receives a signal of a certain type.

val signal : int -> signal_behavior -> signal_behavior

The first argument is the signal number and the second argument, a value of type signal_behavior, indicates the desired behavior for the signal. With:

Signal_ignoreThe signal is ignored.
Signal_defaultThe default behavior occurs.
Signal_handle fThe function f is invoked each time the signal is received.

Forking a process with the system call fork preserves signal behavior: the initial definitions for the child are those of the parent at the time when fork was executed. The execve system call sets all the behaviors to Signal_default except that signals ignored before are still ignored afterward.

Example

Occasionally we want to log-off or end a session while allowing background tasks (large calculations, “spyware” programs, etc.) to continue to run. If this is desired, processes which normally exit on receiving sighup (sent at the time the user disconnects) should be prevented from doing so. The Unix command nohup does exactly this:

nohup cmd arg1 ... argn

executes the command cmd arg1 ... argn in a way unaffected by the signal sighup (certain shells execute nohup automatically for all processes launched as background tasks). Here’s how to implement this in three lines:

open Sys;; signal sighup Signal_ignore;; Unix.execvp argv.(1) (Array.sub argv 1 (Array.length argv - 1));;

The system call execvp preserves the fact that sighup is ignored.

* * *
Example

Carefully exiting when a program is misbehaving. For example, a program like tar can try to save important information in a file or destroy the corrupted file before terminating. For this it is possible to include the following lines at the beginning of the program:

signal sigquit (Signal_handle quit); signal sigsegv (Signal_handle quit); signal sigfpe (Signal_handle quit);

where the function quit is of the form:

let quit _ = (* Try to save important information in a file *); exit 100;;
* * *
Example

Capturing user-initiated interruptions. Some interactive programs need to return to a main control loop when a user presses ctrl-C. For this we just need to raise an exception when the sigint signal is received.

exception Break;; let break _ = raise Break;; ... let main_loop () = signal sigint (Signal_handle break); while true do try (* Read and evaluate user commands *) with Break -> (* Display "stopped" *) done;;
* * *
Example

To carry out periodic tasks (animations, etc.) interleaved with the execution of the main program. For example, here is how to create “beep” sounds every 30 seconds, regardless of the activity of the main program (calculations or input/output).

let beep _ = output_char stdout '\007'; flush stdout; ignore (alarm 30);; ... signal sigalrm (Signal_handle beep); ignore (alarm 30);;
* * *

Checkpoints

Signals are useful for asynchronous communication — indeed, it is their raison d’être — but this asynchronous nature also makes them one of the major difficulties of system programming.

The signal handling function is executed asynchronously and thus pseudo-concurrently with the main program of the process. As signal handling functions cannot return a value, they usually modify global variables. This can result in race conditions between the signal handler and the main program if they try to modify the same variable. As explained in the next section one solution is to temporarily block signals when this variable is accessed by the main program.

In fact, OCaml does not treat signals in a strictly asynchronous fashion. On receiving a signal, OCaml records the receipt of the signal but the signal handling function will only be executed at certain checkpoints. These are frequent enough to provide the illusion of asynchronous execution. The checkpoints typically occur during allocations, loop controls, or interactions with the system (particularly system calls). OCaml guarantees that a program that does not loop, does not allocate, and does not interact with the system will not have its execution interleaved with that of a signal handler. In particular, storing an unallocated value (integer, boolean, etc. — but not a float!) in a reference cell cannot result in the race condition described above.

4.4  How to mask signals

Signals may be blocked. Blocked signals are not ignored, but put on standby, generally to be delivered later. The sigprocmask system call makes it possible to change the mask for incoming signals:

val sigprocmask : sigprocmask_command -> int list -> int list

sigprocmask cmd sigs changes the list of blocked signals and returns the list of signals that were blocked before the execution of the function. This makes it possible to later reset the mask to its previous state. The argument sigs is a list of signals and cmd a value of type sigprocmask_command which determines the effect of the call:

SIG_BLOCKThe signals sigs are added to the list of blocked signals.
SIG_UNBLOCKThe signals sigs are removed from the set of blocked signals.
SIG_SETMASKThe signals sigs are exactly the signals to be blocked.

A typical usage of sigprocmask is to mask certain signals temporarily.

let old_mask = sigprocmask cmd sigs in (* do something *) let _ = sigprocmask SIG_SETMASK old_mask

Often, one has to guard against possible errors by using the following pattern:

let old_mask = sigprocmask cmd sigs in let treat () = ((* do something *)) in let reset () = ignore (sigprocmask SIG_SETMASK old_mask) in Misc.try_finalize treat () reset ()

4.5  Signals and system calls

Certain system calls can be interrupted by unignored signals. These system calls are known as slow calls, which can take an arbitrary amount of time (for example terminal i/o, select, system, etc.). If an interruption occurs, the system call is not completed and raises the exception EINTR. However file i/o is not interruptible: although these operations can suspend the running process to execute another process for disk i/o, when this occurs the interruption will always be brief if the disk functions correctly. In particular, the throughput of data depends only on the system, and not another user’s process.

Ignored signals are never delivered and a masked signal is not delivered until unmasked. But in all other cases we must protect our system calls against unwanted interruptions. A typical example is a parent waiting for the termination of a child. In this case, the parent executes waitpid [] pid where pid is the process id of the child. This is a blocking system call, it is thus slow and could be interrupted by the arrival of a signal, especially since the sigchld signal is sent to the parent when a child process dies.

The module Misc define the function restart_on_EINTR which makes it possible to repeat a system call when it is interrupted by a signal, i.e. when the EINTR exception is raised.

let rec restart_on_EINTR f x = try f x with Unix_error (EINTR, _, _) -> restart_on_EINTR f x

To wait on a child correctly, call restart_on_EINTR (waitpid flags) pid.

Example

Children can also be recovered asynchronously in the signal handler of sigchld, especially when their return value does not matter to the parent. But when the process receives the sigchld signal, it is not possible to know the exact number of terminated processes, since if the signal is received several times within a short period of time the handler is invoked only once. This leads to the library function Misc.free_children function to handle the sigchld signal.

let free_children _ = try while fst (waitpid [ WNOHANG ] (-1)) > 0 do () done with Unix_error (ECHILD, _, _) -> ()

free_children executes waitpid in non-blocking mode (option WNOHANG) to recover any dead children and repeats until either there are only live children (zero is returned instead of a child id) or there are no children (ECHILD exception).

Note that it is not important to guard against the EINTR exception because waitpid is non-blocking when called with the WNOHANG option.

* * *
Example

The function system in the Unix module is simply defined as:

let system cmd = match fork () with | 0 -> begin try execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] with _ -> exit 127 end | id -> snd (waitpid [] id);;

The specification of the system function in the C standard library states that the parent ignores sigint and sigquit signals and masks the sigchld signal during the command’s execution. This makes it possible to stop or kill the child process without affecting the main program’s execution.

We prefer to define the function system as a specialization of the more general function exec_as_system which does not necessarily go through the shell.

1 let exec_as_system exec args = 2 let old_mask = sigprocmask SIG_BLOCK [ sigchld ] in 3 let old_int = signal sigint Signal_ignore in 4 let old_quit = signal sigquit Signal_ignore in 5 let reset () = 6 ignore (signal sigint old_int); 7 ignore (signal sigquit old_quit); 8 ignore (sigprocmask SIG_SETMASK old_mask) in 9 let system_call () = match fork () with 10 | 0 -> 11 reset (); 12 (try exec args with _ -> exit 127) 13 | k -> 14 snd (restart_on_EINTR (waitpid []) k) in 15 try_finalize system_call () reset ();; 16 17 let system cmd = 18 exec_as_system (execv "/bin/sh") [| "/bin/sh"; "-c"; cmd |];;

Note that the signal changes must be made before the call to fork is executed because the parent could receive signals (e.g. sigchld if the child were to finish immediately) before it proceeds. These changes are reset for the child on line 11 before executing the command. Indeed, all ignored signals are preserved by fork and exec and their behavior is preserved by fork. The exec system call uses the default behavior of signals except if the calling process ignores a signal in which case it also does.

Finally, the parent must also reset the changes immediately after the call, even if an error occurs. This is why try_finalize is used on line 15.

* * *

4.6  The passage of time

Legacy approach to time

Since the earliest versions of Unix, time has been counted in seconds. For compatibility reasons, therefore, one can always measure time in seconds. The current time is defined as the number of seconds since January 1st, 1970 at 00:00:00 gmt. It is returned by the function:

val time : unit -> float

The sleep system call can pause the execution of a program for the number of seconds specified in its argument:

val sleep : int -> unit

However, this function is not primitive. It is programmable with more elementary system calls using the function alarm (see above) and sigsuspend:

val sigsuspend : int list -> unit

The sigsuspend l system call temporarily suspends the signals in the list l and then halts the execution of the program until the reception of a signal which is not ignored or suspended (on return, the signal mask is reset to its old value).

Example

Now we may program the sleep function:

1 let sleep s = 2 let old_alarm = signal sigalrm (Signal_handle (fun s -> ())) in 3 let old_mask = sigprocmask SIG_UNBLOCK [ sigalrm ] in 4 let _ = alarm s in 5 let new_mask = List.filter (fun x -> x <> sigalrm) old_mask in 6 sigsuspend new_mask; 7 let _ = alarm 0 in 8 ignore (signal sigalrm old_alarm); 9 ignore (sigprocmask SIG_SETMASK old_mask);;

Initially, the behavior of the sigalrm signal does nothing. Note that “doing nothing” is the same as ignoring the signal. To ensure that the process will be awakened by the reception of the signal, the sigalrm signal is put in an unblocked state. Then the process is put on standby by suspending all other signals which were not already suspended (old_mask). After the alarm is signaled, the preceding modifications are erased. (Note that line 9 could be placed immediately after line 2 because the call to sigsuspend preserves the signal mask.)

* * *

Modern times

In more modern versions of Unix, time can also be measured in microseconds. In OCaml, time measured in microseconds is represented by a float. The gettimeofday function is the equivalent of the time function for modern systems.

val gettimeofday : unit -> float

Timers

In present-day Unix each process is equipped with three timers, each measuring time from a different perspective. The timers are identified by a value of type interval_timer :

ITIMER_REALReal time (sigalrm).
ITIMER_VIRTUALUser time (sigvtalrm).
ITIMER_PROFUser time and system time (sigprof).

The state of a timer is described by the interval_timer_status type which is a record with two fields (each a float) representing time:

A timer is therefore inactive when its two fields are 0. The timers can be queried or modified with the following functions:

val getitimer : interval_timer -> interval_timer_status val setitimer : interval_timer -> interval_timer_status -> interval_timer_status

The value returned by setitimer is the old value of the timer at the time of the modification.

Exercise 11

To manage several timers, write a module with the following interface:

module type Timer = sig open Unix type t val new_timer : interval_timer -> (unit -> unit) -> t val get_timer : t -> interval_timer_status val set_timer : t -> interval_timer_status -> interval_timer_status end

The function new_timer k f should create a new timer of the timer-type k starting the action f, and inactive on creation; the function set_timer t should set the value of the timer t (and return the old value).

* * *

Date calculations

Modern versions of Unix also provide functions to handle dates, see the structure tm which allows dates and times to be expressed according to a calendar (year, month, etc.) and the conversion functions: gmtime, localtime, mktime, etc.

4.7  Problems with signals

Owing to their asynchronous nature, the use of signals for inter-process communication presents some limitations and difficulties:

Signals offer only a limited form of asynchronous communication but carry all the difficulties and problems associated with it. If possible, it is therefore better not to use them. For example, to wait for a small amount of time, select can be used instead of alarms. But in certain situations signals must be taken into account (for example in command line interpreters).

Signals are possibly the least useful concept in the Unix system. On certain older versions of Unix (System V, in particular) the behavior of a signal is automatically reset to Signal_default when it is received. The signal handling function can of course register itself again. For example in the “beep” example on page ??, it would be necessary to write:

let rec beep _ = set_signal sigalrm (Signal_handle beep); output_char stdout '\007'; flush stdout; ignore (alarm 30);;

However the problem is that the signals that are received between the instant were the behavior is automatically reset to Signal_default and the moment were set_signal is invoked are not treated correctly and depending on the type of the signal they may be ignored or cause the process to die instead of invoking the signal handling function.

Other flavors of Unix (bsd or Linux) provide better support: the behavior associated with a signal is not altered when it is received, and during the handling of a signal other signals of the same type are put on hold.


1
These are the default keystrokes, but it is possible to change them by modifying the properties of the terminal, see section 2.13.

Previous Up Next