2. 字符设备驱动进阶:深入理解file_operations结构体,实现阻塞与非阻塞IO,以及poll机制的应用

好,咱们接着往下聊。上一章我们把字符设备驱动的骨架搭起来了,能注册、能打开、能读写。但说实话,那只是「能用」的水平。真正的驱动,要面对的是各种复杂的用户态行为——比如用户用O_NONBLOCK打开设备,或者用select()poll()来等数据。这时候,如果你驱动里什么都不做,系统就会直接返回,用户态那边就炸了。

这一章,我们就来把这些坑填上。我会带着你,把file_operations里那些看似吓人的回调函数,一个一个拆开揉碎。尤其是阻塞与非阻塞IO,还有那个让很多人头疼的poll机制。

2.1 file_operations 结构体:不只是 read 和 write

先看个全貌。这是Linux内核里定义的一个巨型结构体,里面全是函数指针。你注册驱动时,其实就是告诉内核:「这些函数,我来处理。」

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*fasync) (int, struct file *, int);
    // ... 还有不少,但常用的就这些
};

我个人习惯,写驱动时至少把openreleasereadwritepollunlocked_ioctl这六个实现了。其他的按需添加。你想想看,用户态程序最常用的系统调用,无非就是openclosereadwriteioctlpoll。你把这几个伺候好了,驱动就稳了一大半。

小技巧: 如果你不确定某个回调要不要实现,可以先去内核源码里搜一下其他同类驱动是怎么做的。比如你要写一个GPIO驱动,就去看看 drivers/gpio/ 下的例子。这是最快的学习方式。

2.2 阻塞与非阻塞IO:两种截然不同的等待方式

这是驱动开发里最容易出bug的地方。说白了,阻塞就是「等不到数据我就不回去」,非阻塞就是「有就有,没有拉倒,立刻返回」。

用户态通过open()时的O_NONBLOCK标志,或者通过fcntl()设置,来决定用哪种方式。驱动里怎么感知呢?看file->f_flags

if (file->f_flags & O_NONBLOCK) {
    // 非阻塞模式
    if (数据不可用)
        return -EAGAIN;
} else {
    // 阻塞模式
    wait_event_interruptible(wq, 数据可用);
}

我在项目中遇到过一个问题:一个UART驱动,用户态用非阻塞方式读,但驱动里没检查O_NONBLOCK,直接调了wait_event_interruptible。结果用户态程序卡死了,还以为是硬件坏了。排查了半天,才发现是驱动没处理非阻塞。

避坑指南: 我曾经在实现一个SPI驱动时,忘了在非阻塞路径上返回 -EAGAIN,而是返回了0。用户态以为读到数据了,结果buf里全是垃圾。记住:非阻塞模式下,没有数据就返回 -EAGAIN,不要返回0,也不要阻塞。

2.3 等待队列:阻塞IO的核心机制

阻塞IO的实现,依赖内核的等待队列机制。说白了,就是让进程「睡」在某个条件上,等条件满足了再「醒」过来。

使用步骤很简单,就三步:

  1. 定义一个等待队列头:DECLARE_WAIT_QUEUE_HEAD(my_wq);
  2. 在read/write里调用wait_event_interruptible让进程睡眠
  3. 在中断或数据到达时调用wake_up_interruptible唤醒进程
// 驱动中定义
static DECLARE_WAIT_QUEUE_HEAD(read_wq);
static int data_ready = 0;

// read 函数
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    if (file->f_flags & O_NONBLOCK) {
        if (!data_ready)
            return -EAGAIN;
    } else {
        // 阻塞等待数据
        wait_event_interruptible(read_wq, data_ready);
        if (signal_pending(current))
            return -ERESTARTSYS;
    }

    // 拷贝数据到用户空间
    data_ready = 0;
    // ... copy_to_user ...
    return bytes_read;
}

// 中断处理函数中
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
    data_ready = 1;
    wake_up_interruptible(&read_wq);
    return IRQ_HANDLED;
}

嗯,这里要注意:wait_event_interruptible返回后,一定要检查signal_pending(current)。因为进程可能在等待时被信号打断,这时候你应该返回-ERESTARTSYS,让系统重新调用read。否则用户态会收到一个莫名其妙的错误。

2.4 poll 机制:让用户态优雅地等待

用户态用select()poll()epoll()时,内核最终都会调用驱动的poll回调。这个回调的任务很简单:告诉内核,当前设备可读吗?可写吗?有没有异常?

但这里有个关键点:poll回调不能阻塞!它必须立刻返回。那如果数据还没准备好怎么办?答案是:把等待队列注册到poll_table里,让内核知道去哪里唤醒。

