amiga-bootcamp/06_exec_os/semaphores.md
2026-04-26 14:46:18 -04:00

9 KiB

← Home · Exec Kernel

Semaphores — SignalSemaphore, ObtainSemaphore, Shared/Exclusive

Overview

Semaphores are the AmigaOS mechanism for mutual exclusion and shared-read access to resources. Unlike Forbid() (which blocks all scheduling), semaphores allow other tasks to run while waiting — the waiting task simply sleeps until the resource is available. They are the correct synchronization primitive for anything that takes more than a few microseconds.


Architecture

stateDiagram-v2
    [*] --> FREE : InitSemaphore()
    FREE --> EXCLUSIVE : ObtainSemaphore() (by Task A)
    FREE --> SHARED : ObtainSemaphoreShared() (by Task A)
    SHARED --> SHARED : ObtainSemaphoreShared() (by Task B — multiple OK)
    SHARED --> FREE : All shared holders release
    EXCLUSIVE --> NESTED : ObtainSemaphore() (by same Task A — reentrant)
    NESTED --> EXCLUSIVE : ReleaseSemaphore() (decrement nest count)
    EXCLUSIVE --> FREE : ReleaseSemaphore() (nest count reaches 0)
    EXCLUSIVE --> BLOCKED : ObtainSemaphore() by Task B → Task B sleeps
    BLOCKED --> EXCLUSIVE : Task A releases → Task B wakes

Shared vs Exclusive

Mode Multiple holders? Use case
Exclusive (ObtainSemaphore) No — only one task at a time Writing/modifying shared data
Shared (ObtainSemaphoreShared) Yes — multiple readers allowed Read-only access to shared data

When a task requests exclusive access while shared holders exist, it blocks until ALL shared holders release. When a task requests shared access while an exclusive holder exists, it blocks until the exclusive holder releases.


struct SignalSemaphore

/* exec/semaphores.h — NDK39 */
struct SignalSemaphore {
    struct Node  ss_Link;       /* ln_Type = NT_SIGNALSEM */
                                /* ln_Name = semaphore name (public) */
    WORD         ss_NestCount;  /* how many times THIS task has obtained it */
    struct MinList ss_WaitQueue;/* tasks waiting for access */
    struct SemaphoreRequest ss_MultipleLink; /* shared-reader management */
    struct Task *ss_Owner;      /* task holding exclusive lock (or NULL) */
    WORD         ss_QueueCount; /* internal waiter tracking */
};
Field Description
ss_Link.ln_Name Name string — set for public (findable) semaphores
ss_NestCount How many times the current owner has obtained it (reentrant)
ss_WaitQueue Queue of tasks waiting for access
ss_Owner Task holding exclusive lock, or NULL if free/shared-only
ss_QueueCount Internal — tracks waiting tasks and shared readers

Initializing a Semaphore

/* Stack or heap — always initialize before use: */
struct SignalSemaphore sem;
InitSemaphore(&sem);   /* LVO -558 */

/* Public (named) semaphore — findable by other tasks: */
sem.ss_Link.ln_Name = "myapp.lock";
sem.ss_Link.ln_Pri  = 0;
AddSemaphore(&sem);    /* LVO -564 — adds to SysBase→SemaphoreList */

/* Find from another task: */
Forbid();
struct SignalSemaphore *found = FindSemaphore("myapp.lock");  /* LVO -576 */
Permit();

/* Cleanup: */
RemSemaphore(&sem);    /* LVO -570 */

Exclusive (Write) Lock

/* Block until this task holds the semaphore exclusively: */
ObtainSemaphore(&sem);    /* LVO -534 */

/* --- critical section: only one task in here at a time --- */
ModifySharedData();

ReleaseSemaphore(&sem);   /* LVO -546 */

Non-Blocking Try

/* Returns TRUE if obtained, FALSE if someone else holds it: */
if (AttemptSemaphore(&sem))     /* LVO -540 */
{
    /* Got exclusive access */
    ModifySharedData();
    ReleaseSemaphore(&sem);
}
else
{
    /* Resource busy — do something else or retry later */
}

Shared-to-Exclusive Upgrade (OS 3.0+)

/* AttemptSemaphoreShared — try shared lock without blocking */
if (AttemptSemaphoreShared(&sem))   /* LVO -774 */
{
    /* Got shared access */
    ReadSharedData();
    ReleaseSemaphore(&sem);
}

Shared (Read) Lock

Multiple tasks may hold a shared lock simultaneously. An exclusive lock request blocks until all shared holders release.

ObtainSemaphoreShared(&sem);   /* LVO -768 */

/* --- read-only access: multiple tasks may be here at once --- */
result = ReadSharedData();

ReleaseSemaphore(&sem);        /* Same release for both modes */

Nesting (Reentrancy)

Semaphores are reentrant — the same task can call ObtainSemaphore multiple times without deadlocking itself:

