驱动专题:第一章驱动框架 3. Linux设备驱动之Kobject/Kset2

Linux设备驱动之Kobject/Kset2 uevent mdev ktype type

LDD3中说,Kobject的作用为:

    1、sysfs 表述:在 sysfs 中出现的每个对象都对应一个 kobject, 它和内核交互来创建它的可见表述。

    2、热插拔事件处理 :kobject 子系统将产生的热插拔事件通知用户空间。
    3、数据结构关联:整体来看, 设备模型是一个极端复杂的数据结构,通过其间的大量链接而构成一个多层次的体系结构。kobject 实现了该结构并将其聚合在一起。 

    其中,第一条已经在前一篇文章中介绍过了,如果不了解请移驾 http://blog.csdn.net/lizuobin2/article/details/51523693

    此文,将从设备总线驱动模型里的设备注册过程, device_register函数入手,分析kobject、kset 在设备这一层面的体系结构,同时主要是分析uevent机制以及 mdev 如何自动创建设备节点,实现自己想要的一些功能,比如U盘自动挂载。


整个设备的起源,应该是/drives/base/core.c 在这里实现了一系列函数,并导出供我们使用。

        EXPORT_SYMBOL_GPL(device_for_each_child);
        EXPORT_SYMBOL_GPL(device_find_child);

        EXPORT_SYMBOL_GPL(device_initialize);
        EXPORT_SYMBOL_GPL(device_add);
        EXPORT_SYMBOL_GPL(device_register);

扫描二维码关注公众号,回复: 1919020 查看本文章

        EXPORT_SYMBOL_GPL(device_del);
        EXPORT_SYMBOL_GPL(device_unregister);
        EXPORT_SYMBOL_GPL(get_device);
        EXPORT_SYMBOL_GPL(put_device);

        EXPORT_SYMBOL_GPL(device_create_file);
        EXPORT_SYMBOL_GPL(device_remove_file);

 

在内核 do_base_setup 初始化的过程中调用driver_init函数,间接调用device_init函数,我们先来看看device_init函数。

    int __init devices_init(void)

    {
              devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);

        ……
    }

    创建kset 并 add 到内核里去,它的名字是devices,parent==NULL,devices_kset 对应于/sys/devices目录,device_uevent_ops后面分析。


    在设备总线驱动模型中,我们要构造一个 device 结构对象,设置它所属的总线(i2c_bus_type、platform_bus_type…),然后将它注册到内核中去,其中都避免不了调用 device_register 函数。现在我们来看 device_register

    int device_register(struct device *dev)
    {
        device_initialize(dev);
         //dev.devt = MKDEV(xxx, yyyy);  // 有些时候会提供设备的 主次设备号
        return device_add(dev);

         ......          

    }

    void device_initialize(struct device *dev)
    {
        dev->kobj.kset = devices_kset;   // 将设备的 kset 成员指向 devices_kset
        kobject_init(&dev->kobj, &device_ktype);   // 初始化 设备的 kobject 成员,并设置它的 Ktype 为 device_ktype,并没有add
        ......

    }

kobject_init(&dev->kobj, &device_ktype)需要注意。

    void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
    {
        kobject_init_internal(kobj);
        kobj->ktype = ktype;
    }

    static struct kobj_type device_ktype = {
        .release    = device_release,
        .sysfs_ops    = &dev_sysfs_ops, // 它有必要 看一看
    };

    static struct sysfs_ops dev_sysfs_ops = {
        . show    = dev_attr_show, // 后面分析 
        .store    = dev_attr_store,
    };


    int device_add(struct device *dev)
    {

         parent = get_device(dev->parent); // 如果 dev 指定了 parent ,注意别和 dev.kobject.parent 混淆了
 setup_parent(dev, parent); // 如果 parent 不是 NULL , dev.kobject = dev.parent.kobject 
         // 将 dev 的 kobject 成员链 parent  链表,如果 dev.kobject == null , 则链入 device_keset ,也就是在 devices 目录下创建目录
         error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);

          // 创建属性 dev 文件 ,后边我们会知道 cat dev 会得到设备号,供 mdev 来创建设备节点  
          if (MAJOR(dev->devt)) {
             error = device_create_file(dev, &devt_attr); // 值得一看

             error = device_create_sys_dev_entry(dev);
         }
         // 上报一个 KOBJ_ADD 事件
         kobject_uevent(&dev->kobj, KOBJ_ADD);   
    }

