TOC PREV NEXT INDEX DOC LIST MASTER INDEX



Multithreaded Ada

This document describes Apex support for Multithreaded Ada, which implements Ada tasking semantics over the POSIX threads interface (IEEE POSIX 1003.1c). Multithreaded Ada is available for all native (self-hosted) Apex implementations over UNIX, which includes Sun Solaris, DEC OSF-1 for Alpha, SGI Irix, HP/UX, and IBM AIX operating systems. It is the only option for Rational Apex Embedded to PowerPC for LynxOS.

Topics discussed in this section are:


Getting Started

Native (Self-hosted) Apex for UNIX

If you use the default model views supplied with Apex, the simplest way to use the POSIX Threaded runtime is to specify the <arch>.<lang>.x.y.z model, where <lang> is one of ada83 or ada95 and <arch> is architecture specific (see table below).

Architecture
<arch>
DEC OSF-1
alpha_osf1
HP/UX
hppa_hpux
SGI Irix
mips_irix_n32
IBM AIX
rs6k_aix
Sun Solaris
sun4_solaris2

In the closure of the program, typically in the main unit or a pragma.1.ada file, specify the configuration pragma:

pragma Concurrent_Processing_Policy (Threaded_Processing);  

This pragma directs the prelinker to include the POSIX Threaded runtime when linking an application, together with the OS libraries required to support it.

Apex Embedded to PowerPC for LynxOS

Because Apex for LynxOS is an embedded product, and because it only supports Multithreaded Ada, its usage differs from that of native products. For usage instructions, see the Rational Apex Embedded Programming Guide for LynxOS.


The POSIX Threaded Runtime

Overview

This section gives an overview of the POSIX Threaded Ada runtime and the other operating system facilities on which it depends.

A threaded runtime system provides support for Ada tasks running in a "threaded" environment. Threaded Ada tasking takes advantage of operating system facilities which implement the parallel execution of "threads" within a single process address space. Often, the operating system's implementation of threads exploits the ability to run threads in parallel on multiple processors, but this is not the only advantage of using threads. An Ada application using a threaded runtime can make use of software from threaded libraries, with tasks and threads coexisting (though not in general interacting) in the same process. Also, OS threads can be more integrated with other OS services than multithreading facilities implemented without operating system complicity (e.g. the Rational Exec microkernel). For example, a thread waiting for the completion of an I/O operation will not in general prevent other threads in the same process from executing.

Note: With Rational Exec an Ada task waiting on an Ada I/O operation will not block other tasks. This is accomplished by avoiding OS blocking operations in the predefined package implementations. Direct use of POSIX blocking operations from a Rational Exec application can block the entire process.

Every operating system implements threads differently, but all thread implementations share some common characteristics:

Threads offer an efficient primitive for managing concurrency; they make an ideal starting place for the implementation of an Ada runtime system. While the threads library focuses on scheduling threads for execution on one or more processors, the Ada runtime need only concern itself with implementing Ada tasking semantics.

The POSIX Threaded Ada runtime provides tasking services for Ada programs running on systems implementing the POSIX 1003.1c System Application Program Interface (API). In particular, this API provides services that:

Ada tasks are built on top of the POSIX threads facility. For each Ada task there is a corresponding POSIX thread. The Ada runtime system implements the semantics of Ada tasks, including elaboration, rendezvous, abort, and exceptions. This layering is as described in Chapter 1 (Runtime System Overview) of the Runtime Guide, with a POSIX 1003.1c implementation serving as the Target Kernel layer.

In brief, the compiler translates Ada tasking constructs into calls to the Ada Tasking Layer, whose interface and implementation are common to all Apex runtime systems. This layer is implemented using calls to an Ada Kernel layer whose interface is common to all runtime systems but whose implementation varies depending on the Target Kernel layer over which it is implemented. In the case of the POSIX Threaded runtime, a POSIX 1003.1c implementation serves as the Target Kernel layer, so the corresponding Ada Kernel layer is implemented using threads, mutexes, condition variables, and other POSIX constructs.

Four Ada binding packages define the runtime's Ada interface to the POSIX API:

Pthread
POSIX thread services
Psignal
POSIX signal services
Ptime
POSIX clock and timer services
Putil
Other POSIX services

P1003.1c is a C language interface, so varying amounts of "glue" code are required to provide this functionality to the Ada kernel implementation, which is written in Ada. This requires reimplementing part of the API in Ada, in particular the POSIX parameter types. For this reason, these packages are architecture specific. In general, providing this binding to the C API can be complicated, since there are C features, such as macro processing, which cannot be accessed directly by Ada. There are also cases where the required POSIX features are not provided by the OS and so must be implemented in these packages. However, in most cases they access POSIX functions directly using Import pragmas.

Note that this is a special-purpose "thin" binding to the C interface. It is intended to provide an Ada version of enough of the P1003.1c API to support Ada tasking semantics. This "thin" binding conforms to no standard, and in particular not to the IEEE POSIX 1003.5b Ada binding. The use of these packages may conflict with the Ada runtime in ways that violate Ada tasking semantics, the POSIX 1003.1c definition, or both.

