mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-12 16:16:28 +00:00
292 lines
9 KiB
Markdown
292 lines
9 KiB
Markdown
[← Home](../README.md) · [Exec Kernel](README.md)
|
|
|
|
# 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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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+)
|
|
|
|
```c
|
|
/* 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.
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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`:
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```mermaid
|
|
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)
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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
|
|
|
|
```c
|
|
/* 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](multitasking.md) — priority inversion and synchronization strategies
|
|
- *Amiga ROM Kernel Reference Manual: Exec* — semaphores chapter
|