Fundamentals 13 min read

Understanding IO Models: Blocking, Non‑Blocking, Multiplexing, Signal‑Driven and Asynchronous IO

This article explains the fundamentals of input/output (IO) in operating systems, covering the basic IO concept, the role of the OS, the two‑phase IO call process, and detailed descriptions of blocking, non‑blocking, multiplexed (select, poll, epoll), signal‑driven and asynchronous IO models with example code.

JD Tech Talk
JD Tech Talk
JD Tech Talk
Understanding IO Models: Blocking, Non‑Blocking, Multiplexing, Signal‑Driven and Asynchronous IO

What is IO?

IO stands for Input/Output. The IO model describes how data is read from or written to devices such as disks or networks.

What is Operating System IO?

Applications cannot directly perform IO operations; they must request the operating system (OS) via APIs. Each process has a user space and a protected kernel space.

Two‑stage IO operation from an application

IO call : The application process issues a system call to the OS kernel.

IO execution : The kernel carries out the actual IO.

OS handling of a single IO operation

Data preparation: the kernel waits for the device to place data into a kernel buffer.

Data copy: the kernel copies data from the kernel buffer to the user‑process buffer.

A complete IO flow includes:

Application requests IO.

OS loads data from the external device into the kernel buffer.

OS copies data from the kernel buffer to the user buffer.

while waiting + copying = the essence of an IO operation

IO Models

1. Blocking IO

Typical server code (pseudo‑code):

listenfd = socket(); // open a network socket
bind(listenfd);      // bind
listen(listenfd);    // listen
while (true) {
  buf = new buf[1024]; // buffer for reading
  connfd = accept(listenfd); // block until a connection is established
  int n = read(connfd, buf); // block until data is ready
  doSomeThing(buf);           // process data
  close(connfd);              // close connection
}

Blocking IO blocks at accept and read , waiting for the device to become ready and then copying data.

2. Non‑Blocking IO

Non‑blocking read returns –1 when data is not ready, requiring the program to poll repeatedly.

arr = new Arr[];
listenfd = socket(); // open socket
bind(listenfd);
listen(listenfd);
while (true) {
  connfd = accept(listenfd); // block only for connection
  arr.add(connfd);
}
// Asynchronous thread checks readability
new Thread(){
  for (connfd : arr){
    buf = new buf[1024];
    // non‑blocking read
    int n = read(connfd, buf);
    if (n != -1){
      newThreadDeal(buf);
      close(connfd);
      arr.remove(connfd);
    }
  }
}

newThreadDeal(buf){
  doSomeThing(buf);
}

Only the first stage (waiting for readiness) becomes non‑blocking; the actual data read remains blocking.

3. IO Multiplexing (select, poll, epoll)

Multiplexing lets the kernel monitor many file descriptors and notifies when any become ready, avoiding a thread per descriptor.

select

select traverses a fixed‑size bitmap (max 1024 fds on Linux) to test readability.

arr = new Arr[];
listenfd = socket();
bind(listenfd);
listen(listenfd);
while (true) {
  connfd = accept(listenfd);
  arr.add(connfd);
}
// Asynchronous thread uses select
new Thread(){
  while (select(arr) > 0){
    for (connfd : arr){
      if (connfd can read){
        newThreadDeal(connfd);
        arr.remove(connfd);
      }
    }
  }
}

newThreadDeal(connfd){
  buf = new buf[1024];
  int n = read(connfd, buf);
  doSomeThing(buf);
  close(connfd);
}

Advantages: fewer system calls and kernel‑side traversal.

poll

poll removes the 1024‑fd limit by using a dynamic array instead of a bitmap.

epoll

epoll solves three major inefficiencies of select/poll:

Eliminates copying the fd set between user and kernel by keeping the set inside the kernel.

Uses an event‑wake‑up mechanism instead of linear traversal.

Returns only the ready fds to user space, avoiding a second traversal.

Core epoll operations: epoll_create , epoll_ctl , epoll_wait .

// epoll pseudo‑code
listenfd = socket();
bind(listenfd);
listen(listenfd);
int epfd = epoll_create(...); // create epoll instance
while (1) {
  connfd = accept(listenfd);
  epoll_ctl(connfd, ...); // add new connection to epoll
}
// Worker thread waits for events
new Thread(){
  while (arr = epoll_wait()){
    for (connfd : arr){
      newThreadDeal(connfd);
    }
  }
}

newThreadDeal(connfd){
  buf = new buf[1024];
  int n = read(connfd, buf);
  doSomeThing(buf);
  close(connfd);
}

LT (Level‑Triggered) vs ET (Edge‑Triggered)

LT continuously notifies while data remains readable; ET notifies only once when the state changes, requiring the application to drain the kernel buffer in one go.

Signal‑Driven IO

When data becomes ready, the kernel sends a SIGIO signal to the process, allowing the thread to continue without blocking.

Asynchronous IO

The application issues a single read request and returns; the kernel notifies the process when the data is ready and copies it to user space.

Synchronous vs Asynchronous

Synchronous IO requires the caller to wait for the operation to complete; asynchronous IO lets the caller proceed immediately after issuing the request.

Scan the QR code to join the technical discussion group.

Operating SystemsepollIOMultiplexingNon‑Blocking IOblocking-io
JD Tech Talk
Written by

JD Tech Talk

Official JD Tech public account delivering best practices and technology innovation.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.