For example, package Pthread provides operations that can be used to create a thread from an Ada application. The thread can execute most sequential Ada statements, but it lacks the Ada Task Control Block (ATCB) used by the runtime system to implement Ada tasking semantics over a thread. Attempts to use Ada tasking features (e.g. calling an entry) from such an "Ada thread" will have unpredictable results.

Conflicts can also result from manipulating Ada tasks as POSIX threads. For example, if the thread cancellation facility were applied to an Ada thread, none of the cleanup needed to terminate an Ada task would be performed, since the threads runtime neither uses nor supports thread cancellation. Among other violations of Ada semantics, dependent tasks would not be awaited, finalization of controlled objects would not be performed, and pending entry calls would not be cancelled.

In addition to the packages providing the "thin" POSIX binding, there is one additional package that provides OS memory allocation services:

Os_Alloc
memory allocation

Task Attributes

Most of the time you do not need to be concerned with the layering of Ada tasks onto operating system threads. However, when you require specific scheduling behavior or you need to control the resources used by an Ada task, you need a method to pass information from the Ada program to the operating system's threads library. The object that passes thread-related scheduling information for an associated Ada task is known as the task attribute record.

Each Ada task is created with the attributes specified by a task attribute record. The task attribute record can be specified for individual tasks using the Task_Attributes pragma. In the absence of the pragma, it is specified by default task attributes in the configuration table.

The Task_Attributes Pragma

Task_Attributes is a special Ada pragma used to associate a given task attribute setting to an Ada task when that task is created.

The first form of the pragma implies the name of the task object or type declaration that contains pragma Task_Attributes. The second form of pragma Task_Attributes is used to select a particular task object.

Default Task Attributes

Whenever the runtime creates a task to which no Task_Attributes pragma applies, a default task attribute record is used. These defaults are defined in the user configuration table in v_usr_conf.2.ada and as such can be changed; instructions for making these modifications and incorporating them into your application can be found in the Runtime Guide under Configuring the User Library.

The default task attribute record is used any time an Ada task is created and no other task attribute applies. This is defined by the Default_Task_Attributes field in the user configuration table.

The remaining global task attributes apply to language or runtime defined tasks. They are all specified by addresses in the user configuration table that point to attribute records defined elsewhere. Setting these addresses to Null_Address causes the runtime system to use Default_Task_Attributes to create the corresponding tasks.

The Main_Task_Attr_Address parameter in the user configuration table points to the task attributes for the environment task. Since the environment task cannot be named in a Task_Attributes pragma, Main_Task_Attr_Address is the only way to access its task attributes.

The Signal_Task_Attr_Address parameter is used to create tasks that rendezvous with interrupt entries attached to signals; these tasks are awakened by handlers for the corresponding signals. In the case of the threaded runtime, the signal handlers themselves are tasks. These handler tasks are created with the Intr_Task_Attr_Address parameter.

The Task Attribute Record

The task attribute record type is defined in the file ada_krn_defs.1.ada. This record is specific to the Ada Kernel implementation; the one for POSIX threads is:

Each of the task attribute record components plays an important role in the execution of an Ada task and the operating system thread associated with it. Each field is introduced below:

The Ada runtime system overrides certain of the thread attributes specified by the Krn_Task_Attr component, specifically thread priority and stack size. The thread priority is determined by the Prio or by the application program, as described above. The stack size of a thread is specified using the Storage_Size pragma or by the Default_Task_Stack_Size and Main_Task_Stack_Size configuration table components in the V_Usr_Conf package. Since the runtime sets these attributes, setting their values in the Krn_Task_Attr component directly using Pthread_Attr_Setstacksize or Pthread_Attr_Setschedparam will have no effect.

As an object of an opaque type, the Krn_Task_Attr component cannot be copied. This means that when a task is created with the default task attributes, the corresponding thread is created with the object that you provide. Because the runtime overrides the stack size and perhaps the priority attributes specified by the Krn_Task_Attr component, it must modify it. Since multiple tasks may create new tasks simultaneously, there can be conflicting demands for the default Krn_Task_Attr object. For this reason, the runtime locks a special mutex whenever accessing Krn_Task_Attr to synchronize its use between tasks. Note that this means that task creation can reduce the opportunities for parallel execution of tasks, though the effect should only become important if task creation is very frequent.

There are similar considerations for task attribute records used with the Task_Attributes pragma. A single task attributes record can be used in many Task_Attributes pragmas, and the runtime system will synchronize its use of this record between multiple tasks. However, care should be taken that the application not manipulate this record while it might be in use in creating a task.

The Default_Task_Attributes are initialized in V_Usr_Conf.V_Init_Attr. Most of the time you will use the default V_Init_Attr provided with the Apex distribution. This attempts to choose reasonable defaults for the POSIX implementation over which it is run. In general, it will use the SCHED_OTHER scheduling policy unless the Ada real-time scheduling policy Fifo_Within_Priorities is requested using the Task_Dispatching_Policy pragma, in which case SCHED_FIFO is used. There are other architecture-specific considerations, such as whether system privileges are needed to use SCHED_FIFO, whether SCHED_FIFO is supported, whether SCHED_OTHER supports priority scheduling, and whether priority ceiling mutexes are supported (see Priority Ceiling Locking, below) that may effect the defaults chosen. Consult the version of V_Init_Attr supplied with your version of Apex for details.

