Fundamentals 17 min read

Understanding Low‑Level I/O: From BIO to NIO and Multiplexing (select, poll, epoll)

The article explains low‑level I/O fundamentals—kernel versus user space, blocking (BIO) versus non‑blocking (NIO) models, and the evolution to multiplexing with select, poll, and epoll—showing how these mechanisms address the C10K problem and are exposed in Java through NIO and Netty APIs.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Understanding Low‑Level I/O: From BIO to NIO and Multiplexing (select, poll, epoll)

In this article the author draws an analogy between the "martial arts" concepts of "mind‑methods" (心法) and computer technology, calling the fundamental theories "Dao" and the concrete techniques "Shu". The focus is on the low‑level I/O subsystem, which belongs to the "Dao" category, and on how higher‑level languages such as Java expose this functionality through APIs like NIO or frameworks such as Netty.

1. What is I/O? I/O stands for Input and Output. The discussion is limited to I/O operations that act on hardware resources such as memory, network cards and disks, not peripheral devices like mouse or keyboard.

The author points out that many readers get confused by terms such as blocking, non‑blocking, synchronous and asynchronous because the literature often explains them from different perspectives – kernel level, Java API level, or Netty level. This article adopts the kernel‑level viewpoint.

2. User space vs. Kernel space A brief review of operating‑system concepts explains that the kernel runs in a privileged space (ring0) and can access all hardware, while user processes run in a restricted space and must use system calls (e.g., open() , read() , write() , mmap() ) to interact with the kernel.

3. I/O models

• BIO (Blocking I/O) – The article shows a Java pseudo‑code example of a blocking server socket. The code is reproduced below:

ServerSocket serverSocket = new ServerSocket(8080); // step1: create ServerSocket and listen on port 8080
while (true) { // step2: main thread loops forever
    Socket socket = serverSocket.accept(); // step3: thread blocks, waiting for a connection
    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    System.out.println("read data: " + reader.readLine()); // step4: read data
    PrintWriter print = new PrintWriter(socket.getOutputStream(), true);
    print.println("write data"); // step5: write data
}

The three operations accept() , read() and write() all block the thread, which makes a single‑threaded server inefficient under heavy load.

• "C10K" problem – Handling ten thousand concurrent clients with a thread‑per‑connection model leads to excessive memory consumption, thread‑creation overhead, and costly context switches.

• NIO (Non‑blocking I/O) – By setting the SOCK_NONBLOCK flag on a socket, the accept() system call becomes non‑blocking; the kernel returns EAGAIN or EWOULDBLOCK when no connection is ready. The article provides a simplified non‑blocking loop:

// loop forever
while (1) {
    // iterate over file‑descriptor set
    for (fdx in range(fd1, fdn)) {
        if (fdx.data != null) {
            // read and handle
            read(fdx) & handle(fdx);
        }
    }
}

Non‑blocking I/O, however, forces the user process to poll the kernel continuously, which motivates the need for I/O multiplexing.

4. I/O multiplexing models

The article introduces three classic system calls used for multiplexing: select() , poll() and epoll() .

4.1 select() – Prototype:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

The call blocks until at least one file descriptor becomes ready. Internally it uses a fixed‑size bitmap (FD_SETSIZE = 1024), which limits the number of descriptors and incurs O(n) scanning cost.

4.2 poll() – Prototype:

int poll(struct pollfd *fds, nfds_t nfds, int *timeout);

It replaces the bitmap with an array of pollfd structures, removing the 1024‑descriptor limit but still suffering from O(n) complexity and frequent user‑kernel transitions.

4.3 epoll() – The most widely used multiplexing API in Linux. It follows a three‑step workflow:

epoll_create(int size) – creates an epoll instance and returns a file descriptor.

epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) – registers, modifies or deletes interest events (ADD, MOD, DEL).

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) – blocks until at least one registered descriptor is ready, then returns a list of ready events. It eliminates the per‑call O(n) scan, achieving O(1) performance and avoiding repeated user‑kernel switches.

These calls are the foundation of the Reactor pattern (active polling) and the Proactor pattern (kernel‑driven completion), which the article briefly describes.

5. Synchronous vs. Asynchronous – The author notes that in practice "blocking" ↔ "non‑blocking" maps to "synchronous" ↔ "asynchronous" when viewed from the application layer.

Conclusion – The article summarizes that it has covered the evolution from BIO to NIO, compared the three multiplexing system calls, and hinted at future topics such as zero‑copy techniques and deeper Java NIO/Netty details.

I/OOperating SystemselectMultiplexingJava NIOnon-blocking
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

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.