Blocking and Non-Blocking I/O
Non-Blocking I/O
Read aloud is unavailable in this browser.
The Immediate Return Model
What if your program could ask "is there data available?" without getting stuck waiting? What if every I/O call returned instantly, telling you either "here's your data" or "nothing available right now, try again later"? This is the essence of non-blocking I/O.
Non-blocking I/O inverts the blocking model's fundamental assumption. Instead of suspending your process until data is ready, non-blocking calls return immediately with whatever status is currently available. This shifts responsibility: the kernel no longer manages your waiting; you must explicitly decide when and how to retry.
By the end of this page, you will understand the precise semantics of non-blocking I/O, how to enable it on file descriptors, the critical role of EAGAIN/EWOULDBLOCK, and the polling patterns required to use non-blocking I/O effectively. You'll see why non-blocking I/O alone is rarely sufficient and how it sets the stage for I/O multiplexing.
Defining Non-Blocking I/O
Non-blocking I/O is an I/O model where system calls return immediately, regardless of whether the requested operation can complete. If the operation would block (e.g., no data available for read), the call returns an error indication rather than waiting.
The formal definition:
A non-blocking I/O operation behaves as follows:
- If the operation can complete immediately, it does so and returns success
- If the operation would need to wait, it returns immediately with errno set to EAGAIN or EWOULDBLOCK
- The caller must interpret this as "resource temporarily unavailable" and decide how to proceed
This is in contrast to blocking I/O, where the call would suspend the process until the operation could complete.
Historically, EAGAIN and EWOULDBLOCK were different error codes on some Unix systems. In modern Linux, they're the same value (11). POSIX allows them to be either the same or different. Robust code checks for both: if (errno == EAGAIN || errno == EWOULDBLOCK)
The mental model:
Think of non-blocking I/O like a fast-food counter with multiple pickup windows. You place your order and immediately check window #1: "Is my order ready?" "Not yet." You check window #2: "Is my order ready?" "Yes, here it is!" You're never stuck waiting at one window—you can always move on and check again later.
This model is sometimes called synchronous non-blocking. It's synchronous because each call gives you an immediate answer about its status; it's non-blocking because you're never suspended waiting.
| Operation | Returns Immediately When | Returns EAGAIN When | Error Behavior |
|---|---|---|---|
| read() | Data available in buffer | Buffer empty, no data yet | Returns -1, errno set |
| write() | Buffer has space | Buffer full, backpressure | Returns -1, errno set |
| accept() | Connection pending | No pending connections | Returns -1, errno set |
| connect() | Connection initiated (returns EINPROGRESS) | N/A - EINPROGRESS indicates 'in progress' | Returns -1, errno set |
| recv() | Data in socket buffer | Socket buffer empty | Returns -1, errno set |
| send() | Send buffer has space | Send buffer full | Returns -1, errno set |
Enabling Non-Blocking Mode
By default, file descriptors operate in blocking mode. Non-blocking behavior must be explicitly enabled. There are several ways to do this:
Method 1: O_NONBLOCK with open()
When opening a file or device, include the O_NONBLOCK flag:
1234567891011121314151617181920212223242526272829303132333435
#include <fcntl.h>#include <stdio.h>#include <errno.h> int main() { // Open a named pipe (FIFO) in non-blocking mode // Without O_NONBLOCK, this would block until a writer opens the FIFO int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open"); return 1; } // All reads on this fd will be non-blocking char buffer[1024]; ssize_t n = read(fd, buffer, sizeof(buffer)); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("No data available yet (not an error)"); } else { perror("read error"); } } else if (n == 0) { printf("EOF reached"); } else { printf("Read %zd bytes", n); } return 0;}Method 2: fcntl() to Modify Existing Descriptor
For already-open file descriptors (including those from socket() or inherited from parent processes), use fcntl() to add or remove the O_NONBLOCK flag:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
#include <fcntl.h>#include <unistd.h>#include <stdio.h> /** * Set a file descriptor to non-blocking mode. * Returns 0 on success, -1 on error. */int set_nonblocking(int fd) { // Get current flags int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { perror("fcntl F_GETFL"); return -1; } // Add O_NONBLOCK flag if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { perror("fcntl F_SETFL"); return -1; } return 0;} /** * Set a file descriptor back to blocking mode. */int set_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) return -1; // Remove O_NONBLOCK flag return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);} int main() { // Create a socket (blocking by default) int sockfd = socket(AF_INET, SOCK_STREAM, 0); // Make it non-blocking if (set_nonblocking(sockfd) < 0) { return 1; } // Now all I/O on sockfd will be non-blocking // connect(), read(), write() all return immediately return 0;}Method 3: SOCK_NONBLOCK with socket()
Linux provides a convenience flag when creating sockets:
12345678910
#include <sys/socket.h>#include <netinet/in.h> // Create a non-blocking TCP socket in one call// Avoids race condition between socket() and fcntl()int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); // Similarly for accept4():int client_fd = accept4(server_fd, NULL, NULL, SOCK_NONBLOCK);// The new client socket is created in non-blocking modeUsing SOCK_NONBLOCK in socket() or accept4() is preferred over calling fcntl() afterward. The atomic approach avoids a race condition where another thread could perform I/O on the socket between creation and the fcntl() call. It also requires one fewer system call.
Non-Blocking Read Semantics
Non-blocking read() behaves differently from blocking read() in crucial ways. Understanding these semantics is essential for correct programming.
Return values for non-blocking read():
| Return Value | Meaning |
|---|---|
0 | Success: returned number of bytes read |
| 0 | End-of-file (EOF) on file, or connection closed for sockets |
| -1, errno=EAGAIN | No data available; would have blocked |
| -1, errno=EINTR | Interrupted by signal; retry |
| -1, other errno | Actual error (EBADF, EIO, etc.) |
The critical difference from blocking read:
With blocking read, a return of -1 always indicates an error (except for EINTR). With non-blocking read, EAGAIN is not an error—it's the normal indication that no data is currently available.
This fundamentally changes how you write I/O code:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
#include <unistd.h>#include <errno.h>#include <stdio.h> /** * Attempt to read available data from a non-blocking fd. * Returns: * > 0: bytes read * 0: EOF * -1: would block (not an error!) * -2: actual error */ssize_t try_read(int fd, void *buf, size_t count) { ssize_t n = read(fd, buf, count); if (n > 0) { // Success! Got some data return n; } if (n == 0) { // EOF / connection closed return 0; } // n == -1, check errno if (errno == EAGAIN || errno == EWOULDBLOCK) { // NOT an error: just no data available return -1; // Signal "would block" } if (errno == EINTR) { // Interrupted by signal - typically retry immediately return try_read(fd, buf, count); // Recursive retry } // Actual error perror("read"); return -2;} /** * Non-blocking read pattern: read all available data * Useful for draining a socket of all pending data */ssize_t read_all_available(int fd, void *buf, size_t bufsize) { size_t total = 0; char *ptr = (char *)buf; while (total < bufsize) { ssize_t n = read(fd, ptr + total, bufsize - total); if (n > 0) { total += n; // Continue reading - there might be more data continue; } if (n == 0) { // EOF reached break; } // n == -1 if (errno == EAGAIN || errno == EWOULDBLOCK) { // No more data available right now // This is NOT an error - we've read all available data break; } if (errno == EINTR) { continue; // Retry } // Actual error return -1; } return total;}When read() returns EAGAIN, it means 'no data now.' Immediately retrying in a tight loop wastes CPU (busy waiting). Instead, use I/O multiplexing (select/poll/epoll) to wait efficiently until data arrives, then retry.
Non-Blocking Write Semantics
Non-blocking writes present their own challenges, particularly around partial writes and buffer management.
Return values for non-blocking write():
| Return Value | Meaning |
|---|---|
0 | Success: returned number of bytes written (may be < count!) |
| 0 | No bytes written; rare but possible |
| -1, errno=EAGAIN | Buffer full; would have blocked |
| -1, errno=EINTR | Interrupted by signal; retry |
| -1, errno=EPIPE | Connection closed by peer |
| -1, other errno | Actual error |
The partial write challenge:
Non-blocking write() may write fewer bytes than requested. Unlike blocking write (which would wait until all bytes can be accepted), non-blocking write returns as soon as it's written whatever it can. You must track what's been written and retry the remainder later.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
#include <unistd.h>#include <errno.h>#include <string.h>#include <stdlib.h> /** * Write buffer for handling partial writes in non-blocking I/O. * Tracks pending data that couldn't be written immediately. */typedef struct { char *data; // Buffer of pending data size_t capacity; // Total buffer capacity size_t size; // Amount of pending data size_t offset; // Start of pending data (bytes already sent)} WriteBuffer; /** * Initialize a write buffer. */WriteBuffer *write_buffer_create(size_t capacity) { WriteBuffer *wb = malloc(sizeof(WriteBuffer)); wb->data = malloc(capacity); wb->capacity = capacity; wb->size = 0; wb->offset = 0; return wb;} /** * Add data to the write buffer. * Returns 0 on success, -1 if buffer is full. */int write_buffer_append(WriteBuffer *wb, const void *data, size_t len) { // Compact buffer if needed if (wb->offset > 0 && wb->offset + wb->size + len > wb->capacity) { memmove(wb->data, wb->data + wb->offset, wb->size); wb->offset = 0; } if (wb->size + len > wb->capacity) { return -1; // Buffer full } memcpy(wb->data + wb->offset + wb->size, data, len); wb->size += len; return 0;} /** * Attempt to flush write buffer to non-blocking fd. * Returns: * 1: all data flushed * 0: partial write, data remains * -1: error (connection closed or other) */int write_buffer_flush(WriteBuffer *wb, int fd) { while (wb->size > 0) { ssize_t n = write(fd, wb->data + wb->offset, wb->size); if (n > 0) { wb->offset += n; wb->size -= n; continue; // Try to write more } if (n == 0) { // Unusual but handle defensively return 0; // Partial progress } // n == -1 if (errno == EAGAIN || errno == EWOULDBLOCK) { // Kernel buffer full, can't write more now return 0; // Partial: data remains } if (errno == EINTR) { continue; // Retry immediately } // EPIPE, ECONNRESET, or other error return -1; } // All data flushed! wb->offset = 0; return 1;} /** * Check if buffer has pending data. */int write_buffer_has_data(WriteBuffer *wb) { return wb->size > 0;} // Usage examplevoid example_usage(int sockfd) { WriteBuffer *wb = write_buffer_create(65536); // Application wants to send a message const char *msg = "Hello, World!"; write_buffer_append(wb, msg, strlen(msg)); // Try to flush int result = write_buffer_flush(wb, sockfd); if (result == 1) { // All sent! } else if (result == 0) { // Partial write - need to monitor fd for writability // When epoll says fd is writable, call flush again } else { // Error - connection problems }}With non-blocking I/O, you almost always need application-level write buffering. When write() returns EAGAIN, you must save the unsent data and retry later when the socket becomes writable. This is why event-driven frameworks all have built-in write buffering.
Non-Blocking Connect
Establishing TCP connections with non-blocking sockets requires special handling because connect() cannot complete instantly—the TCP three-way handshake takes time.
How non-blocking connect() works:
connect()is called on a non-blocking socket- The kernel initiates the TCP handshake (sends SYN)
connect()returns immediately with -1, errno=EINPROGRESS- EINPROGRESS means "connection in progress, check back later"
- Use select/poll/epoll to wait for writability
- When socket becomes writable, check connection status with getsockopt()
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <fcntl.h>#include <errno.h>#include <poll.h>#include <stdio.h>#include <string.h>#include <unistd.h> /** * Initiate a non-blocking connect. * Returns: * 1: connected immediately (local connection, rare) * 0: connection in progress * -1: error */int start_connect(int sockfd, const char *host, int port) { struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET, host, &addr.sin_addr); int result = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)); if (result == 0) { // Connected immediately (rare - usually loopback only) return 1; } if (errno == EINPROGRESS) { // Connection initiated, check back later return 0; } // Actual error (ECONNREFUSED, ENETUNREACH, etc.) return -1;} /** * Check if a non-blocking connect has completed. * Call this after poll/epoll indicates the socket is writable. * Returns: * 0: success, connected * -1: connection failed (errno set) */int check_connect_result(int sockfd) { int error = 0; socklen_t len = sizeof(error); // Get pending error on socket if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { return -1; // getsockopt failed } if (error != 0) { // Connection failed - error contains the reason errno = error; return -1; } // Success! return 0;} /** * Complete example: non-blocking connect with timeout */int connect_with_timeout(const char *host, int port, int timeout_ms) { // Create non-blocking socket int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (sockfd < 0) return -1; // Start connection int result = start_connect(sockfd, host, port); if (result == 1) { // Already connected return sockfd; } if (result < 0) { close(sockfd); return -1; } // Wait for socket to become writable (or timeout) struct pollfd pfd = { .fd = sockfd, .events = POLLOUT, // Wait for writability .revents = 0 }; result = poll(&pfd, 1, timeout_ms); if (result == 0) { // Timeout close(sockfd); errno = ETIMEDOUT; return -1; } if (result < 0) { close(sockfd); return -1; } // Socket is writable - check if connect succeeded if (check_connect_result(sockfd) < 0) { close(sockfd); return -1; } // Success! Optionally switch back to blocking mode // (or keep non-blocking for the data phase) return sockfd;} int main() { int sock = connect_with_timeout("192.168.1.1", 80, 5000); if (sock < 0) { perror("connect failed"); return 1; } printf("Connected!"); close(sock); return 0;}When poll/epoll signals that a connecting socket is writable, it does NOT mean the connection succeeded. A failed connection also makes the socket 'writable' (you can try to write, it will just fail). Always use getsockopt(SO_ERROR) to get the actual connection result.
The Polling Problem
Non-blocking I/O solves the "thread gets stuck" problem but introduces a new challenge: how do you know when to retry?
The naive approach: Busy polling
123456789101112131415161718
// BAD: Busy polling wastes CPU!while (1) { ssize_t n = read(fd, buffer, sizeof(buffer)); if (n > 0) { // Got data, process it process(buffer, n); } else if (n == 0) { break; // EOF } else if (errno == EAGAIN) { // No data yet - but we immediately retry! // This loop runs millions of times per second // consuming 100% CPU doing nothing useful continue; // BAD! } else { break; // Error }}This approach is terrible for performance. The CPU spins in a tight loop, continuously making system calls that return EAGAIN. This wastes CPU, generates heat, increases power consumption, and steals cycles from other processes.
The naive fix: Sleep between polls
12345678910111213141516171819
// MEDIOCRE: Sleep reduces CPU but adds latencywhile (1) { ssize_t n = read(fd, buffer, sizeof(buffer)); if (n > 0) { process(buffer, n); } else if (n == 0) { break; } else if (errno == EAGAIN) { // Sleep before retrying usleep(10000); // 10 milliseconds // Problem: We ALWAYS wait 10ms, even if data arrives in 1ms // Average added latency: 5ms // This is too slow for many applications continue; } else { break; }}Sleep-based polling is a band-aid that trades CPU usage for latency. If you sleep 10ms, you might miss data that arrives 1ms after you started sleeping, adding up to 10ms of unnecessary delay.
The real solution: I/O multiplexing
What we really want is to tell the kernel: "Wake me up when any of these file descriptors has data." This is exactly what I/O multiplexing (select/poll/epoll) provides, which we'll cover in detail.
Non-blocking I/O is rarely used in isolation. It's almost always paired with an I/O multiplexing mechanism that efficiently waits for I/O readiness.
| Approach | CPU Usage | Latency | Practicality |
|---|---|---|---|
| Busy polling | 100% (terrible) | Minimal | Never acceptable in production |
| Sleep polling (1ms) | Low | +0.5ms average | Okay for low-volume, latency-tolerant |
| Sleep polling (10ms) | Very low | +5ms average | Only for background tasks |
| I/O multiplexing | Minimal | Near-zero | Standard approach for all serious applications |
Non-Blocking I/O Use Cases
Despite the polling complexity, non-blocking I/O enables important patterns that blocking I/O cannot achieve.
- •Event-driven servers — Handle thousands of connections in a single thread by using epoll with non-blocking sockets. Each socket is non-blocking; epoll tells us when each one is ready.
- •GUI applications — The main thread must never block (or the UI freezes). Non-blocking I/O allows I/O operations alongside event processing.
- •Game loops — Games need to run at 60+ FPS. A blocking network read would cause frame drops. Non-blocking reads fit naturally into the game loop.
- •Timeouts and cancellation — With blocking I/O, implementing timeouts requires threads or signals. With non-blocking I/O, you can check elapsed time and stop retrying.
- •Prioritized I/O — Service high-priority requests even when low-priority I/O is pending. Blocking I/O can't be preempted.
- •Progress monitoring — Update a progress bar while data transfers. With blocking I/O, you can't do anything until the transfer completes.
123456789101112131415161718192021222324252627282930313233343536373839404142
#include <time.h> /** * Example: Game loop with non-blocking network I/O * The game continues running smoothly regardless of network conditions */void game_loop(int network_socket) { // Socket is non-blocking set_nonblocking(network_socket); struct timespec last_frame, current; clock_gettime(CLOCK_MONOTONIC, &last_frame); while (game_running) { // 1. Handle network I/O (non-blocking, never stalls) char net_buffer[1024]; ssize_t n = read(network_socket, net_buffer, sizeof(net_buffer)); if (n > 0) { process_network_message(net_buffer, n); } // EAGAIN is fine - no network data this frame, carry on // 2. Process input (from queue, non-blocking) process_input_queue(); // 3. Update game state clock_gettime(CLOCK_MONOTONIC, ¤t); double delta = (current.tv_sec - last_frame.tv_sec) + (current.tv_nsec - last_frame.tv_nsec) / 1e9; update_game_state(delta); last_frame = current; // 4. Render frame render_frame(); // 5. Send pending network messages (non-blocking) flush_network_queue(network_socket); // Target 60 FPS sleep_until_next_frame(); }}While servers are the classic use case for non-blocking I/O, it's equally important for clients—especially clients with real-time requirements or graphical interfaces. Any application that must remain responsive while doing I/O benefits from non-blocking techniques.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.