What You Will Learn
When Linux gives you a notification that a file descriptor is “ready”, there are two completely different ways it can behave. Understanding this difference is the foundation of all advanced I/O programming — epoll, select, poll, and signal-driven I/O all depend on it. This tutorial explains both models clearly with diagrams and code.
Key Terms
Imagine you are waiting for a parcel to arrive. There are two ways a delivery notification system can work:
- Level-triggered: The system keeps ringing your doorbell as long as the parcel is outside. You can answer it now or later — the bell will keep ringing until you collect the parcel.
- Edge-triggered: The system rings your doorbell only once when the parcel arrives. If you miss it, there is no second ring until another parcel shows up.
This is exactly how Linux I/O readiness notifications work for file descriptors.
| Data arrives in buffer |
No data
↑
Data in buffer (unread)
|
| Level-Triggered notification |
Keeps notifying as long as data is present
|
| Edge-Triggered notification |
Only notifies once when data first arrives
|
Definition: A file descriptor is considered ready if it is possible to perform an I/O system call on it without blocking. This means every time you check, the kernel looks at the current state of the fd and tells you if there is data or not.
Which APIs use it: select(), poll(), and epoll (when not using EPOLLET flag)
Typical usage pattern:
// Level-triggered pattern (select/poll/default epoll)
while (1) {
// 1. Check which fds are ready
select(nfds, &readfds, NULL, NULL, NULL);
// 2. Handle ready fds — no need to read ALL data at once
if (FD_ISSET(fd, &readfds)) {
read(fd, buf, sizeof(buf)); // read some data
// It's OK — next select() will report fd ready again
// if more data is still in the buffer
}
// 3. Reinitialize fd sets (select modifies them!)
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
}
Definition: Notification fires only when there is new I/O activity on the fd since it was last monitored. If data was already there before monitoring started, you may never get notified.
Which APIs use it: Signal-driven I/O, epoll with EPOLLET flag
EAGAIN or EWOULDBLOCK. Otherwise you will never know there is leftover data until new data arrives.Two mandatory rules for edge-triggered programs:
- After notification, read/write as much as possible — loop until EAGAIN
- Always set the file descriptor to nonblocking mode (
O_NONBLOCK)
Why nonblocking is mandatory for edge-triggered:
If you loop reading until the buffer is empty, and the fd is in blocking mode, the last read() call will block forever because there is nothing left to read. By using nonblocking mode, the read() returns immediately with EAGAIN, telling you to stop.
Edge-triggered usage pattern (with nonblocking):
// Edge-triggered pattern — MUST drain all data
void handle_edge_triggered_read(int fd) {
char buf[4096];
ssize_t n;
// Keep reading until EAGAIN or EWOULDBLOCK
while (1) {
n = read(fd, buf, sizeof(buf));
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// No more data — stop looping
break;
} else {
perror("read error");
break;
}
}
if (n == 0) {
// EOF — connection closed
break;
}
// Process n bytes of data
process_data(buf, n);
}
}
| Feature | Level-Triggered | Edge-Triggered |
|---|---|---|
| When notified? | Whenever fd is ready (state) | Only on new I/O activity (event) |
| APIs | select(), poll(), epoll (default) | Signal-driven I/O, epoll+EPOLLET |
| Drain required? | No — partial reads are fine | Yes — must drain until EAGAIN |
| O_NONBLOCK needed? | Often recommended | Mandatory |
| Risk of missing events? | No — fd keeps reporting ready | Yes — if you don’t drain fully |
| Complexity | Simpler | More complex — careful design needed |
| Best for | General use, simpler servers | High-performance servers (like nginx) |
The O_NONBLOCK flag makes a file descriptor nonblocking. When there is no data, instead of waiting (blocking), the system call returns immediately with errno = EAGAIN. This is heavily used with all alternative I/O models.
Four reasons to use nonblocking I/O:
How to set a file descriptor to nonblocking mode:
#include <fcntl.h>
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return -1;
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL");
return -1;
}
return 0;
}
Spurious readiness — real-world example:
A client connects to a server and immediately sends a TCP RST (reset). The server calls select() between these two events. select() says the listening socket is readable. But when the server calls accept(), the connection is gone — accept() blocks or returns an error. With nonblocking mode, accept() returns EAGAIN instead of blocking forever.
// Handling spurious readiness with nonblocking accept
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind, listen ...
set_nonblocking(listen_fd);
// In event loop:
int conn_fd = accept(listen_fd, NULL, NULL);
if (conn_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Spurious readiness — no real connection, just ignore
continue;
}
perror("accept");
}
This shows a proper nonblocking read loop that can be used in both level-triggered and edge-triggered contexts:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
/* Set fd to nonblocking mode */
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/* Read all available data from fd (for edge-triggered use)
Returns: total bytes read, 0 on EOF, -1 on error */
ssize_t drain_fd(int fd) {
char buf[4096];
ssize_t total = 0;
ssize_t n;
while (1) {
n = read(fd, buf, sizeof(buf));
if (n > 0) {
total += n;
/* process buf[0..n-1] here */
printf("Read %zd bytes: %.*s\n", n, (int)n, buf);
} else if (n == 0) {
/* EOF */
printf("Connection closed\n");
return total;
} else {
/* n == -1 */
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* No more data — done draining */
break;
}
perror("read");
return -1;
}
}
return total;
}
int main(void) {
/* Use stdin as example fd */
int fd = STDIN_FILENO;
if (set_nonblocking(fd) == -1) {
perror("set_nonblocking");
exit(EXIT_FAILURE);
}
printf("fd is now nonblocking. Reading...\n");
ssize_t bytes = drain_fd(fd);
printf("Total bytes read: %zd\n", bytes);
return 0;
}
Next: I/O Multiplexing with select()
Now that you understand notification models, see how select() implements level-triggered multiplexing