Ada Priority and POSIX Priority

The POSIX priority model is more general than that of Ada. Each POSIX scheduling policy can have a separate range of priorities, and different threads within the same process can use different scheduling policies. Scheduling policies can also be changed dynamically. The range of the Ada Any_Priority type is fixed for a given execution, and in Apex it is fixed for a given architecture. This range applies to all tasks and all scheduling policies. If a task dispatching policy has been specified using the Task_Dispatching_Policy configuration pragma, it applies to all tasks in the application and cannot be changed without recompilation.

This mismatch means that by default the Ada runtime implements task priority scheduling over a subset of POSIX thread scheduling, using the same thread scheduling policy for all threads. It also means that the fixed range of Ada priorities must be mapped over whatever range is implemented by that scheduling policy. Since this can be different for different scheduling policies, the mapping must be dynamic.

To maintain a one-to-one mapping between Ada and POSIX priorities, System.Any_Priority has the minimum range of any of the standard POSIX scheduling policies SCHED_OTHER, SCHED_FIFO, and SCHED_RR. For each task, the runtime calculates the difference:

where P is the scheduling policy with which the underlying thread is created. All Ada priorities are converted into POSIX priorities by adding Priority_Bias to them. Note that the lowest thread priority is reserved for use by the runtime system; this is why 1 is added to Priority_Bias.

This allows the runtime to use appropriate thread scheduling policies (such as SCHED_FIFO) to implement POSIX priority scheduling. It can in some cases disallow the use of some POSIX priorities with Ada tasks. Note that this cannot be solved by assigning or changing the priority of a task by using POSIX operations (e.g. pthread_setschedparam()). The Tasking Layer keeps track of the Ada priority of each task, and will reset the task to this priority as dictated by Ada semantics (e.g. when completing a rendezvous). This means that any priority assigned using POSIX operations will be overridden by the runtime system in ways difficult to predict or control.

The Priority_Bias is per-task, so it will allow tasks to be created with different POSIX scheduling policies, though such mixed policies may interact in ways that violate Ada priority scheduling. Note also that while Priority_Bias is per-task, it is assigned statically when the task is created. If POSIX operation such as pthread_setschedparam() are used to change the scheduling policy of the underlying thread after the task has been created, the mapping between Ada and POSIX priorities will not change and may not be appropriate for the new policy. This can cause the runtime to panic, halting the application.

Priority Ceiling Locking

The Ada Real-Time Systems Annex provides for priority ceiling locking using the Locking_Policy pragma (LRM D.3). When Ceiling_Locking is specified, ceiling priorities are implemented for protected objects. A task running at a priority below this ceiling will have its priority raised to the ceiling during the execution of a protected action.

POSIX provides ceiling locking in the form of mutexes initialized with the PTHREAD_PRIO_PROTECT locking protocol, as set using pthread_mutexattr_setprotocol(). Since Ada protected objects are implemented using mutexes, this provides an efficient implementation of the Ceiling_Locking protocol. However, support for PTHREAD_PRIO_PROTECT is optional.

When the POSIX implementation does not support PTHREAD_PRIO_PROTECT, priority ceiling locking is emulated in the Ada Kernel layer. The implementation of Ada_Krn_I.Mutex_Lock explicitly raises the priority of a task locking a ceiling mutex and Ada_Krn_I.Mutex_Unlock explicitly lowers it using the POSIX pthread_setschedparam() operation.

There are two disadvantages to the ceiling emulation provided by the Ada Kernel. One is that it is less efficient than an OS implementation, using multiple POSIX operations for functionality to which the OS has more direct access. The other is that it does not exactly comply with the semantics of task priority scheduling as documented in the Ada Real-Time Systems Annex. When the Ceiling_Locking protocol is in effect, completing a protected action may lower the priority of a task. Such a loss of active priority is not supposed to cause the task to be placed at the end of the ready queue for its priority, that is, it is not supposed to yield to ready tasks at the same priority. However, there is no POSIX operation that will change the priority of a thread without such a yield under the defined priority scheduling policies SCHED_FIFO and SCHED_RR. Unlocking a PTHREAD_PRIO_PROTECT mutex has the correct semantics, but there is no way to emulate that semantics with other POSIX features. Because of these disadvantages, PTHREAD_PRIO_PROTECT is used by the runtime wherever available.

Concurrency Control

Ada tasks are implemented using POSIX threads which must contend for processing resources in order to execute. The set of threads with which a thread contends for processing resources is the scheduling contention scope of the thread (see P1003.1c 13.4.2). The processors for which it contends is the scheduling allocation domain of the POSIX implementation (see P1003.1c 13.4.3).

There are two possible contention scopes for a thread: PTHREAD_SCOPE_SYSTEM and PTHREAD_SCOPE_PROCESS. All of the threads in the system with the PTHREAD_SCOPE_SYSTEM contention scope contend with each other. Threads with PTHREAD_SCOPE_PROCESS contention scope only contend with other such threads within the same process.

The threaded runtime system uses the default contention scope provided by the implementation; consult your OS documentation for this default. The contention scope can be changed using task attributes by initializing the Krn_Task_Attr component with the POSIX pthread_attr_setscope() operation; an Ada interface to this operation is provided in package Pthread.

