阻塞 vs 非阻塞 I/O ------ 同一个 read 的两种世界
同样调
read(fd, buf, n),加不加O_NONBLOCK行为完全不一样:阻塞模式下没数据就睡到天荒地老;非阻塞模式下立即返回EAGAIN,剩下的得自己 poll。这篇把阻塞与非阻塞的行为差异、各自适用场景、EAGAIN处理、跟事件循环的搭配讲清楚。
0. 引言:read 没数据怎么办?
c
int fd = open("/dev/tty", O_RDONLY); /* 终端 */
char buf[100];
ssize_t n = read(fd, buf, 100);
/* 终端没人输入,read 一直不返回,进程卡死 */
c
int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
char buf[100];
ssize_t n = read(fd, buf, 100);
/* n = -1, errno = EAGAIN ------ 立即返回 */
默认是阻塞 。加 O_NONBLOCK 就是非阻塞。这是 Linux I/O 编程的根本分水岭。
1. 阻塞 I/O:让内核帮你等
1.1 阻塞 read 的行为
c
ssize_t n = read(fd, buf, 100);
| 情况 | 行为 | 返回值 |
|---|---|---|
| 有数据 | 立刻返回 | 实际字节数(≤ 100) |
| 无数据 + 有数据来 | 进程睡眠 → 醒来读到 | 实际字节数 |
| 无数据 + 对端关闭 | 进程睡眠 → 醒来 | 0(EOF) |
| 无数据 + 出错(信号、磁盘错) | 进程睡眠 → 醒来 | -1,errno 给原因 |
阻塞期间进程在内核的等待队列里睡,不消耗 CPU。
1.2 阻塞 write 的行为
c
ssize_t n = write(fd, buf, 100);
| 情况 | 行为 |
|---|---|
| 缓冲足够 | 写完返回(可能短写,要循环写) |
| 缓冲满(pipe / socket) | 进程睡眠,等到有空间再继续写 |
1.3 阻塞 accept、connect
c
int conn = accept(srv, NULL, NULL);
connect(s, &addr, sizeof addr);
| 调用 | 阻塞条件 | 醒来时机 |
|---|---|---|
accept |
没有新连接到达 | 三次握手完成,新 fd 出现在 backlog 队列 |
connect |
三次握手未完成 | 收到 SYN-ACK(成功)/ 超时 / RST(失败) |
1.4 阻塞模式的优点
c
/* 实现一个简单的服务器只需要 */
while (1) {
int conn = accept(srv, NULL, NULL);
handle_request(conn);
close(conn);
}
代码线性、直观------"做完这一步再做下一步"的思维。适合的场景:
| 场景 | 典型例子 |
|---|---|
| 单连接客户端 | curl 风格的 CLI 工具 |
| 每连接一进程/线程的服务 | pre-fork(Apache prefork)、per-thread |
| 简单脚本和工具 | 任何不追求并发的小程序 |
1.5 阻塞模式的局限
一个进程/线程只能等一个 fd。要同时处理 1000 个连接,要么 1000 个线程(开销大),要么改用非阻塞 + 多路复用。
2. 非阻塞 I/O:来不及就立即返回
2.1 怎么开启
两种方式:
c
/* 方式 1:open 时设 */
int fd = open(path, O_RDONLY | O_NONBLOCK);
/* 方式 2:fcntl 改 */
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
socket 也一样:
c
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); /* 一步到位 */
2.2 非阻塞 read 的行为
c
ssize_t n = read(fd, buf, 100);
- 有数据:立刻返回(≤ 100 字节)
- 无数据:立刻返回 -1 ,errno =
EAGAIN(==EWOULDBLOCK) - 对端关闭:返回 0
- 出错:返回 -1,errno 不是 EAGAIN
c
ssize_t n = read(fd, buf, 100);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* 没数据,不是错误,稍后再试 */
} else if (errno == EINTR) {
/* 信号打断,重试 */
} else {
/* 真的出错了 */
perror("read");
}
}
2.3 非阻塞 write 的行为
c
ssize_t n = write(fd, buf, 1000);
- 缓冲有空间:写一部分(可能短于 1000),返回写入字节数
- 缓冲完全满:返回 -1,errno = EAGAIN
- 不会阻塞等空间
工程上 write 经常短写,非阻塞下要循环 + EAGAIN 检测:
c
ssize_t total = 0;
while (total < n) {
ssize_t w = write(fd, buf + total, n - total);
if (w < 0) {
if (errno == EINTR) continue;
if (errno == EAGAIN) {
/* 缓冲满了,先停下,等可写事件再继续 */
break;
}
return -1;
}
total += w;
}
return total;
2.4 非阻塞 accept、connect
c
/* 非阻塞 accept */
int conn = accept(srv, NULL, NULL);
if (conn < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* 暂时没新连接 */
}
/* 非阻塞 connect 比较特殊 */
int rc = connect(s, &addr, sizeof addr);
if (rc < 0 && errno == EINPROGRESS) {
/* 三次握手还在进行,要 select/epoll 等可写事件 */
}
3. 阻塞 vs 非阻塞 行为对比

