In Linux-based network programming, I/O operations involve two distinct phases: waiting for data to become available in the kernel, and copying that data from kernel space to user space. Different I/O models vary in how these phases are handled.
Blocking I/O is the default behavior for sockets. When a process calls recvfrom(), it blocks until both phases complete—data arrives and is copied to user memory. While simple, this model doesn't scale well under high concurrency. Threading or multiprocessing can mitigate this by isolating blocking per connection, but resource consumption becomes prohibitive with thousands of concurrent clients.
Non-blocking I/O changes socket behavior so that system calls like recvfrom() return immediately if data isn’t ready—typically with an error like EWOULDBLOCK. The application must then poll repeatedly until data is available. Although the process isn’t blocked during the wait phase, the copy phase remains synchronous and blocking. This approach suffers from high CPU usage due to busy-waiting and increased latency from polling intervals.
# Non-blocking server example
from socket import *
s = socket(AF_INET, SOCK_STREAM)
s.bind(('127.0.0.1', 8080))
s.listen(5)
s.setblocking(False)
connections = []
to_remove = []
while True:
try:
conn, addr = s.accept()
connections.append(conn)
except BlockingIOError:
for conn in connections[:]:
try:
data = conn.recv(1024)
if not data:
to_remove.append(conn)
continue
conn.send(data.upper())
except (BlockingIOError, ConnectionResetError):
to_remove.append(conn)
for conn in to_remove:
connections.remove(conn)
conn.close()
to_remove.clear()
I/O Multiplexing (e.g., select, poll, epoll) allows a single thread to monitor multiple file descriptors. The process blocks on a system call like select() untill any monitored descriptor is ready. Once notified, it performs the actual read. Though still synchronous (the copy phase blocks), multiplexing enables handling many connections efficiently in one thread.
# select-based server
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8081))
server.listen(5)
server.setblocking(False)
read_list = [server]
while True:
ready, _, _ = select.select(read_list, [], [])
for obj in ready:
if obj is server:
conn, _ = obj.accept()
conn.setblocking(False)
read_list.append(conn)
else:
try:
data = obj.recv(1024)
if not data:
read_list.remove(obj)
obj.close()
else:
obj.send(data.upper())
except ConnectionResetError:
read_list.remove(obj)
obj.close()
select has limitations: it copies descriptor sets on every call, scans all descriptors linearly, and is capped at ~1024 FDs. poll removes the FD limit but retains O(n) scanning. epoll (Linux-only) uses event-driven callbacks and memory mapping to avoid repeated copies and full scans, making it scalable to tens of thousands of connections.
Asynchronous I/O (AIO) fully decouples the application from both I/O phases. After initiating a read, the process continues immediately. The kernel handles data arrival and copying autonomously, then notifies the process via a signal or callback upon completion. Unlike non-blocking I/O, AIO requires no polling and involves no blocking at any stage. However, Linux AIO support is limited and rarely used in practice for sockets.
Per POSIX definitions:
- Synchronous I/O: The initiating process blocks until the operation completes (includes blocking, non-blocking, and multiplexed I/O).
- Asynchronous I/O: The proces never blocks and is notified only upon full completion.
Python’s selectors module abstracts platform-specific multiplexing APIs (select, epoll, kqueue) and provides a uniform interface. It automatically selects the most efficient mechanism available.
# selectors-based server
from socket import *
import selectors
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn, _ = sock.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
try:
data = conn.recv(1024)
if not data:
sel.unregister(conn)
conn.close()
else:
conn.send(data.upper() + b'_SB')
except Exception:
sel.unregister(conn)
conn.close()
sock = socket(AF_INET, SOCK_STREAM)
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8088))
sock.listen(5)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)