Threads with PTHREAD_SCOPE_PROCESS may (and probably will) contend for processing resources with threads in other processes and with other contention scopes, but the form of that contention is not specified by the standard. However, most of the platforms supported by the Apex threads runtime provide the X/Open standard interface pthread_setconcurrency(). This function takes an integer argument that serves as a hint to the OS about how many threads should execute simultaneously, that is, how many processors should be used by the process for PTHREAD_SCOPE_PROCESS threads.

An application can take advantage of this hint in one of the following ways:

1 . Use pragma Main with the Concurrency_Level argument. For example, to cause the runtime to call pthread_setconcurrency() with a level of 4, add

2 . Create and modify a new usr_conf.ss view as described in the Runtime Guide under Configuring the User Library. The value of Concurrency_Level in the configuration table should be changed from its default value of 1 to the desired value.

3 . Leave the value of Concurrency_Level set to 1 and set the environment variable, ADA_CONCURRENCY_LEVEL, to the desired value. Since the environment variable can be changed without recompiling and linking the application, this method of setting the concurrency level makes it easy to experiment during the development cycle.

4 . Call the Pthread.Pthread_Setconcurrency from your Ada application.

If the OS does not provide pthread_setconcurrency(), none of these will have any effect. In any case, since the concurrency level is just a hint to the OS, the application should make no assumptions about its effect.

There is no standard POSIX operation for the control of scheduling allocation domain. This is an implementation defined property of the POSIX implementation which is detailed in your OS documentation.

Signals

This section describes signal handling in POSIX Threaded Ada. First it provides an overview of signal handling in POSIX Threads. Then it shows how Ada layers on POSIX Threads to provide signal handling in an application program. Differences from Rational Exec signals are highlighted. The section concludes by talking about the configuration parameters added to v_usr_conf to support signals in POSIX Threaded Ada.

Overview of Signals in POSIX

Signal Handling

Each thread has its own signal mask. A thread's initial signal mask is inherited from the parent thread. Thereafter, it can be modified using the pthread_sigmask() call.

This lets a thread block some signals while it uses memory or other state that is also used by a signal handler. All threads in a process share the set of signal handlers set up by sigaction().

If a signal handler is marked Sig_Dfl or Sig_Ign, the action on receipt of the signal (exit, core dump, stop, continue or ignore) is performed on the entire receiving process and affects all the threads in the process.

There are two delivery modes for signals: synchronous and asynchronous.

Signals that are the direct result of a threads execution, e.g. the execution of an illegal instruction, are delivered synchronously to the thread that caused them. Only certain signals (such as SIGILL, SIGFPE, SIGSEGV, SIGTRAP) are the direct result of thread execution, and so are the only ones that can be delivered synchronously. Note that these signals can also be delivered asynchronously. However, the target process cannot determine in general the delivery mode of a signal, so programs usually assume that signals that can be delivered synchronously actually have been. The Apex threaded runtime makes this assumption.

Asynchronous delivery results from certain asynchronous events. They could be used to notify a process that an asynchronous event has taken place (e.g. SIGIO to notify of I/O completion). They can also be directed at one process by another using the kill() system call, or at one thread by another using the pthread_kill() system call. A signal delivered asynchronously to a process can be handled by any thread whose signal mask has it enabled. If more than one thread is able to receive the interrupt, only one is chosen. One result is that several threads can be in the process of handling the same kind of signal simultaneously. If all threads mask a signal, it will remain pending on the process until some thread enables the signal.

As in single-threaded processes, the number of signals received by the process is less than or equal to the number sent. For example, an application can enable several threads to handle a particular I/O signal. As each new signal comes in, another thread is chosen to handle the signal until all the enabled threads are active. An additional delivery of this signal remains pending, and further deliveries while the signal is pending may be lost.

Waiting for Signals

In a single-threaded process, the primary means of handling signals is through signal handler functions installed using sigaction(). Such a process can wait for a signal (e.g. using sigpause()), but it can't do anything in the meantime. With the introduction of threads, a process can dedicate a thread to wait for the occurrence of a signal while other threads continue to execute. POSIX defines the sigwait() call for this purpose.

The sigwait() call waits for a pending signal from the set specified by the argument, regardless of the signal mask. sigwait() clears the pending signal and returns its number. If more than one thread is waiting for the same signal, then one thread is chosen and it returns from sigwait().

sigwait() is typically used by creating one or more threads that wait for signals. Since sigwait() can retrieve even masked signals, the signals of interest are usually blocked in all threads so they are not delivered accidentally. When a signal arrives a thread returns from sigwait(), handles the signal, and waits for more signals.

There are a number of advantages to waiting for signals in this way. The primary advantage is the execution environment afforded by the handler thread is the same as that of any other thread, so it can use arbitrary OS operations. Signal handler functions execute in a restricted environment and cannot use async-unsafe operations. The signal handling thread is not restricted to using async-safe functions and can synchronize with other threads in the usual way.

Signals in POSIX Threaded Ada

The Ada RTS installs handlers for the SIGILL, SIGFPE, SIGBUS and SIGSEGV synchronous signals. These handlers map the signals into Ada exceptions. In addition, SIGABRT is reserved for Ada preemptive abort.

