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
... 
Because this sequence of operations (read, test, set) is itself a critical section, special handling is required. On a uniprocessor system, disabling interrupts during the critical section prevents interruption by a context switch. But on a multiprocessor system, the hardware must provide a test-and-set primitive, usually with a special machine instruction. In addition, special processor-dependent synchronization instructions called import and export fences are used to temporarily block other reads or writes. They protect against concurrent access by several processors and against the read and write reordering performed by modern processors and are defined as follows:
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

The multiprocessor-safe locking subroutines can be used to create custom high-level routines independent of the threads library. The example that follows shows partial implementations of subroutines similar to the pthread_mutex_lock and pthread_mutex_unlock subroutines in the threads library:
#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 */