4 种 IO 模型
- 阻塞 IO
- 非阻塞 IO
- poll /epoll 支持(多路复用)
- 异步通知(信号驱动 IO,AIO 简化版)
1. 阻塞 IO(默认模式)
流程
应用调用 read() → 进入内核 → 内核没数据 → 把当前进程放进等待队列 → 进程休眠、让出 CPU→ 硬件 / 驱动有数据了 唤醒进程 → 拷贝数据到用户态 → 返回
特点
- Linux 设备文件默认就是阻塞 IO
cat /dev/xxx就是典型阻塞 IO- 没数据就卡死等待,不占用 CPU
驱动对应内核机制
等待队列 wait_queue_head_t 驱动里 wait_event_interruptible() 就是干这个的
2. 非阻塞 IO(O_NONBLOCK)
流程
应用打开设备加 O_NONBLOCK 标志调用 read() → 内核没数据 不休眠 直接立刻返回 -EAGAIN,告诉应用:现在没数据,你待会再来
特点
- 不阻塞、不睡觉,马上返回
- 应用要自己轮询 while 循环不停读
- 浪费 CPU
驱动里怎么适配
file->f_flags & O_NONBLOCK 判断是否非阻塞,没数据直接返回 -EAGAIN
3. IO 多路复用(select /poll/epoll)
核心思想
一个线程监听多个文件描述符,哪个有数据就处理哪个,不用每个 fd 开一个线程阻塞等。
Linux 内核实现
- 应用:
epoll_create/epoll_ctl/epoll_wait - 驱动:必须实现 poll 函数
- 内核维护等待队列,设备有数据时主动唤醒 epoll 监听进程
地位
Linux 高并发服务器底层全靠 epoll,是 Linux 性能最强的同步 IO 多路复用模型。
4. 异步 IO(AIO /io_uring)
流程
应用发起读请求,不用等 ,直接去干别的事内核自己后台把数据准备好,完事通知应用(信号 / 回调)
两个阶段
- 传统:
libaio老式异步 IO - 现代 Linux 5.0+:io_uring 全新高性能异步 IO 框架,目前最强
特点
- 全程不阻塞、不轮询
- 内核主动帮你做完 IO 再通知
- 高性能存储、数据库、网络框架都在用
一张表看懂四种 IO 模型区别
| IO 模型 | 是否阻塞 | 是否轮询 | 内核关键机制 | 典型用法 |
|---|---|---|---|---|
| 阻塞 IO | 阻塞等待 | 不轮询 | 等待队列、进程休眠 | 普通 read/cat |
| 非阻塞 IO | 不阻塞 | 应用轮询 | 判断 O_NONBLOCK | 循环 while 读设备 |
| 多路复用 epoll | 阻塞在 epoll_wait | 内核监听 | 驱动 poll 函数 + 等待队列 | 高并发网络服务 |
| 异步 AIO/io_uring | 完全不等 | 无需轮询 | 内核异步回调 | 高性能磁盘 / 网络 |
io_model_demo.c
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <linux/wait.h>
#define MAJOR_NUM 250
#define DEV_NAME "io_model_demo"
// 全局环形缓冲区(模拟硬件数据)
#define BUF_SIZE 100
static char rbuf[BUF_SIZE];
static int rlen = 0;
// 等待队列头:阻塞 IO 核心
static wait_queue_head_t r_wait;
// 异步 IO:异步通知结构体
static struct fasync_struct *r_fasync;
// 字符设备核心
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_dev;
/* =============================================
* 1. 阻塞 IO + 非阻塞 IO:read 函数
* ============================================= */
static ssize_t my_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
// 没有数据
if (rlen == 0) {
// 判断是否是非阻塞模式
if (file->f_flags & O_NONBLOCK) {
return -EAGAIN; // 非阻塞:直接返回,不睡觉
}
// 阻塞模式:进程休眠,等待数据
wait_event_interruptible(r_wait, rlen != 0);
}
//os.system('echo polled | sudo tee /dev/io_model_demo >/dev/null')
// 有数据:拷贝到用户空间
if (size > rlen)
size = rlen;
if (copy_to_user(buf, rbuf, size))
return -EFAULT;
rlen = 0; // 读完清空
return size;
}
/* =============================================
* 2. IO 多路复用:poll 函数(epoll/select/poll)
* ============================================= */
static __poll_t my_poll(struct file *file, poll_table *wait)
{
__poll_t mask = 0;
// 把当前进程挂到等待队列
poll_wait(file, &r_wait, wait);
// 数据可读 → 返回 POLLIN
if (rlen > 0)
mask |= POLLIN | POLLRDNORM;
return mask;
}
/* =============================================
* 3. 异步通知:信号驱动 IO(AIO 简化版)
* ============================================= */
static int my_fasync(int fd, struct file *file, int on)
{
return fasync_helper(fd, file, on, &r_fasync);
}
/* =============================================
* 模拟写入数据(测试用)
* ============================================= */
static void set_data(const char *str)
{
strncpy(rbuf, str, BUF_SIZE - 1);
rbuf[BUF_SIZE - 1] = '\0';
rlen = strlen(rbuf);
// 唤醒阻塞的进程
wake_up_interruptible(&r_wait);
// 异步通知:发信号给应用
kill_fasync(&r_fasync, SIGIO, POLL_IN);
}
static ssize_t my_write(struct file *file, const char __user *buf,
size_t size, loff_t *ppos)
{
char kbuf[BUF_SIZE];
if (size == 0)
return 0;
if (size >= BUF_SIZE)
size = BUF_SIZE - 1;
if (copy_from_user(kbuf, buf, size))
return -EFAULT;
kbuf[size] = '\0';
set_data(kbuf);
return size;
}
/* =============================================
* 文件操作集合
* ============================================= */
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
.poll = my_poll, // 多路复用
.fasync = my_fasync, // 异步通知
};
static int __init my_init(void)
{
dev_t devno = MKDEV(MAJOR_NUM, 0);
// 初始化等待队列
init_waitqueue_head(&r_wait);
// 注册字符设备
register_chrdev_region(devno, 1, DEV_NAME);
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, devno, 1);
// 自动创建设备节点
my_class = class_create("io_model");
my_dev = device_create(my_class, NULL, devno, NULL, DEV_NAME);
printk("IO model driver init ok\n");
return 0;
}
static void __exit my_exit(void)
{
dev_t devno = MKDEV(MAJOR_NUM, 0);
device_destroy(my_class, devno);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
printk("driver exit\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
1. 编译加载
cpp
cd ~/project
make
sudo insmod io_model_demo.ko
sudo dmesg | tail -5
若 insmod 报 Device or resource busy,多半是固定主设备号 250 已被占用(例如 dax)。那时要先换动态设备号改驱动,或使用未被占用的主设备号------这里暂且假设能成功加载。
设备节点一般由 device_create 生成:/dev/io_model_demo。若权限不够,可先:
cpp
sudo chmod 666 /dev/io_model_demo
2. 测「阻塞 read」
开两个终端。
终端 A(会卡住直到有数据):
cpp
sudo cat /dev/io_model_demo
终端 B(写入触发 wake_up,cat 会读到并退出):
cpp
echo "hello" | sudo tee /dev/io_model_demo
终端 A 应打印 hello(可能没有换行,取决于内核里缓冲区内容)。
3. 测「非阻塞 read」(应立刻返回,Resource temporarily unavailable)
Bash 不好设 O_NONBLOCK,用 Python 最省事:
python
sudo python3 - <<'PY'
import os
fd = os.open("/dev/io_model_demo", os.O_RDONLY | os.O_NONBLOCK)
try:
print(os.read(fd, 4096))
except BlockingIOError as e:
print("EAGAIN/nonblocking ok:", e.errno)
os.close(fd)
PY
无数据时应出现 BlockingIOError / errno 11 (EAGAIN);先 echo ... | sudo tee,再跑一次非阻塞读,应能读到数据。
4. 测「poll / select」(有数据才可读)
python
sudo python3 - <<'PY'
import select, os
fd = os.open("/dev/io_model_demo", os.O_RDONLY | os.O_NONBLOCK)
r, _, _ = select.select([fd], [], [], 0.5)
print("before write:", r) # 多半 []
os.close(fd)
# 先写入
os.system('echo polled | sudo tee /dev/io_model_demo >/dev/null')
fd = os.open("/dev/io_model_demo", os.O_RDONLY | os.O_NONBLOCK)
r, _, _ = select.select([fd], [], [], 0.5)
print("after write:", r) # 应有 fd
print(os.read(fd, 4096))
os.close(fd)
PY
5. 测「异步信号 SIGIO」(需要先打开设备并注册)
需要自己写小段 C:对 fd 调用 fcntl 打开 O_ASYNC、F_SETOWN 、F_SETSIG(可选);否则内核不会把你的进程登记进 r_fasync。这是进阶项,书里一般会配示例程序。
6. 卸载
python
sudo rmmod io_model_demo
sudo dmesg | tail -3