The Ada RTS uses the sigwait() service for handling all other signals.

When the application attaches a signal handler either explicitly via V_Xtasking.Attach_Isr or implicitly via an interrupt entry or protected procedure interrupt handler, an interrupt task is created for the attached signal handler. The interrupt task loops doing a sigwait() for the attached signal. When the process gets the asynchronous signal, sigwait() returns and the interrupt task calls the ISR procedure. Since the ISR procedure is executing in the context of an Ada task, there are no restrictions on the Ada operations it can perform or the VADS Exec services it can call. This differs radically from the Rational Exec microkernel where the ISR procedure executes in the context of the UNIX signal handler and is very limited in what it can do or call.

The lt command in the debugger can be used to display the interrupt tasks. Here is the debugger lt output when the application has attached handlers for the SIGINT, SIGUSR1 and SIGUSR2 signals:

The handler at value is the address of the ISR procedure called in the interrupt task upon return from sigwait().

A necessary requirement for using sigwait() reliably is to ensure that the signal it waits on is disabled in all threads. To satisfy this need, pthread_sigmask() is called during program initialization to disable all asynchronous signals. This signal mask is inherited by all subsequently created threads.

The VADS Exec services, Current_Interrupt_Status and Set_Interrupt_Status call the thread service, pthread_sigmask() to get or set the signal mask for the current thread. This differs from Rational Exec where there is one signal mask applicable to all the tasks. In Rational Exec, Set_Interrupt_Status is called to disable signals for all tasks. POSIX Threads does not have a service to atomically change the signal mask for all threads.

Given the above requirement for sigwait(), asynchronous signals should always remain disabled. The application should avoid calling Set_Interrupt_Status to enable any additional signals.

Since ISRs execute in the context of a task and since signals can only be disabled/enabled on a per task basis, the application can not use Set_Interrupt_Status to protect critical regions using POSIX Threads as can be done using Rational Exec. However, since the ISR executes in the context of a task it can block waiting on a rendezvous, passive task entry, mutex or semaphore.

Configuration Table Parameters for POSIX Threaded Ada Signals

Parameters exist in the configuration table in v_usr_conf.2.ada to support signal handling in POSIX Threaded Ada. These parameters specify which signals are always disabled, which signals when not attached cause the program to exit, and the priority and stack size of all interrupt tasks.

An overview of the POSIX Threaded Ada signal configuration parameters follows.

Enable_Signals_Mask

A separate Ada interrupt task is created for each attached signal handler. This task does a sigwait() for the attached signal. Therefore, a handler may only be attached for a signal disabled by these enable masks. On all architectures, the following signals are not blocked in ENABLE_SIGNALS_MASK, and can therefore not be caught by the application:

SIGILL*
SIGTRAP
SIGIOT
SIGABRT+
SIGEMT
SIGFPE*
SIGKILL
SIGBUS*
SIGSEGV*
SIGSYS
SIGSTOP 
SIGCONT

* Mapped to an Ada exception.
+ Used by the Ada runtime to implement abort.

Additional signals are unblocked in particular architectures, and can therefore not be handled by applications:

DEC OSF-1
SIGTSTP
HP/UX
SIGTSTP
IBM AIX
SIGDANGER, SIGMIGRATE, SIGPRE, SIGVIRT, SIGPIPE
Solaris
SIGTSTP, SIGWAITING, SIGLWP, SIGTERM
SGI Irix
SIGK32
Lynx
SIGPRIO

Effectively, only the synchronous, exception related signals are enabled.

Exit_Signals_Mask

Since we normally disable all asynchronous signals, we have another signal mask that indicates which signals should cause the program to exit if a handler is not attached. An exit signals thread is created that waits for one of these signals. However, attached signals are automatically removed from the exit signal mask.

By default Exit_Signals_Mask causes the program to exit if it receives one of the following signals without an attached handler:

SIGINT, SIGQUIT, SIGUSR1, SIGUSR2

Intr_Task_Prio

Intr_Task_Prio is the priority of all the interrupt tasks doing a sigwait() for an attached signal. The default is Any_Priority'Last.

Intr_Task_Stack_Size

Intr_Task_Stack_Size is the size of the stack for all interrupt tasks. The default is the same as Default_Task_Stack_Size.

Intr_Task_Attr_Address

Intr_Task_Attr_Address points to the task attribute used to create all interrupt tasks. If this is Null_Address, Default_Task_Attributes is used (see Task Attributes, above).

Ada Kernel: POSIX Threaded Implementation

The POSIX Threaded implementation of the Ada Kernel differs from the Rational Exec version. The attributes of the Ada Kernel objects are different. Some of the Ada Kernel services are not supported or have slightly different semantics.

The write-up on the Ada Kernel in the Ada Runtime Guide walks through all the objects in the Ada Kernel. The following topics are addressed for each object: types, constants, attributes, services and support subprograms. The information provided assumes the Ada Kernel is layered upon the Rational Exec microkernel.

We would like to revisit each of the Ada Kernel objects. This time the discussion will focus on the POSIX Threaded implementation. Only differences from the Rational Exec version will be provided.

