TĐ

Lecture 19-20 Notes — Asynchronous I/O, SIGIO, POSIX AIO

Synchronous vs Asynchronous I/O

  • Definitions
    • Synchronous I/O: Calling thread blocks until the operation finishes. Example: reading from stdin with scanf, cin, or read().
    • Asynchronous I/O: Request returns immediately; the kernel / device signals completion later.
  • Every-day Examples
    • Terminal programs you have written: block on input → whole program "hangs" until EOF/Return.
    • Commercial apps (Word, Chrome, computer games): keep rendering/playing while simultaneously accepting input → non-blocking, asynchronous.
  • Observable behaviour
    • If synchronous: video in a browser would pause whenever you click the URL bar.
    • If asynchronous: video keeps playing, game enemies keep moving while you type.

Server–Client Scenario

  • Environment: One server process, many independent clients sending sporadic requests.
  • Key issues
    • Handling arrivals while server is mid-analysis.
    • Writing to a client whose buffer is full without stalling other outgoing sends.
    • Need a structure to queue or log pending events (requests, replies).

Level Tracking vs Edge Tracking

  • Level Tracking (state table)
    • Maintain a list/array of all possible entities + their current state.
    • Pros: Conceptually simple, works for small, fixed sets (e.g., Unix signals internal kernel bitmask).
    • Cons: Memory heavy, O(N) scans to find active items.
    • Absurd example: array of 10^{10} people with a boolean "bought_ticket"; scanning to find the 10 true entries.
  • Edge Tracking (event list)
    • Record only changes (edges) – dynamic push/pop of active elements.
    • Ideal when active set « potential set (web-servers: millions registered ⟶ thousands active).
    • Analogy: Human sensory adaptation – brain registers change, suppresses constant stimuli (continuous tone "vanishes"; static image disappears after fixation).

Solution #1 – Threads

  • Spawn separate thread(s): one computes, another blocks on I/O.
  • Synchronise with mutexes, condition variables.
  • Drawbacks: extra stack memory, context-switch cost; not all MCUs/embedded CPUs support threads; debugging can be painful.

Solution #2 – Signals (SIGIO)

  • Signals = genuinely asynchronous: kernel delivers an interrupt-like notification.
  • Already used for SIGFPE, SIGSEGV; can also flag I/O readiness with SIGIO.
  • Normal terminal I/O problems to overcome:
    • Canonical (line-buffered) mode waits for Return.
    • Driver echoes and interprets arrows, Backspace, Ctrl-C, etc.

SIGIO life-cycle

  1. Enable async on the desired file descriptor (fd).
  2. Link fd to owner process that will receive SIGIO.
  3. Adjust terminal to raw/no-echo so data comes byte-for-byte.
  4. Install signal()/sigaction() handler.

fcntl(2) – File-descriptor Control

  • Header: #include <fcntl.h>
  • Prototype: int fcntl(int fd, int cmd, ...);
  • Steps to arm SIGIO
    • Set owning process: fcntl(fd, F_SETOWN, getpid());
    • Get current flag bits: int flags = fcntl(fd, F_GETFL);
    • OR in O_ASYNC: fcntl(fd, F_SETFL, flags | O_ASYNC);
  • Conceptual bitmask: flags{new}=flags{old}\ \lor\ O_ASYNC.

STTY – Tweaking Terminal Behaviour

  • Need raw mode & no echo for unprocessed chars:
    • Enter: system("stty raw -echo");
    • Restore: system("stty cooked +echo");
  • Alternatives: do once manually before/after program, or via termios POSIX APIs (more portable).

POSIX AIO (Asynchronous I/O) – "Let’s do it again …"

  • Compile with -lrt to link librt (where AIO lives).
  • Uses giant control block struct aiocb (≃ 14 fields). Key fields used in lecture:
    • aio_fildes: file descriptor (0 = stdin).
    • aio_buf: pointer to user buffer.
    • aio_nbytes: transfer length.
    • aio_offset: file offset (unused for tty).
    • aio_sigevent: sub-struct defining completion notification → here SIGEV_SIGNAL + SIGIO.

Minimal setup code

static char input[1];
struct aiocb my_buffer;

void setup(void) {
    my_buffer.aio_fildes              = 0;          // stdin
    my_buffer.aio_buf                 = input;
    my_buffer.aio_nbytes              = 1;          // 1 byte
    my_buffer.aio_offset              = 0;
    my_buffer.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    my_buffer.aio_sigevent.sigev_signo  = SIGIO;
    aio_read(&my_buffer);             // arm first request
    signal(SIGIO, handler);           // install handler
}
  • Inside handler(int signum)
    • Check for errors: if (aio_error(&my_buffer) == 0)
    • Obtain return value (#bytes): aio_return(&my_buffer);
    • Access character: char c = *(char*)my_buffer.aio_buf;
    • Re-issue next read: aio_read(&my_buffer);

STRACE – System-Call & Signal Tracer

  • Launch: strace ./my_exe (records every syscall + delivered signals).
  • Flags
    • -f: follow into children after fork()/clone().
  • Learning method: run it, inspect output, consult man <syscall> for unknown names.

Practical/Philosophical Implications

  • Asynchronicity ≠ perfection: program must be written in event-driven style, or with non-blocking loops, not just sprinkled with signals.
  • Trade-off triangle: threads vs signals vs polling (CPU, memory, debuggability).
  • Embedded devices: signals/AIO often favoured because single-core, tight RAM, but require careful design.
  • Large-scale services (web, games) lean toward event-driven edge tracking for scalability.