exec_os: enrich all stubs to bootcamp-quality reference articles

Complete rewrite of 14 exec_os articles from stubs to comprehensive
deep-dive technical references with architecture diagrams, pitfalls,
and best practices.

New: multitasking.md (scheduler, IPC, memory safety, real-world scenarios)
Enriched: exec_base, tasks_processes, library_system, library_vectors,
interrupts, exceptions_traps, memory_management, message_ports, signals,
semaphores, io_requests, lists_nodes, resident_modules

Updated indexes: 06_exec_os/README.md, root README.md
This commit is contained in:
Ilia Sharin 2026-04-23 17:55:31 -04:00
parent 4d136b0672
commit 59929047d4
16 changed files with 4463 additions and 678 deletions

View file

@ -4,7 +4,34 @@
## 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.
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.
---
@ -16,27 +43,41 @@ 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 exclusive access */
struct SemaphoreRequest ss_MultipleLink; /* shared-reader slot */
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; /* number of waiters */
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 |
---
## Initialising a Semaphore
```c
/* Stack or AllocMem — always initialise before use: */
/* Stack or heap — always initialise before use: */
struct SignalSemaphore sem;
InitSemaphore(&sem); /* LVO -558 */
/* Public (named) semaphore — so other tasks can find it: */
/* Public (named) semaphore — findable by other tasks: */
sem.ss_Link.ln_Name = "myapp.lock";
AddSemaphore(&sem); /* LVO -564 */
sem.ss_Link.ln_Pri = 0;
AddSemaphore(&sem); /* LVO -564 — adds to SysBase→SemaphoreList */
/* Later: */
/* Find from another task: */
Forbid();
struct SignalSemaphore *found = FindSemaphore("myapp.lock"); /* LVO -576 */
Permit();
/* Cleanup: */
RemSemaphore(&sem); /* LVO -570 */
```
@ -49,6 +90,7 @@ RemSemaphore(&sem); /* LVO -570 */
ObtainSemaphore(&sem); /* LVO -534 */
/* --- critical section: only one task in here at a time --- */
ModifySharedData();
ReleaseSemaphore(&sem); /* LVO -546 */
```
@ -57,11 +99,27 @@ ReleaseSemaphore(&sem); /* LVO -546 */
```c
/* Returns TRUE if obtained, FALSE if someone else holds it: */
if (AttemptSemaphore(&sem)) { /* LVO -540 */
/* got 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);
} else {
/* resource busy — do something else */
}
```
@ -69,45 +127,166 @@ if (AttemptSemaphore(&sem)) { /* LVO -540 */
## Shared (Read) Lock
Multiple tasks may hold a shared lock simultaneously. An exclusive lock blocks until all shared holders release.
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 */
ReleaseSemaphore(&sem); /* Same release for both modes */
```
---
## Nesting
## Nesting (Reentrancy)
Semaphores are **reentrant** — the same task can call `ObtainSemaphore` multiple times. The `ss_NestCount` tracks how many times the current owner has obtained it. `ReleaseSemaphore` must be called the same number of times.
Semaphores are **reentrant** — the same task can call `ObtainSemaphore` multiple times without deadlocking itself:
```c
ObtainSemaphore(&sem); /* NestCount = 1 */
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.
---
## Semaphore vs Forbid/Disable
## Obtaining Multiple Semaphores
| Mechanism | Blocks | Other tasks run while waiting? | Interrupt safe? |
|---|---|---|---|
| `Forbid()` | All task switching | ❌ No | ✅ (interrupts still run) |
| `Disable()` | All task switching + interrupts | ❌ No | ✅ |
| `ObtainSemaphore()` | Only contending tasks | ✅ Yes | ❌ Not from interrupt context |
To avoid deadlocks when you need multiple semaphores, use `ObtainSemaphoreList`:
Use semaphores for anything that may take more than a few microseconds. Use `Forbid()` only for very short list manipulations.
```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`
- 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