Ada Program

There is no support for multiple programs. The following services are not supported:

Program_Start
always returns NO_PROGRAM_ID
Program_Get_Key
always returns No_Addr

Ada Task

The following task management services (VADS Exec augmentation) are supported differently from the Rational Exec version:

Task_Disable_Preemption
Task_Enable_Preemption

Disabling preemption is not meaningful in a multiple CPU environment such as POSIX Threads. Nevertheless, the preemption services are implemented as follows. A preemption depth count is maintained for each Ada task. It is initialized to zero. Task_Disable_Preemption increments the depth. When it is incremented from 0 to 1, the current task's priority is saved and the priority is elevated System.Any_Priority'Last. Task_Enable_Preemption decrements the depth. When decremented to 0, the task priority saved when the preemption was initially disabled, is restored.

The following task management services (VADS Exec augmentation) are not supported:

Task_Suspend
always returns False
Task_Resume
always returns False
Task_Set_Time_Slice
silently ignored
Task_Get_Supervisor_State
always returns False
Task_Enter_Supervisor_State
silently ignored
Task_Leave_Supervisor_State
silently ignored

The following sporadic task services (CIFO augmentation) are not supported:

Task_Is_Sporadic
always returns False
Task_Set_Force_High_Priority
silently ignored

Ada Task Master

No differences.

Ada "new" Allocation

The allocation services map directly to the malloc() and free() routines in the multi threaded MT-safe C library.

Kernel Scheduling

There is no support for controlling kernel scheduling policies. Similar functionality can be obtained by using task attributes to create tasks with the POSIX SCHED_RR scheduling policy (where supported):

Kernel_Get_Time_Slicing_Enabled
always returns False
Kernel_Set_Time_Slicing_Enabled
silently ignored
Task_Get_Time_Slice
returns sched_get_rr_interval()

Callout

Callout_Event_T only has the two program events: EXIT_EVENT and UNEXPECTED_EXIT_EVENT. The Rational Exec idle and task events are not supported.

Task Storage

Task_Storage_Get2, which gets per-task storage given an object of type Krn_Task_Id, is not supported. Krn_Task_Id is implemented as a POSIX thread ID, and the runtime provides no access to the Ada tasking structures of an arbitrary thread:

Task_Storage_Get2
always returns No_Addr

Interrupts

Enable_Intr_Status

Since sigwait() is used to wait for and handle all asynchronous signals, all asynchronous signals are always disabled. Enable_Intr_Status masks the same signals as Disable_Intr_Status.

The interrupt services are supported differently from the Rational Exec version as follows:

Interrupts_Get_Status
Calls the POSIX interface pthread_sigmask() to get the signal mask for the current thread. Signal masks are maintained on a per thread basis and not a per process basis.
Interrupts_Set_Status
Calls the POSIX interface pthread_sigmask() to set the signal mask for the current thread. Interrupts_Set_Status must not be called to enable any signals with handlers attached via Isr_Attach.
Isr_Attach
Creates an interrupt task that does a sigwait() for the interrupt vector. The ISR procedure is called in the context of an Ada task.
Isr_Detach
If we subsequently receive the detached signal, it is ignored unless it is set in Exit_Signals_Mask in v_usr_conf's configuration table. When not ignored, the program exits.
Isr_In_Check
Since ISR handlers execute in the context of an Ada task, Isr_In_Check always returns False.

Time

No differences.

Mutex

Since attached signal handlers (ISRs) execute in the context of an Ada task, the function, they can lock ordinary mutexes; there is no need for a special ISR mutex type as there is in Rational Exec. Default_Intr_Attr is the same as Default_Mutex_Attr, and ISR mutex operations are essentially the same as ordinary mutex operations:

Isr_Mutex_Lockable
always returns True
Isr_Mutex_Lock
maps directly onto Mutex_Lock
Isr_Mutex_Unlock
maps directly onto Mutex_Unlock

The mutex support subprograms behave differently from Rational Exec as follows:

Fifo_Mutex_Attr_Init Prio_Mutex_Attr_Init
POSIX Threads gives mutexes to waiting tasks according to the scheduling policy of the underlying thread; these subprograms are therefore equivalent.
Prio_Inherit_Mutex_Attr_Init
Supported if the POSIX implementation provides the PTHREAD_PRIO_INHERIT locking protocol; raises Program_Error otherwise.
Prio_Ceiling_Mutex_Attr_Init
Implemented using the PTHREAD_PRIO_PROTECT locking policy if provided by the POSIX implementation; with an emulation otherwise (see Priority Ceiling Locking, above).
Intr_Attr_Init
Since ISRs execute in the context of an Ada task, the Disable_Status argument is ignored and the mutex is initialized using the corresponding Prio_Ceiling_Mutex_Attr_Init subprogram with a priority of System.Interrupt_Priority'Last.

Condition Variable

The Cond_Wait subprogram behaves differently. On POSIX Threads, a condition variable may be prematurely signaled by an OS stimulus such as delivery of a signal or a fork.

Since ISRs execute in the context of an Ada task, Isr_Cond_Signal maps directly to Cond_Signal.

The condition variable support subprograms behave differently from Rational Exec as follows:

