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

9.6 KiB

← Home · Exec Kernel

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

  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

/* 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 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

/* 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

  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, Multitasking — IPC strategies comparison
  • Amiga ROM Kernel Reference Manual: Exec — messages and ports chapter