BitCtrl Systems GmbH
Foto Weissenfelserstr. 67
Suche:     
 BitCtrl Systems GmbH
Produkte Support News & Events Download Shop Partner Kontakt
  QNX® - Allgemeine Info
  QNX® News
  QNX® Produkt Katalog
  Free Downloads
  FAQ
  Support/Service
  Links
  Repository
  Produkt Dokumentation
QNX® 6 - System Architecture
Chapter 2: Neutrino Microkernel (Part II)
Table of Contents Zurück zum Seitenanfang
Threads and processes
Thread scheduling
Synchronization services
Threads and processes Zurück zum Seitenanfang Top

When building an application (realtime, embedded, graphical, or otherwise), the developer may want several algorithms within the application to execute concurrently. Within Neutrino, this concurrency is achieved by using the POSIX thread model, which defines a process as containing one or more threads of execution.

A thread can be thought of as the minimum "unit of execution," the unit of scheduling and execution in the microkernel. A process, on the other hand, can be thought of as a "container" for threads, defining the "address space" within which threads will execute. A process will always contain at least one thread.

Depending on the nature of the application, threads might execute independently with no need to communicate between the algorithms (unlikely), or they may need to be tightly coupled, with high-bandwidth communications and tight synchronization. To assist in this communication and synchronization, Neutrino provides a rich variety of IPC and synchronization services.

The following pthreads (POSIX Threads) library calls don't involve any microkernel thread calls:

  • pthread_attr_destroy()
  • pthread_attr_getdetachstate()
  • pthread_attr_getinheritsched()
  • pthread_attr_getschedparam()
  • pthread_attr_getschedpolicy()
  • pthread_attr_getscope()
  • pthread_attr_getstackaddr()
  • pthread_attr_getstacksize()
  • pthread_attr_init()
  • pthread_attr_setdetachstate()
  • pthread_attr_setinheritsched()
  • pthread_attr_setschedparam()
  • pthread_attr_setschedpolicy()
  • pthread_attr_setscope()
  • pthread_attr_setstackaddr()
  • pthread_attr_setstacksize()
  • pthread_cleanup_pop()
  • pthread_cleanup_push()
  • pthread_equal()
  • pthread_getspecific()
  • pthread_setspecific()
  • pthread_testcancel()
  • pthread_key_create()
  • pthread_key_delete()
  • pthread_once()
  • pthread_self()
  • pthread_setcancelstate()
  • pthread_setcanceltype()

The following table lists the POSIX thread calls that have a corresponding microkernel thread call, allowing you to choose either interface:

POSIX call Microkernel call Description
pthread_create() ThreadCreate() Create a new thread of execution.
pthread_exit() ThreadDestroy() Destroy a thread.
pthread_detach() ThreadDetach() Detach a thread so it doesn't need to be joined.
pthread_join() ThreadJoin() Join a thread waiting for its exit status.
pthread_cancel() ThreadCancel() Cancel a thread at the next cancellation point.
N/A ThreadCtl() Change a thread's Neutrino-specific thread characteristics.
pthread_mutex_init() SyncCreate() Create a mutex.
pthread_mutex_destroy() SyncDestroy() Destroy a mutex.
pthread_mutex_lock() SyncMutexLock() Lock a mutex.
pthread_mutex_trylock() SyncMutexLock() Conditionally lock a mutex.
pthread_mutex_unlock() SyncMutexUnlock() Unlock a mutex.
pthread_cond_init() SyncCreate() Create a condition variable.
pthread_cond_destroy() SyncDestroy() Destroy a condition variable.
pthread_cond_wait() SyncCondvarWait() Wait on a condition variable.
pthread_cond_signal() SyncCondvarSignal() Signal a condition variable.
pthread_cond_broadcast() SyncCondvarSignal() Broadcast a condition variable.
pthread_getschedparam() SchedGet() Get scheduling parameters and policy of thread.
pthread_setschedparam() SchedSet() Set scheduling parameters and policy of thread.
pthread_sigmask() SignalProcM ask() Examine or set a thread's signal mask.
pthread_kill() SignalKill() Send a signal to a specific thread.

