11、非阻塞I/O与异步I/O:fcntl设置非阻塞、非阻塞connect、异步I/O(AIO)、I/O模型对比总结
这一章我们来聊聊网络编程里一个绕不开的话题——I/O模型。说实话,很多新手甚至工作两三年的朋友,对阻塞、非阻塞、同步、异步这些概念还是有点模糊。我记得刚入行那会儿,面试官问我「epoll是同步还是异步」,我愣是答错了。嗯,今天咱们就把这块彻底理清楚。
11.1 阻塞与非阻塞:最直观的区别
先说说最基础的。你写一个recv()调用,如果socket上没有数据,程序就停在那等——这就是阻塞。反过来,如果没数据直接返回一个错误码,程序继续往下跑——这就是非阻塞。
说白了,阻塞就是「死等」,非阻塞就是「不等」。我在项目中遇到过一个问题:一个网关程序同时处理几百个连接,如果每个连接都用阻塞模式,那一个连接卡住,整个进程就卡死了。所以高并发场景下,非阻塞是必须的。
11.2 fcntl设置非阻塞
把一个socket变成非阻塞,最常用的方法就是fcntl。我个人习惯在创建socket之后立刻设置,省得后面忘了。
int set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
return -1;
}
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
// 使用示例
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (set_nonblock(sock) == -1) {
perror("set nonblock failed");
exit(1);
}
小提示:设置非阻塞时,一定要先F_GETFL拿到原来的标志位,再F_SETFL加上O_NONBLOCK。直接覆盖可能会丢掉其他重要的标志位。我曾经见过有人直接写fcntl(fd, F_SETFL, O_NONBLOCK),结果把socket的某些属性搞丢了,排查了半天。
11.3 非阻塞connect
这个知识点挺有意思。你想想看,如果connect()是阻塞的,那连接一个远端服务器可能要等几十秒超时。非阻塞connect就能解决这个问题——它立即返回,然后你通过select/poll/epoll去检查连接是否完成。
int nonblock_connect(const char *ip, int port) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
set_nonblock(sock);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &addr.sin_addr);
int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
if (ret == 0) {
// 连接立即成功(比如本地回环)
return sock;
}
if (errno != EINPROGRESS) {
// 真正的错误
close(sock);
return -1;
}
// 连接正在进行中,用 poll 等待
struct pollfd pfd = {sock, POLLOUT, 0};
ret = poll(&pfd, 1, 5000); // 5秒超时
if (ret <= 0) {
close(sock);
return -1;
}
// 检查连接是否成功
int error = 0;
socklen_t len = sizeof(error);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
if (error != 0) {
close(sock);
return -1;
}
return sock;
}
注意:非阻塞connect返回EINPROGRESS是正常现象,不是错误。很多新手看到这个错误码就慌了,其实它只是告诉你「连接正在路上」。另外,检查连接是否成功要用getsockopt拿SO_ERROR,别直接用connect的返回值判断。
11.4 异步I/O(AIO)
说到异步I/O,很多人第一反应是Linux的AIO(aio_read/aio_write)。但说实话,我在实际项目中很少直接用POSIX AIO。为什么?因为它的实现方式比较重,而且对socket的支持并不完美。
Linux AIO更适合磁盘I/O,网络I/O还是用epoll更靠谱。不过了解一下也没坏处,万一哪天碰到特殊场景呢?
#include <aio.h>
// 异步读示例(仅供参考,实际网络编程不推荐)
struct aiocb cb;
memset(&cb, 0, sizeof(cb));
cb.aio_fildes = fd;
cb.aio_buf = buffer;
cb.aio_nbytes = BUFFER_SIZE;
cb.aio_offset = 0; // 对socket无效
if (aio_read(&cb) == -1) {
perror("aio_read failed");
return;
}
// 稍后检查完成状态
while (aio_error(&cb) == EINPROGRESS) {
// 等待或做其他事情
}
ssize_t ret = aio_return(&cb);
核心观点:在Linux网络编程中,真正的「异步I/O」其实是epoll配合非阻塞socket。POSIX AIO更多是理论上的异步,实际工程中用的不多。我个人建议把精力放在epoll上,这才是Linux下高性能网络编程的基石。
11.5 I/O模型对比总结
好了,咱们把常见的I/O模型拉出来遛遛。我画了一张图,帮你理清它们之间的关系。
这张图把四种模型放在一起对比。你可能会问:「非阻塞和异步到底啥区别?」嗯,关键区别在于「谁来做等待」。非阻塞是你自己轮询,异步是内核做完后通知你。
| 模型 | 是否阻塞 | 数据就绪通知 | 数据拷贝 | 典型API |
|---|---|---|---|---|
| 阻塞I/O | 是 | 无(阻塞等待) | 进程自己拷贝 | recv/send |
| 非阻塞I/O | 否 | 无(轮询) | 进程自己拷贝 | recv + O_NONBLOCK |
| I/O多路复用 | 否 | 内核通知就绪 | 进程自己拷贝 | epoll_wait + recv |
| 异步I/O | 否 | 内核通知完成 | 内核完成拷贝 | aio_read / io_uring |
我的建议:如果你刚开始学网络编程,先把阻塞I/O和非阻塞I/O搞明白。然后重点攻克epoll(I/O多路复用)。至于异步I/O,了解一下概念就够了,等遇到具体需求再深入。我在项目中用了这么多年,99%的场景都是「非阻塞socket + epoll」搞定。
11.6 避坑指南
最后分享几个我踩过的坑:
- 非阻塞connect的返回值判断:我曾经以为
connect返回-1就是失败,结果忽略了EINPROGRESS。后来排查了好久才发现,原来非阻塞模式下这个错误码是正常的。 - fcntl设置非阻塞后忘记检查:有一次我设置完非阻塞,后面直接调
recv,没处理EAGAIN。结果程序在高并发下疯狂报错。记住:非阻塞模式下,recv返回-1且errno为EAGAIN或EWOULDBLOCK是正常现象,不是错误。 - AIO的坑:我试过用POSIX AIO做网络编程,结果发现它对socket的支持很有限,而且性能还不如epoll。后来果断放弃了。如果你真的需要异步I/O,建议看看
io_uring,这是Linux 5.1引入的新特性,比传统AIO靠谱得多。
好了,这一章的内容就到这里。非阻塞和异步的概念确实容易混淆,但只要你动手写几个demo,跑一跑,很快就能理解。记住:理论是基础,实践才是检验真理的唯一标准。
小练习:试着写一个程序,用非阻塞connect同时连接多个服务器,然后用poll等待所有连接完成。这个练习能帮你巩固本章的知识点。