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.
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 operationIO 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.
JD Tech Talk
Official JD Tech public account delivering best practices and technology innovation.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.