zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux驱动之Misc子系统剖析

Linux驱动 剖析 子系统 Misc
2023-06-13 09:15:12 时间

何为Misc设备

Linux驱动分为字符设备驱动、块设备驱动和网络设备驱动,而字符设备又包括很多种,内核使用主设备号来区分各个字符设备驱动,在include/linux/major.h文件中已经预先定义好了各类字符设备的主设备号,但是即便如此,仍然存在着大量字符设备无法准确归类,对于这些设备,内核提供了一种Misc(杂项)设备来安放它们的去处。

使用Misc设备的好处

Misc子系统使用一个统一的主设备号来管理,当需要注册Misc驱动时,内核会为其分配次设备号。而如果采用普通字符设备驱动的方式,无论主设备号是静态分配还是动态分配,都会消耗掉一个主设备号,而且如果系统存在着大量的无法准确归类的字符设备驱动,那会大量浪费主设备号;当需要开发一个功能较简单的字符设备驱动,导出接口让用户空间程序方便地控制硬件,只需要使用Misc框架提供的接口即可快速地实现一个Misc设备驱动。

源码分析

Misc框架位于driver/char/misc.c文件中,从misc_init函数开始分析

static int __init misc_init(void)
{
    int err;

#ifdef CONFIG_PROC_FS
    proc_create("misc", 0, NULL, &misc_proc_fops);
#endif
    misc_class = class_create(THIS_MODULE, "misc");      // 创建misc类
    err = PTR_ERR(misc_class);
    if (IS_ERR(misc_class))
        goto fail_remove;

    err = -EIO;
    if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))  // misc通过实现为字符设备驱动来注册
        goto fail_printk;
    misc_class->devnode = misc_devnode;
    return 0;

fail_printk:
    printk("unable to get major %d for misc devices\n", MISC_MAJOR);
    class_destroy(misc_class);
fail_remove:
    remove_proc_entry("misc", NULL);
    return err;
}

先是创建了Misc类,随后将Misc子系统实现为字符设备驱动来注册到内核中,并为其绑定了fops。

static const struct file_operations misc_fops = {
    .owner      = THIS_MODULE,
    .open       = misc_open,
};

fops只实现了open方法,暂且不分析fops,先分析内核为驱动开发人员导出的注册接口misc_register

int misc_register(struct miscdevice * misc)
{
    struct miscdevice *c;
    dev_t dev;
    int err = 0;

    INIT_LIST_HEAD(&misc->list);

    mutex_lock(&misc_mtx);
    // 查找设备是否已经注册 
    list_for_each_entry(c, &misc_list, list) {
        if (c->minor == misc->minor) {
            mutex_unlock(&misc_mtx);
            return -EBUSY;
        }
    }

    // 动态分配次设备号
    if (misc->minor == MISC_DYNAMIC_MINOR) {
        int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);   // 找到位图中第一个为0的bit
        if (i >= DYNAMIC_MINORS) {       // 没有找到
            mutex_unlock(&misc_mtx);
            return -EBUSY;
        }
        misc->minor = DYNAMIC_MINORS - i - 1;   // 分配次设备号
        set_bit(i, misc_minors);                // 将分配的次设备号加入位图
    }

    // 生成设备号
    dev = MKDEV(MISC_MAJOR, misc->minor);

    // 注册设备
    misc->this_device = device_create(misc_class, misc->parent, dev,
                      misc, "%s", misc->name);
    if (IS_ERR(misc->this_device)) {
        int i = DYNAMIC_MINORS - misc->minor - 1;
        if (i < DYNAMIC_MINORS && i >= 0)
            clear_bit(i, misc_minors);
        err = PTR_ERR(misc->this_device);
        goto out;
    }

    /*
     * Add it to the front, so that later devices can "override"
     * earlier defaults
     */ 
    // 将已注册的驱动添加到链表上,open时可遍历链表来替换真正的fops
    list_add(&misc->list, &misc_list);
 out:
    mutex_unlock(&misc_mtx);
    return err;
}

从上面可以看到,先查找设备是否已经注册(内核采用一个链表来管理已经注册的Misc设备驱动),然后判断是否需要动态分配次设备号(内核使用位图来管理已经注册的Misc次设备号),然后生成设备号,通过device_create函数在Misc类下创建设备,这时候/dev目录下就会根据misc->name的值生成设备节点,然后将已经注册的驱动添加到链表上。

把关注点放到该接口需要传递的结构体struct miscdevice

struct miscdevice  {
    int minor;
    const char *name;
    const struct file_operations *fops;   // 真正的fops
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const char *nodename;
    mode_t mode;
};

可以看到该结构体内部也定义了一个fops,需要驱动开发者使用该接口时实现一个fops,其实这个才是真正的fops,而在misc_init函数中调用register_chrdev来绑定的fops是用来中转数据的,具体情况可以从其open方法可以分析出来。

static int misc_open(struct inode * inode, struct file * file)
{
    int minor = iminor(inode);
    struct miscdevice *c;
    int err = -ENODEV;
    const struct file_operations *old_fops, *new_fops = NULL;

    mutex_lock(&misc_mtx);

    // 遍历链表来查找真正的fops
    list_for_each_entry(c, &misc_list, list) {
        if (c->minor == minor) {
            new_fops = fops_get(c->fops);   // 获取真正的fops
            break;
        }
    }
        
    if (!new_fops) {
        mutex_unlock(&misc_mtx);
        request_module("char-major-%d-%d", MISC_MAJOR, minor);
        mutex_lock(&misc_mtx);

        list_for_each_entry(c, &misc_list, list) {
            if (c->minor == minor) {
                new_fops = fops_get(c->fops);
                break;
            }
        }
        if (!new_fops)
            goto fail;
    }

    err = 0;
    old_fops = file->f_op;
    // 替换真正的fops,之后再调用其他接口(write、ioctl、close)时调用的是真正的fops
    file->f_op = new_fops;
    // 调用真正的fops中的open方法
    if (file->f_op->open) {
        file->private_data = c;
        err=file->f_op->open(inode,file);
        if (err) {
            fops_put(file->f_op);
            file->f_op = fops_get(old_fops);
        }
    }
    fops_put(old_fops);
fail:
    mutex_unlock(&misc_mtx);
    return err;
}

遍历用来管理Misc设备驱动的链表,根据次设备号来找到真正的由驱动开发者用misc_register接口注册的Misc驱动,然后获取其fops,该fops就是真正的fops。然后替换真正的fops,之后再调用其他接口(write、ioctl、close)时调用的则是真正的fops,所以用来中转数据的那个fops只定义了一个open方法。最后,该open方法并不是真正的open方法,所以需要调用真正的fops中的open方法。

总结

Misc子系统使用同一个驱动来向上提供多个设备文件节点,向下控制多个(相应的)设备。Misc驱动本质上也是字符驱动,只是它借用Misc子系统的框架来更方便的向内核注册而已。驱动开发人员只需要把Misc设备的一些基本信息通过struct miscdevice来构建,再通过misc_register接口向内核注册即可。

本文作者: Ifan Tsai  (菜菜) 本文链接: https://www.caiyifan.cn/p/ac4a5699.html 版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!