static unsigned int my_poll(struct file *file, struct poll_table_struct *wait)
{
    unsigned int mask = 0;

    // 把等待队列注册到 poll 机制中
    poll_wait(file, &read_wq, wait);

    // 检查数据是否可用
    if (data_ready)
        mask |= POLLIN | POLLRDNORM;

    // 检查是否可写(这里假设总是可写)
    mask |= POLLOUT | POLLWRNORM;

    return mask;
}

你看,代码其实很简单。但背后的逻辑要搞清楚:poll_wait不是阻塞,它只是把等待队列挂到内核的poll机制里。当数据到达时,你调用wake_up_interruptible,内核就会重新调用你的poll回调,检查返回值。如果返回了POLLIN,用户态的select()就会返回。

核心要点: poll回调是「非阻塞的查询 + 等待队列注册」的组合。它不等待数据,只告诉内核「如果数据来了,请通过这个等待队列通知我」。

2.5 实战:一个支持阻塞/非阻塞和poll的虚拟设备

光说不练假把式。我们来写一个完整的虚拟字符设备驱动。这个设备有一个缓冲区,用户写数据进去,用户读数据出来。支持阻塞读、非阻塞读,以及poll机制。

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/poll.h>

#define DEVICE_NAME "mydemo"
#define BUF_SIZE 1024

static int major;
static struct cdev my_cdev;
static char *buffer;
static int buf_len = 0;
static DECLARE_WAIT_QUEUE_HEAD(read_wq);

static int my_open(struct inode *inode, struct file *file)
{
    return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    if (file->f_flags & O_NONBLOCK) {
        if (buf_len == 0)
            return -EAGAIN;
    } else {
        wait_event_interruptible(read_wq, buf_len > 0);
        if (signal_pending(current))
            return -ERESTARTSYS;
    }

    count = min(count, (size_t)buf_len);
    if (copy_to_user(buf, buffer, count))
        return -EFAULT;

    // 模拟消费数据
    buf_len -= count;
    memmove(buffer, buffer + count, buf_len);

    return count;
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
    count = min(count, (size_t)(BUF_SIZE - buf_len));
    if (copy_from_user(buffer + buf_len, buf, count))
        return -EFAULT;

    buf_len += count;
    wake_up_interruptible(&read_wq);

    return count;
}

static unsigned int my_poll(struct file *file, struct poll_table_struct *wait)
{
    unsigned int mask = 0;

    poll_wait(file, &read_wq, wait);

    if (buf_len > 0)
        mask |= POLLIN | POLLRDNORM;

    // 只要缓冲区没满,就可写
    if (buf_len < BUF_SIZE)
        mask |= POLLOUT | POLLWRNORM;

    return mask;
}

static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
    .poll = my_poll,
};

static int __init my_init(void)
{
    dev_t dev;

    alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    major = MAJOR(dev);

    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev, 1);

    buffer = kmalloc(BUF_SIZE, GFP_KERNEL);
    printk(KERN_INFO "mydemo: loaded, major=%d\n", major);
    return 0;
}

static void __exit my_exit(void)
{
    dev_t dev = MKDEV(major, 0);

    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);
    kfree(buffer);
    printk(KERN_INFO "mydemo: unloaded\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

这个驱动虽然简单,但五脏俱全。你可以在用户态用echo "hello" > /dev/mydemo写数据,然后用cat /dev/mydemo读出来。如果用O_NONBLOCK打开,没数据时立刻返回EAGAIN。用select()监听,也能正常工作。

测试建议: 写一个用户态程序,用 select() 同时监听这个设备和标准输入。你会发现,驱动里的 poll 回调被调用的频率比你想象的高得多——内核会定期轮询,确保不会漏掉事件。

2.6 常见陷阱与调试技巧

最后,分享几个我踩过的坑:

  • 忘记调用 poll_wait: 如果不调用poll_wait,内核不知道你的等待队列,poll机制就失效了。用户态调用select()会立即返回,但永远不会有事件。
  • 在 poll 回调里阻塞: 这是大忌!poll回调必须在原子上下文中执行,不能睡眠。如果你在里面调了wait_event,系统会直接崩溃。
  • 返回值掩码错误: POLLIN表示可读,POLLOUT表示可写,POLLERR表示错误。不要搞混了。我见过有人把POLLIN写成POLLOUT,结果用户态读不到数据,以为是驱动坏了。
  • 信号处理遗漏: 阻塞读时,一定要检查signal_pending。否则用户态用Ctrl+C终止程序时,驱动可能还在傻等。

好了,这一章的内容就到这里。阻塞与非阻塞IO,加上poll机制,是字符设备驱动进阶的必修课。掌握了这些,你写的驱动才能真正应对复杂的用户态场景。下一章,我们会聊聊ioctl和内存映射,那又是另一片天地了。