指定属性文件 是在 error = device_create_file(dev, &devt_attr);

    int device_create_file(struct device *dev, struct device_attribute *attr)
    {
        error = sysfs_create_file(&dev->kobj, &attr->attr);

    }

    devt_attr 定义 在 static struct device_attribute devt_attr =  __ATTR(dev, S_IRUGO, show_dev, NULL);

    #define __ATTR(_name,_mode,_show,_store) { \
        .attr = {.name = __stringify(_name), .mode = _mode },    \
        .show    = _show,                    \
        .store    = _store,                    \
    }

    将宏展开:

    static struct device_attribute devt_attr = {

        .attr = {.name = __stringify(dev),.mode = S_IRUGO},   // cat dev   dev的来源
        .show    = show_dev,   // 就一行 return print_dev_t(buf, dev->devt) 返回dev的设备号         
        .store    = NULL,

    }

    真正的属性文件是:attr = {.name = __stringify(dev),.mode = S_IRUGO},我们在用户空间cat dev的时候调用的是Kobject->ktye->show,也就是:

    static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr, char *buf)
    {
        struct device_attribute *dev_attr = to_dev_attr(attr); // 转换成上边devt_attr 的结构类型
        struct device *dev = to_dev(kobj);

        if (dev_attr->show)    // 调用 show_dev 传递 主次设备号 
            ret = dev_attr->show(dev, dev_attr, buf);
    }

真是大费周章~~,但是这里值得注意,dev 是内核帮我们自动创建的属性文件,根据它我们可以分析出很多东西

    1、device_ktype里提供的 show store 函数 仅仅是个通用接口,我们在用户空间访问属性文件时,首先会访问这两个函数

    2、show 与 store 会将 attr 转换为 device_attribute,再调用具体的 device->attr->show 或者 store 函数。

    3、我们想创建自己的属性文件,就需要提供 一个deivce_attribute 结构体,在里面提供 sttr show store ,这个device_attribute 往往在 dev->type->groups 里

    4、在devie_register 最后的过程中 device_add_attrs(dev) 会帮我们创建这些在 dev->type->groups里的属性


    至此,我们可以发现,今后每一个创建 device ,只要你调用 device_register ,device 的 Kobject 都将链入device_kset 链表,然后通过contain_of 函数,就可以实现对 device 的访问。也就是说 kobject 往往是嵌入在其他模块中,通过Kobject、kset之前的关系,实现对更大的模块的关系管理。 也就是我们说的 Kobject 作用的第三条。

可以总结一下 device_register 上面的工作了

    1、每一个 device 的kobkect都将被链入kset 链表

    2、每一个 device 的kobkect 在dev.parents 目录下创建子目录(如果没有parents 就在 devices 目录下创建)

    3、每一个 device 的kobkect 在它的目录下创建属性文件 dev

    4、每一个 device 的kobkect 的ktype 都被设置为device_ktype,这里提供通用的show与store接口,外部访问的时候会先将attr转换为device_attr,再调用具体的device_addr->show or stror 函数

    5、创建其它属性文件,依赖于 dev->type ,主要就是将 device_attribute 利用 device_creat_file 生成文件, device_attribute 里含有具体的show store函数。

    6、建立dev 到 bus  class 的符号连接


现在我们来看看 kobject_uevent

    int kobject_uevent(struct kobject *kobj, enum kobject_action action)
    {
        return kobject_uevent_env(kobj, action, NULL);
    }

    int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
               char *envp_ext[])
    {

         /* 如果kobject 不属于一个Kset,则向上查找到一个 属于一个kset的kobject为止 */
            top_kobj = kobj;
            while (!top_kobj->kset && top_kobj->parent)
            top_kobj = top_kobj->parent;

                kset = top_kobj->kset;    // 找到 最接近的 kset,这里就是device_kset

                uevent_ops = kset->uevent_ops;    // 获取 uevent_ops == device_uevent_ops

            // 如果 uevent_suppress 被设置 则屏蔽 uevent 
            // 如果设置了 filter 则用 filter 过滤事件,稍后我们会看,只要设置了 bus 或 class 的device 都会通过

            // 调用name函数得到subsystem的名字;否则,subsystem的名字是kset中kobject的名字
            if (uevent_ops && uevent_ops->name)
                subsystem = uevent_ops->name(kset, kobj);
            else
                subsystem = kobject_name(&kset->kobj);

         /*  申请env内存 */
            env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);

        /* 获取Path 也就是kobj的路径 /sys/devices/xxx */
            devpath = kobject_get_path(kobj, GFP_KERNEL);

        /*  设置环境变量 */
            retval = add_uevent_var(env, "ACTION=%s", action_string);  // KOBJ_ADD

            retval = add_uevent_var(env, "DEVPATH=%s", devpath);  // 路径

            retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);   // 子系统的名字

        // 如果 uevent_ops->uevent 存在则调用,显然存在,后面分析。
               if (uevent_ops && uevent_ops->uevent) {
                   retval = uevent_ops->uevent(kset, kobj, env);
               }

        retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)seq);
               // uevent_helper[0] == /sbin/mdev     这个是通过 /etc/ini.d/rcS 指定的
               if (uevent_helper[0]) {
                    char *argv [3];

                    argv [0] = uevent_helper;
                    argv [1] = (char *)subsystem;
                    argv [2] = NULL;
                    retval = add_uevent_var(env, "HOME=/");
                    retval = add_uevent_var(env,"PATH=/sbin:/bin:/usr/sbin:/usr/bin");

                    // 调用用户空间程序,程序名 argv[0], 并把环境变量当作参数传递过去

                    retval = call_usermodehelper(argv[0], argv, env->envp, UMH_WAIT_EXEC);
               }

    }

   显然 uevent 的机制,就是设置环境变量,然后调用用户空间程序 mdev 进行更新设备。