Fifo_Cond_Attr_Init Prio_Cond_Attr_Init
POSIX Threads wakes waiting tasks according to the scheduling policy of the underlying thread; these subprograms are therefore equivalent.

Binary Semaphore

POSIX threads has no direct support for binary semaphores. Binary semaphores are implemented in the Ada kernel using a mutex to protect the semaphore data structure and using a condition variable when blocking on the semaphore.

Counting Semaphore

Counting semaphores are implemented in the Ada kernel using the POSIX semaphore API.

The following counting semaphore services are supported differently from Rational Exec:

Count_Semaphore_Wait
POSIX does not have a timed wait service for semaphores. The timed wait option (wait_time > 0.0) is emulated by doing a Count_Semaphore_Trywait followed by a Time_Delay. This trywait/delay is repeated until the trywait is successful or it has timed out. The Time_Delay is passed the following delay times: 0.1, 0.5, 1.0, 2.0, 5.0 and 10.0. Once the delay time of 10.0 is used, its used for all subsequent delays.

Mailbox

POSIX Threads has no direct support for mailboxes. Consequently, mailboxes are implemented in the Ada Kernel using a mutex to protect the mailbox data structures and using a condition variable when waiting to read a message.

VADS Exec: POSIX Threaded Ada Differences

The VADS Exec services are layered upon the Ada Kernel. In the POSIX Threaded implementation of the Ada Kernel, some of the services are not supported or have slightly different semantics from the Rational Exec version. Therefore, how the Ada Kernel is implemented using POSIX Threads, has a direct impact on the behavior of the VADS Exec services in POSIX Threaded Ada.

All of the VADS Exec services are documented in the Ada Runtime Services Guide. The services are partitioned across the following packages: V_Interrupts, V_Mailboxes, V_Memory, V_Semaphores, V_Xtasking and V_Stack. The write-up assumes the Ada kernel is layered over the Rational Exec microkernel.

Assuming that POSIX Threads is the underlying microkernel, only differences from the Rational Exec version are provided.

V_Interrupts

The following interrupt services are supported differently from the Rational Exec version:

Attach_Isr

Attach_Isr creates an interrupt task that does a sigwait() for the interrupt vector. Since the ISR procedure is called in the context of an Ada task, there are no restrictions on the services that can be called.

Detach_Isr

For Detach_Isr, if we subsequently receive the detached signal, it is ignored unless it is set in Exit_Signals_Mask in the V_Usr_Conf Configuration Table. When not ignored, the program exits.

Current_Interrupt_Status

Current_Interrupt_Status calls the routine Ada_Krn_I.Interrupts_Get_Status, which uses pthread_sigmask() to get the signal mask for the current thread. Signal masks are maintained on a per thread basis and not a per process basis.

Set_Interrupt_Status

Set_Interrupt_Status calls the routine Ada_Krn_I.Interrupts_Set_Status, which uses pthread_sigmask() to set the signal mask for the current thread. Set_Interrupt_Status must not be called to enable any signals with handlers attached via Attach_Isr.

The following interrupt services are not supported:

Current_Supervisor_State
always returns False
Enter_Supervisor_State
silently ignored
Leave_Supervisor_State
silently ignored
Set_Supervisor_State
silently ignored

V_Mailboxes

The services in V_Mailboxes are layered upon the mailbox services provided in the Ada Kernel. Refer to the previous section on the POSIX Threaded implementation of mailboxes in the Ada Kernel.

Since ISRs execute in the context of an Ada task, an ISR can access the mailbox without disabling interrupts.

For Delete_Mailbox, since it is unable to detect if any task is waiting to read from the mailbox, it always assumes the mailbox is in use. Therefore, if Delete_Mailbox is called with delete_option set to Delete_Option_Warning, it will never be deleted. On the other hand, if Delete_Mailbox is called with delete_option set to DELETE_OPTION_FORCE, then it does a dummy mailbox write and waits 3.0 seconds before freeing the mailbox resources.

V_Memory

No differences.

V_Semaphores

The services in V_Semaphores are layered upon the binary or counting semaphore services provided in the Ada Kernel. Refer to the previous section on the POSIX Threaded implementation of semaphores in the Ada Kernel.

Since ISRs execute in the context of an Ada task, an ISR can perform any semaphore service without disabling interrupts.

For Delete_Semaphore, since it is unable to detect if any task is waiting on the semaphore, it always assumes the semaphore is in use. Therefore, if Delete_Semaphore is called with delete_option set to Delete_Option_Warning, it will never be deleted. On the other hand, if Delete_Semaphore is called with delete_option set to DELETE_OPTION_FORCE, it does a dummy semaphore signal and waits 3.0 seconds before freeing the semaphore resources.

V_Stack

No differences.

V_Xtasking

The following tasking services are supported differently from the Rational Exec version:

Disable_Preemption and Enable_Preemption

These services are layered on the Ada Kernel services, Task_Disable_Preemption and Task_Enable_Preemption. Refer to the previous section on the POSIX Threaded implementation of preemption in the Ada Kernel.

Install_Callout

Only supports the program events EXIT_EVENT and UNEXPECTED_EXIT_EVENT.

Os_Id

Returns the POSIX Thread ID.

