amiga-bootcamp/06_exec_os/message_ports.md
Ilia Sharin 59929047d4 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
2026-04-23 17:55:31 -04:00

336 lines
9.6 KiB
Markdown

[← Home](../README.md) · [Exec Kernel](README.md)
# Message Ports — MsgPort, Message, PutMsg, GetMsg, WaitPort
## Overview
AmigaOS inter-task communication uses a **message passing** system. Tasks send `Message` structures to `MsgPort` queues. The receiving task either polls (`GetMsg`) or blocks (`WaitPort`) for incoming messages. This is the fundamental IPC mechanism — everything from [IDCMP](../09_intuition/idcmp.md) to device I/O to ARexx scripting is built on top of message ports.
Unlike pipes or sockets in Unix, Amiga messages are **zero-copy pointer exchanges**. The sender and receiver share the same physical memory — no data is copied. This makes message passing extremely fast but requires careful ownership discipline.
---
## Architecture
```mermaid
sequenceDiagram
participant S as Sender Task
participant P as MsgPort Queue
participant R as Receiver Task
S->>S: Prepare Message (mn_ReplyPort = replyPort)
S->>P: PutMsg(port, msg)
Note over P: Message added to mp_MsgList<br/>Signal(mp_SigTask, mp_SigBit)
P->>R: Signal wakes receiver
R->>P: GetMsg(port) → returns msg pointer
R->>R: Process message
R->>S: ReplyMsg(msg) → PutMsg to mn_ReplyPort
Note over S: Sender receives reply on replyPort
```
### Ownership Rules
This is the most critical concept in Amiga message passing:
| Phase | Message Owned By | Who Can Read/Write? |
|---|---|---|
| Before `PutMsg()` | Sender | Sender only |
| After `PutMsg()`, before `GetMsg()` | Port (in transit) | **Nobody** — message is in the queue |
| After `GetMsg()`, before `ReplyMsg()` | Receiver | Receiver only |
| After `ReplyMsg()` | Sender (via reply port) | Sender only |
> **Critical**: The sender must **not** modify or free the message between `PutMsg()` and receiving the reply. The message structure lives in the sender's memory, but it's logically owned by the receiver until replied.
---
## Core Structures
```c
/* exec/ports.h — NDK39 */
struct MsgPort {
struct Node mp_Node; /* ln_Name = port name (for public ports) */
UBYTE mp_Flags; /* PA_SIGNAL, PA_SOFTINT, PA_IGNORE */
UBYTE mp_SigBit; /* signal bit used for PA_SIGNAL ports */
APTR mp_SigTask; /* task to signal on message arrival */
struct List mp_MsgList; /* queue of pending messages */
};
struct Message {
struct Node mn_Node; /* ln_Type = NT_MESSAGE */
struct MsgPort *mn_ReplyPort; /* port to send reply to (or NULL) */
UWORD mn_Length; /* total size of message including header */
};
```
### MsgPort Field Reference
| Field | Description |
|---|---|
| `mp_Node.ln_Name` | Port name — set for public (findable) ports, NULL for private |
| `mp_Flags` | Notification method when message arrives |
| `mp_SigBit` | Which signal bit to set when message arrives (for `PA_SIGNAL`) |
| `mp_SigTask` | Which task to signal (for `PA_SIGNAL`) or softint to trigger |
| `mp_MsgList` | Standard exec List — pending messages queue (FIFO) |
### mp_Flags Values
| Value | Constant | Behavior |
|---|---|---|
| 0 | `PA_SIGNAL` | Signal `mp_SigTask` with `1L << mp_SigBit` when message arrives |
| 1 | `PA_SOFTINT` | Trigger the software interrupt pointed to by `mp_SigTask` |
| 2 | `PA_IGNORE` | Do nothing — port must be polled with `GetMsg()` |
---
## Creating and Destroying Ports
### OS 2.0+ (Preferred)
```c
struct MsgPort *port = CreateMsgPort(); /* LVO -732 */
/* Automatically:
- Allocates memory
- Allocates a signal bit
- Sets mp_Flags = PA_SIGNAL
- Sets mp_SigTask = FindTask(NULL)
- Initializes mp_MsgList */
if (!port) { /* All signal bits exhausted */ }
/* Cleanup: */
DeleteMsgPort(port); /* LVO -738 */
/* Frees signal bit and memory */
```
### Manual Creation (OS 1.x Compatible)
```c
struct MsgPort *port = AllocMem(sizeof(struct MsgPort), MEMF_PUBLIC | MEMF_CLEAR);
port->mp_Node.ln_Type = NT_MSGPORT;
port->mp_Flags = PA_SIGNAL;
port->mp_SigBit = AllocSignal(-1);
port->mp_SigTask = FindTask(NULL);
NewList(&port->mp_MsgList);
/* Cleanup (reverse order): */
FreeSignal(port->mp_SigBit);
FreeMem(port, sizeof(struct MsgPort));
```
---
## Sending Messages
```c
/* PutMsg: add message to queue, signal receiver */
PutMsg(targetPort, (struct Message *)myMsg);
/* Non-blocking — returns immediately
Can be called from interrupt context
Sender must NOT modify msg until reply received */
```
### What PutMsg Does Internally
1. Adds the message to the tail of `targetPort->mp_MsgList`
2. Sets `mn_Node.ln_Type = NT_MESSAGE`
3. If `mp_Flags == PA_SIGNAL`: calls `Signal(mp_SigTask, 1L << mp_SigBit)`
4. If `mp_Flags == PA_SOFTINT`: triggers the software interrupt
5. Returns — does not wait
---
## Receiving Messages
### Blocking Wait
```c
/* Block until at least one message is pending: */
WaitPort(myPort); /* LVO -384 */
/* Equivalent to: Wait(1L << myPort->mp_SigBit) + signal clear */
/* Drain the queue (may have multiple messages): */
struct Message *msg;
while ((msg = GetMsg(myPort)) != NULL)
{
/* Process message */
ReplyMsg(msg); /* Send reply — required for request-reply pattern */
}
```
### Non-Blocking Poll
```c
struct Message *msg = GetMsg(myPort); /* LVO -372 */
/* Returns NULL if queue is empty — never blocks */
```
### Multi-Port Wait
```c
/* Wait on multiple ports simultaneously */
ULONG portSig1 = 1L << port1->mp_SigBit;
ULONG portSig2 = 1L << port2->mp_SigBit;
ULONG received = Wait(portSig1 | portSig2 | SIGBREAKF_CTRL_C);
if (received & portSig1)
{
struct Message *msg;
while ((msg = GetMsg(port1))) { /* ... */ ReplyMsg(msg); }
}
if (received & portSig2)
{
struct Message *msg;
while ((msg = GetMsg(port2))) { /* ... */ ReplyMsg(msg); }
}
```
---
## The Request-Reply Pattern
The standard bidirectional communication idiom:
```c
/* --- Sender --- */
struct MyMessage {
struct Message msg; /* MUST be first field */
ULONG command;
ULONG result;
APTR data;
};
struct MsgPort *replyPort = CreateMsgPort();
struct MyMessage request;
request.msg.mn_ReplyPort = replyPort;
request.msg.mn_Length = sizeof(struct MyMessage);
request.command = CMD_DO_WORK;
request.data = myData;
PutMsg(serverPort, &request.msg);
/* Wait for reply */
WaitPort(replyPort);
GetMsg(replyPort);
/* Now request.result contains the server's response */
DeleteMsgPort(replyPort);
/* --- Receiver (Server) --- */
WaitPort(serverPort);
struct MyMessage *req;
while ((req = (struct MyMessage *)GetMsg(serverPort)))
{
/* Process */
req->result = DoWork(req->command, req->data);
/* Reply — wakes sender */
ReplyMsg(&req->msg);
}
```
---
## Public Named Ports
Public ports are registered on `SysBase→PortList` and findable by name:
```c
/* Register: */
port->mp_Node.ln_Name = "myapp.port";
AddPort(port); /* LVO -354 — adds to SysBase→PortList */
/* Find from another task: */
Forbid();
struct MsgPort *remote = FindPort("myapp.port"); /* LVO -390 */
if (remote)
PutMsg(remote, myMsg);
Permit();
/* Unregister: */
RemPort(port); /* LVO -360 */
```
> **Critical**: `Forbid()` is required around `FindPort()` + `PutMsg()`. Without it, the server task could call `RemPort()` between your `FindPort()` and `PutMsg()`, leaving you with a dangling pointer.
---
## Pitfalls
### 1. Not Replying to Messages
```c
/* BUG — sender is blocked waiting for reply forever */
msg = GetMsg(port);
/* ... process but forget to ReplyMsg ... */
/* Sender hangs on WaitPort(replyPort) indefinitely */
```
### 2. Modifying Message Before Reply
```c
/* BUG — message is logically owned by receiver */
PutMsg(server, &msg);
msg.data = newData; /* WRONG — receiver may be reading msg.data right now */
WaitPort(replyPort); /* Race condition */
```
### 3. Deleting Port with Pending Messages
```c
/* BUG — messages in queue still reference the port */
DeleteMsgPort(port); /* Leaked messages, dangling reply port pointers */
/* SAFE — drain first */
struct Message *msg;
while ((msg = GetMsg(port)))
ReplyMsg(msg);
DeleteMsgPort(port);
```
### 4. FindPort Without Forbid
```c
/* RACE — server may RemPort between find and send */
struct MsgPort *p = FindPort("SERVER"); /* Port exists... */
/* Context switch — server task exits, calls RemPort() */
PutMsg(p, msg); /* CRASH — p is now freed memory */
```
### 5. Stack-Allocated Messages with Async Reply
```c
/* BUG — message on stack, function returns before reply */
void SendRequest(struct MsgPort *server)
{
struct MyMessage msg;
msg.msg.mn_ReplyPort = replyPort;
PutMsg(server, &msg.msg);
/* Function returns — stack frame destroyed */
/* Reply arrives later → writes to invalid stack memory */
}
```
---
## Best Practices
1. **Always drain ports** before deleting — `while (GetMsg(port)) ReplyMsg(msg)`
2. **Always reply** to received messages — the sender may be waiting
3. **Use `Forbid()`** around `FindPort()` + `PutMsg()` for public ports
4. **Use heap-allocated messages** for async patterns — never stack-local
5. **Set `mn_Length`** correctly — some system code uses it for validation
6. **Set `mn_ReplyPort`** to NULL if no reply is expected — receiver checks this
7. **Use `PA_SIGNAL`** for normal ports — `PA_SOFTINT` only for device-level code
8. **Create separate reply ports** per conversation — don't share reply ports between multiple pending requests
---
## References
- NDK39: `exec/ports.h`, `exec/messages.h`
- ADCD 2.1: `CreateMsgPort`, `DeleteMsgPort`, `PutMsg`, `GetMsg`, `WaitPort`, `ReplyMsg`, `AddPort`, `RemPort`, `FindPort`
- See also: [Signals](signals.md), [Multitasking](multitasking.md) — IPC strategies comparison
- *Amiga ROM Kernel Reference Manual: Exec* — messages and ports chapter