上面多次使用到了device_kset->uevent_fops,是时候来看看了(并没有什么卵用)

    static struct kset_uevent_ops device_uevent_ops = {
        .filter =        dev_uevent_filter, // 只要设置了bus or class 就不会过滤
        .name =      dev_uevent_name, // 返回bus or class的name
        .uevent =    dev_uevent,  // 设置主次设备号的环境变量
    };
    // 如果设置了总线 或 类 返回1
    static int dev_uevent_filter(struct kset *kset, struct kobject *kobj)
    {
        struct kobj_type *ktype = get_ktype(kobj);

        if (ktype == &device_ktype) {
            struct device *dev = to_dev(kobj);
            if (dev->bus)
                return 1;
            if (dev->class)
                return 1;
         }
    }
    // 返回 Bus  或 类的名字 
    static const char *dev_uevent_name(struct kset *kset, struct kobject *kobj)
     {
        struct device *dev = to_dev(kobj);

        if (dev->bus)
               return dev->bus->name;
        if (dev->class)
               return dev->class->name;
     }

    static int dev_uevent(struct kset *kset, struct kobject *kobj,
              struct kobj_uevent_env *env)
    {

        if (MAJOR(dev->devt)) {

            add_uevent_var(env, "MAJOR=%u", MAJOR(dev->devt));// 在环境变量中设置主次设备号
            add_uevent_var(env, "MINOR=%u", MINOR(dev->devt)); // 然而mdev并不是从这里读取的
            name = device_get_devnode(dev, &mode, &tmp);
            if (name) {
                add_uevent_var(env, "DEVNAME=%s", name);
            if (mode)
                add_uevent_var(env, "DEVMODE=%#o", mode & 0777);
        }
    }

    error = device_add_class_symlinks(dev); // 创建到 类 的符号连接

    error = device_add_attrs(dev); // 根据dev->type里提供的device_attribute信息,创建其它的属性文件

    error = bus_add_device(dev); // 创建到 bus 的符号链接

    return retval;

}

kobject_uevent的工作

    1、将 device 的 kobject 的PATH 、name、主次设备号等等设置到环境变量nev->nevp里

    2、调用用户空间 mdev

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    mdev 是啥,mdev 可以说是udev的精简版,在 busybox 制作文件系统的时候被编译进去,它主要的工作就是根据/sys 目录的信息来帮助我们自动创建设备节点,更详细的概念请自行百度。向要搞清,mdev 创建设备几点的过程,那只能看Busybox的源码了。。


附上一个我做实验时打印出来的环境变量

       env[0] ACTION=add
       env[1] DEVPATH=/devices/platform/myled
       env[2] SUBSYSTEM=platform
       env[3] MAJOR=251
       env[4] MINOR=0
       env[5] DEVNAME=myled
       env[6] MODALIAS=platform:myled
       env[7] SEQNUM=642
       env[8] HOME=/
       env[9] PATH=/sbin:/bin:/usr/sbin:/usr/bin

