42、非阻塞IO与异步IO:fcntl设置非阻塞,aio异步读写

IO这个话题,说简单也简单,说复杂能写一本书。

我做了十几年嵌入式,见过太多程序卡死在read()上。一个传感器没就绪,整个系统就挂了。说白了,这就是阻塞IO的锅。

今天咱们聊聊怎么让程序不卡住。两个方向:非阻塞IO和异步IO。前者是轮询,后者是通知。各有各的玩法。

阻塞IO:最朴素的模式

先看个最基础的例子:

int fd = open("/dev/ttyS0", O_RDWR);
char buf[64];
int n = read(fd, buf, sizeof(buf));  // 这里会卡住
printf("读到 %d 字节\n", n);

read()调用下去,如果设备没数据,线程就挂起了。这叫阻塞。

我早期做串口通信时,就吃过这个亏。主循环里调了read(),结果设备半天没发数据,整个界面假死。用户点啥都没反应。

⚠️ 注意:阻塞IO在多任务系统里是灾难。一个线程卡住,其他任务也别想好过。

非阻塞IO:用fcntl搞定

非阻塞IO的思路很简单:调用不等待,立即返回。有数据就处理,没数据就干别的。

设置方法就一行:

int fd = open("/dev/ttyS0", O_RDWR);
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);  // 设置非阻塞

char buf[64];
int n = read(fd, buf, sizeof(buf));
if (n == -1 && errno == EAGAIN) {
    // 没数据,不卡住
    printf("设备还没准备好,先干别的\n");
} else {
    printf("读到 %d 字节\n", n);
}

关键点:返回-1且errno是EAGAIN,表示没数据。不是出错,是告诉你"现在没有"。

💡 个人习惯:我一般把非阻塞IO和select()配合用。select()等一段时间,超时了就返回。这样既不会死等,也不会空转CPU。

非阻塞IO的典型场景:

  • 串口通信:轮询接收,不阻塞主循环
  • 网络socket:处理多个连接
  • 管道/消息队列:非阻塞读取
  • 键盘输入:不卡住界面

异步IO:让系统通知你

非阻塞IO虽然不卡了,但还得自己轮询。轮询多了,CPU就浪费了。

异步IO不一样。你告诉系统:"帮我读数据,读完了叫我。"然后你就可以去睡觉了。

Linux下用aio_read()实现:

#include <aio.h>

struct aiocb cb;
memset(&cb, 0, sizeof(cb));
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = sizeof(buf);
cb.aio_offset = 0;

// 发起异步读
int ret = aio_read(&cb);
if (ret != 0) {
    perror("aio_read failed");
    return;
}

// 这里可以做其他事情
printf("读操作已提交,我先忙别的\n");

// 检查完成状态
while (aio_error(&cb) == EINPROGRESS) {
    // 还没完成,继续干别的
    usleep(1000);
}

// 获取结果
int n = aio_return(&cb);
printf("异步读取完成,共 %d 字节\n", n);

嗯,这里要注意:aio_read()只是提交请求,不等待。系统会在后台帮你读,读完了你再去拿结果。

🔑 核心区别:
  • 阻塞IO:调用者等待,直到完成
  • 非阻塞IO:调用者轮询,检查是否完成
  • 异步IO:调用者提交,系统通知完成

实战对比:三种模式

我曾经在一个数据采集项目里,三种模式都用过。做个对比:

模式 CPU占用 响应延迟 编程复杂度 适用场景
阻塞IO 低(挂起) 高(等数据) 简单单任务
非阻塞IO 中(轮询) 中(可控制) 多路复用
异步IO 低(回调) 低(立即通知) 高性能场景

你想想看,如果只是读个配置文件,阻塞IO就够了。但如果是网络服务器,同时处理上千个连接,非阻塞IO加epoll才是正道。至于异步IO,适合那种数据量大、延迟要求高的场景,比如高速数据采集。

避坑指南

我踩过的坑,分享给你:

  • 非阻塞IO的EAGAIN:一定要检查errno。我曾经忘了检查,以为读失败了,其实只是没数据。
  • aio的缓冲区:异步IO的缓冲区必须在整个操作期间有效。我犯过错误,函数返回后局部变量被销毁,aio还在读,结果数据全乱了。
  • 信号处理:aio完成会发信号。如果信号处理不当,程序可能莫名其妙退出。
  • 平台差异:aio在Linux上支持,但在某些嵌入式RTOS上可能没有。移植时要注意。
⚠️ 我曾经:在一个项目里用了aio,结果发现目标平台不支持。最后只能改成非阻塞IO加select。所以,选方案前先确认平台能力。

知识体系图

下面这张图,帮你理清IO模型的关系:

IO模型分类与选择 IO操作 阻塞IO 非阻塞IO 异步IO read() / write() 阻塞 线程挂起,等待数据 fcntl() 设置 O_NONBLOCK 返回 EAGAIN,不阻塞 配合 select() / epoll() aio_read() / aio_write() 系统后台处理,回调通知 信号或回调获取结果

总结

非阻塞IO和异步IO,本质都是解决"等待"的问题。非阻塞IO让你自己控制节奏,异步IO让系统帮你干活。

选哪个?看场景:

  • 简单任务:阻塞IO就够了
  • 多路复用:非阻塞IO + select/epoll
  • 高性能:异步IO

我个人更常用非阻塞IO加select。因为可控,调试也方便。异步IO虽然高大上,但坑也多,新手慎用。


专注资料整理