Fundamentals 13 min read

Understanding IO Models: From Blocking to Epoll and Asynchronous I/O

This article explains the fundamentals of input/output (IO) in operating systems, covering the basic IO concept, the role of the OS, the two-stage IO process, and various IO models such as blocking, non‑blocking, select/poll/epoll, signal‑driven and asynchronous approaches.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
Understanding IO Models: From Blocking to Epoll and Asynchronous I/O

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 OS through APIs. Each process has a user space and a protected kernel space. The OS mediates all read/write actions.

Two Stages of an IO Operation

IO Call: The application process invokes an OS kernel call.

IO Execution: The kernel completes the actual IO.

OS‑Managed IO Process

Data preparation: the kernel waits for the device (e.g., network card) to place data into a kernel buffer.

Data copy: the kernel copies data from its 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.

<code>// Essence of an IO operation: wait + copy</code>

IO Models

1. Blocking IO

Server code blocks on

accept()

and

read()

until data is ready.

<code>listenfd = socket(); // open socket
bind(listenfd);      // bind
listen(listenfd);    // listen
while (true) {
    buf = new buf[1024];
    connfd = accept(listenfd); // block until connection
    int n = read(connfd, buf); // block until data
    doSomeThing(buf);
    close(connfd);
}</code>

2. Non‑Blocking IO

Read returns immediately with

-1

if data is not ready; the program must poll the descriptor.

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

3. I/O Multiplexing (select, poll, epoll)

Multiplexing lets a single thread monitor many file descriptors, reducing the need for one thread per descriptor.

select

select() asks the kernel to check a set of descriptors for readiness.

<code>listenfd = socket();
bind(listenfd);
listen(listenfd);
while (true) {
    connfd = accept(listenfd);
    arr.add(connfd);
}
new Thread(){
    while (select(arr) > 0) {
        for (connfd : arr) {
            if (connfd can read) {
                newThreadDeal(connfd);
                arr.remove(connfd);
            }
        }
    }
};
void newThreadDeal(connfd){
    buf = new buf[1024];
    int n = read(connfd, buf);
    doSomeThing(buf);
    close(connfd);
}</code>

Advantages:

Reduces system calls.

Kernel performs readiness checks.

Drawbacks:

Copying descriptor arrays between user and kernel space can be costly under high concurrency.

Kernel must traverse the entire descriptor set, which is slow for large arrays.

After kernel detection, user space may need another traversal.

poll

Similar to select but removes the 1024‑descriptor limit by using a dynamic array.

epoll

epoll solves the above problems by keeping descriptors in the kernel, using an event‑driven wake‑up mechanism, and returning only ready descriptors.

<code>listenfd = socket();
bind(listenfd);
listen(listenfd);
int epfd = epoll_create(...);
while (1) {
    connfd = accept(listenfd);
    epoll_ctl(connfd, ...); // add to epoll set
}
new Thread(){
    while ((arr = epoll_wait()) != null) {
        for (connfd : arr) {
            newThreadDeal(connfd);
        }
    }
};
void newThreadDeal(connfd){
    buf = new buf[1024];
    int n = read(connfd, buf);
    doSomeThing(buf);
    close(connfd);
}</code>

LT vs. ET Modes

LT (Level‑Triggered): The kernel repeatedly notifies when a socket remains readable or writable until the application consumes the data.

ET (Edge‑Triggered): The kernel notifies only once when a state change occurs; the application must read all available data in that notification.

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 read request and returns immediately; 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, while asynchronous IO lets the caller proceed and receive a notification once the operation finishes.

Operating SystemsepollIONon-blocking I/OBlocking I/O
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

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.