现在我们来看看,从内核空间调用的这个用户程序mdev

    int mdev_main(int argc, char **argv)
    {
        // mdev -s 开机扫描/sys 目录创建设备节点,这里不分析
        if (argc == 2 && !strcmp(argv[1],"-s")) {
            ......
        } else {
            action = getenv("ACTION");    // 获得action 就是add or remove
            env_path = getenv("DEVPATH"); // 获得DEVPATH
            if (!action || !env_path)
                bb_show_usage();

            sprintf(temp, "/sys%s", env_path);   // /sys+DEVPATH 比如/sys/devices/xxx
            if (!strcmp(action, "remove"))  // 移除 dev
                make_device(temp, 1);
            else if (!strcmp(action, "add")) { // 增加 dev 
                make_device(temp, 0);
            }
        }
    }

    static void make_device(char *path, int delete)
    {
        // 获取主次设备号,看看是如何获取的
        if (!delete) {
            // 在path 后边 + “/dev”  那么path == /sys/devices/xxx/dev
            strcat(path, "/dev");
            len = open_read_close(path, temp + 1, 64); //读dev 我们前边说过了,这里会调用show 传递主次设备号~
            *temp++ = 0;
            if (len < 1) return;
        }
    
        // 获得设备名字,根据最后一个"/"
        device_name = bb_basename(path);
    
        // 根据 path 的第五个字符来判断设备类型,如果是在/sys/class 目录的话 就是字符设备,其他的都是块设备
        type = path[5]=='c' ? S_IFCHR : S_IFBLK;
    
        // 如果 /etc/mdev.conf 有这个配置文件的话,根据配置文件的规则来 创建设备节点 并执行一些命令
        if (ENABLE_FEATURE_MDEV_CONF) {
            // 这个不如直接来看 mdev.conf 来得实在
            fd = open("/etc/mdev.conf", O_RDONLY);
            ......
        }

        if (!delete) {
            if (sscanf(temp, "%d:%d", &major, &minor) != 2) return;
            // mknod 创建设备节点
            if (mknod(device_name, mode | type, makedev(major, minor)) && errno != EEXIST)
                bb_perror_msg_and_die("mknod %s", device_name);

        }
    }

    关于 /etc/mdev.conf 真是太有用处了 ,附上韦东山老师 uevent 的文档,我就不卖弄了。

---------------------------------------------------------------------------------------------------------------------------------------------------------------

  1. 我接上U盘,想自动挂载,怎么办?
  2. mdev.conf的格式:
  3. <device regex> <uid>:<gid> <octal permissions> [<@|$|*> <command>]
  4. device regex:正则表达式,表示哪一个设备
  5. uid: owner
  6. gid: 组ID
  7. octal permissions:以八进制表示的属性
  8. @:创建设备节点之后执行命令
  9. $:删除设备节点之前执行命令
  10. *: 创建设备节点之后 和 删除设备节点之前 执行命令
  11. command:要执行的命令
  12. <span style= "white-space:pre"> // 韦东山老师写了个驱动,有 led led1 led2 led3 这四个设备</span>
  13. 写mdev.conf
  14. 1.
  15. leds 0: 0 777
  16. led1 0: 0 777
  17. led2 0: 0 777
  18. led3 0: 0 777
  19. 2.
  20. leds?[ 123]? 0: 0 777
  21. 3.
  22. leds?[ 123]? 0: 0 777 @ echo create /dev/$MDEV > /dev/console
  23. 4.
  24. leds?[ 123]? 0: 0 777 * if [ $ACTION = "add" ]; then echo create /dev/$MDEV > /dev/console; else echo remove /dev/$MDEV > /dev/console; fi
  25. 5.
  26. leds?[ 123]? 0: 0 777 * /bin/add_remove_led.sh
  27. 把命令写入一个脚本:
  28. add_remove_led.sh
  29. #!/bin/sh
  30. if [ $ACTION = "add" ];
  31. then
  32. echo create /dev/$MDEV > /dev/console;
  33. else
  34. echo remove /dev/$MDEV > /dev/console;
  35. fi
  36. 6. U盘自动加载
  37. sda[ 1 -9]+ 0: 0 777 * if [ $ACTION = "add" ]; then mount /dev/$MDEV /mnt; else umount /mnt; fi
  38. 7.
  39. sda[ 1 -9]+ 0: 0 777 * /bin/add_remove_udisk.sh
  40. add_remove_udisk.sh
  41. #!/bin/sh
  42. if [ $ACTION = "add" ];
  43. then
  44. mount /dev/$MDEV /mnt;
  45. else
  46. umount /mnt;
  47. fi

-------------------------------------------------------------------------------------------------------------------------------------------

附上一个 我做实验的代码,仅供参考,基于Linux2.6.32.2内核

