amiga-bootcamp/06_exec_os/io_requests.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

12 KiB
Raw Permalink Blame History

← Home · Exec Kernel

IO Requests — IORequest, DoIO, SendIO, CheckIO, AbortIO

Overview

AmigaOS device I/O uses a message-based asynchronous protocol. Every device operation is described by an IORequest structure sent to a device's command port. The device processes it (synchronously or in the background) and replies when done. This model unifies all hardware — disks, serial, parallel, audio, timers, network — under a single consistent API.

The IO system is built directly on top of message ports. An IORequest contains an embedded Message, and device I/O is literally message passing between your task and the device's task.


Architecture

sequenceDiagram
    participant App as Application
    participant Port as Reply Port
    participant Dev as Device Task

    App->>Dev: OpenDevice("trackdisk.device", 0, ior, 0)
    Note over Dev: Fills ior->io_Device, io_Unit

    App->>Dev: SendIO(ior) [async]
    Note over Dev: Device processes command
    Dev->>Port: ReplyMsg (PutMsg to reply port)
    App->>Port: WaitIO(ior) or Wait(portSig)
    App->>App: Check ior->io_Error

    App->>Dev: CloseDevice(ior)

The IORequest Lifecycle

1. CreateIORequest / CreateStdIO → allocate request
2. OpenDevice → bind to device/unit
3. Fill io_Command, io_Data, io_Length, io_Offset
4. DoIO (sync) or SendIO (async) → submit
5. Check io_Error, io_Actual
6. Repeat 35 as needed
7. CloseDevice → unbind
8. DeleteIORequest / DeleteStdIO → free

Structures

/* exec/io.h — NDK39 */

struct IORequest {
    struct Message io_Message;  /* embedded Message (has MsgPort reply port) */
    struct Device *io_Device;   /* filled by OpenDevice */
    struct Unit   *io_Unit;     /* filled by OpenDevice */
    UWORD          io_Command;  /* CMD_READ, CMD_WRITE, TD_FORMAT, ... */
    UBYTE          io_Flags;    /* IOF_QUICK = attempt synchronous fast path */
    BYTE           io_Error;    /* result: 0 = success, negative = error code */
};

struct IOStdReq {               /* extended version with data fields */
    struct IORequest io_Request;
    ULONG  io_Actual;           /* actual bytes transferred */
    ULONG  io_Length;           /* requested byte count */
    APTR   io_Data;             /* data buffer pointer */
    ULONG  io_Offset;           /* byte offset (for random-access devices) */
};

IORequest Field Reference

Field Set By Description
io_Message.mn_ReplyPort App Reply port — device sends reply here when done
io_Device OpenDevice Pointer to device base — do not modify
io_Unit OpenDevice Pointer to device unit — do not modify
io_Command App Operation to perform (CMD_READ, CMD_WRITE, etc.)
io_Flags App IOF_QUICK for synchronous fast path attempt
io_Error Device 0 = success, negative = error (set after completion)
io_Actual Device Bytes actually transferred
io_Length App Bytes to transfer
io_Data App Buffer pointer
io_Offset App Device-specific offset

Standard Command Codes

/* exec/io.h */
#define CMD_INVALID   0   /* not a valid command */
#define CMD_RESET     1   /* reset the device/unit to initial state */
#define CMD_READ      2   /* read io_Length bytes into io_Data from io_Offset */
#define CMD_WRITE     3   /* write io_Length bytes from io_Data at io_Offset */
#define CMD_UPDATE    4   /* flush write cache to media */
#define CMD_CLEAR     5   /* discard device read buffers */
#define CMD_STOP      6   /* suspend device operation */
#define CMD_START     7   /* resume device operation */
#define CMD_FLUSH     8   /* abort all pending requests */
#define CMD_NONSTD    9   /* first device-specific command number */

Device-specific commands start at CMD_NONSTD (9):

Device Command Number Description
trackdisk.device TD_FORMAT 10 Low-level format a track
trackdisk.device TD_MOTOR 11 Motor on/off control
trackdisk.device TD_CHANGESTATE 14 Check disk insertion
serial.device SDCMD_SETPARAMS 10 Set baud rate, stop bits
timer.device TR_ADDREQUEST 9 Schedule a timer event
audio.device ADCMD_ALLOCATE 32 Allocate audio channels

