An interrupt represents a class of events that are detected by the hardware or system software. The occurrence of an interrupt consists of its generation and its delivery: the generation of an interrupt is the event in the underlying hardware or system which makes the interrupt available to the program; delivery is the action which invokes a part of the program (called the interrupt handler) in response to the interrupt occurrence. In between the generation of the interrupt and its delivery, the interrupt is said to be pending. The handler is invoked once for each delivery of the interrupt. While an interrupt is being handled, further interrupts from the same source are blocked; all future occurrences of the interrupt are prevented from being generated. It is usually device dependent as to whether a blocked interrupt remains pending or is lost.
Ada allows to associate an interrupt to a protected procedure or a task entry declared at library level. However, the association of a task entry is considered an obsolescent feature of the language [AAR95, Section J.7]. For this reason, in this chapter we will focus our attention on user-defined protected-procedure interrupt-handlers.
Certain interrupts are reserved. The programmer is not allowed to provide a handler for a reserved interrupt. Usually, a reserved interrupt is handled directly by the Ada run-time (for example, a clock interrupt used to implement the delay statement). Each non-reserved interrupt has a default handler that is assigned by the run-time system.
There are two two styles of interrupt-handler installation and removal: nested and non-nested. In the nested style, an interrupt handler in a given protected object is implicitly installed when the protected object comes into existence, and the treatment that had been in effect beforehand is implicitly restored when the protected object ceases to exist. In the non-nested style, interrupt handlers are installed explicitly by procedure calls, and handlers that are replaced are not restored except by explicit request [Coh96, Section 19.6.1].
The front-end identifies a handler to be installed in the nested style because it must have the pragma Attach_Handler specifying the corresponding interrupt_id. Dynamic allocation of protected objects gives greater flexibility. Allocating a protected object with an interrupt handler installs the handler associated with that object, and deallocating the protected object restores the handler previously in effect. Similarly, a handler to be installed in the non-nested style is identified by pragma Interrupt_Handler. This pragma imposes a restriction on the object: it must be dynamically created [Coh96, Section 19.6.1]. Non-nested installation and removal of interrupt handlers relies on additional facilities of package Ada.Interrupts [AAR95, Section C.3(2)].
To foster a simple, efficient and multi-platform implementation, GNAT reuses the POSIX support for signals and adds the minimum set of run-time subprograms required to achieve the Ada semantics. This work is simplified because POSIX signals are delivered to individual threads in a multi-threaded process using much of the same semantics as for delivery to a single-threaded process [GB92, Section 5.1].
A POSIX signal is a form of software interrupt which can be generated in several ways. A signal may be generated [DIBM96, Section 2]:
Each POSIX thread has a signal mask: when a signal is generated for a thread and the thread has the signal masked, the signal remains pending until the thread unmasks it; the interface for manipulating the thread signal mask is pthread_sigmask. Only one pending instance of a masked signal is required to be retained; that is, if a signal is generated N times while it is masked the number of signal instances that are delivered to the thread when it finally unmasks the signal may be any number between 1 and N.
Each POSIX signal is associated with some action. The action may be to ignore the signal, terminate the process, continue the process, or execute a call to user-defined handler function (asynchronously and preemptively with respect to normal execution of the process). POSIX.1 specifies a default action for each signal. For most signals the application may override the default action by calling the function sigaction. The use of asynchronous handler procedures for signals is not recommended for POSIX threads, because the POSIX thread synchronization operations are not safe to be called within an asynchronous signal handler; instead, POSIX.1c recommends use of the pthread_sigwait function, which ``accepts'' one of a specified set of masked signals.
The definitions of ``reserved'' differs slightly between the ARM and POSIX. ARM specifies [AAR95, Section C.3(1)]:
The set of reserved interrupts is implementation defined. A reserved interrupt is either an interrupt for which user-defined handlers are not supported, or one which already has an attached handler by some other implementation-defined means. Program unit can be connected to non-reserved interrupts.
POSIX.5b/.5c specifies further [s-intman.adb]:
Signals which the application cannot accept, and for which the application cannot modify the signal action or masking, because the signals are reserved for use by the Ada language implementation. The reserved signals defined by this standard are:
The signals defined by POSIX.5b/5c that are not specified as being reserved are SIGHUP, SIGINT, SIGPIPE, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2, SIGCHLD, SIGCONT, SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGIO, SIGURG and all the real-time signals.
The GNAT FSU Linux implementation handles 32 signals. In this case the reserved signals are:
Number Name REASON Description ------ ---------- ------ ---------------------------------- 2 * SIGINT GNAT Abort (used for CTRL-C) 4 * SIGILL POSIX (HW) Illegal Instruction 5 * SIGTRAP GNAT Trace trap 6 * SIGABRT GNAT Tasks abortion 7 * SIGBUS POSIX (HW) Bus error 8 * SIGFPE POSIX (HW) Floating Point Exception 9 SIGKILL POSIX Abort (kill) 11 * SIGSEGV POSIX (HW) Segmentation Violation 14 SIGALRM POSIX Alarm Clock 19 SIGSTOP Stop 20 * SIGTSTP GNAT User stop requested from tty 21 * SIGTTIN GNAT Background tty read attempted 22 * SIGTTOU GNAT Background tty write attempted 26 SIGVTALRM Virtual timer expired 27 * SIGPROF GNAT Profiling timer expired 31 SIGUNUSED Unused signal
Signals marked with * are not allowed to be masked by the GNAT Run-Time. SIGINT can not be masked because it is used to terminate the Ada program when the CTRL-C sequence is pressed in the terminal that is controlling the process. By keeping SIGINT reserved, the programmer allows the user to do Ctrl-C but, in the same way, disable the ability of handling this signal in the Ada program. GNAT Pragma Unreserve_All_Interrupts [Cor04] gives the programmer the ability to change this behavior. SIGILL, SIGFPE and SIGSEV can not be masked because they are used by the CPU to notify errors to the run-time. SIGTRAP is used by GNAT to enable debugging on multi-threaded applications. SIGABRT can not be masked because it is used by GNAT to implement the tasks abortion (described in chapter 20). SIGTTIN, SIGTTOU and SIGTSTP are not allowed to be masked so that background processes and IO behaves as normal C applications. Finally, SIGPROF can not be masked to avoid confusing the profiler.
No matter the association style used, GNARL always uses the following tables indexed by the Interrupt_ID to handle interrupts.
Figure 19.2 represents one protected procedure attached to signal SIGUSR1 in nested style (static style). The GNAT compiler associates two subprograms P and N to each protected subprogram (described in section 11.2.2). As the reader can see, the run-time links the signal with the P subprogram: the reference to the P subprogram is stored in the corresponding field of the table, and the Static field is set to True to remember that it is a nested style association.
The GNAT run-time uses one Interrupts Manager task to serialize the execution of subprograms involved in the management of signals: attachment, detachment, replacement, etc. Figure 19.3 presents a simplified version of the automaton implemented by the Interrupt Manager. For simplicity we have considered only two basic operations: Binding and Unbinding User-Defined Interrrupt Procedures (UDIP) to interrupts.
First the automaton calls GNARL subprogram Make_Independent to do the Interrupt Manager Task independent of its masters. GNARL Independent tasks are associated with master 0, and their ATCBs are not registered in All Tasks List (described in section 14.1); thus they last until the end of the program. After the signal mask is set, the automaton goes to one state in which it waits for the next signal management operation.
The Ada run-time must provide a thread to execute the UDIP. There is a choice between dedicating one server task for all signals and providing a server task for each signal. The former approach looks attractive, since it saves run-time space, but it blocks other signals during the protected procedure call. This may result in delayed or lost signals. For this reason, GNARL provides a separate Server Task for each signal [DIBM96].
Instead of create/abort Server Tasks when the user-defined interrupt handlers are attached/detached, GNARL keeps them alive until the program terminates. Thus they are reused by all UDIPs associated with the same interrupt during the life of the program. The run-time has a Server_ID Table which saves Server Tasks references (cf. Figure 19.4).
Figure 19.5 presents a simplified version of the Server Tasks Automaton.
Previous sections have been concerned with the basic functionality of the Interrupt Manager Task and the Server Tasks. However, the GNARL implementation is a little more complex because:
The second requirement (locks) is easy to solve by means of POSIX mutexes. However, the first requirement is more complex. So let's focus our attention on the GNARL solution of the first requirement.
In order to better understand the GNARL implementation, we need to simplify the Server Tasks Automaton to its main states:
In order to notify the automaton that it must jump from State 1 to State 2 GNARL uses one POSIX Condition Variable; in order to force the automaton to jump from State 2 (waiting in the POSIX sigwait operation) to State 1 the POSIX signal SIGABORT is used (this signal is used to kill the POSIX thread, and thus forces the Server Task to return from the POSIX sigwait operation). Figure 19.7 presents this automaton.
If we add these new transitions to our basic Task Server Automaton (cf. Figure 19.5) we have the real automaton implemented in GNARL (cf. Figure 19.7). In order to help the reading of the automaton all the states have been numbered. Inside dotted rectangles we find the states associated with the simplified states of the previous example (State_1 and State_2).
After the initializations (states numbered 1 to 3), the automaton verifies if any UDIP has been registered by the Interrupt Manager (state 4). Initially, because no UDIP has been registered, it takes the POSIX default action (state 9) and waits in the Condition Variable (cond_wait, state 10) until some UDIP is registered by the Interrupt_Manager.
When any UDIP is registered, the Interrupt_Manager signals the Condition Variable and the Server Task Automaton jumps to state 4, checks if some UDIP has been registered (now this evaluates to True) and jumps to state 5 to wait for the next signal occurrence. When the signal is received, it again checks if the UDIP is still registered (state 6), because it may have been removed by the Interrupt Manager while the automaton was waiting for the signal. Then it calls the UDIP (state 7) and again jumps to state 4.
While the Server Task is in state 5 waiting for the signal occurrence, it may happen that all UDIPs have been removed the Interrupt Manager. In this case the Interrupt Manager sends the SIGABRT signal to the Server Task to force it to jump to state 9. This signal wakes up the Server Task Automaton, which jumps to state 8 to reply to the Interrupt Manager with the same signal to inform it is not in state 5 (waiting for the signal). After this notification the automaton jumps to state 4 and, because no UDIP is found, it jumps to state 9.
In the nested style the expander generates a call to Install_Handlers in the initialization procedure of the protected object. This subprogram saves the previous handlers in one additional field of the object (Previous_Handlers) and installs the new handlers.
In the non-nested style, nothing special needs to be done since the default handlers will be restored as part of task completion which is done just before global finalization.
In order to verify at run-time that all the non-nested style interrupt procedures have been annotated with pragma Interrupt_Handler ([AAR95, Section C.3.2] requirement) the compiler adds calls to the GNARL subprogram Register_Interrupt_Handler to register these interrupt procedures in a GNARL single-linked list. The Head and Tail of this list are stored in two GNARL variables (Registered_Handler_Head and System.Interrupts.Registered_Handler_Tail, cf. Figure 19.8). Every node keeps the address of one protected procedure associated with an interrupt in non-nested style. For simplicity, a single access to a protected procedure has been represented; however, each node has the access to its corresponding P subprogram. Before the attachment of one non-nested style interrupt handler to one signal, GNARL traverses this list to verify that the protected procedure is registered in the list; otherwise it raises the exception Program_Error.
In this chapter we have dealt with the main aspects related to Interrupts Management. Although Ada allows us to attach a task entry to an interrupt, nowadays this is considered an obsolescent feature of the language. Thus, we have only discussed the attachment of User-Defined Protected-Procedures to interrupts. The main features of the GNAT implementation are: