Linux阻塞与非阻塞I/O:从原理到实践详解
1. 阻塞与非阻塞I/O基础概念
1.1 阻塞与非阻塞简介
在Linux系统编程中,I/O操作可以分为两种基本模式:阻塞I/O和非阻塞I/O。这两种模式决定了当设备或资源不可用时,程序的行为方式。
阻塞I/O就像你在餐厅点餐后坐在座位上等待服务员上菜。在此期间你不能做其他事情,只能等待食物送到面前。在编程中,这意味着当程序执行I/O操作时,如果数据未准备好,进程会进入睡眠状态,直到条件满足才会继续执行。
非阻塞I/O则像是自助餐厅的就餐方式。你拿着餐盘去取食物,如果某个菜品暂时没有,你不会站在那里等待,而是先去拿其他食物,过一会儿再来查看。在编程中,这意味着当I/O操作无法立即完成时,操作会立即返回一个错误码(如EAGAIN),而不会阻塞进程。
代码示例对比:
// 阻塞方式读取串口
fd = open("/dev/ttyS1", O_RDWR);
read(fd, &buf, 1); // 串口有输入才返回
// 非阻塞方式读取串口
fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK);
while(read(fd, &buf, 1) != 1) continue; // 循环尝试读取
1.2 两种模式的优缺点
阻塞I/O的优点:
- CPU利用率高,因为等待时不占用CPU资源
- 编程模型简单直接
- 适合顺序处理任务
阻塞I/O的缺点:
- 响应性差,无法同时处理多个I/O操作
- 可能导致进程长时间挂起
非阻塞I/O的优点:
- 响应性好,可以同时监控多个I/O操作
- 进程不会被长时间挂起
- 适合高并发场景
非阻塞I/O的缺点:
- CPU利用率高,因为需要不断轮询
- 编程复杂度较高
- 可能导致忙等待(busy waiting)
2. 等待队列机制
2.1 等待队列的概念
等待队列是Linux内核中实现阻塞I/O的核心机制。它允许进程在条件不满足时进入睡眠状态,当条件满足时再被唤醒。这就像医院候诊室的叫号系统,病人(进程)可以坐着休息(睡眠),当轮到他们时会被叫醒(唤醒)。
2.2 等待队列的操作接口
- 定义和初始化等待队列头:
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
// 或者使用宏一次性完成
DECLARE_WAIT_QUEUE_HEAD(my_queue);
- 定义等待队列项:
DECLARE_WAITQUEUE(name, tsk); // tsk一般为current表示当前进程
- 添加/移除等待队列:
add_wait_queue(&my_queue, &wait); // 添加
remove_wait_queue(&my_queue, &wait); // 移除
- 等待事件:
wait_event(wq, condition); // 无条件等待
wait_event_timeout(wq, condition, timeout); // 带超时等待
wait_event_interruptible(wq, condition); // 可被信号中断的等待
- 唤醒队列:
wake_up(&queue); // 唤醒所有等待的进程
wake_up_interruptible(&queue); // 只唤醒可中断的进程
2.3 等待队列的使用模板
一个典型的驱动中使用等待队列的模板如下:
static ssize_t device_read(struct file *file, char *buffer, size_t count, loff_t *ppos)
{
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(&dev->read_queue, &wait);
// 等待数据可用
while (data_not_ready()) {
if (file->f_flags & O_NONBLOCK) { // 非阻塞模式检查
remove_wait_queue(&dev->read_queue, &wait);
return -EAGAIN;
}
__set_current_state(TASK_INTERRUPTIBLE);
schedule(); // 让出CPU
if (signal_pending(current)) { // 检查是否有信号
remove_wait_queue(&dev->read_queue, &wait);
return -ERESTARTSYS;
}
}
// 数据已准备好,进行读取操作
copy_to_user(buffer, dev->data, count);
remove_wait_queue(&dev->read_queue, &wait);
set_current_state(TASK_RUNNING);
return count;
}
3. 轮询机制
3.1 轮询的概念
对于非阻塞I/O,当操作不能立即完成时,应用程序需要通过轮询的方式不断检查设备是否就绪。这就像你等待快递时不断查看物流信息,而不是坐在门口一直等待。
3.2 常见的轮询机制
- select系统调用:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select使用位图(fd_set)来表示文件描述符集合,有以下操作宏:
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加描述符
FD_CLR(int fd, fd_set *set); // 移除描述符
FD_ISSET(int fd, fd_set *set); // 检查描述符
- poll系统调用:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll使用动态数组而非固定大小的位图,没有文件描述符数量限制。pollfd结构体定义如下:
struct pollfd {
int fd; // 文件描述符
short events; // 等待的事件
short revents; // 实际发生的事件
};
- epoll机制:
epoll是为处理大并发而设计的更高效的机制,使用红黑树管理文件描述符,避免了select/poll的线性扫描问题。
3.3 轮询机制的选择
- 少量文件描述符:select或poll都可以
- 大量文件描述符:优先选择epoll
- 跨平台需求:select兼容性最好
- 精确事件通知:poll或epoll更合适
4. 驱动中的poll操作函数
4.1 poll操作函数的作用
在Linux驱动中,poll操作函数用于支持select/poll系统调用,它需要完成两个主要任务:
- 将当前文件描述符加入适当的等待队列
- 返回设备当前的状态掩码
4.2 poll函数的实现模板
static unsigned int device_poll(struct file *filp, poll_table *wait)
{
struct device_data *dev = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &dev->read_queue, wait);
poll_wait(filp, &dev->write_queue, wait);
if (data_available(dev)) // 检查是否可读
mask |= POLLIN | POLLRDNORM;
if (space_available(dev)) // 检查是否可写
mask |= POLLOUT | POLLWRNORM;
return mask;
}
4.3 poll支持的事件标志
POLLIN
:有普通或优先级带数据可读POLLRDNORM
:有普通数据可读POLLRDBAND
:有优先级带数据可读POLLPRI
:有高优先级数据可读POLLOUT
:写数据不会导致阻塞POLLWRNORM
:写普通数据不会导致阻塞POLLWRBAND
:写优先级带数据不会导致阻塞POLLERR
:发生错误POLLHUP
:设备已断开连接POLLNVAL
:文件描述符未打开
5. 阻塞I/O实验:实现一个FIFO设备驱动
5.1 实验目标
实现一个支持阻塞读写的全局FIFO设备驱动:
- 当FIFO为空时,读进程阻塞
- 当FIFO满时,写进程阻塞
- 支持select/poll监控
5.2 关键数据结构
#define FIFO_SIZE 4096
struct globalfifo_dev {
struct cdev cdev;
unsigned int current_len;
unsigned char mem[FIFO_SIZE];
struct mutex mutex;
wait_queue_head_t read_wait;
wait_queue_head_t write_wait;
};
5.3 读函数实现
static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
struct globalfifo_dev *dev = filp->private_data;
DECLARE_WAITQUEUE(wait, current);
int ret = 0;
mutex_lock(&dev->mutex);
add_wait_queue(&dev->read_wait, &wait);
while (dev->current_len == 0) {
if (filp->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE);
mutex_unlock(&dev->mutex);
schedule();
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out2;
}
mutex_lock(&dev->mutex);
}
if (count > dev->current_len)
count = dev->current_len;
if (copy_to_user(buf, dev->mem, count)) {
ret = -EFAULT;
goto out;
}
memmove(dev->mem, dev->mem + count, dev->current_len - count);
dev->current_len -= count;
wake_up_interruptible(&dev->write_wait);
ret = count;
out:
mutex_unlock(&dev->mutex);
out2:
remove_wait_queue(&dev->read_wait, &wait);
set_current_state(TASK_RUNNING);
return ret;
}
5.4 写函数实现
static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
struct globalfifo_dev *dev = filp->private_data;
DECLARE_WAITQUEUE(wait, current);
int ret = 0;
mutex_lock(&dev->mutex);
add_wait_queue(&dev->write_wait, &wait);
while (dev->current_len == FIFO_SIZE) {
if (filp->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE);
mutex_unlock(&dev->mutex);
schedule();
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out2;
}
mutex_lock(&dev->mutex);
}
if (count > FIFO_SIZE - dev->current_len)
count = FIFO_SIZE - dev->current_len;
if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
ret = -EFAULT;
goto out;
}
dev->current_len += count;
wake_up_interruptible(&dev->read_wait);
ret = count;
out:
mutex_unlock(&dev->mutex);
out2:
remove_wait_queue(&dev->write_wait, &wait);
set_current_state(TASK_RUNNING);
return ret;
}
5.5 poll函数实现
static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
struct globalfifo_dev *dev = filp->private_data;
unsigned int mask = 0;
mutex_lock(&dev->mutex);
poll_wait(filp, &dev->read_wait, wait);
poll_wait(filp, &dev->write_wait, wait);
if (dev->current_len != 0)
mask |= POLLIN | POLLRDNORM;
if (dev->current_len != FIFO_SIZE)
mask |= POLLOUT | POLLWRNORM;
mutex_unlock(&dev->mutex);
return mask;
}
5.6 测试方法
- 加载驱动模块:
insmod globalfifo.ko
- 创建设备节点:
mknod /dev/globalfifo c 250 0
- 测试阻塞读:
# 终端1
cat /dev/globalfifo
# 终端2
echo "Hello World" > /dev/globalfifo
- 测试非阻塞读:
cat /dev/globalfifo &
# 应该立即返回,显示资源暂时不可用
6. 非阻塞I/O实验:按键驱动实现
6.1 实验目标
实现一个支持非阻塞读的按键驱动:
- 当没有按键事件时,非阻塞读立即返回
- 当有按键事件时,读取按键值
- 支持中断处理
6.2 关键数据结构
struct key_dev {
int gpio;
int irq;
char name[10];
atomic_t key_value;
atomic_t key_pressed;
wait_queue_head_t waitq;
};
6.3 读函数实现
static ssize_t key_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
struct key_dev *dev = filp->private_data;
int ret;
unsigned char value;
if (filp->f_flags & O_NONBLOCK) {
if (!atomic_read(&dev->key_pressed))
return -EAGAIN;
} else {
wait_event_interruptible(dev->waitq,
atomic_read(&dev->key_pressed));
}
value = atomic_read(&dev->key_value);
if (copy_to_user(buf, &value, 1))
return -EFAULT;
atomic_set(&dev->key_pressed, 0);
return 1;
}
6.4 中断处理函数
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct key_dev *dev = dev_id;
int gpio_value = gpio_get_value(dev->gpio);
if (gpio_value == 0) { // 按键按下
atomic_set(&dev->key_value, KEY_VALUE_PRESSED);
} else { // 按键释放
atomic_set(&dev->key_value, KEY_VALUE_RELEASED);
atomic_set(&dev->key_pressed, 1);
wake_up_interruptible(&dev->waitq);
}
return IRQ_HANDLED;
}
6.5 测试方法
- 加载驱动模块:
insmod key.ko
- 创建设备节点:
mknod /dev/key c 240 0
- 测试非阻塞读:
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd = open("/dev/key", O_RDONLY | O_NONBLOCK);
char key_val;
while (1) {
if (read(fd, &key_val, 1) == 1) {
printf("Key event: %d\n", key_val);
} else {
printf("No key event, doing other work...\n");
sleep(1);
}
}
close(fd);
return 0;
}
7. 总结与选择建议
7.1 阻塞与非阻塞I/O对比
特性 | 阻塞I/O | 非阻塞I/O |
---|---|---|
行为 | 等待直到操作完成 | 立即返回,成功或失败 |
CPU使用 | 等待时不占用CPU | 需要主动轮询 |
响应性 | 低 | 高 |
编程复杂度 | 简单 | 较复杂 |
适用场景 | 简单同步操作 | 高并发或快速响应 |
7.2 选择建议
-
选择阻塞I/O当:
- 处理简单的顺序任务
- 不需要同时处理多个I/O操作
- 资源通常能快速就绪
-
选择非阻塞I/O当:
- 需要同时监控多个I/O操作
- 要求快速响应
- 处理高并发连接
-
对于驱动开发者:
- 通常需要同时支持阻塞和非阻塞模式
- 正确实现等待队列和poll操作
- 注意并发控制和竞态条件
通过理解阻塞和非阻塞I/O的原理和实现方式,开发者可以根据具体应用场景选择最合适的I/O模型,编写出高效可靠的Linux驱动程序。