Error Codes (io_Error)

/* exec/errors.h — NDK39 */
#define IOERR_OPENFAIL   -1   /* device/unit could not be opened */
#define IOERR_ABORTED    -2   /* request was aborted via AbortIO */
#define IOERR_NOCMD      -3   /* unknown command */
#define IOERR_BADLENGTH  -4   /* io_Length invalid for this command */
#define IOERR_BADADDRESS -5   /* io_Data not aligned or accessible */
#define IOERR_UNITBUSY   -6   /* unit in use, cannot complete */
#define IOERR_SELFTEST   -7   /* hardware self-test failed */

Device-specific error codes are positive values defined in each device's header.


Opening and Closing Devices

Opening

/* Create a reply port and IO request */
struct MsgPort *replyPort = CreateMsgPort();
struct IOStdReq *ior = (struct IOStdReq *)
    CreateIORequest(replyPort, sizeof(struct IOStdReq));

/* Open the device */
BYTE err = OpenDevice("trackdisk.device", 0,   /* unit 0 = DF0: */
    (struct IORequest *)ior, 0);                /* flags = 0 */

if (err != 0)
{
    Printf("OpenDevice failed: %ld\n", err);
    DeleteIORequest(ior);
    DeleteMsgPort(replyPort);
    return RETURN_FAIL;
}

Closing

/* Close and free — MUST abort pending requests first */
CloseDevice((struct IORequest *)ior);
DeleteIORequest((struct IORequest *)ior);
DeleteMsgPort(replyPort);

Synchronous I/O: DoIO

Blocks the calling task until the device completes the request:

ior->io_Command = CMD_READ;
ior->io_Data    = buffer;
ior->io_Length  = 512;
ior->io_Offset  = 0;

LONG err = DoIO((struct IORequest *)ior);   /* LVO -456 */

if (err == 0)
{
    /* Success — io_Actual contains bytes read */
    Printf("Read %ld bytes\n", ior->io_Actual);
}
else
{
    Printf("Error: %ld\n", ior->io_Error);
}

How DoIO Works Internally

  1. Sets IOF_QUICK in io_Flags
  2. Calls the device's BeginIO vector
  3. If the device completes synchronously (kept IOF_QUICK set), returns immediately
  4. If the device cleared IOF_QUICK (will complete async), calls WaitIO to block

Asynchronous I/O: SendIO + WaitIO

/* Queue the request — returns immediately: */
ior->io_Command = CMD_READ;
ior->io_Data    = buffer;
ior->io_Length  = 65536;
ior->io_Offset  = 0;

SendIO((struct IORequest *)ior);   /* LVO -462 */

/* Do other work while device operates... */
UpdateUI();
ProcessInput();

/* Block until this specific request completes: */
WaitIO((struct IORequest *)ior);   /* LVO -474 */
BYTE err = ior->io_Error;

Combining Async IO with Event Loop

ULONG ioSig = 1L << replyPort->mp_SigBit;
ULONG idcmpSig = 1L << win->UserPort->mp_SigBit;

SendIO((struct IORequest *)ior);

BOOL ioDone = FALSE;
while (!ioDone)
{
    ULONG sigs = Wait(ioSig | idcmpSig | SIGBREAKF_CTRL_C);

    if (sigs & idcmpSig) HandleGUI();

    if (sigs & ioSig)
    {
        if (CheckIO((struct IORequest *)ior))
        {
            WaitIO((struct IORequest *)ior);  /* Must still call to clean up */
            ioDone = TRUE;
        }
    }

    if (sigs & SIGBREAKF_CTRL_C)
    {
        AbortIO((struct IORequest *)ior);
        WaitIO((struct IORequest *)ior);
        break;
    }
}

CheckIO — Non-Blocking Status Check

/* Returns non-NULL if request is complete: */
struct IORequest *result = CheckIO((struct IORequest *)ior);   /* LVO -468 */

