字符设备驱动框架:file_operations结构体、模块加载与卸载、设备号注册与注销

大家好,我是你们的老朋友。今天咱们来聊聊字符设备驱动最核心的骨架——驱动框架。说实话,我刚入行那会儿,对着这个框架也是一头雾水。但后来我发现,搞懂了它,Linux驱动开发就算入门了。

字符设备,说白了就是像串口、按键、LED这类设备。它们的数据传输方式是一个字节一个字节来的,不像块设备那样可以整块整块地读写。你想想看,我们平时用的鼠标、键盘,不都是字符设备吗?

1. file_operations结构体:驱动与应用的桥梁

这个结构体,我愿称之为驱动的“灵魂”。它定义了应用层能对设备做哪些操作。比如打开、关闭、读、写、控制等等。

先看代码,这是它的核心定义:

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 *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_read) (struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    ssize_t (*splice_write) (struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
};

嗯,看着挺长,但常用的其实就几个:openreleasereadwriteunlocked_ioctl。其他的,说实话,很多驱动都用不上。

我在项目中遇到过这样一个坑:有个同事把owner字段忘了赋值,结果模块卸载时系统直接崩溃。为什么?因为内核不知道这个驱动属于哪个模块,卸载时没法正确清理资源。

重要提醒:owner字段一定要设置为THIS_MODULE,这是惯例,也是安全底线。

来看一个实际例子,我习惯这样初始化:

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

注意,这里用的是C99的指定初始化语法。我个人觉得这样写更清晰,哪个函数对应哪个操作一目了然。

2. 模块加载与卸载:驱动的生命周期

每个驱动模块都有两个关键函数:module_initmodule_exit。它们定义了驱动什么时候“活过来”,什么时候“死去”。

加载函数一般做这些事情:

  • 注册设备号
  • 初始化硬件
  • 创建设备节点
  • 分配内存等资源

卸载函数则相反:

  • 注销设备号
  • 释放硬件资源
  • 删除设备节点
  • 释放内存

我曾经犯过一个低级错误:卸载时忘了释放中断。结果下次加载时,中断号已经被占用了,设备根本没法用。嗯,从那以后我每次写卸载函数都会反复检查,确保所有资源都释放干净。

看一个标准的模板:

static int __init my_init(void)
{
    int ret;
    printk(KERN_INFO "my_driver: initializing\n");
    
    // 注册设备号
    ret = register_chrdev(0, "my_device", &my_fops);
    if (ret < 0) {
        printk(KERN_ERR "my_driver: failed to register device\n");
        return ret;
    }
    
    printk(KERN_INFO "my_driver: initialized, major=%d\n", ret);
    return 0;
}

static void __exit my_exit(void)
{
    printk(KERN_INFO "my_driver: exiting\n");
    unregister_chrdev(major, "my_device");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

小技巧:__init__exit宏告诉内核,这些函数只在初始化/退出时使用,之后可以回收内存。省一点是一点嘛。

3. 设备号注册与注销:给设备一个身份证

设备号就像设备的身份证。主设备号标识驱动类型,次设备号标识具体设备。比如,你的电脑上可能有两个串口,它们共享同一个主设备号,但次设备号不同。

注册设备号有两种方式:

方式 函数 特点
静态注册 register_chrdev_region() 需要提前知道主设备号,容易冲突
动态分配 alloc_chrdev_region() 内核自动分配,推荐使用

我个人强烈建议用动态分配。为什么?你想想看,如果你写死一个主设备号,万一别的驱动已经用了呢?系统就起不来了。动态分配让内核帮你挑一个空闲的,省心又安全。

来看动态分配的代码:

dev_t dev_num;
int major;

// 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
    printk(KERN_ERR "Failed to allocate device number\n");
    return ret;
}

major = MAJOR(dev_num);
printk(KERN_INFO "Allocated major number: %d\n", major);

注销时,记得用unregister_chrdev_region()

unregister_chrdev_region(dev_num, 1);

注意:设备号是系统全局资源,用完一定要还回去。我曾经见过一个驱动,卸载时忘了注销设备号,结果系统里留下了一个“幽灵设备”,其他驱动想用这个号都用不了。

4. 完整的驱动框架示例

好了,我们把上面这些串起来,写一个完整的字符设备驱动框架。这个框架虽然简单,但五脏俱全:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static int major;
static struct cdev my_cdev;
static struct class *my_class;

static int my_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, 
                       size_t count, loff_t *pos)
{
    printk(KERN_INFO "Read called\n");
    return 0;
}

static ssize_t my_write(struct file *file, const char __user *buf,
                        size_t count, loff_t *pos)
{
    printk(KERN_INFO "Write called\n");
    return count;
}

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

static int __init my_init(void)
{
    dev_t dev;
    int ret;
    
    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate device number\n");
        return ret;
    }
    major = MAJOR(dev);
    
    // 2. 初始化cdev并添加到内核
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev, 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add cdev\n");
        unregister_chrdev_region(dev, 1);
        return ret;
    }
    
    // 3. 创建设备类(用于自动创建设备节点)
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        printk(KERN_ERR "Failed to create class\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_class);
    }
    
    // 4. 创建设备节点
    device_create(my_class, NULL, dev, NULL, "my_device");
    
    printk(KERN_INFO "my_driver loaded, major=%d\n", major);
    return 0;
}

static void __exit my_exit(void)
{
    dev_t dev = MKDEV(major, 0);
    
    // 逆序释放资源
    device_destroy(my_class, dev);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);
    
    printk(KERN_INFO "my_driver unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Complete character device driver example");

这个框架里,我用了cdev接口,这是现代Linux驱动推荐的方式。老式的register_chrdev虽然简单,但功能有限,不支持次设备号管理。

核心要点:

  • 加载顺序:分配设备号 → 添加cdev → 创建类 → 创建设备节点
  • 卸载顺序:销毁设备节点 → 销毁类 → 删除cdev → 释放设备号
  • 顺序不能乱,否则系统会出问题

好了,这就是字符设备驱动的完整框架。你想想看,其实没那么复杂对吧?就是几个结构体、几个函数、一套固定的流程。搞懂了这些,后面再学什么中断、DMA、内存映射,都是在这个框架上添砖加瓦。

下一章,我会带大家深入readwrite的实现细节,看看数据到底是怎么在用户空间和内核空间之间传递的。到时候见!