1、平台总线设备,它的父设备是 platform_bus,按照我们的推测,它应该在出现在 /sys/devices/platform/ 目录下

  1. #include <linux/module.h>
  2. #include <linux/kernel.h>
  3. #include <linux/fs.h>
  4. #include <linux/init.h>
  5. #include <linux/device.h>
  6. #include <linux/interrupt.h>
  7. #include <linux/sched.h>
  8. #include <linux/irq.h>
  9. #include <asm/uaccess.h>
  10. #include <linux/input.h>
  11. #include <linux/platform_device.h>
  12. // 设备资源
  13. static struct resource led_resource[] = { //jz2440的参数,驱动未测试
  14. [ 0] = {
  15. .start = 0x56000010,
  16. .end = 0x56000010 + 8 - 1,
  17. .flags = IORESOURCE_MEM,
  18. },
  19. [ 1] = {
  20. .start = 5,
  21. .end = 5,
  22. .flags = IORESOURCE_IRQ,
  23. },
  24. };
  25. static void led_release(struct device *dev){
  26. }
  27. // 创建一个设备
  28. static struct platform_device led_dev = {
  29. .name = "myled", //设备名字 与 驱动相匹配
  30. .id = -1,
  31. .num_resources = ARRAY_SIZE(led_resource),
  32. .resource = led_resource,
  33. .dev = {
  34. .release = led_release,
  35. .devt = MKDEV( 252, 1),
  36. },
  37. };
  38. static int led_dev_init(void){
  39. //向bus注册led_dev match drv链表进行配对
  40. platform_device_register(&led_dev);
  41. return 0;
  42. }
  43. static void led_dev_exit(void){
  44. platform_device_unregister(&led_dev);
  45. }
  46. module_init(led_dev_init);
  47. module_exit(led_dev_exit);
  48. MODULE_LICENSE( "GPL");
打印出来的环境变量:

env[0] ACTION=add
env[1] DEVPATH=/devices/platform/myled
env[2] SUBSYSTEM=platform
env[3] MAJOR=251
env[4] MINOR=0
env[5] DEVNAME=myled
env[6] MODALIAS=platform:myled
env[7] SEQNUM=642
env[8] HOME=/
env[9] PATH=/sbin:/bin:/usr/sbin:/usr/bin



我们创建了 dev 并设置它的主次设备号,mdev 就自动为我们创建设备节点了~


2、什么都不依赖的单纯设备,它的父设备是NULL,按照我们的推测,它应该在出现在 /sys/devices/目录下

  1. /*
  2. * MINI2440 GPB5~8为LED引脚
  3. */
  4. #include <linux/module.h>
  5. #include <linux/kernel.h>
  6. #include <linux/fs.h>
  7. #include <linux/init.h>
  8. #include <linux/delay.h>
  9. #include <asm/uaccess.h>
  10. #include <asm/irq.h>
  11. #include <asm/io.h>
  12. #include <mach/regs-gpio.h>
  13. #include <mach/hardware.h>
  14. #include <linux/device.h>
  15. static int first_drv_open(struct inode *inode, struct file *file)
  16. {
  17. return 0;
  18. }
  19. static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
  20. {
  21. return 0;
  22. }
  23. static struct file_operations first_drv_fops = {
  24. .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
  25. .open = first_drv_open,
  26. .write = first_drv_write,
  27. };
  28. int major;
  29. static int first_drv_init(void)
  30. {
  31. major = register_chrdev( 0, "first", &first_drv_fops);
  32. struct device dev = {
  33. .init_name = "my_first_drv",
  34. .devt = MKDEV(major, 0),
  35. };
  36. device_register(&dev);
  37. return 0;
  38. }
  39. static void first_drv_exit(void)
  40. {
  41. unregister_chrdev(major, "first_drv"); // 卸载
  42. iounmap(gpbcon);
  43. }
  44. module_init(first_drv_init);
  45. module_exit(first_drv_exit);
  46. MODULE_LICENSE( "GPL");

最后,我们再来回顾一下整个流程~

kobject:

    1、每一个 device 的kobkect都将被链入kset 链表

    2、每一个 device 的kobkect 在 device.kobj.parent 目录下创建子目录

    3、每一个 device 的kobkect 在它的目录下创建属性文件 dev

    4、每一个 device 的kobkect 的ktype 都被设置为device_ktype,通过 show 可以访问到device的主次设备号

uevent:

    5、将 device 的 kobject 的PATH 、name、主次设备号等等设置到环境变量nev->nevp里

    6、调用用户空间 mdev

mdev:

    7、读取环境变量,创建设备节点~


    至此,我们应该对kobject的作用有了一个全面的了解。写的不好,还请多批评指正。

猜你喜欢

转载自blog.csdn.net/chichi123137/article/details/80946321