| 行为 | 阻塞 | 非阻塞 |
|---|---|---|
| read 没数据 | 进程睡眠等 | 返回 -1, errno=EAGAIN |
| write 缓冲满 | 进程睡眠等 | 返回 -1 或部分写入 |
| accept 没连接 | 睡眠等 | 返回 -1, errno=EAGAIN |
| connect 三次握手 | 等到完成 | 立刻返回,errno=EINPROGRESS |
| CPU 占用 | 等的时候不占 | 一样不占(前提是配合 poll/epoll) |
| 代码风格 | 线性、易读 | 事件驱动、状态机 |
| 一线程能服务的 fd 数 | 1(每个 fd 一个线程) | 海量(配合多路复用) |
4. EAGAIN:不是错误,是"稍后再试"
EAGAIN 跟 EWOULDBLOCK 在 Linux 上是同一个值(11),有些 Unix 上不同。代码里两个都判断更稳:
c
if (errno == EAGAIN || errno == EWOULDBLOCK) { ... }
或者直接用宏:
c
#ifndef EWOULDBLOCK
#define EWOULDBLOCK EAGAIN
#endif
4.1 EAGAIN 的处理选项
-
忙轮询(busy poll) :立即重试。几乎从不该这么写,浪费 100% CPU。
cwhile (1) { ssize_t n = read(fd, buf, 100); if (n >= 0) break; if (errno != EAGAIN) { error; break; } /* spin again */ } -
加 sleep 退避:每次失败 sleep 几 ms。简单粗暴,延迟大。
-
用多路复用 :
select/poll/epoll等"可读"事件再读。唯一推荐的方式。
5. 非阻塞 + 多路复用:现代服务器的标配
非阻塞本身没有意义,必须配合多路复用才发挥价值:

伪代码:
c
int epfd = epoll_create1(EPOLL_CLOEXEC);
/* 注册若干非阻塞 fd 到 epfd */
while (1) {
struct epoll_event evs[64];
int n = epoll_wait(epfd, evs, 64, -1);
for (int i = 0; i < n; i++) {
int fd = evs[i].data.fd;
if (evs[i].events & EPOLLIN) {
/* 可读:循环 read 直到 EAGAIN */
while (1) {
ssize_t r = read(fd, buf, sizeof buf);
if (r > 0) handle(fd, buf, r);
else if (r == 0) { close(fd); break; }
else if (errno == EAGAIN) break;
else { close(fd); break; }
}
}
}
}
关键:每个 fd 都要"读到 EAGAIN 才停",否则下一次 epoll_wait 不一定再触发(取决于 LT/ET 模式,下篇细讲)。
6. 阻塞 vs 非阻塞 选型指南