if (result)
{
    WaitIO((struct IORequest *)ior);  /* Must still call to dequeue reply */
    /* io_Error is valid now */
}
else
{
    /* Still in progress */
}

Warning

: Even after CheckIO() returns non-NULL, you must call WaitIO() to remove the reply message from the port. Failing to do so leaves a stale message in the queue.


AbortIO — Cancel a Pending Request

AbortIO((struct IORequest *)ior);   /* LVO -480 — request cancellation */
WaitIO((struct IORequest *)ior);    /* Wait for acknowledgment */
/* io_Error will be IOERR_ABORTED (-2) */

Important: AbortIO is a request — the device may not abort immediately. Always follow with WaitIO to ensure the request is fully complete before reusing or freeing the IORequest.


The IOF_QUICK Fast Path

When IOF_QUICK is set in io_Flags, the device MAY complete the request synchronously within BeginIO() — without queuing, signaling, or replying via message. This avoids the overhead of message passing for trivial operations.

/* Fast-path attempt */
ior->io_Flags = IOF_QUICK;
BeginIO((struct IORequest *)ior);

if (ior->io_Flags & IOF_QUICK)
{
    /* Completed synchronously — no WaitIO needed */
}
else
{
    /* Device cleared IOF_QUICK — must wait */
    WaitIO((struct IORequest *)ior);
}

DoIO uses this mechanism internally.


Practical: Timer Device Usage

/* Open timer.device */
struct MsgPort *timerPort = CreateMsgPort();
struct timerequest *tr = (struct timerequest *)
    CreateIORequest(timerPort, sizeof(struct timerequest));

OpenDevice(TIMERNAME, UNIT_VBLANK, (struct IORequest *)tr, 0);

/* Set a 2-second timer */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs    = 2;
tr->tr_time.tv_micro   = 0;
SendIO((struct IORequest *)tr);

/* Wait for timer or user interrupt */
ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG sigs = Wait(timerSig | SIGBREAKF_CTRL_C);

if (sigs & timerSig)
{
    WaitIO((struct IORequest *)tr);
    Printf("Timer expired!\n");
}
else
{
    AbortIO((struct IORequest *)tr);
    WaitIO((struct IORequest *)tr);
}

/* Cleanup */
CloseDevice((struct IORequest *)tr);
DeleteIORequest((struct IORequest *)tr);
DeleteMsgPort(timerPort);

Pitfalls

1. Reusing IORequest While In Flight

/* BUG — request is still being processed by device */
SendIO(ior);
ior->io_Command = CMD_WRITE;  /* WRONG — device is reading from this struct */
SendIO(ior);                   /* Double-send = corruption */

2. Forgetting WaitIO After CheckIO

/* BUG — reply message left in port */
if (CheckIO(ior)) { /* done! */ }
/* But WaitIO was never called — stale message in reply port */
/* Next Wait() on this port returns immediately with garbage */

3. Freeing IORequest Without CloseDevice

/* BUG — device still references this unit */
DeleteIORequest(ior);  /* Device's internal pointers now dangle */

Best Practices

  1. Always pair OpenDevice / CloseDevice and CreateIORequest / DeleteIORequest
  2. Always call WaitIO after CheckIO returns non-NULL
  3. Always call WaitIO after AbortIO
  4. Use SendIO for async operations — DoIO blocks your task completely
  5. Combine IO with event loop using Wait() on the reply port's signal
  6. Don't reuse IORequest until the previous operation completes
  7. Check io_Error after every operation — errors are common with removable media
  8. Use UNIT_VBLANK for timer.device unless you need microsecond precision (use UNIT_MICROHZ)

References

  • NDK39: exec/io.h, exec/errors.h, exec/devices.h
  • ADCD 2.1: OpenDevice, CloseDevice, DoIO, SendIO, WaitIO, CheckIO, AbortIO, CreateIORequest, DeleteIORequest
  • See also: Message Ports — IORequest is built on message passing
  • See also: 10_devices/ — per-device command codes and structures
  • Amiga ROM Kernel Reference Manual: Exec — I/O requests chapter