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
9.6 KiB
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 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
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
/* 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)
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)
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
/* 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
- Adds the message to the tail of
targetPort->mp_MsgList - Sets
mn_Node.ln_Type = NT_MESSAGE - If
mp_Flags == PA_SIGNAL: callsSignal(mp_SigTask, 1L << mp_SigBit) - If
mp_Flags == PA_SOFTINT: triggers the software interrupt - Returns — does not wait
Receiving Messages
Blocking Wait
/* 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
struct Message *msg = GetMsg(myPort); /* LVO -372 */
/* Returns NULL if queue is empty — never blocks */
Multi-Port Wait
/* 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:
/* --- 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:
/* 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 aroundFindPort()+PutMsg(). Without it, the server task could callRemPort()between yourFindPort()andPutMsg(), leaving you with a dangling pointer.
Pitfalls
1. Not Replying to Messages
/* 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
/* 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
/* 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
/* 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
/* 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
- Always drain ports before deleting —
while (GetMsg(port)) ReplyMsg(msg) - Always reply to received messages — the sender may be waiting
- Use
Forbid()aroundFindPort()+PutMsg()for public ports - Use heap-allocated messages for async patterns — never stack-local
- Set
mn_Lengthcorrectly — some system code uses it for validation - Set
mn_ReplyPortto NULL if no reply is expected — receiver checks this - Use
PA_SIGNALfor normal ports —PA_SOFTINTonly for device-level code - 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, Multitasking — IPC strategies comparison
- Amiga ROM Kernel Reference Manual: Exec — messages and ports chapter