7. 几个常被搞错的点
7.1 fcntl 改 flags 时要先读再改
c
/* ⛔ 错:直接覆盖了所有 flags */
fcntl(fd, F_SETFL, O_NONBLOCK);
/* ✅ 对:保留其他 flags,只加 O_NONBLOCK */
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
7.2 dup 出来的 fd 共享 flags
O_NONBLOCK 是 file 级的(不是 fd 级),所以同一个 file 的所有 fd 行为一致:
c
int fd1 = open(...);
int fd2 = dup(fd1);
fcntl(fd1, F_SETFL, O_NONBLOCK);
/* fd2 也变非阻塞了 */
7.3 stdin 别随便设非阻塞
stdin 跟 stdout、stderr 经常是同一个 file(终端)。给 stdin 设 O_NONBLOCK 会让 stdout 也变非阻塞,printf 行为可能错乱。
要么 dup 一份再改,要么用 unlocked stdio。
7.4 阻塞模式下也可能短读
c
ssize_t n = read(fd, buf, 100); /* n 可能是 50,不是 100 */
阻塞 read 等到"有任何数据"就返回,不一定填满 buf。循环 read 是必须的(这一点跟非阻塞一样)。
7.5 connect 非阻塞返回 EINPROGRESS 不是错误
c
int rc = connect(s, &addr, sizeof addr);
if (rc == 0) {
/* 立即成功(罕见,本机 unix socket 可能)*/
} else if (errno == EINPROGRESS) {
/* 三次握手在进行,select/epoll 等可写事件,再用 SO_ERROR 取结果 */
int err;
socklen_t len = sizeof err;
getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len);
if (err == 0) { /* 连接成功 */ }
else { /* 连接失败,err 是真正的错误码 */ }
}
7.6 SIGPIPE 在两种模式下都会发
写关闭的 socket / pipe 触发 SIGPIPE,跟阻塞/非阻塞无关。忽略 SIGPIPE 永远是对的:
c
signal(SIGPIPE, SIG_IGN);
8. 异步 I/O:跟非阻塞的区别
很多人混淆"非阻塞"和"异步"。两者完全不同:
| 维度 | 非阻塞 | 异步 (POSIX AIO / io_uring) |
|---|---|---|
| 调用 | 立即返回(成功或 EAGAIN) | 立即返回(请求已提交) |
| 数据搬运 | 你自己 read/write | 内核帮你做 |
| 完成通知 | 配合 epoll 你自己 read | 完成事件通知(信号/cqe) |
| 模型 | "Reactor" | "Proactor" |
| 适用 | 网络 I/O 主流 | 磁盘 I/O,io_uring 也用于网络 |
经典 epoll 风格是 non-blocking I/O + readiness notification ,不是真正的异步。Linux 的 io_uring(5.1+)才是真异步。
9. 一个完整对比例子:echo server
9.1 阻塞版本(简单但每连接一线程)
c
void *handle(void *arg) {
int conn = (intptr_t)arg;
char buf[1024];
ssize_t n;
while ((n = read(conn, buf, sizeof buf)) > 0) {
write(conn, buf, n);
}
close(conn);
return NULL;
}
int main(void) {
int srv = socket(AF_INET, SOCK_STREAM, 0);
/* bind + listen ... */
while (1) {
int conn = accept(srv, NULL, NULL); /* 阻塞 */
pthread_t tid;
pthread_create(&tid, NULL, handle, (void*)(intptr_t)conn);
pthread_detach(tid);
}
}
简单直观。但每连接一个线程,10K 连接 = 10K 线程 = 80MB 栈(默认 8MB / 线程,一些系统起码 8KB),调度开销也大。
9.2 非阻塞 + epoll 版本(C10K)
c
int main(void) {
int srv = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
/* bind + listen ... */
int epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = srv };
epoll_ctl(epfd, EPOLL_CTL_ADD, srv, &ev);
while (1) {
struct epoll_event evs[64];
int n = epoll_wait(epfd, evs, 64, -1);
for (int i = 0; i < n; i++) {
int fd = evs[i].data.fd;
if (fd == srv) {
int conn;
while ((conn = accept4(srv, NULL, NULL, SOCK_NONBLOCK)) >= 0) {
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev);
}
} else {
char buf[1024];
ssize_t r;
while ((r = read(fd, buf, sizeof buf)) > 0) {
/* 简单回写(生产代码要处理写不完的情况)*/
write(fd, buf, r);
}
if (r == 0 || (r < 0 && errno != EAGAIN)) {
close(fd);
}
}
}
}
}
单线程能扛 10K~100K 连接。代码复杂得多,要管 fd 状态、缓冲、超时等等。
10. 总结
| 选项 | 阻塞 I/O | 非阻塞 I/O |
|---|---|---|
| 默认 | ✅ 不设 O_NONBLOCK | 显式 O_NONBLOCK |
| 编程模型 | 同步线性 | 事件驱动 / 状态机 |
| 等待方式 | 内核让进程睡 | 用户层用 epoll |
| 适合连接数 | 小到中 | 大到极大 |
| 代码复杂度 | 低 | 高 |
| 学习曲线 | 平 | 陡 |
经验法则:
- 简单工具 / 客户端 / 小服务:阻塞 + 多线程,写得快、跑得稳
- 大并发服务:非阻塞 + epoll,C10K+ 必备
- 磁盘 IO 性能敏感:考虑 io_uring 真正异步