Neutrino and the process manager can be configured to provide a mix of threads and processes (as defined by POSIX). Each process is MMU-protected from each other, and each process may contain one or more threads that share the process's address space.

The environment you choose affects not only the concurrency capabilities of the application, but also the IPC and synchronization services the application might make use of.

Even though the common term "IPC" refers to communicating processes, we use it here to describe the communication between threads, whether they're within the same process or separate processes.

Thread attributes

Although threads within a process share everything within the process's address space, each thread still has some "private" data. In some cases, this private data is protected within the kernel (e.g. the tid or thread ID), while other private data resides unprotected in the process's address space (e.g. each thread has a stack for its own use). Some of the more noteworthy thread-private resources are:

tid
Each thread is identified by an integer thread ID, starting at 1. The tid is unique within the thread's process.
register set
Each thread has its own program counter (PC), stack pointer (SP), and other processor-specific register context.
stack
Each thread executes on its own stack, stored within the address space of its process.
signal mask
Each thread has its own signal mask.
thread local storage
A thread has a system-defined data area called "thread local storage" (TLS). The TLS is used to store "per-thread" information (such as tid, pid, stack base, errno, and thread-specific key/data bindings). The TLS doesn't need to be accessed directly by a user application. A thread can have user-defined data associated with a thread-specific data key.
cancellation handlers
Callback functions that are executed when the thread terminates.

Thread-specific data, implemented in the pthread library and stored in the TLS, provides a mechanism for associating a process global integer key with a unique per-thread data value. To use thread-specific data, a new key is first created and a unique data value is then bound to the key (per thread). The data value may, for example, be an integer or a pointer to a dynamically allocated data structure. Subsequently, the key is used to return the bound data value per thread.

A typical application of thread-specific data is for a thread-safe function that needs to maintain a context for each calling thread.


Sparse matrix (tid,key) to value mapping
Sparse matrix (tid,key) to value mapping.

The following functions are used to create and manipulate this data:

Function Description
pthread_key_create() Create a data key with destructor function
pthread_key_delete() Destroy a data key
pthread_setspecific() Bind a data value to a data key
pthread_getspecific() Return the data value bound to a data key

Thread life cycle

The number of threads within a process can vary widely, with threads being created and destroyed dynamically. Thread creation (pthread_create()) involves allocating and initializing the necessary resources within the process's address space (thread stack, etc.) and starting the execution of the thread at some function in the address space.

Thread termination (pthread_exit(), pthread_cancel()) involves stopping the thread and reclaiming the thread's resources. As a thread executes, its state can generally be described as either "ready" or "blocked." More specifically, it can be one of the following:


Process states
Possible thread states in a Neutrino system.

RUNNING
The thread is being executed by a processor.
READY
The thread is waiting to be executed while the processor executes another thread of equal or higher priority.
STOPPED
The thread is blocked waiting for a SIGCONT signal.
SEND
The thread is blocked on a message send (called MsgSend()).
REPLY
The thread is blocked on a message reply (called MsgSend()).
RECEIVE
The thread is blocked on a message receive (called MsgReceive()).
SIGSUSPEND
The thread is blocked waiting for a signal (called sigsuspend()).
SIGWAIT
The thread is blocked waiting for a signal (called sigwaitinfo()).
NANOSLEEP
The thread is sleeping for a short time interval (called nanosleep()).
MUTEX
The thread is blocked on a mutual exclusion lock (called pthread_mutex_lock()).
CONDVAR
The thread is blocked on a condition variable (called pthread_condvar_wait()).
JOIN
The thread is blocked waiting to join another thread (called pthread_join()).
INTERRUPT
The thread is blocked waiting for an interrupt (called InterruptWait()).
DEAD
The thread has terminated and is waiting for a join by another thread.
Thread scheduling Zurück zum Seitenanfang Top

