《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 8 章 Linux 设备驱动中的阻塞与非阻塞 I/O
参考:宋宝华 著,机械工业出版社,2015年版
8.1 阻塞与非阻塞 I/O
8.1.1 阻塞 I/O 的概念
阻塞 I/O(Blocking I/O)是指当设备资源不可用时(如读取时没有数据,写入时缓冲区满),进程会进入睡眠状态,让出 CPU,直到资源可用后被唤醒继续执行。
阻塞 I/O 工作流程:
应用程序调用 read(fd, buf, n)
↓
驱动检查:是否有数据可读?
↓ 没有数据
进程进入睡眠(TASK_INTERRUPTIBLE)
↓ 等待...
硬件产生数据(如串口收到数据)
↓
中断处理函数唤醒等待的进程
↓
进程重新调度,继续执行 read
↓
数据复制到用户空间,返回
特点:
✓ 编程简单,符合直觉
✓ 进程睡眠期间不占用 CPU
✗ 进程被阻塞,无法同时处理其他事务
✗ 不适合需要同时监控多个设备的场景
8.1.2 非阻塞 I/O 的概念
非阻塞 I/O(Non-Blocking I/O)是指当设备资源不可用时,系统调用立即返回错误 (-EAGAIN 或 -EWOULDBLOCK),而不是让进程睡眠等待。
非阻塞 I/O 工作流程:
应用程序调用 read(fd, buf, n)(fd 以 O_NONBLOCK 打开)
↓
驱动检查:是否有数据可读?
↓ 没有数据
立即返回 -EAGAIN(不睡眠)
↓
应用程序处理 EAGAIN(可以做其他事情,或稍后重试)
↓ 稍后重试
再次调用 read(fd, buf, n)
↓ 有数据了
数据复制到用户空间,返回
特点:
✓ 进程不被阻塞,可以同时处理其他事务
✓ 适合需要同时监控多个设备的场景
✗ 需要应用程序主动轮询,可能浪费 CPU
✗ 编程复杂度较高
8.1.3 阻塞与非阻塞的对比
┌─────────────────────────────────────────────────────────────┐
│ 阻塞 I/O vs 非阻塞 I/O │
├──────────────────┬──────────────────────┬───────────────────┤
│ 对比项 │ 阻塞 I/O │ 非阻塞 I/O │
├──────────────────┼──────────────────────┼───────────────────┤
│ 资源不可用时 │ 进程睡眠等待 │ 立即返回 -EAGAIN │
│ CPU 使用 │ 睡眠期间不占用 CPU │ 轮询时占用 CPU │
│ 编程复杂度 │ 简单 │ 较复杂 │
│ 适用场景 │ 单设备,简单应用 │ 多设备,高性能应用 │
│ 打开方式 │ open(path, O_RDWR) │ open(path, O_RDWR │
│ │ │ |O_NONBLOCK) │
│ 驱动返回值 │ 等待直到有数据 │ 无数据返回-EAGAIN │
└──────────────────┴──────────────────────┴───────────────────┘
8.1.4 用户空间的使用方式
c
/* ── 阻塞 I/O(默认方式)──────────────────────────────── */
int fd = open("/dev/globalfifo", O_RDWR);
char buf[128];
/* 如果没有数据,read 会阻塞直到有数据 */
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("读取到 %zd 字节\n", n);
}
/* ── 非阻塞 I/O ────────────────────────────────────────── */
int fd = open("/dev/globalfifo", O_RDWR | O_NONBLOCK);
char buf[128];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("读取到 %zd 字节\n", n);
} else if (n < 0 && errno == EAGAIN) {
printf("暂无数据,稍后重试\n");
/* 可以做其他事情 */
} else if (n < 0) {
perror("read error");
}
/* ── 运行时切换阻塞/非阻塞模式 ────────────────────────── */
#include <fcntl.h>
/* 获取当前文件标志 */
int flags = fcntl(fd, F_GETFL, 0);
/* 设置为非阻塞 */
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
/* 恢复为阻塞 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
8.1.5 驱动中判断阻塞/非阻塞模式
c
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct my_dev *dev = filp->private_data;
/* 检查是否有数据可读 */
if (dev->data_len == 0) {
/* 没有数据 */
if (filp->f_flags & O_NONBLOCK) {
/* 非阻塞模式:立即返回 */
return -EAGAIN;
}
/* 阻塞模式:等待数据(使用等待队列,见 8.2 节) */
if (wait_event_interruptible(dev->read_queue,
dev->data_len > 0))
return -ERESTARTSYS;
}
/* 有数据,复制到用户空间 */
/* ... */
return count;
}
8.2 等待队列
8.2.1 等待队列的概念
等待队列(Wait Queue)是 Linux 内核实现阻塞 I/O 的核心机制。它是一个进程链表,当进程需要等待某个条件时,将自己加入等待队列并进入睡眠;当条件满足时,其他代码(通常是中断处理函数)唤醒等待队列中的进程。
等待队列的工作原理:
等待队列头(wait_queue_head_t)
↓
┌─────────────────────────────────────────────────────────┐
│ 等待队列 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 进程A │→ │ 进程B │→ │ 进程C │ │
│ │ (睡眠中) │ │ (睡眠中) │ │ (睡眠中) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
条件满足时(如数据到来):
wake_up(&wait_queue_head)
→ 唤醒队列中的进程
→ 进程重新检查条件
→ 条件满足则继续执行
→ 条件不满足则再次睡眠
8.2.2 等待队列的 API
c
#include <linux/wait.h>
/* ── 定义和初始化等待队列头 ────────────────────────────── */
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue); /* 动态初始化 */
DECLARE_WAIT_QUEUE_HEAD(my_queue); /* 静态定义并初始化 */
/* ── 睡眠等待(进程加入等待队列并睡眠)────────────────── */
/*
* wait_event(wq, condition)
* 等待 condition 为真,不可被信号中断
* condition:C 表达式,为真时唤醒
*/
wait_event(my_queue, dev->data_len > 0);
/*
* wait_event_interruptible(wq, condition)
* 等待 condition 为真,可被信号中断(推荐在驱动中使用)
* 返回:0(条件满足),-ERESTARTSYS(被信号中断)
*/
if (wait_event_interruptible(my_queue, dev->data_len > 0))
return -ERESTARTSYS;
/*
* wait_event_timeout(wq, condition, timeout)
* 等待 condition 为真,带超时
* 返回:剩余 jiffies(>0 条件满足),0(超时)
*/
long ret = wait_event_timeout(my_queue, dev->data_len > 0,
msecs_to_jiffies(5000));
if (ret == 0) {
pr_err("等待超时\n");
return -ETIMEDOUT;
}
/*
* wait_event_interruptible_timeout(wq, condition, timeout)
* 可被信号中断 + 带超时
* 返回:>0(条件满足),0(超时),-ERESTARTSYS(被信号中断)
*/
ret = wait_event_interruptible_timeout(my_queue,
dev->data_len > 0,
msecs_to_jiffies(5000));
/* ── 唤醒等待队列 ──────────────────────────────────────── */
/*
* wake_up(wq)
* 唤醒等待队列中所有处于 TASK_UNINTERRUPTIBLE 和
* TASK_INTERRUPTIBLE 状态的进程
*/
wake_up(&my_queue);
/*
* wake_up_interruptible(wq)
* 只唤醒处于 TASK_INTERRUPTIBLE 状态的进程
* (与 wait_event_interruptible 配对使用)
*/
wake_up_interruptible(&my_queue);
/*
* wake_up_all(wq)
* 唤醒所有等待的进程(包括不可中断睡眠的进程)
*/
wake_up_all(&my_queue);
8.2.3 等待队列的底层操作
除了 wait_event 系列宏,还可以使用更底层的 API 实现更精细的控制:
c
/* ── 底层等待队列操作 ──────────────────────────────────── */
/* 定义等待队列项 */
wait_queue_t wait;
init_waitqueue_entry(&wait, current); /* current:当前进程 */
/* 将当前进程加入等待队列 */
add_wait_queue(&my_queue, &wait);
/* 设置进程状态为可中断睡眠 */
set_current_state(TASK_INTERRUPTIBLE);
/* 检查条件(必须在设置状态后检查,防止竞态) */
while (dev->data_len == 0) {
if (filp->f_flags & O_NONBLOCK) {
/* 非阻塞:恢复状态并返回 */
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait);
return -EAGAIN;
}
/* 让出 CPU,进入睡眠 */
schedule();
/* 被唤醒后检查是否被信号中断 */
if (signal_pending(current)) {
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait);
return -ERESTARTSYS;
}
/* 重新设置状态,继续等待 */
set_current_state(TASK_INTERRUPTIBLE);
}
/* 条件满足,恢复运行状态 */
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait);
/* 读取数据 */
8.2.4 等待队列的使用注意事项
等待队列使用的关键注意事项:
1. 条件检查与睡眠必须是原子的
错误模式:
if (dev->data_len == 0) ← 检查条件
schedule(); ← 睡眠(中间可能被唤醒!)
正确模式(wait_event 宏内部实现):
set_current_state(TASK_INTERRUPTIBLE);
if (dev->data_len == 0) ← 设置状态后再检查
schedule();
2. 唤醒后必须重新检查条件
wait_event 宏使用 while 循环,不是 if
因为可能有多个进程等待同一条件,唤醒后条件可能已被其他进程消耗
3. 在中断上下文中只能使用 wake_up,不能使用 wait_event
wait_event 会导致睡眠,中断上下文不能睡眠
4. 唤醒操作通常在中断处理函数或其他进程中执行
数据到来 → 中断 → wake_up → 等待进程被唤醒
8.3 支持阻塞操作的 globalfifo 设备驱动
8.3.1 globalfifo 设备描述
宋宝华在书中以 globalfifo(全局 FIFO)设备作为阻塞 I/O 的教学案例。globalfifo 是一个先进先出的环形缓冲区,模拟真实设备(如串口)的行为:
globalfifo 设备规格:
设备名称:globalfifo
设备文件:/dev/globalfifo
缓冲区大小:4096 字节(FIFO 环形缓冲区)
读操作:
有数据 → 立即返回数据
无数据 + 阻塞模式 → 睡眠等待写者写入数据
无数据 + 非阻塞模式 → 立即返回 -EAGAIN
写操作:
有空间 → 立即写入数据
无空间 + 阻塞模式 → 睡眠等待读者读取数据
无空间 + 非阻塞模式 → 立即返回 -EAGAIN
等待队列:
r_wait:读等待队列(等待数据可读)
w_wait:写等待队列(等待空间可写)
globalfifo 的 FIFO 结构:
┌─────────────────────────────────────────────────────────┐
│ globalfifo_dev │
│ ┌───────────────────────────────────────────────────┐ │
│ │ buf[GLOBALFIFO_SIZE](4KB 环形缓冲区) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ ... 已读 ... │ 有效数据 │ ... 空闲 ... │ ... │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ ↑ r_pos(读指针) ↑ w_pos(写指针) │ │
│ └───────────────────────────────────────────────────┘ │
│ current_len:当前有效数据长度 │
│ r_wait:读等待队列 │
│ w_wait:写等待队列 │
│ mutex:互斥锁 │
└─────────────────────────────────────────────────────────┘
8.3.2 globalfifo 完整驱动代码
c
/*
* globalfifo.c ------ 支持阻塞操作的 globalfifo 字符设备驱动
*
* 实现一个 4KB 的 FIFO 缓冲区,支持:
* - 阻塞读:无数据时睡眠等待
* - 阻塞写:缓冲区满时睡眠等待
* - 非阻塞读写:立即返回 -EAGAIN
*
* 参考:宋宝华《Linux设备驱动开发详解》第8章
*/
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/poll.h>
/* ── 宏定义 ──────────────────────────────────────────────── */
#define GLOBALFIFO_SIZE 0x1000 /* FIFO 缓冲区大小:4096 字节 */
#define GLOBALFIFO_MAJOR 231 /* 主设备号 */
#define FIFO_CLEAR 0x1 /* ioctl 命令:清空 FIFO */
/* ── 设备结构体 ──────────────────────────────────────────── */
struct globalfifo_dev {
struct cdev cdev;
/* FIFO 缓冲区 */
unsigned char buf[GLOBALFIFO_SIZE];
unsigned int r_pos; /* 读指针 */
unsigned int w_pos; /* 写指针 */
unsigned int current_len; /* 当前有效数据长度 */
/* 并发控制 */
struct mutex mutex; /* 互斥锁 */
/* 等待队列 */
wait_queue_head_t r_wait; /* 读等待队列:等待数据可读 */
wait_queue_head_t w_wait; /* 写等待队列:等待空间可写 */
};
static int globalfifo_major = GLOBALFIFO_MAJOR;
static struct globalfifo_dev *globalfifo_devp;
static struct class *globalfifo_class;
/* ── open ────────────────────────────────────────────────── */
static int globalfifo_open(struct inode *inode, struct file *filp)
{
struct globalfifo_dev *dev = container_of(inode->i_cdev,
struct globalfifo_dev, cdev);
filp->private_data = dev;
return 0;
}
/* ── release ─────────────────────────────────────────────── */
static int globalfifo_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* ── ioctl ───────────────────────────────────────────────── */
static long globalfifo_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct globalfifo_dev *dev = filp->private_data;
switch (cmd) {
case FIFO_CLEAR:
mutex_lock(&dev->mutex);
dev->r_pos = 0;
dev->w_pos = 0;
dev->current_len = 0;
mutex_unlock(&dev->mutex);
/* 唤醒等待写入的进程(FIFO 已清空,有空间了) */
wake_up_interruptible(&dev->w_wait);
pr_info("globalfifo: FIFO 已清空\n");
break;
default:
return -EINVAL;
}
return 0;
}
/* ── read ────────────────────────────────────────────────── */
static ssize_t globalfifo_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
struct globalfifo_dev *dev = filp->private_data;
DECLARE_WAITQUEUE(wait, current); /* 定义等待队列项 */
mutex_lock(&dev->mutex);
/* 如果 FIFO 为空,等待数据 */
while (dev->current_len == 0) {
if (filp->f_flags & O_NONBLOCK) {
/* 非阻塞模式:立即返回 */
ret = -EAGAIN;
goto out;
}
/* 阻塞模式:将当前进程加入读等待队列并睡眠 */
__add_wait_queue(&dev->r_wait, &wait);
set_current_state(TASK_INTERRUPTIBLE);
/* 释放互斥锁,让写者可以写入数据 */
mutex_unlock(&dev->mutex);
/* 让出 CPU,进入睡眠 */
schedule();
/* 被唤醒后重新获取互斥锁 */
mutex_lock(&dev->mutex);
/* 从等待队列中移除 */
__remove_wait_queue(&dev->r_wait, &wait);
set_current_state(TASK_RUNNING);
/* 检查是否被信号中断 */
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out;
}
}
/* 有数据可读,调整读取长度 */
if (count > dev->current_len)
count = dev->current_len;
/* 从 FIFO 读取数据(处理环形缓冲区的回绕)*/
if (dev->r_pos + count <= GLOBALFIFO_SIZE) {
/* 数据没有跨越缓冲区末尾 */
if (copy_to_user(buf, dev->buf + dev->r_pos, count)) {
ret = -EFAULT;
goto out;
}
} else {
/* 数据跨越缓冲区末尾,分两次复制 */
unsigned int first = GLOBALFIFO_SIZE - dev->r_pos;
if (copy_to_user(buf, dev->buf + dev->r_pos, first)) {
ret = -EFAULT;
goto out;
}
if (copy_to_user(buf + first, dev->buf, count - first)) {
ret = -EFAULT;
goto out;
}
}
/* 更新读指针和有效数据长度 */
dev->r_pos = (dev->r_pos + count) % GLOBALFIFO_SIZE;
dev->current_len -= count;
ret = count;
pr_info("globalfifo: 读取 %zu 字节,剩余 %u 字节\n",
count, dev->current_len);
/* 唤醒等待写入的进程(有空间了) */
wake_up_interruptible(&dev->w_wait);
out:
mutex_unlock(&dev->mutex);
return ret;
}
/* ── write ───────────────────────────────────────────────── */
static ssize_t globalfifo_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
struct globalfifo_dev *dev = filp->private_data;
DECLARE_WAITQUEUE(wait, current);
mutex_lock(&dev->mutex);
/* 如果 FIFO 已满,等待空间 */
while (dev->current_len == GLOBALFIFO_SIZE) {
if (filp->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
goto out;
}
/* 阻塞模式:将当前进程加入写等待队列并睡眠 */
__add_wait_queue(&dev->w_wait, &wait);
set_current_state(TASK_INTERRUPTIBLE);
mutex_unlock(&dev->mutex);
schedule();
mutex_lock(&dev->mutex);
__remove_wait_queue(&dev->w_wait, &wait);
set_current_state(TASK_RUNNING);
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out;
}
}
/* 有空间可写,调整写入长度 */
if (count > GLOBALFIFO_SIZE - dev->current_len)
count = GLOBALFIFO_SIZE - dev->current_len;
/* 向 FIFO 写入数据(处理环形缓冲区的回绕)*/
if (dev->w_pos + count <= GLOBALFIFO_SIZE) {
if (copy_from_user(dev->buf + dev->w_pos, buf, count)) {
ret = -EFAULT;
goto out;
}
} else {
unsigned int first = GLOBALFIFO_SIZE - dev->w_pos;
if (copy_from_user(dev->buf + dev->w_pos, buf, first)) {
ret = -EFAULT;
goto out;
}
if (copy_from_user(dev->buf, buf + first, count - first)) {
ret = -EFAULT;
goto out;
}
}
/* 更新写指针和有效数据长度 */
dev->w_pos = (dev->w_pos + count) % GLOBALFIFO_SIZE;
dev->current_len += count;
ret = count;
pr_info("globalfifo: 写入 %zu 字节,当前 %u 字节\n",
count, dev->current_len);
/* 唤醒等待读取的进程(有数据了) */
wake_up_interruptible(&dev->r_wait);
out:
mutex_unlock(&dev->mutex);
return ret;
}
/* ── file_operations(暂不含 poll,见 8.5 节)────────────── */
static const struct file_operations globalfifo_fops = {
.owner = THIS_MODULE,
.read = globalfifo_read,
.write = globalfifo_write,
.unlocked_ioctl = globalfifo_ioctl,
.open = globalfifo_open,
.release = globalfifo_release,
};
/* ── 模块加载 ─────────────────────────────────────────────── */
static int __init globalfifo_init(void)
{
int ret;
dev_t devno = MKDEV(globalfifo_major, 0);
ret = register_chrdev_region(devno, 1, "globalfifo");
if (ret < 0) return ret;
globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
if (!globalfifo_devp) {
ret = -ENOMEM;
goto fail_malloc;
}
/* 初始化并发控制和等待队列 */
mutex_init(&globalfifo_devp->mutex);
init_waitqueue_head(&globalfifo_devp->r_wait);
init_waitqueue_head(&globalfifo_devp->w_wait);
cdev_init(&globalfifo_devp->cdev, &globalfifo_fops);
globalfifo_devp->cdev.owner = THIS_MODULE;
ret = cdev_add(&globalfifo_devp->cdev, devno, 1);
if (ret) goto fail_cdev;
globalfifo_class = class_create(THIS_MODULE, "globalfifo");
if (IS_ERR(globalfifo_class)) {
ret = PTR_ERR(globalfifo_class);
goto fail_class;
}
device_create(globalfifo_class, NULL, devno, NULL, "globalfifo");
pr_info("globalfifo: 驱动加载成功\n");
return 0;
fail_class:
cdev_del(&globalfifo_devp->cdev);
fail_cdev:
kfree(globalfifo_devp);
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
/* ── 模块卸载 ─────────────────────────────────────────────── */
static void __exit globalfifo_exit(void)
{
device_destroy(globalfifo_class, MKDEV(globalfifo_major, 0));
class_destroy(globalfifo_class);
cdev_del(&globalfifo_devp->cdev);
kfree(globalfifo_devp);
unregister_chrdev_region(MKDEV(globalfifo_major, 0), 1);
pr_info("globalfifo: 驱动已卸载\n");
}
module_init(globalfifo_init);
module_exit(globalfifo_exit);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("支持阻塞操作的 globalfifo 字符设备驱动");
8.3.3 阻塞读写的关键流程分析
阻塞读的完整流程:
globalfifo_read 阻塞流程:
读进程调用 read(fd, buf, n)
↓
获取 mutex
↓
检查 current_len == 0?
↓ 是(FIFO 为空)
├── 非阻塞模式?→ 释放 mutex,返回 -EAGAIN
└── 阻塞模式:
├── 将进程加入 r_wait 等待队列
├── 设置进程状态为 TASK_INTERRUPTIBLE
├── 释放 mutex(让写者可以写入)
├── schedule()(让出 CPU,进入睡眠)
│
│ ← 写进程写入数据,调用 wake_up_interruptible(&r_wait)
│
├── 被唤醒,重新获取 mutex
├── 从等待队列移除
├── 检查信号?→ 返回 -ERESTARTSYS
└── 重新检查 current_len(while 循环)
↓ current_len > 0(有数据)
从 FIFO 读取数据(处理环形缓冲区回绕)
↓
更新 r_pos 和 current_len
↓
wake_up_interruptible(&w_wait)(唤醒等待写入的进程)
↓
释放 mutex,返回读取字节数
环形缓冲区的读写操作:
环形缓冲区(Ring Buffer)示意:
初始状态:
┌─────────────────────────────────────────────────────────┐
│ buf[0..4095] │
│ r_pos=0, w_pos=0, current_len=0 │
└─────────────────────────────────────────────────────────┘
写入 "ABCDE"(5字节)后:
┌─────────────────────────────────────────────────────────┐
│ A B C D E _ _ _ _ _ _ _ _ _ _ _ ... │
│ ↑r_pos=0 ↑w_pos=5, current_len=5 │
└─────────────────────────────────────────────────────────┘
读取 3 字节("ABC")后:
┌─────────────────────────────────────────────────────────┐
│ _ _ _ D E _ _ _ _ _ _ _ _ _ _ _ ... │
│ ↑r_pos=3 ↑w_pos=5, current_len=2 │
└─────────────────────────────────────────────────────────┘
写入到末尾并回绕:
┌─────────────────────────────────────────────────────────┐
│ X Y _ D E F G H I J K L M N O P ... W w_pos=2 │
│ ↑r_pos=3 │
└─────────────────────────────────────────────────────────┘
(数据从 r_pos=3 到末尾,再从头到 w_pos=2)
8.4 轮询操作
8.4.1 轮询的概念
轮询(Poll)是一种 I/O 多路复用机制,允许应用程序同时监控多个文件描述符,等待其中任何一个就绪(可读、可写或异常)。
Linux 提供了三种轮询系统调用:
三种轮询系统调用:
select():最古老,有文件描述符数量限制(通常1024)
poll():改进版 select,无数量限制,但每次调用需要传递所有 fd
epoll():Linux 特有,高效,适合大量 fd 的场景(如服务器)
驱动层面:三种系统调用最终都调用驱动的 .poll 函数
8.4.2 应用程序使用 poll/select
c
#include <poll.h>
#include <sys/select.h>
/* ── 使用 poll 同时监控多个设备 ────────────────────────── */
struct pollfd fds[2];
/* 监控 globalfifo 的可读事件 */
fds[0].fd = fd_fifo;
fds[0].events = POLLIN; /* 等待可读 */
/* 监控串口的可写事件 */
fds[1].fd = fd_uart;
fds[1].events = POLLOUT; /* 等待可写 */
int ret = poll(fds, 2, 5000); /* 超时 5000ms */
if (ret < 0) {
perror("poll error");
} else if (ret == 0) {
printf("poll 超时,没有事件发生\n");
} else {
/* 检查哪个 fd 就绪 */
if (fds[0].revents & POLLIN) {
printf("globalfifo 有数据可读\n");
read(fd_fifo, buf, sizeof(buf));
}
if (fds[1].revents & POLLOUT) {
printf("串口可以写入\n");
write(fd_uart, data, len);
}
}
/* ── 使用 select ────────────────────────────────────────── */
fd_set readfds, writefds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(fd_fifo, &readfds); /* 监控 fifo 可读 */
FD_SET(fd_uart, &writefds); /* 监控 uart 可写 */
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int nfds = max(fd_fifo, fd_uart) + 1;
ret = select(nfds, &readfds, &writefds, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(fd_fifo, &readfds))
read(fd_fifo, buf, sizeof(buf));
if (FD_ISSET(fd_uart, &writefds))
write(fd_uart, data, len);
}
8.4.3 poll 事件类型
c
/* poll 事件标志(定义在 <linux/poll.h>)*/
/* 输入事件(events 字段设置,revents 字段返回)*/
POLLIN /* 有数据可读(普通数据或优先级数据)*/
POLLRDNORM /* 有普通数据可读(等价于 POLLIN)*/
POLLRDBAND /* 有优先级数据可读 */
POLLPRI /* 有紧急数据可读(如 TCP 带外数据)*/
/* 输出事件 */
POLLOUT /* 可以写入数据(不会阻塞)*/
POLLWRNORM /* 可以写入普通数据 */
POLLWRBAND /* 可以写入优先级数据 */
/* 错误事件(只在 revents 中返回,events 中设置无效)*/
POLLERR /* 发生错误 */
POLLHUP /* 设备挂起(如串口断开)*/
POLLNVAL /* 文件描述符无效 */
/* 驱动 poll 函数的返回值是这些标志的组合 */
/* 例如:有数据可读且可以写入 */
return POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
8.4.4 驱动中实现 poll 函数
驱动的 poll 函数需要完成两件事:
- 将当前进程加入设备的等待队列(通过
poll_wait) - 返回当前设备的就绪状态掩码
c
#include <linux/poll.h>
/*
* poll 函数的标准实现框架
*
* 参数:
* filp:文件结构体
* wait:poll 表(由内核传入,用于注册等待队列)
*
* 返回值:就绪事件的掩码(POLLIN/POLLOUT 等的组合)
*/
static unsigned int my_poll(struct file *filp,
struct poll_table_struct *wait)
{
struct my_dev *dev = filp->private_data;
unsigned int mask = 0;
mutex_lock(&dev->mutex);
/*
* poll_wait:将当前进程注册到等待队列
* 注意:poll_wait 不会阻塞!它只是注册,让内核知道
* 当这些等待队列被唤醒时,需要重新调用 poll 函数
*/
poll_wait(filp, &dev->r_wait, wait); /* 注册读等待队列 */
poll_wait(filp, &dev->w_wait, wait); /* 注册写等待队列 */
/* 检查当前就绪状态 */
if (dev->current_len != 0) {
/* 有数据可读 */
mask |= POLLIN | POLLRDNORM;
}
if (dev->current_len != GLOBALFIFO_SIZE) {
/* 有空间可写 */
mask |= POLLOUT | POLLWRNORM;
}
mutex_unlock(&dev->mutex);
return mask;
}
8.4.5 poll 函数的工作原理
poll/select 系统调用的内核处理流程:
1. 应用程序调用 poll(fds, n, timeout)
↓
2. 内核为每个 fd 调用驱动的 .poll 函数
↓
3. 驱动的 poll 函数:
a. 调用 poll_wait 将进程注册到等待队列
b. 返回当前就绪状态掩码
↓
4. 如果有 fd 就绪(掩码非零):
→ 立即返回就绪的 fd 数量
↓
5. 如果没有 fd 就绪:
→ 进程进入睡眠,等待任一等待队列被唤醒
↓
6. 某个设备就绪(如数据到来),调用 wake_up
→ 唤醒等待的进程
↓
7. 内核再次为每个 fd 调用驱动的 .poll 函数
→ 获取最新的就绪状态
↓
8. 返回就绪的 fd 数量给应用程序
关键点:
poll_wait 不阻塞,只是注册
真正的阻塞发生在内核的 poll 框架中
驱动的 poll 函数可能被调用多次
8.5 支持轮询操作的 globalfifo 设备驱动
8.5.1 在 globalfifo 中添加 poll 支持
在 8.3 节的 globalfifo 驱动基础上,添加 poll 函数支持:
c
/* ── globalfifo 的 poll 函数 ─────────────────────────────── */
static unsigned int globalfifo_poll(struct file *filp,
struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct globalfifo_dev *dev = filp->private_data;
mutex_lock(&dev->mutex);
/*
* 将当前进程注册到读写等待队列
* 当 r_wait 或 w_wait 被唤醒时,poll 会重新检查就绪状态
*/
poll_wait(filp, &dev->r_wait, wait);
poll_wait(filp, &dev->w_wait, wait);
/* 检查读就绪:FIFO 中有数据 */
if (dev->current_len != 0)
mask |= POLLIN | POLLRDNORM;
/* 检查写就绪:FIFO 中有空间 */
if (dev->current_len != GLOBALFIFO_SIZE)
mask |= POLLOUT | POLLWRNORM;
mutex_unlock(&dev->mutex);
pr_info("globalfifo: poll 返回 mask=0x%x,current_len=%u\n",
mask, dev->current_len);
return mask;
}
/* ── 更新 file_operations,添加 poll ─────────────────────── */
static const struct file_operations globalfifo_fops = {
.owner = THIS_MODULE,
.read = globalfifo_read,
.write = globalfifo_write,
.poll = globalfifo_poll, /* ← 新增 poll 支持 */
.unlocked_ioctl = globalfifo_ioctl,
.open = globalfifo_open,
.release = globalfifo_release,
};
8.5.2 用户空间验证阻塞与轮询
测试程序一:验证阻塞读
c
/*
* test_blocking_read.c ------ 验证 globalfifo 的阻塞读
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#define DEVICE "/dev/globalfifo"
/* 写线程:3秒后写入数据 */
void *writer_thread(void *arg)
{
int fd = *(int *)arg;
sleep(3); /* 等待3秒,让读线程先阻塞 */
const char *msg = "Hello from writer!";
write(fd, msg, strlen(msg));
printf("[写线程] 写入数据:\"%s\"\n", msg);
return NULL;
}
int main(void)
{
int fd;
char buf[128] = {0};
pthread_t tid;
fd = open(DEVICE, O_RDWR);
if (fd < 0) { perror("open"); return -1; }
/* 启动写线程 */
pthread_create(&tid, NULL, writer_thread, &fd);
printf("[主线程] 开始阻塞读,等待数据...\n");
ssize_t n = read(fd, buf, sizeof(buf)); /* 阻塞,等待写线程写入 */
printf("[主线程] 读取到 %zd 字节:\"%s\"\n", n, buf);
pthread_join(tid, NULL);
close(fd);
return 0;
}
/* 预期输出:
* [主线程] 开始阻塞读,等待数据...
* (等待约3秒)
* [写线程] 写入数据:"Hello from writer!"
* [主线程] 读取到 18 字节:"Hello from writer!"
*/
测试程序二:验证非阻塞读
c
/*
* test_nonblocking.c ------ 验证 globalfifo 的非阻塞读
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define DEVICE "/dev/globalfifo"
int main(void)
{
int fd;
char buf[128] = {0};
ssize_t n;
int retry = 0;
/* 以非阻塞模式打开 */
fd = open(DEVICE, O_RDWR | O_NONBLOCK);
if (fd < 0) { perror("open"); return -1; }
/* 非阻塞读:FIFO 为空时立即返回 -EAGAIN */
n = read(fd, buf, sizeof(buf));
if (n < 0 && errno == EAGAIN) {
printf("非阻塞读:FIFO 为空,返回 EAGAIN\n");
}
/* 写入数据 */
const char *msg = "Test data";
write(fd, msg, strlen(msg));
printf("写入数据:\"%s\"\n", msg);
/* 再次非阻塞读:现在有数据了 */
memset(buf, 0, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("非阻塞读成功:读取 %zd 字节:\"%s\"\n", n, buf);
}
close(fd);
return 0;
}
测试程序三:使用 poll 同时监控读写就绪
c
/*
* test_poll.c ------ 验证 globalfifo 的 poll 操作
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <string.h>
#include <pthread.h>
#define DEVICE "/dev/globalfifo"
/* 写线程:2秒后写入数据 */
void *writer_thread(void *arg)
{
int fd = *(int *)arg;
sleep(2);
const char *msg = "Poll test data";
write(fd, msg, strlen(msg));
printf("[写线程] 写入:\"%s\"\n", msg);
return NULL;
}
int main(void)
{
int fd;
char buf[128] = {0};
struct pollfd pfd;
pthread_t tid;
int ret;
fd = open(DEVICE, O_RDWR);
if (fd < 0) { perror("open"); return -1; }
/* 启动写线程 */
pthread_create(&tid, NULL, writer_thread, &fd);
/* 使用 poll 等待可读事件 */
pfd.fd = fd;
pfd.events = POLLIN | POLLOUT; /* 同时监控可读和可写 */
printf("[主线程] 开始 poll,等待事件...\n");
/* 第一次 poll:FIFO 为空,只有 POLLOUT 就绪 */
ret = poll(&pfd, 1, 5000);
if (ret > 0) {
printf("[主线程] poll 返回,revents=0x%x\n", pfd.revents);
if (pfd.revents & POLLIN)
printf(" → POLLIN:有数据可读\n");
if (pfd.revents & POLLOUT)
printf(" → POLLOUT:可以写入\n");
}
/* 等待写线程写入数据 */
sleep(3);
/* 第二次 poll:FIFO 有数据,POLLIN 和 POLLOUT 都就绪 */
pfd.events = POLLIN | POLLOUT;
ret = poll(&pfd, 1, 1000);
if (ret > 0) {
printf("[主线程] 第二次 poll,revents=0x%x\n", pfd.revents);
if (pfd.revents & POLLIN) {
ssize_t n = read(fd, buf, sizeof(buf));
printf(" → 读取 %zd 字节:\"%s\"\n", n, buf);
}
}
pthread_join(tid, NULL);
close(fd);
return 0;
}
/* 预期输出:
* [主线程] 开始 poll,等待事件...
* [主线程] poll 返回,revents=0x4(POLLOUT)
* → POLLOUT:可以写入
* [写线程] 写入:"Poll test data"
* [主线程] 第二次 poll,revents=0x5(POLLIN|POLLOUT)
* → 读取 14 字节:"Poll test data"
*/
测试程序四:使用 select 实现 I/O 多路复用
c
/*
* test_select.c ------ 使用 select 监控 globalfifo
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#define DEVICE "/dev/globalfifo"
int main(void)
{
int fd;
fd_set readfds;
struct timeval timeout;
char buf[128] = {0};
int ret;
fd = open(DEVICE, O_RDWR);
if (fd < 0) { perror("open"); return -1; }
/* 先写入一些数据 */
write(fd, "Hello Select!", 13);
/* 使用 select 等待可读 */
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 5;
timeout.tv_usec = 0;
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(fd, &readfds)) {
ssize_t n = read(fd, buf, sizeof(buf));
printf("select 读取 %zd 字节:\"%s\"\n", n, buf);
} else if (ret == 0) {
printf("select 超时\n");
}
close(fd);
return 0;
}
8.5.3 阻塞与非阻塞的综合对比验证
bash
# 编译驱动
make
sudo insmod globalfifo.ko
# 验证阻塞读(终端1:读,终端2:写)
# 终端1:
cat /dev/globalfifo # 阻塞等待数据
# 终端2(另一个终端):
echo "Hello" > /dev/globalfifo # 写入数据,终端1 立即显示
# 验证非阻塞读
dd if=/dev/globalfifo of=/dev/null bs=1 count=1 iflag=nonblock
# 如果 FIFO 为空:
# dd: error reading '/dev/globalfifo': Resource temporarily unavailable
# 0+0 records in
# 0+0 records out
# 验证 poll(使用 bash 的超时机制)
timeout 3 cat /dev/globalfifo
# 3秒内没有数据则超时退出
# 查看内核日志
dmesg | grep globalfifo | tail -20
本章小结
| 章节 | 核心知识点 | 关键 API |
|---|---|---|
| 8.1 阻塞与非阻塞I/O | 阻塞/非阻塞的概念与对比;O_NONBLOCK 标志;驱动中判断模式;-EAGAIN 返回值 |
filp->f_flags & O_NONBLOCK、-EAGAIN |
| 8.2 等待队列 | 等待队列的原理;wait_event_interruptible;wake_up_interruptible;底层操作 API;注意事项 |
init_waitqueue_head()、wait_event_interruptible()、wake_up_interruptible() |
| 8.3 阻塞globalfifo | FIFO 环形缓冲区设计;阻塞读写完整实现;读写等待队列的协作;环形缓冲区回绕处理 | DECLARE_WAITQUEUE()、schedule()、signal_pending() |
| 8.4 轮询操作 | poll/select/epoll 概念;poll 事件类型;驱动 poll 函数框架;poll_wait 的作用;内核处理流程 |
poll_wait()、POLLIN、POLLOUT |
| 8.5 支持轮询的globalfifo | globalfifo poll 函数实现;四个测试程序(阻塞读/非阻塞/poll/select);完整验证流程 | globalfifo_poll()、poll()、select() |
阻塞 I/O 实现的关键要点
1. 等待队列的正确使用
读等待队列(r_wait):等待数据可读
写等待队列(w_wait):等待空间可写
读操作唤醒写等待队列,写操作唤醒读等待队列
2. 互斥锁与等待队列的配合
等待前:获取 mutex
睡眠前:释放 mutex(让其他进程可以访问设备)
唤醒后:重新获取 mutex
3. 信号处理
使用 wait_event_interruptible 而非 wait_event
被信号中断时返回 -ERESTARTSYS
4. 非阻塞模式的处理
检查 filp->f_flags & O_NONBLOCK
资源不可用时立即返回 -EAGAIN
5. poll 函数的实现要点
必须调用 poll_wait 注册等待队列
必须返回当前就绪状态(不能只返回0)
poll_wait 不会阻塞,只是注册
6. 唤醒时机
写操作完成后:wake_up_interruptible(&r_wait)
读操作完成后:wake_up_interruptible(&w_wait)
ioctl 清空后:wake_up_interruptible(&w_wait)
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年