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);
// ... 还有不少,但常用的就这些
};
我个人习惯,写驱动时至少把open、release、read、write、poll、unlocked_ioctl这六个实现了。其他的按需添加。你想想看,用户态程序最常用的系统调用,无非就是open、close、read、write、ioctl、poll。你把这几个伺候好了,驱动就稳了一大半。
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。结果用户态程序卡死了,还以为是硬件坏了。排查了半天,才发现是驱动没处理非阻塞。
-EAGAIN,而是返回了0。用户态以为读到数据了,结果buf里全是垃圾。记住:非阻塞模式下,没有数据就返回 -EAGAIN,不要返回0,也不要阻塞。
2.3 等待队列:阻塞IO的核心机制
阻塞IO的实现,依赖内核的等待队列机制。说白了,就是让进程「睡」在某个条件上,等条件满足了再「醒」过来。
使用步骤很简单,就三步:
- 定义一个等待队列头:
DECLARE_WAIT_QUEUE_HEAD(my_wq); - 在read/write里调用
wait_event_interruptible让进程睡眠 - 在中断或数据到达时调用
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()就会返回。
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和内存映射,那又是另一片天地了。