When scheduling decisions are made

The execution of a running thread is temporarily suspended whenever the microkernel is entered as the result of a kernel call, exception, or hardware interrupt. A scheduling decision is made whenever the execution state of any thread changes - it doesn't matter which processes the threads might reside within. Threads are scheduled globally across all processes.

Normally, the execution of the suspended thread will resume, but the scheduler will perform a context switch from one thread to another whenever the running thread:

  • is blocked
  • is preempted
  • yields.

When thread is blocked

The running thread will block when it must wait for some event to occur (response to an IPC request, wait on a mutex, etc.). The blocked thread is removed from the ready queue and the highest priority ready thread is then run. When the blocked thread is subsequently unblocked, it is placed on the end of the ready queue for that priority level.

When thread is preempted

The running thread will be preempted when a higher-priority thread is placed on the ready queue (it becomes READY, as the result of its block condition being resolved). The preempted thread remains at the start of the ready queue for that priority and the higher-priority thread runs.

When thread yields

The running thread voluntarily yields the processor (sched_yield()) and is placed on the end of the ready queue for that priority. The highest-priority thread then runs (which may still be the thread that just yielded).

Scheduling priority

Every thread is assigned a priority. The scheduler selects the next thread to run by looking at the priority assigned to every thread that is READY (i.e. capable of using the CPU). The thread with the highest priority is selected to run.


Figure showing READY processes
Possible thread states in a Neutrino system.

The ready queue for six threads (A-F) that are READY. All other threads (G-Z) are BLOCKED. Thread A is currently running. Thread A, B, and C are at the highest priority, so will share the processor based on the running thread's scheduling algorithm.

Each thread can have a scheduling priority ranging from 0 to 63 (the highest priority), independent of scheduling policy. A thread inherits the priority of its parent thread by default. The idle thread has priority 0 and is always ready to run.

A thread has both a real priority and an effective priority, and is scheduled in accordance with its effective priority. The real and effective priority may be changed together by the thread itself. The effective priority may change due to priority inheritance or the scheduling policy (see below). Normally the effective priority is the same as the real priority.

The threads on the ready queue are ordered by priority. The ready queue is actually implemented as 64 separate queues, one for each priority. Threads are queued in FIFO order in the queue of their priority. The first thread in the highest priority queue is selected to run.

Scheduling algorithms

To meet the needs of various applications, Neutrino provides these scheduling algorithms:

  • FIFO scheduling
  • round-robin scheduling

Each thread in the system may run using either method. They are effective on a per-thread basis, not on a global basis for all threads and processes on a node.

Remember that these scheduling algorithms apply only when two or more threads that share the same priority are READY (i.e. the threads are directly competing with each other). If a higher-priority thread becomes READY, it immediately preempts all lower-priority threads.

In the following diagram, three threads of equal priority are READY. If Thread A blocks, Thread B will run.

For this release of Neutrino, adaptive scheduling is treated the same as round-robin scheduling.

Each thread in the system may run using any one of these methods. They are effective on a per-thread basis, not on a global basis for all threads and processes on a node.

Remember that these scheduling algorithms apply only when two or more threads that share the same priority are READY (i.e. the threads are directly competing with each other). If a higher-priority thread becomes READY, it immediately preempts all lower-priority threads.

In the following diagram, three threads of equal priority are READY. If Thread A blocks, Thread B will run.


Figure showing scheduling of equal-priority threads
Thread A blocks, Thread B runs.

Although a thread inherits its scheduling algorithm from its parent process, the thread can request to change the algorithm applied by the kernel.

FIFO scheduling

In FIFO scheduling, a thread selected to run continues executing until it:

  • voluntarily relinquishes control (e.g. it blocks)
  • is preempted by a higher-priority thread

Figure showing scheduling of equal-priority threads
FIFO scheduling. Thread A runs until it blocks.