ObtainSemaphore(&sem);   /* NestCount = 1, Owner = thisTask */
ObtainSemaphore(&sem);   /* NestCount = 2 — safe, same task */
ObtainSemaphore(&sem);   /* NestCount = 3 */
ReleaseSemaphore(&sem);  /* NestCount = 2 */
ReleaseSemaphore(&sem);  /* NestCount = 1 */
ReleaseSemaphore(&sem);  /* NestCount = 0 — fully released, waiters wake */

This is essential for recursive functions or library code that may be called from contexts that already hold the lock.


Obtaining Multiple Semaphores

To avoid deadlocks when you need multiple semaphores, use ObtainSemaphoreList:

/* Build a list of semaphores to obtain atomically: */
struct SemaphoreRequest reqA, reqB;
struct List semList;
NewList(&semList);

reqA.sr_Semaphore = &semA;
reqB.sr_Semaphore = &semB;
AddTail(&semList, &reqA.sr_Link);
AddTail(&semList, &reqB.sr_Link);

ObtainSemaphoreList(&semList);   /* LVO -582 */
/* Both semaphores held — no deadlock risk */

ReleaseSemaphore(&semA);
ReleaseSemaphore(&semB);

Semaphore vs Forbid vs Disable

Mechanism Blocks Other tasks run? Interrupt safe? Cost Max duration
Forbid() All task switching No Ints still run Very low ~100 ms
Disable() All ints + tasks No (is the lock) Lowest ~250 µs
ObtainSemaphore() Only contending tasks Yes Not from IRQ Medium Unlimited
ObtainSemaphoreShared() Only if exclusive held Yes Not from IRQ Medium Unlimited

Decision Guide

graph TD
    Q1{"In interrupt<br/>context?"} -->|Yes| DISABLE["Use Disable()"]
    Q1 -->|No| Q2{"Duration<br/>< 10 µs?"}
    Q2 -->|Yes| FORBID["Use Forbid()"]
    Q2 -->|No| Q3{"Multiple<br/>readers OK?"}
    Q3 -->|Yes| SHARED["ObtainSemaphoreShared()"]
    Q3 -->|No| EXCL["ObtainSemaphore()"]

    style DISABLE fill:#ffcdd2,stroke:#e53935,color:#333
    style FORBID fill:#fff3e0,stroke:#ff9800,color:#333
    style SHARED fill:#e8f5e9,stroke:#4caf50,color:#333
    style EXCL fill:#e8f4fd,stroke:#2196f3,color:#333

Pitfalls

1. Deadlock (Lock Ordering)

/* Task A */                    /* Task B */
ObtainSemaphore(&semX);        ObtainSemaphore(&semY);
ObtainSemaphore(&semY); /*!*/  ObtainSemaphore(&semX); /*!*/
/* Task A waits for Y           Task B waits for X
   → DEADLOCK — both tasks sleep forever */

Solution: Always obtain semaphores in the same global order (alphabetical, by address, etc.).

2. Priority Inversion

/* Low-pri task holds semaphore, medium-pri task runs,
   high-pri task waits for semaphore → high-pri starves.
   AmigaOS has NO priority inheritance. */

Solution: Keep critical sections short; don't hold semaphores across I/O.

3. ObtainSemaphore from Interrupt Context

/* CRASH — ObtainSemaphore may Wait(), which is illegal from interrupts */
void __interrupt MyHandler(void)
{
    ObtainSemaphore(&sem);  /* DEADLOCK or crash */
}

Solution: Use Disable()/Enable() for interrupt-level synchronization, or use AttemptSemaphore() (non-blocking) and skip if busy.

4. Forgetting to Release

/* BUG — semaphore held forever */
ObtainSemaphore(&sem);
if (error) return;          /* Returns without releasing! */
ReleaseSemaphore(&sem);

/* CORRECT — use cleanup pattern */
ObtainSemaphore(&sem);
if (error) goto cleanup;
/* ... work ... */
cleanup:
ReleaseSemaphore(&sem);

5. Shared/Exclusive Mismatch

/* Not a bug, but confusing — ReleaseSemaphore works for both modes */
ObtainSemaphoreShared(&sem);
ReleaseSemaphore(&sem);      /* Correct — same function for both */

Best Practices

  1. Use semaphores instead of Forbid() for anything > ~10 µs
  2. Prefer shared locks for read-only access — maximizes parallelism
  3. Keep critical sections short — don't do I/O while holding a semaphore
  4. Use consistent lock ordering to prevent deadlocks
  5. Use AttemptSemaphore() for non-blocking try-lock patterns
  6. Always release on every code path — use goto-cleanup pattern
  7. Never call from interrupts — use Disable() or AttemptSemaphore() instead
  8. Use ObtainSemaphoreList() when you need multiple semaphores atomically

References

  • NDK39: exec/semaphores.h
  • ADCD 2.1: InitSemaphore, ObtainSemaphore, ObtainSemaphoreShared, ReleaseSemaphore, AttemptSemaphore, ObtainSemaphoreList
  • See also: Multitasking — priority inversion and synchronization strategies
  • Amiga ROM Kernel Reference Manual: Exec — semaphores chapter