1. Linux I/O模型概述
在Linux系统开发中,I/O操作是最基础也是最关键的部分之一。当用户程序调用read()或recv()等系统调用时,数据通常需要经历两个关键阶段:
- 数据准备阶段:内核等待数据到达缓冲区
- 数据拷贝阶段:将数据从内核空间拷贝到用户空间
这两个阶段的处理方式不同,演化出了五种典型的I/O模型:
- 阻塞I/O(Blocking I/O)
- 非阻塞I/O(Non-blocking I/O)
- I/O多路复用(I/O Multiplexing)
- 信号驱动I/O(Signal-driven I/O)
- 异步I/O(Asynchronous I/O)
从应用层角度看,这些模型体现在不同的系统调用行为上:read()/write()可能阻塞,也可能因O_NONBLOCK标志而立即返回;select/poll/epoll负责等待"就绪"状态;aio_read()等接口实现异步提交;而F_SETOWN + O_ASYNC则让内核通过SIGIO信号主动通知进程。
从驱动层实现来看,这些模型通常对应不同的驱动接口实现:
- 阻塞/非阻塞:主要体现在read()/write()中是否睡眠等待
- 多路复用:依赖驱动实现的poll()函数
- 信号驱动:需要驱动实现fasync(),并在事件到来时调用kill_fasync()
- 异步I/O:本质是"提交请求后立即返回,稍后再获取完成结果",强调的是完成通知而非文件描述符就绪通知
2. 阻塞I/O模型详解
2.1 阻塞I/O基本原理
阻塞I/O的特点是简单直观:当应用调用read(fd, buf, len)时,如果设备当前没有数据可读,当前线程不会立即返回,而是进入睡眠状态,直到以下三种情况之一发生:
- 数据到达
- 被信号中断
- 发生错误
这里所说的"阻塞"特指用户空间的阻塞。在内核中,等待条件成立最常见的方式就是使用等待队列机制。
2.2 驱动中的阻塞读实现
典型的阻塞读实现思路如下:
- 驱动维护一个接收缓冲区或环形队列
- read()发现当前无数据时,调用wait_event_interruptible()进入睡眠
- 中断处理函数、DMA完成回调、定时器或底半部在数据到达后将数据放入缓冲区
- 调用wake_up_interruptible()唤醒等待进程
2.3 等待队列机制
在Linux内核驱动开发中,**等待队列(Wait Queue)**是实现阻塞I/O的核心机制。简单来说,它就是内核里的一个"等候室",专门用来存放那些因为等待某个硬件条件(如串口有数据、按键被按下)而暂时无法继续执行的进程。
等待队列由两部分组成:
2.3.1 等待队列头(wait_queue_head_t)
它是等候室的门牌,通常定义在驱动的全局变量中:
c复制struct wait_queue_head {
spinlock_t lock; // 自旋锁,保护队列链表
struct list_head head; // 双向链表头,挂载所有在该队列等待的进程
};
2.3.2 等待队列项(wait_queue_entry_t)
它代表每一个正在等候的进程。内核会自动创建它并把它挂到队列头上。
2.4 等待队列API详解
2.4.1 定义与初始化
等待队列可以静态定义,也可以动态初始化:
c复制// 方式1:静态定义并初始化
DECLARE_WAIT_QUEUE_HEAD(my_wq);
// 方式2:动态初始化
wait_queue_head_t my_wq;
init_waitqueue_head(&my_wq);
2.4.2 进入等待(睡眠)
最常用的宏是wait_event_interruptible(),其逻辑是:"如果condition为假,我就睡眠;如果condition为真,我就直接通过。"
c复制wait_event_interruptible(my_wq, condition);
参数说明:
- my_wq:定义的队列头
- condition:布尔表达式。重要:进程被唤醒后会重新检查这个条件,如果条件依然为假,它会继续睡眠。通常我们会定义一个flag变量作为条件。
2.4.3 唤醒队列
通常在中断处理函数或write()函数中调用:
c复制wake_up_interruptible(&wq_head);
这个函数会扫描my_wq指向的链表,唤醒所有满足条件的进程。
2.5 阻塞I/O实验
2.5.1 驱动程序实现
c复制#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#include <linux/wait.h>
struct cdev_test_dev {
dev_t dev_num;
int major;
int minor;
struct cdev cdev_test;
struct class* class_test;
struct device* device_test;
char kbuf[100];
int flag; // 等待队列标志
};
struct cdev_test_dev dev1;
struct cdev_test_dev dev2;
DECLARE_WAIT_QUEUE_HEAD(my_wq); // 定义等待队列头
static int cdev_test_open(struct inode* inode, struct file* filp)
{
dev1.minor = 0;
dev2.minor = 1;
printk("cdev_test_open\n");
// 判断是哪个设备被打开
struct cdev_test_dev* dev = container_of(inode->i_cdev, struct cdev_test_dev, cdev_test);
filp->private_data = dev; // 将设备结构体指针保存在文件私有数据中
printk("major = %d, minor = %d\n", dev->major, dev->minor);
return 0;
}
static ssize_t cdev_test_read(struct file* filp, char __user* buf, size_t count, loff_t* offset)
{
char kbuf[100] = "hello cdev_test";
wait_event_interruptible(my_wq, dev1.flag); // 等待队列,当dev1.flag不为0时才会继续执行
if (copy_to_user(buf, kbuf, strlen(kbuf))) // 将内核空间数据复制到用户空间
return -EFAULT;
printk("cdev_test_read\n");
return strlen(kbuf);
}
static ssiz