Round-robin scheduling

In round-robin scheduling, a thread selected to run continues executing until it:

  • voluntarily relinquishes control
  • is preempted by a higher-priority thread
  • consumes its timeslice

Figure showing round-robin scheduling
Round-robin scheduling. Thread A ran until it consumed its timeslice; the next READY thread (Thread B) now runs.

A timeslice is the unit of time assigned to every process. Once it consumes its timeslice, a thread is preempted and the next READY thread at the same priority level is given control. A timeslice is 50 milliseconds.

For this release of Neutrino, adaptive scheduling is treated the same as round-robin scheduling.
Apart from time slicing, round-robin scheduling is identical to FIFO scheduling.

Manipulating priority and scheduling algorithms

A thread's priority can vary during its execution, either from direct manipulation by the thread itself or from the kernel adjusting the thread's priority as it receives a message from a higher-priority thread.

In addition to priority, the scheduling algorithm employed by the kernel as the thread runs can also be selected. Here are the POSIX calls for performing these manipulations, along with the microkernel calls used by these library routines:

POSIX call Microkernel call Description
sched_getparam() SchedGet() get scheduling priority
sched_setparam() SchedSet() set scheduling priority
sched_getscheduler() SchedGet() get scheduling policy
sched_setscheduler() SchedSet() set scheduling policy

IPC issues

Since all the threads in a process have unhindered access to the shared data space, wouldn't this execution model "trivially" solve all of our IPC problems? Can't we just communicate the data through shared memory and dispense with any other execution models and IPC mechanisms?

If only it were that simple!

One issue is that the access of individual threads to common data must be synchronized. Having one thread read inconsistent data because another thread is part way through modifying it is a recipe for disaster. For example, if one thread is updating a linked list, no other threads can be allowed to traverse or modify the list until the first thread has finished. A code passage that must execute "serially" (i.e. by only one thread at a time) in this manner is termed a "critical section." The program would fail (intermittently, depending on how frequently a "collision" occurred) with irreparably damaged links unless some synchronization mechanism ensured serial access.

Mutexes, semaphores, and condvars are examples of synchronization tools that can be used to address this problem. These tools are described later in this section.

Although synchronization services can be used to allow threads to cooperate, shared memory per se can't address a number of IPC issues. For example, although threads can communicate through the common data space, this works only if all the threads communicating are within a single process. What if our application needs to communicate a query to a database server? We need to pass the details of our query to the database server, but the thread we need to communicate with lies within a database server process and the address space of that server isn't addressable to us.

The OS takes care of the network-distributed IPC issue because the one interface - message passing - operates identically in both the local and network-remote cases, and can be used to access all OS services. Since messages can be exactly sized, and since most messages tend to be quite tiny (e.g. the error status on a write request, or a tiny read request), the data moved around the network can be far less with message passing than with network-distributed shared memory, which would tend to copy 4K pages around.

Thread complexity issues

Although threads are very appropriate for some system designs, it's important to respect the Pandora's box of complexities their use unleashes. In some ways, it's ironic that while MMU-protected multitasking has become common, computing fashion has made popular the use of multiple threads in an unprotected address space. This not only makes debugging difficult, but also hampers the generation of reliable code.

Threads were initially introduced to UNIX systems as a "light-weight" concurrency mechanism to address the problem of slow context switches between "heavy weight" processes. Although this is a worthwhile goal, an obvious question arises: Why are process-to-process context switches slow in the first place?

Architecturally, Neutrino addresses the context-switch performance issue first. In fact, threads and processes provide nearly identical context-switch performance numbers. Neutrino's process-switch times are faster than UNIX thread-switch times. As a result, Neutrino threads don't need to be used to solve the IPC performance problem; instead, they're a tool for achieving greater concurrency within application and server processes.

