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
- Enable async on the desired file descriptor (fd).
- Link fd to owner process that will receive SIGIO.
- Adjust terminal to raw/no-echo so data comes byte-for-byte.
- 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.