The abort statement is intended for use in response to those error conditions where recovery by the errant task is deemed to be impossible. Tasks which are aborted are said to become abnormal, and are thus prevented from interacting with any other task. Ideally, an abnormal task will stop executing immediately: the aborted task becomes abnormal and any non-completed tasks that depend upon an aborted task also become abnormal. Once all named tasks are marked as abnormal, then the abort statement is complete, and the task executing the abort can continue. It does not wait until named tasks have actually terminated [BW98, Section 10.2].
After a task has been marked as abnormal, execution of its body is aborted. This means that the execution of every construct in the task body is aborted. However, certain actions must be protected in order that the integrity of the remaining tasks and their data be assured. The following operations are defined to be abort-deferred : a protected action, waiting for an entry call to complete, waiting for termination of dependent tasks, and the execution of an ``initialize'' procedure, a ``finalize'' procedure, or an assignment operation of an object with a controlled part. In addition, the run-time also needs to defer abortion during the execution of some run-time subprograms to ensure the integrity of its data structures. The language also defines the abort-completion points: 1) The end of activation of a task, 2) The point where the execution initiates the activation of another task, 3) The start or end of an entry call, accept statement, delay statement or abort statement, and 4) The start of the execution of a select statement, or of the sequence or statements of an exception handler.
In general, processing an abort requires unwinding the stack of the target task, rather than immediately jumping out of the aborted part (or killing the task, in the case of entire-task abortion). There may be local controlled objects, which require the execution of a finalization routine. There also may be dependent tasks, which require the aborted processing block until they have been aborted, finalized, and terminated. The finalization must be done in LIFO order and the stack contexts of the objects requiring finalization must be preserved until the objects are finalized [GB94, Section 3.4]
Abort-deferral implementation can be divided into two parts [GB94, Section 3.3]: 1) determine whether abort is deferred for a given task, at the point it is targeted for abortion, and 2) ensure deferred aborts are processed immediately when abort-deferral is lifted. In general, the determination of whether a given task is abort-deferred must be carried out by the task itself. In a single-processor system, it may be possible for the task initiating an abort to determine whether the target task is abort-deferred. However, in a multi-processor system, or a single processor system where the Ada run-time is not in direct control of task scheduling, this is not possible. The abort-deferral state of the target task may change between the point it is tested and the point the target task is interrupted.
There are two obvious techniques for recording whether a task is abort-deferred. One technique is sometimes termed PCmapping. The compiler and link-editor generate a map of abort-deferred regions. Whether the task is abort-deferred can then be determined by comparing the current instruction-pointer value, and all the saved return addresses of active subprogram calls, against the map. To ensure the abort is processed on exit from the abort-deferred region, one overwrites the saved return address of the outermost abort-deferred call frame with the address of the abort-processing routine (saving the old return address elsewhere). The test for abort deferral may take time proportional to the depth of subprogram call nesting, but that occurs only if an ATC is attempted. Until that occurs, no runtime overhead is incurred for abort deferral. A restriction of this method is that abort-deferred regions must correspond to callable units of code. Another restriction is that the subprogram calling convention is constrained to (1) ensure the return addresses are always in a predictable and accessible location and (2) ensure this data is always valid, even if the calling sequence is interrupted. Unfortunately, that is not true for some architectures [GB94, Section 3.3].
In the second technique the task increments and decrements a deferral nesting level (e.g. in a dedicated register or the ATCB), whenever it enters and exits an abort-deferred region. On exit from such a region, if the counter goes to zero, the task must check whether there is a pending abort and, if so, process the abort. This deferral-counter method imposes some distributed overhead on entry and exit of abort-deferred regions, but allows quick checking [GB94, Section 3.3]. The GNAT run-time implements this second technique.
ATC implementation must address the following issues [GB94, Section 3]:
ATC is very much like exception propagation, so it is desirable that one mechanism serve for both purposes. Since ATC is not likely to be used in non real-time Ada programs, a key objective of any implementation should be to impose little or no distributed overhead for the existence of this language feature. In principle, some efficiency might be gained by avoiding detailed unwinding of the stack, executing the finalization routines from the top of the stack or from a different stack, then poping the entire stack down to the context where control is to be transferred. However, this presumes there is some way to recover that context without full unwinding. If the compiler uses a callee-save register spilling convention, there may be values of live registers spilled at unpredictable locations on the stack. In this case, it seems one must create a register save area for each potential target of ATC (analogous to jump-buffer implementation of C's setjmp() and longjmp() operations). While asynchronous select statements may not be very common, exception handlers are common (some implicitly provided by the compiler), and controlled objects are also expected to be common. Thus, the overhead of creating a jump-buffer for every potential asynchronous transfer point is objectionable [GB94, Section 3.4].
Some means must be provided for locating finalization routines, and the point at which execution is to resume after an ATC. This problem is very similar to that of finding an exception handler, and the same solutions apply. The main approaches are saving a pointer in the stack frame for each scope, PC-mapping, and various hybrids of the two. The PC-mapping approach is generally preferable, since it imposes no distributed overhead on execution. If PC-mapping is used for the latter purpose, there is strong motivation to try to make the same technique serve double duty, for abort-deferral [GB94, Section 3.4].
The GNAT implementation of abortion is made up of:
Figure 20.1 presents the sequence of run-time subprograms involved in the task abortion, which are described in the following sections.
The GNAT run-time subprogram Task_Entry_Call (cf. Section 15.5.4) not only gives support to normal entry-calls but also the ATC entry-calls. In this latter case, because ATCs can be nested, the run-time needs to store all these pending entry-calls. For this purpose, the GNAT run-time associates an entry-call stack to each Ada task (cf. Figure 20.2). The Pending_ATC_Level ATCB field is used to signal an ATC abortion. In order to distinguish the Abort statement from the ATC abortion, the run-time defines the following rules:
GNARL.Undefer_Abort subprogram is the universal polling point for deferred processing. It gives support to base priority changes, exception handling, and asynchronous transfer of control (ATC). In case of base-priority change, after the new priority is set, it yields the processor so that the scheduler chooses the next tasks to execute. In the other cases, it verifies if there is some pending exception to raise (ATC abortion raises the internal exception Abort_Signal).
Locked_Abort_To_Level sets to true the ATCB flag Pending_Action. and, depending on the current state of the target task (blocked or running) it calls GNARL.Wakeup or GNARL.Abort_Task:
In both cases the internal exception Abort_Signal unwinds the stack of the aborted task.
The GNARL implementation of the Ada abort statement is made up of one flag in the ATCB: Aborting, one exception _Abort_Signal, and one POSIX signal (SIGABRT). The flag prevents a race between multiple aborters and the aborted task. The exception is can only be handled by run-time system code. The POSIX signal can not be masked.