Without resorting to threads, fast process-to-process context switching makes it reasonable to structure an application as a team of cooperating processes sharing an explicitly allocated shared-memory region. An application thus exposes itself to bugs in the cooperating processes only so far as the effects of those bugs on the contents of the shared-memory region. The private memory of the process is still protected from the other processes. In the purely threaded model, the private data of all threads is openly accessible, vulnerable to stray pointer errors in any thread in the process. For example, each process would have an MMU-protected stack, while threads within a process run with unprotected stacks.

Nevertheless, threads can also provide concurrency advantages that a pure process model cannot address. For example, a filesystem server process that executes requests on behalf of many clients (where each request takes significant time to complete), definitely benefits from having multiple threads of execution. If one client process requests a block from disk, while another client requests a block already in cache, the filesystem process can utilize a pool of threads to concurrently service client requests, rather than remain "busy" until the disk block is read for the first request.

As requests arrive, each thread is able to respond directly from the buffer cache or to block and wait for disk I/O without increasing the response latency seen by other client processes. The filesystem server can "precreate" a team of threads, ready to respond in turn to client requests as they arrive. Although this complicates the architecture of the filesystem manager, the gains in concurrency are significant.

Synchronization services Zurück zum Seitenanfang Top

Neutrino provides the POSIX-standard thread-level synchronization primitives, some of which are useful even between threads in different processes. The synchronization services include at least the following:

Synchronization service Supported between processes Supported across a Neutrino LAN
Mutexes Yes No
Condvars Yes No
Sleepon locks No No
Reader/writer locks No No
Semaphores Yes Yes (named only)
FIFO scheduling Yes No
Send/Receive/Reply Yes Yes
Atomic operations Yes No

These synchronization primitives are implemented directly by Neutrino, except for:
  • sleepon locks and reader/writer locks (which are built from mutexes and condvars)
  • atomic operations (which are either implemented directly by the processor or emulated in the kernel).

Mutual exclusion locks

Mutual exclusion locks, or mutexes, are the simplest of the Neutrino synchronization services. A mutex is used to ensure exclusive access to data shared between threads. It is typically acquired (pthread_mutex_lock()) and released (pthread_mutex_unlock()) around the code that accesses the shared data (usually a critical section).

Only one thread may have the mutex locked at any given time. Threads attempting to lock an already locked mutex will block until the thread is later unlocked. When the thread unlocks the mutex, the highest-priority thread waiting to lock the mutex will unblock and become the new owner of the mutex. In this way, threads will sequence through a critical region in priority-order.

On most processors, acquisition of a mutex doesn't require entry to the kernel for a free mutex. What allows this is the use of the compare-and-swap opcode on x86 processors and the load/store conditional opcodes on most RISC processors.

Entry to the kernel is done at acquisition time only if the mutex is already held so that the thread can go on a blocked list; kernel entry is done on exit if other threads are waiting to be unblocked on that mutex. This allows acquisition and release of an uncontested critical section or resource to be very quick, incurring work by the OS only to resolve contention.

A non-blocking lock function (pthread_mutex_trylock()) can be used to test whether the mutex is currently locked or not. For best performance, the execution time of the critical section should be small and of bounded duration. A condvar should be used if the thread may block within the critical section.

Priority inheritance

If a thread with a higher priority than the mutex owner attempts to lock a mutex, then the effective priority of the current owner will be increased to that of the higher-priority blocked thread waiting for the mutex. The owner will return to its real priority when it unlocks the mutex. This scheme not only ensures that the higher-priority thread will be blocked waiting for the mutex for the shortest possible time, but also solves the classic priority-inversion problem.

The attributes of the mutex can also be modified (using pthread_mutex_setrecursive()) to allow a mutex to be recursively locked by the same thread. This can be useful to allow a thread to call a routine that might attempt to lock a mutex that the thread already happens to have locked.

Recursive mutexes are non-POSIX services - they don't work with condvars.

Condition variables

A condition variable, or condvar, is used to block a thread within a critical section until some condition is satisfied. The condition can be arbitrarily complex and is independent of the condvar. However, the condvar must always be used with a mutex lock in order to implement a monitor.