Resume_Task and Suspend_Task

Since POSIX does not provide a mechanism to asynchronously suspend or resume a thread, these services are unsupported and will always fail.

Current_Time_Slicing_Enabled
always returns False
Get_Program_Key
always returns No_Addr
Get_Task_Storage2
always returns No_Addr
Set_Time_Slice
silently ignored
Set_Time_Slicing_Enabled
silently ignored
Start_Program
always returns NO_PROGRAM_ID

Annex D: POSIX Threaded Ada Differences

Annex D of the Ada Language Reference Manual specifies additional real-time characteristics of Ada implementations. Most of the features specified by this annex are supported by the POSIX threaded runtime, but some have so far proven impractical over existing POSIX implementations.

Support for priority ceiling mutex locking (POSIX_THREAD_PRIO_PROTECT) is optional in POSIX, and it is not provided on all Apex supported operating systems. This POSIX limitation translates to a failure of the corresponding POSIX Threaded Ada runtime to correctly implement the semantics of the Fifo_Within_Priorities task dispatching policy in the presence of the Ceiling_Locking protected object locking policy. When a task completes a protected action, its priority may be lowered from the corresponding object's priority ceiling; if this happens, the task will be moved to the end of the ready queue for the new priority. This is not one of the circumstances under which a task should be moved to the end of a ready queue, and so does not comply with Fifo_Within_Priorities. This is described in more detail under the Priority Ceiling Locking section above.

POSIX does not provide a means of asynchronously suspending a thread, that is, a means whereby one thread can cause another to stop executing without the target thread's cooperation. Because of this, POSIX Threaded Ada does not implement the Asynchronous_Task_Control package (LRM D.11). Calling any of the subprograms defined in this package will raise Program_Error.


Debugging in the Multithreaded Environment

Introduction

All of the normal debugger commands are available in the Multithreaded Ada environment with the addition of lt thread.

Synchronous Operation

The POSIX Threaded debugger operates synchronously with the program. Either the program is stopped and the debugger can accept and execute commands, or the program is running and the debugger is waiting for an event to happen in the program.

When the program is running, all of the threads may be executing. This includes executing single step commands. This can lead to surprising behavior, since the operating system may reschedule the set of LWPs onto the physical processors between the times the debugger stops and restarts the program.

When one of the threads hits a breakpoint, the operating system stops and it notifies all of the threads in the program and they stop also.

Debugger Signal Handling

Normally, the debugger intercepts all signals that are being delivered to the target program. However, by default the debugger ignores SIGALRM, since the same debugger is used for the POSIX threads and Rational Exec runtimes, and the Rational Exec kernel uses SIGALRM internally.

Listing Ada Tasks

When the program is stopped, the Windows > Tasks or List Tasks command (or lt in the Command Pane) can be used to display the status of all of the Ada tasks. The task which hit the latest breakpoint or announced the latest signal is marked with an asterisk and its status is listed as executing. Note that in an application running under POSIX Threads, several tasks can be executing concurrently. Only the task that announced the latest event is listed with the executing status, however.

Since the POSIX Threads layer has idle threads, it is possible that, if you stop a program with Control-C, the currently executing task is one of these idle threads. These do not correspond to any Ada task. In this case, the executing task is listed along with all of the Ada tasks in the Windows > Tasks/List Tasks command, but Windows > Tasks/List Tasks will only say that it is a non-Ada task. At this point you can get into the context of an Ada task by selecting the task.

There are some unusual effects that are possible when you select a new task. When you display the call stack, it shows that you are at the bottom of the call stack, but the frame number that appears at the left is not 1. This is because there are call stack frames that belong to the threads layer beneath the bottom Ada frame. If you want to descend to the bottom of the thread layer's call stack, you can type cu 1 in the Command Pane at this point. Typing a cs command in the Command Pane now will provide you some additional information about what your program is doing.

Another unusual effect of selecting a new Ada task is that the debugger may not be able to determine the values of all of the registers. This is because not all register values are saved and restored by context switches either at the Ada or the threads layer. Registers whose values cannot be determined are listed as having the value hexadecimal BEEFFACE.

The task status display of the List Tasks (or lt) command includes the thread_id of the thread corresponding to the Ada task and an identifier indicating its status.

Listing Non-Ada Tasks

If you are using the command line interface to the Apex debugger, you can get more information about tasking status in the threads layer using the lt command.

One option is available: lt thread [thread_id]. It lists all of the threads being managed by the threads layer along with their status. Note that there may be threads that do not correspond to Ada tasks.

The Ada tasking layer may think that an Ada task is executing while the threads layer thinks that the task is blocked. This is because the Ada tasking layer only keeps track of Ada tasking operations. Thus, if you think an Ada task should be executing but you know it is not, you should first do an lt Ada_task_id to determine the Ada status of the task and the thread_id of the thread associated with the Ada task. If the Ada status indicates that the task is ready, you should then do an lt thread thread_id command to get the thread status.


Rational Software Corporation 
http://www.rational.com
support@rational.com
techpubs@rational.com
Copyright © 1993-2002, Rational Software Corporation. All rights reserved.
TOC PREV NEXT INDEX DOC LIST MASTER INDEX TECHNOTES APEX TIPS