Copyright (c) Hyperion Entertainment and contributors.
Exec Semaphores
This page is currently being updated to AmigaOS 4.x. Some of the information contained here may not yet be applicable in part or totally. |
Contents
Exec Semaphores
Semaphores are a feature of Exec which provide a general method for tasks to arbitrate for the use of memory or other system resources they may be sharing. This chapter describes the structure of Exec semaphores and the various support functions provided for their use. Since the semaphore system uses Exec lists and signals, some familiarity with these concepts is helpful for understanding semaphores.
In any multitasking or multi-processing system there is a need to share data among independently executing tasks. If the data is static (that is, it never changes), then there is no problem. However, if the data is variable, then there must be some way for a task that is about to make a change to keep other tasks from looking at the data.
For example, to add a node to a linked list of data, a task would normally just add the node. However, if the list is shared with other tasks, this could be dangerous. Another task could be walking down the list while the change is being made and pick up an incorrect pointer. The problem is worse if two tasks attempt to add an item to the list at the same time. Exec semaphores provide a way to prevent such problems.
A semaphore is much like getting a key to a locked data item. When you have the key (semaphore), you can access the data item without worrying about other tasks causing problems. Any other tasks that try to obtain the semaphore will be put to sleep until the semaphore becomes available. When you have completed your work with the data, you return the semaphore.
For semaphores to work correctly, there are two restrictions that must be observed at all times:
- All tasks using shared data that is protected by a semaphore must always ask for the semaphore first before accessing the data. If some task accesses the data directly without first going through the semaphore, the data may be corrupted. No task will have safe access to the data.
- A deadlock will occur if a task that owns an exclusive semaphore on some data inadvertently calls another task which tries to get an exclusive semaphore on that same data in blocking mode. Deadlocks and other such issues are beyond the scope of this manual. For more details on deadlocks and other problems of shared data in a multitasking system and the methods used to prevent them, refer to a textbook in computer science such as Operating Systems by Tannenbaum (Prentice-Hall).
The Signal Semaphore
Exec semaphores are signal based. Using signal semaphores is the easiest way to protect shared, single-access resources in the Amiga. Your task will sleep until the semaphore is available for use. The SignalSemaphore structure is as follows:
struct SignalSemaphore { struct Node ss_Link; SHORT ss_NestCount; struct MinList ss_WaitQueue; struct SemaphoreRequest ss_MultipleLink; struct Task *ss_Owner; SHORT ss_QueueCount; };
- ss_Link
- is the node structure used to link semaphores together. The ln_Pri and ln_Name fields are used to set the priority of the semaphore in a list and to name the semaphore for public access. If a semaphore is not public the ln_Name and ln_Pri fields may be left NULL.
- ss_NestCount
- is the count of number of locks the current owner has on the semaphore.
- ss_WaitQueue
- is the List header for the list of other tasks waiting for this semaphore.
- ss_MultipleLink
- is the SemaphoreRequest used by ObtainSemaphoreList().
- ss_Owner
- is the pointer to the current owning task.
- ss_QueueCount
- is the number of other tasks waiting for the semaphore.
A practical application of a SignalSemaphore would be to use it as the base of a shared data structure. For example:
struct SharedList { struct SignalSemaphore sl_Semaphore; struct MinList sl_List; };
Creating a SignalSemaphore Structure
To initialize a SignalSemaphore structure use the InitSemaphore() function. This function initializes the list structure and the nesting and queue counters. It does not change the semaphore's name or priority fields.
This fragment creates and initializes a semaphore for a data item such as the SharedList structure above.
struct SharedList *slist; slist = IExec->AllocVecTags(sizeof(struct SharedList), AVT_Type, MEMF_SHARED, AVT_ClearWithValue, 0, TAG_END); if (slist != NULL) { IExec->NewMinList(&slist->sl_List); /* Initialize the MinList */ IExec->InitSemaphore((struct SignalSemaphore *)slist); /* And initialize the semaphore */ /* The semaphore can now be used. */ } else IDOS->Printf("Can't allocate structure\n");
Making a SignalSemaphore Available to the Public
A semaphore should be used internally in your program if it has more than one task operating on shared data structures. There may also be cases when you wish to make a data item public to other applications but still need to restrict its access via semaphores. In that case, you would give your semaphore a unique name and add it to the public SignalSemaphore list maintained by Exec. The AddSemaphore() function does this for you.
To create and initialize a public semaphore for a data item and add it to the public semaphore list maintained by Exec, the following function should be used. (This will prevent the semaphore from being added or removed more than once by separate programs that use the semaphore).
STRPTR name; /* name of semaphore to add */ struct SignalSemaphore *sema; IExec->Forbid(); /* Make sure the semaphore name is unique */ if (!IExec->FindSemaphore(name)) { /* Allocate memory for the structure */ sema = IExec->AllocVecTags(sizeof(struct SignalSemaphore), AVT_Type, MEMF_SHARED, AVT_ClearWithValue, 0, TAG_END); if (sema != NULL) { sema->ss_Link.ln_Pri = 0; /* Set the priority to zero */ sema->ss_Link.ln_Name = name; /* Note that the string 'name' is not copied. If that is needed, allocate memory */ /* for it and copy the string. And add the semaphore the the system list */ IExec->AddSemaphore(sema); } } IExec->Permit();
A value of NULL for semaphore means that the semaphore already exists or that there was not enough free memory to create it.
Before using the data item or other resource which is protected by a semaphore, you must first obtain the semaphore. Depending on your needs, you can get either exclusive or shared access to the semaphore.
Obtaining a SignalSemaphore Exclusively
The ObtainSemaphore() function can be used to get an exclusive lock on a semaphore. If another task currently has an exclusive or shared lock(s) on the semaphore, your task will be put to sleep until all locks on the the semaphore are released.
Semaphore Nesting. SignalSemaphores have nesting. That is, if your task already owns the semaphore, it will get a second ownership of that semaphore. This simplifies the writing of routines that must own the semaphore but do not know if the caller has obtained it yet. |
To obtain a semaphore use:
struct SignalSemaphore *semaphore; IExec->ObtainSemaphore(semaphore);
To get an exclusive lock on a public semaphore, the following code should be used:
STRPTR name; struct SignalSemaphore *semaphore; IExec->Forbid(); /* Make sure the semaphore will not go away if found. */ if (semaphore = IExec->FindSemaphore(name)) IExec->ObtainSemaphore(semaphore); IExec->Permit();
The value of semaphore is NULL if the semaphore does not exist. This is only needed if the semaphore has a chance of going away at any time (i.e., the semaphore is public and might be removed by some other program). If there is a guarantee that the semaphore will not disappear, the semaphore address could be cached, and all that would be needed is a call to the ObtainSemaphore() function.
For read-only purposes, multiple tasks may have a shared lock on a signal semaphore. If a semaphore is already exclusively locked, all attempts to obtain the semaphore shared will be blocked until the exclusive lock is released. At that point, all shared locks will be obtained and the calling tasks will wake up.
To obtain a shared semaphore, use:
struct SignalSemaphore *semaphore; IExec->ObtainSemaphoreShared(semaphore);
To obtain a public shared semaphore, the following code should be used:
STRPTR name; struct SignalSemaphore *semaphore; IExec->Forbid(); if (semaphore = IExec->FindSemaphore(name)) IExec->ObtainSemaphoreShared(semaphore); IExec->Permit();
Checking a SignalSemaphore
When you attempt to obtain a semaphore with ObtainSemaphore(), your task will be put to sleep if the semaphore is not currently available. If you do not want to wait, you can call AttemptSemaphore() instead. If the semaphore is available for exclusive locking, AttemptSemaphore() obtains it for you and returns TRUE. If it is not available, the function returns FALSE immediately instead of waiting for the semaphore to be released.
To attempt to obtain a semaphore, use the following:
struct SignalSemaphore *semaphore; IExec->AttemptSemaphore(semaphore);
To make an attempt to obtain a public semaphore, the following code should be used:
UBYTE *name; struct SignalSemaphore *semaphore; IExec->Forbid(); if (semaphore = IExec->FindSemaphore(name)) IExec->AttemptSemaphore(semaphore); IExec->Permit();
Releasing a SignalSemaphore
Once you have obtained the semaphore and completed any operations on the semaphore protected object, you should release the semaphore. The ReleaseSemaphore() function does this. For each successful ObtainSemaphore(), ObtainSemaphoreShared() and AttemptSemaphore() call you make, you must have a matching ReleaseSemaphore() call.
Removing a SignalSemaphore Structure
Semaphore resources can only be freed if the semaphore is not locked. A public semaphore should first be removed from the system semaphore list with the RemSemaphore() function. This prevents other tasks from finding the semaphore and trying to lock it. Once the semaphore is removed from the system list, the semaphore should be locked exclusively so no other task can lock it. Once the lock is obtained, it can be released again, and the resources can be deallocated.
The following code should be used to remove a public semaphore:
STRPTR name; struct SignalSemaphore *semaphore; IExec->Forbid(); if (semaphore = IExec->FindSemaphore(name)) { IExec->RemSemaphore(semaphore); /* So no one else can find it... */ IExec->ObtainSemaphore(semaphore); /* Wait for us to be last user...*/ IExec->ReleaseSemaphore(semaphore); /* Ready for cleanup... */ } IExec->FreeVec(semaphore); IExec->Permit();
Multiple Semaphores
The semaphore system has the ability to ask for ownership of a complete list of semaphores. This can help prevent deadlocks when there are two or more tasks trying to get the same set of semaphores. If task A gets semaphore 1 and tries to obtain semaphore 2 after task B has obtained semaphore 2 but before task B tries to obtain semaphore 1 then both tasks will hang. Exec provides ObtainSemaphoreList() and ReleaseSemaphoreList() to prevent this problem.
A semaphore list is a list header to a list that contains SignalSemaphore structures. The semaphore list must not contain any public semaphores. This is because the semaphore list functions use the standard node structures in the semaphore.
To arbitrate access to a semaphore list use another semaphore. Create a public semaphore and use it to arbitrate access to the list header of the semaphore list. This also gives you a locking semaphore, protecting the ObtainSemaphoreList() call. Once you have gained access to the list with ObtainSemaphore(), you may obtain all the semaphores on the list via ObtainSemaphoreList() (or get individual semaphores with ObtainSemaphore()). When you are finished with the protected objects, release the semaphores on the list with ReleaseSemaphoreList(), and then release the list semaphore via ReleaseSemaphore().
For example:
IExec->ObtainSemaphore((struct SignalSemaphore *)SemaphoreList); IExec->ObtainSemaphoreList(SemaphoreList->sl_List); /* At this point the objects are protected, and can be manipulated */ IExec->ReleaseSemaphoreList(SemaphoreList->sl_List); IExec->ReleaseSemaphore((struct SignalSemaphore *)SemaphoreList);
See the SharedList structure above for an example of a semaphore structure with a list header.
Semaphore Example
A simple "do nothing" example of Exec signal semaphore use is shown below. When the semaphore is owned by a task, attempted access by other tasks will block. A nesting count is maintained, so the current task can safely call ObtainSemaphore() on the same semaphore.
/* semaphore.c - Exec semaphore example - compile with lc -L semaphore.c */ #include <exec/types.h> #include <exec/semaphores.h> #include <clib/exec_protos.h> #include <stdio.h> #ifdef LATTICE int CXBRK(void) { return(0); } /* Disable Lattice CTRL/C handling */ void chkabort(void) { return; } /* really */ #endif struct SignalSemaphore LockSemaphore = {0}; VOID main(int argc,char *argv[]) { InitSemaphore(&LockSemaphore); ObtainSemaphore(&LockSemaphore); /* Task now owns the semaphore. */ ... ReleaseSemaphore(&LockSemaphore); /* Task has released the semaphore. */ }
Function Reference
The following charts give a brief description of the Exec semaphore functions. See the SDK for details about each call.
Exec Semaphore Function | Description |
---|---|
AddSemaphore() | Initialize and add a signal semaphore to the system. |
AttemptSemaphore() | Try to get an exclusive lock on a signal semaphore without blocking. |
FindSemaphore() | Find a given system signal semaphore. |
InitSemaphore() | Initialize a signal semaphore. |
ObtainSemaphore() | Try to get exclusive access to a signal semaphore. |
ObtainSemaphoreList() | Try to get exclusive access to a list of signal semaphores. |
ObtainSemaphoreShared() | Try to get shared access to a signal semaphore. |
ReleaseSemaphore() | Release the lock on a signal semaphore. |
ReleaseSemaphoreList() | Release the locks on a list of signal semaphores. |
RemSemaphore() | Remove a signal semaphore from the system. |