A condvar supports three operations:

  • wait (pthread_cond_wait())
  • signal (pthread_cond_signal())
  • broadcast (pthread_cond_broadcast()).
Note that there's no connection between a condvar signal and a POSIX signal.

Here's a typical example of how a condvar can be used:


       pthread_mutex_lock( &m );
        . . .
       while (!arbitrary_condition) {
       pthread_cond_wait( &cv, &m );
       }
       . . .
       pthread_mutex_unlock( &m );

                

In this code sample, the mutex is acquired before the condition is tested. This ensures that only this thread has access to the arbitrary condition being examined. While the condition is true, the code sample will block on the wait call until some other thread performs a signal or broadcast on the condvar.

The while loop is required for two reasons. First of all, POSIX cannot guarantee that false wakeups will not occur (e.g. multi-processor systems). Second, when another thread has made a modification to the condition, we need to retest to ensure that the modification matches our criteria. The associated mutex is unlocked atomically by pthread_cond_wait() when the waiting thread is blocked to allow another thread to enter the critical section.

A thread that performs a signal will unblock the highest-priority thread queued on the condvar, while a broadcast will unblock all threads queued on the condvar. The associated mutex is locked atomically by the highest-priority unblocked thread; the thread must then unlock the mutex after proceeding through the critical section.

A version of the condvar wait operation allows a timeout to be specified (pthread_cond_timedwait()). The waiting thread can then be unblocked when the timeout expires.

Sleepon locks

Sleepon locks are very similar to condvars, with a few subtle differences. Like condvars, sleepon locks (pthread_sleepon_lock()) can be used to block until a condition becomes true (like a memory location changing value). But unlike condvars, which must be allocated for each condition to be checked, sleepon locks multiplex their functionality over a single mutex and dynamically allocated condvar, regardless of the number of conditions being checked. The maximum number of condvars ends up being equal to the maximum number of blocked threads. These locks are patterned after the sleepon locks commonly used within the UNIX kernel.

Reader/writer locks

More formally known as "Multiple readers, single writer locks," these locks are used when the access pattern for a data structure consists of many threads reading the data, and (at most) one thread writing the data. These locks are more expensive than mutexes, but can be useful for this data access pattern.

This lock works by allowing all the threads that request a read-access lock (pthread_rwlock_rdlock()) to succeed in their request. But when a thread wishing to write asks for the lock (pthread_rwlock_wrlock()), the request is denied until all the current reading threads release their reading locks (pthread_rwlock_unlock()).

Multiple writing threads can queue (in priority order) waiting for their chance to write the protected data structure, and all the blocked writer-threads will get to run before reading threads are allowed access again. The priorities of the reading threads are not considered.

There are also calls (pthread_rwlock_tryrdlock() and pthread_rwlock_trywrlock()) to allow a thread to test the attempt to achieve the requested lock, without blocking. These calls return with a successful lock or a status indicating that the lock couldn't be granted immediately.

Reader/writer locks aren't implemented directly within the kernel, but are instead built from the mutex and condvar services provided by the kernel.

Semaphores

Semaphores are another common form of synchronization that allows threads to "post" (sem_post()) and "wait" (sem_wait()) on a semaphore to control when threads wake or sleep. The post operation increments the semaphore; the wait operation decrements it.

If you wait on a semaphore that is positive, you will not block. Waiting on a non-positive semaphore will block until some other thread executes a post. It is valid to post one or more times before a wait. This use will allow one or more threads to execute the wait without blocking.

A significant difference between semaphores and other synchronization primitives is that semaphores are "async safe" and can be manipulated by signal handlers. If the desired effect is to have a signal handler wake a thread, semaphores are the right choice.

Another useful property of semaphores is that they were defined to operate between processes. Although Neutrino mutexes work between processes, the POSIX thread standard considers this an optional capability and as such may not be portable across systems. For synchronization between threads in a single process, mutexes will be more efficient than semaphores.

