《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 8 章 Linux 设备驱动中的阻塞与非阻塞 I/O

《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 函数需要完成两件事:

  1. 将当前进程加入设备的等待队列(通过 poll_wait
  2. 返回当前设备的就绪状态掩码
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_interruptiblewake_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()POLLINPOLLOUT
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年