Creating locking services
Some programmers may want to implement their own high-level locking services instead of using the standard locking services (mutexes) provided in the threads library.
For example, a database product may already use a set of internally defined services; it can be easier to adapt these locking services to a new system than to adapt all the internal modules that use these services.
For this reason, AIX provides atomic locking service primitives that can be used to build higher-level locking services. To create services that are multiprocessor-safe (like the standard mutex services), programmers must use the atomic locking services described in this section and not atomic operations services, such as the compare_and_swap subroutine.
Multiprocessor-safe locking services
Locking services are used to serialize access to resources that may be used concurrently. For example, locking services can be used for insertions in a linked list, which require several pointer updates. If the update sequence by one process is interrupted by a second process that tries to access the same list, an error can occur. A sequence of operations that should not be interrupted is called a critical section.
Locking services use a lock word to indicate the lock status: 0 (zero) can be used for free, and 1 (one) for busy. Therefore, a service to acquire a lock would do the following:
test the lock word
if the lock is free
set the lock word to busy
return SUCCESS
...
- Import fences
- The import fence is a special machine instruction that delays until all previously issued instructions are complete. When used in conjunction with a lock, this prevents speculative execution of instructions until the lock is obtained.
- Export fences
- The export fence guarantees that the data being protected is visible to all other processors prior to the lock being released.
To mask this complexity and provide independence from these machine-dependent instructions, the following subroutines are defined:
- _check_lock
- Conditionally updates a single word variable atomically, issuing an import fence for multiprocessor systems. The compare_and_swap subroutine is similar, but it does not issue an import fence and, therefore, is not usable to implement a lock.
- _clear_lock
- Atomically writes a single word variable, issuing an export fence for multiprocessor systems.
Kernel programming
For complete details about kernel programming, see Kernel Extensions and Device Support Programming Concepts. This section highlights the major differences required for multiprocessor systems.
Serialization is often required when accessing certain critical resources. Locking services can be used to serialize thread access in the process environment, but they will not protect against an access occurring in the interrupt environment. New or ported code should use the disable_lock and unlock_enable kernel services, which use simple locks in addition to interrupt control, instead of the i_disable kernel service. These kernel services can also be used for uniprocessor systems, on which they simply use interrupt services without locking. For detailed information, see Locking Kernel Services in Kernel Extensions and Device Support Programming Concepts.
Device drivers by default run in a logical uniprocessor environment, in what is called funneled mode. Most well-written drivers for uniprocessor systems will work without modification in this mode, but must be carefully examined and modified to benefit from multiprocessing. Finally, kernel services for timers now have return values because they will not always succeed in a multiprocessor environment. Therefore, new or ported code must check these return values. For detailed information, see Using Multiprocessor-Safe Timer Services in Kernel Extensions and Device Support Programming Concepts.
Example of locking services
#include <sys/atomic_op.h> /* for locking primitives */
#define SUCCESS 0
#define FAILURE -1
#define LOCK_FREE 0
#define LOCK_TAKEN 1
typdef struct {
atomic_p lock; /* lock word */
tid_t owner; /* identifies the lock owner */
... /* implementation dependent fields */
} my_mutex_t;
...
int my_mutex_lock(my_mutex_t *mutex)
{
tid_t self; /* caller's identifier */
/*
Perform various checks:
is mutex a valid pointer?
has the mutex been initialized?
*/
...
/* test that the caller does not have the mutex */
self = thread_self();
if (mutex->owner == self)
return FAILURE;
/*
Perform a test-and-set primitive in a loop.
In this implementation, yield the processor if failure.
Other solutions include: spin (continuously check);
or yield after a fixed number of checks.
*/
while (_check_lock(mutex->lock, LOCK_FREE, LOCK_TAKEN))
yield();
mutex->owner = self;
return SUCCESS;
} /* end of my_mutex_lock */
int my_mutex_unlock(my_mutex_t *mutex)
{
/*
Perform various checks:
is mutex a valid pointer?
has the mutex been initialized?
*/
...
/* test that the caller owns the mutex */
if (mutex->owner != thread_self())
return FAILURE;
_clear_lock(mutex->lock, LOCK_FREE);
return SUCCESS;
} /* end of my_mutex_unlock */