As a useful variation, a named semaphore service is also available. It uses a resource manager and as such allows semaphores to be used between processes on different machines connected by a network.

Note that named semaphores are slower than the unnamed variety.

Since semaphores, like condition variables, can legally return a non-zero value because of a false wakeup, correct usage requires a loop:


       while (sem_wait(&s) == EINTR) { do_nothing(); }
       do_critical_region();   /* Semaphore was decremented */

                

Synchronization via scheduling algorithm

By selecting the POSIX FIFO scheduling algorithm, we can guarantee that no two threads of the same priority execute the critical section concurrently on a non-SMP system. The FIFO scheduling algorithm dictates that all FIFO-scheduled threads in the system at the same priority will run, when scheduled, until they voluntarily release the processor to another thread.

This "release" can also occur when the thread blocks as part of requesting the service of another process, or when a signal occurs. The critical region must therefore be carefully coded and documented so that later maintenance of the code doesn't violate this condition.

In addition, higher-priority threads in that (or any other) process could still preempt these FIFO-scheduled threads. So, all the threads that could "collide" within the critical section must be FIFO-scheduled at the same priority. Having enforced this condition, the threads can then casually access this shared memory without having to first make explicit synchronization calls.

Note that this exclusive-access relationship doesn't apply in multi-processor systems, since each CPU could run a thread simultaneously through the region that would otherwise be serially scheduled on a single-processor machine.

Synchronization via message passing

The Neutrino Send/Receive/Reply message-passing IPC services (described later) implement an implicit synchronization by their blocking nature. These IPC services can, in many instances, render other synchronization services unnecessary. They are also the only synchronization and IPC primitives (other than named semaphores, which are built on top of messaging) that can be used across the network.

Synchronization via atomic operations

In some cases, you may want to perform a short operation (such as incrementing a variable) with the guarantee that the operation will perform atomically - i.e. the operation won't be preempted by another thread or ISR (Interrupt Service Routine).

Under Neutrino, we provide atomic operations for:

  • adding a value
  • subtracting a value
  • clearing bits
  • setting bits
  • toggling (complementing) bits

These atomic operations are available by including the C header file <atomic.h>.

Although you can use these atomic operations just about anywhere, you'll find them particularly useful in these two cases:

  • between an ISR and a thread
  • between two threads (SMP or single-processor).

Since an ISR can preempt a thread at any given point, the only way that the thread would be able to protect itself would be to disable interrupts. Since you should avoid disabling interrupts in a realtime system, we recommend that you use the atomic operations provided with Neutrino.

On an SMP system, multiple threads can and do run concurrently. Again, we run into the same situation as with interrupts above - you should use the atomic operations where applicable to eliminate the need to disable and reenable interrupts.

Synchronization services implementation

The following table lists the various microkernel calls and the higher-level POSIX calls constructed from them:

Microkernel call POSIX call Description
SyncCreate() pthread_mutex_init(), pthread_cond_init(), sem_init() Create object for mutex, condvars, and semaphore.
SyncDestroy() pthread_mutex_destroy(), pthread_cond_destroy(), sem_destroy() Destroy synchronization object.
SyncCondvarWait() pthread_cond_wait(), pthread_cond_timedwait() Block on a condvar.
SyncCondvarSignal() pthread_cond_broadcast(), pthread_cond_signal() Wake up condvar blocked threads.
SyncMutexLock() pthread_mutex_lock(), pthread_mutex_trylock() Lock a mutex.
SyncMutexUnlock() pthread_mutex_unlock() Unlock a mutex.
SyncSemPost() sem_post() Post a semaphore.
SyncSemWait() sem_wait(), sem_trywait() Wait on a semaphore.

<< Previous | Index | Next >>

Home    Datenschutzerklärung    Haftungsausschluss    Impressum   
© 2009 BitCtrl Systems GmbH