Linux设备驱动模块自加载示例与原理解析

来源:互联网 发布:新塘沙埔菜鸟网络招聘 编辑:程序博客网 时间:2024/06/03 20:37

本文介绍Linux设备驱动模块在设备注册时如何实现自动加载和创建设备节点。
在Linux系统中,基于sysfs文件系统、设备驱动模型和udev工具可以实现在设备模块“冷、热”加载时自动加载设备对应的驱动程序,同时可以按需在/dev目录下创建设备节点。
本文中我搭建好环境并写了两个简单的示例程序demo_device.c和device_driver.c来模拟“设备”与“驱动”的自动加载和设备节点自动创建的过程。最后通过内核源代码来理解其中的原理知识。
实验环境:

内核版本:Linux-3.12.35
工具:(1)udev:udev-137
            (2)交叉编译工具:arm-bcm2708-linux-gnueabi-
示例环境:(1)宿主机:x86(CentOS6.6)
             (2)单板:树莓派b

一、示例演示
1、准备工作(环境搭建)

(1)首先配置内核,确认其支持NETLINK和inotify:

[*] Networking support --->

Networking options  --->

                   <*> Unixdomain sockets

File systems  --->

[*] Inotify support for userspace  

udev使用inotify机制监测udev的规则文件是否发生变化,udev和内核驱动模块之间的uevent交互使用socket,需要内核支持NETLINK。
(2)文件系统支持proc和sysfs文件系统

udev需要sysfs文件系统的支持,同时后续用到的lsmod命令是通过proc和sysfs文件系统获取内核模块信息的,所以需要支持proc和sysfs,最简单的做法可以在init脚本中添加:

mount -n -t proc proc/proc

mount -n -t sysfssysfs /sys

(3)文件系统中安装有udevd、udevadm和驱动加载规则文件

[apple@appleraspberry]$ ls initramfs/lib/udev/rules.d/

80-drivers.rules

[apple@apple raspberry]$ lsinitramfs/bin/udev*

initramfs/bin/udevadm initramfs/bin/udevd

80-drivers.rules是加载驱动的规则,udevadm用于对冷插拔设备模拟热插拔,udevd是udev守护程序,实时监测内核向用户发送的uevent以及rules的变更。
2、示例程序demo_device.c和device_driver.c的源码如下:

demo_device.c:

#include <linux/module.h>#include <linux/types.h>#include <linux/fs.h>#include <linux/init.h>#include <linux/platform_device.h>#include <linux/device.h>#include <linux/io.h>/* 平台设备demo_device */static struct platform_device demo_device = {.name= "demo_device",.id    = -1,};/* 模块初始化函数 */static int __init demo_device_init(void){ /* 注册平台设备 */printk(KERN_INFO "Demo Device Register\n");    platform_device_register(&demo_device);    return 0;}/* 模块去初始化函数 */static void __exit demo_device_exit(void){/* 注销平台设备 */printk(KERN_INFO "Demo Device UnRegister\n");    platform_device_unregister(&demo_device);}module_init(demo_device_init);module_exit(demo_device_exit);MODULE_LICENSE("GPL"); 
由于嵌入式设备驱动较多使用Platform总线,所以这里的demo_device使用Platform总线(其他实体总线如PCI、USB等原理类似)。这里第14行的init函数负责在加载模块时注册demo_device设备,第21行函数负责在模块卸载时注销demo_device设备。第10行的name用于platform设备与驱动的匹配。

demo_driver.c:

#include <linux/module.h>#include <linux/types.h>#include <linux/fs.h>#include <linux/init.h>#include <linux/platform_device.h>#include <linux/device.h>#include <linux/io.h>#include <linux/cdev.h>#include <linux/kmod.h>#include <linux/of_platform.h>#define DEVICE_NAME     "demo_dev"static struct cdev demo_dev;static dev_t ndev;static struct class *demo_class = NULL;/* 字符设备read接口 */static ssize_t demo_read (struct file *filp, char __user *buf, size_t sz, loff_t *off){printk(KERN_INFO "Demo Dev Read\n");return 0;}/* 字符设备open接口 */static int demo_open(struct inode *nd, struct file *filp){printk(KERN_INFO "Demo Dev Open\n");return 0;}/* 字符设备close接口 */static int demo_close (struct inode *nd, struct file *filp){printk(KERN_INFO "Demo Dev Close\n");return 0;}struct file_operations demo_ops ={.owner = THIS_MODULE,.open = demo_open,.release = demo_close,.read = demo_read,};/* 平台驱动probe接口 */static int demo_driver_probe(struct platform_device *pdev){int ret;/* 注册字符设备 */cdev_init(&demo_dev, &demo_ops);ret = alloc_chrdev_region(&ndev, 0, 1, DEVICE_NAME);if(0 > ret){printk(KERN_INFO "Alloc Chrdev Region Fail\n");return ret;}printk(KERN_INFO "Demo Driver: Major=%d Minor=%d Cdev Register\n", MAJOR(ndev), MINOR(ndev));ret = cdev_add(&demo_dev, ndev, 1);if(0 > ret){printk(KERN_INFO "Cdev Add Fail\n");return ret;}/* (创建设备节点) */device_create(demo_class, NULL, ndev, NULL, DEVICE_NAME);return 0;}/* 平台驱动remove接口 */static int demo_driver_remove(struct platform_device *pdev){printk(KERN_INFO "Demo Driver: Cdev UnRegister\n");device_destroy(demo_class, ndev);cdev_del(&demo_dev);unregister_chrdev_region(ndev, 1);return 0;}/* 平台驱动demo_driver */static struct platform_driver demo_driver = {.driver= {.name    = "demo_device",   .owner = THIS_MODULE,},.probe   = demo_driver_probe,.remove  = demo_driver_remove,};/* 模块初始化函数 */static int __init demo_driver_init(void){ /* 创建设备类demo_dev */demo_class = class_create(THIS_MODULE, DEVICE_NAME);if(IS_ERR(demo_class)){printk(KERN_INFO "Class Create Fail\n");return -1;}/* 加载平台驱动 */printk(KERN_INFO "Demo Driver Register\n");    platform_driver_register(&demo_driver);    return 0;}/* 模块去初始化函数 */static void __exit demo_driver_exit(void){/* 注销设备类demo_dev */class_destroy(demo_class);/* 注销平台驱动 */printk(KERN_INFO "Demo Driver UnRegister\n");    platform_driver_unregister(&demo_driver);}module_init(demo_driver_init);module_exit(demo_driver_exit);MODULE_LICENSE("GPL");  MODULE_ALIAS("platform:demo_device");

第89行的init函数用于在加载驱动模块时注册demo_driver驱动,同时创建demo_class(用于自动生成设备节点),第104行的exit函数用于在卸载驱动模块时注销demo_driver驱动;第82行的name和demo_device中的一样,用于和platform设备进行匹配;第44行的probe函数在设备和驱动匹配后会执行,它的主要工作是像内核添加字符设备cdev,同时调用device_create()函数创建设备节点;第18、24和30行是标准的文件操作接口,这里就不详细介绍了。第118行指定了驱动模块的驱动别名,后面详细介绍它的作用。

3、编译与安装demo_device和demo_driver

编译使用的Makefile如下:

ifneq ($(KERNELRELEASE),)obj-m := demo_device.o demo_driver.oelseKDIR := /home/apple/raspberry/build/linux-rpi-3.12.yall:make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-bcm2708-linux-gnueabi-modules_install:make -C $(KDIR) M=$(PWD) modules_install ARCH=arm CROSS_COMPILE=arm-bcm2708-linux-gnueabi-clean:rm -f *.ko *.o *.mod.o *.mod.c *.symvers  modul*endif
其中KDIR指向的是内核源码目录,然后编译与安装:
[apple@apple udev_test]$ ls
demo_device.c  demo_driver.c Makefile
[apple@apple udev_test]$ make
[apple@apple udev_test]$ make modules_install  INSTALL_MOD_PATH=/home/apple/raspberry/sysroot/kernel
其中INSTALL_MOD_PATH是内核安装目录,编译与安装完成后,会将编译生成的demo_device.ko和demo_driver.ko两个文件拷贝到安装目录下的lib/modules/3.12.35/extra/中;
[apple@apple extra]$ ls~/raspberry/sysroot/kernel/lib/modules/3.12.35/extra/
demo_device.ko  demo_driver.ko
同时安装module时会调用depmod工具,更新lib/modules/3.12.35/目录下的modules.alias、modules.alias.bin、modules.dep和modules.dep.bin文件。其中modules.dep和modules.dep.bin记录了模块的依赖关系,在调用modprobe命令加载模块时会用到他(们),由于这里的两个demo模块并不依赖于其他模块,所以他们的依赖为空:
extra/demo_driver.ko:
extra/demo_device.ko:
其中modules.alias、modules.alias.bin记录了驱动模块中记录的驱动别名,在自动加载驱动程序是会用到他(们),查看modules.alias文件中已经添加上了刚才安装的demo_device:
alias platform:demo_devicedemo_driver
也可以通过modinfo命令来查看demo_driver.ko驱动模块的驱动别名:
[apple@apple extra]$ modinfo -F alias demo_driver.ko
platform:demo_device
4、手动加载demo_device.ko时自动加载demo_driver.ko并创建设备节点
首先将demo_device.ko、demo_driver.ko、modules.alias.bin和modules.dep.bin拷贝到目标单板的根文件系统相应目录下(路径需要一致):
[apple@appleraspberry]$ cp modules/3.12.35/modules.alias.bin initramfs/lib/modules/3.12.35/
[apple@appleraspberry]$ cp modules/3.12.35/modules.dep.bin initramfs/lib/modules/3.12.35/
[apple@apple raspberry]$cp modules/3.12.35/extra/demo* initramfs/lib/modules/3.12.35/extra/
启动单板系统,在udevd服务进程启动后手动加载demo_device.ko模块(如果没有启动则使用命令“udevd –daemon”启动):
bash-4.3# modprobe demo_device
[   68.634194] Demo Device Register
[   68.655775] Demo Driver Register
[   68.659129] Demo Driver: Major=248 Minor=0Cdev Register
从内核在终端的输出可以看到不仅仅demo_device.ko被加载了,同时demo_driver.ko也被加载了,设备和驱动正确的匹配上了,查看/dev目录下的设备文件也生成了:
bash-4.3# ls -l /dev/demo*
crw------- 1 0 0 248, 0Jan  1 00:00 /dev/demo_dev
bash-4.3# cat /dev/demo_dev
[  517.527372] Demo Dev Open
[  517.530122] Demo Dev Read
[  517.532878] Demo Dev Close
demo_dev的驱动接口能够正常的被调用并执行。
5、启动udev前“冷加载"demo_device设备时自动加载demo_driver.ko并创建设备节点
Linux中存在一些特别的设备驱动,其设备被编译入内核,在内核启动阶段被加载;但是其驱动程序被编译为模块,在系统启动以后自动识别设备的类型并加载对应的驱动程序,例如Linux发行版中的主硬盘存储设备(内核启动时,PCI子系统初始化,枚举总线上的的设备)。
这种情况在嵌入式设备中倒是不常见,但其本质上就是在系统启动udev服务进程前向内核注册设备但不加载驱动,在udev进程启动后该如何为这些设备匹配并加载驱动。这里的关键就要靠udevadm工具了。
首先启动单板系统,我这里修改了启动init脚本,不启动udevd服务进程。然后加载demo_devices.ko:
bash-4.3# modprobe demo_device
[   27.812482] Demo Device Register
可以看出在没有启动udevd服务进程的情况下,这里仅仅加载了设备模块。接下来启动udev服务进程:
bash-4.3# udevd --daemo
 [  454.385623] udevd[47]: starting version 173
启动完udev守护进程后,udev就可以监听来自内核的uevent事件了,接下来该udevadm出场了:
bash-4.3# udevadm trigger --action=add
[  645.329345] Demo Driver Register
[  645.470937] Demo Driver: Major=248 Minor=0Cdev Register
bash-4.3# ls -l /dev/demo*
crw------- 1 0 0 248, 0Jan  1 00:10 /dev/demo_dev
bash-4.3# cat /dev/demo_dev
[  713.227184] Demo Dev Open
[  713.229928] Demo Dev Read
[  713.232678] Demo Dev Close

在执行“udevadm trigger --action=add”命令后,驱动程序同前面“热”加载设备一样,被自动的识别、加载和匹配上了。

二、原理解析

         设备驱动“自动加载匹配”的根本是基于Linux设备驱动模型,需要sysfs文件系统和udev工具的支持。其主要流程如下图所示:

   

要分析它的原理还是先从示例程序demo_device.c切入,查看设备注册后内核做了哪些工作,如何向用户空间发送注册消息;然后来分析udev接收该消息后又如何找到对应的驱动程序模块demo_driver.ko并加载它;最后分析驱动和设备的绑定以及设备节点(/dev/demo_device)是如何自动生成的。

1、demo_device设备的注册。

demo_device.c中将设备挂载了Platform总线上,这是一根虚拟的总线,在内核启动时调用platform_bus_init()进行初始化:

int __init platform_bus_init(void){int error;early_platform_cleanup();error = device_register(&platform_bus);if (error)return error;error =  bus_register(&platform_bus_type);if (error)device_unregister(&platform_bus);return error;}
在demo_device模块的初始化函数中调用platform_device_register()注册平台设备:

/** * platform_device_register - add a platform-level device * @pdev: platform device we're adding */int platform_device_register(struct platform_device *pdev){device_initialize(&pdev->dev);arch_setup_pdev_archdata(pdev);return platform_device_add(pdev);}

这里主要关注device_initialize和platform_device_add,首先3965行的device_initialize初始化其中的device结构体,这里只需要关注下面这两句:

dev->kobj.kset = devices_kset;kobject_init(&dev->kobj, &device_ktype);

这两句初始化了device结构体的kset指针为devices_kset(用于触发uevent),以及ktype指针为devices_ktype(用于sysfs文件操作),这里先记下,后面会用到。

回到platform_device_register函数第397行的platform_device_add(),它将demo_device结构注册进内核,注册的步骤如下:

if (!pdev->dev.parent)pdev->dev.parent = &platform_bus;pdev->dev.bus = &platform_bus_type;

这里将device结构的父设备设为platform_bus(用于sysfs目录结构),这个结构前面platform_bus_ini中已经看到过了,同时设置device结构的总线bus_type为platform_bus_type(用于匹配驱动程序)。

case PLATFORM_DEVID_NONE:dev_set_name(&pdev->dev, "%s", pdev->name);break;

接下来设置devices结构的name为其父设备的name,这里就是“demo_device”

ret = device_add(&pdev->dev);if (ret == 0)return ret;
接下来就到了最为关键的device_add,它完成了核心部分的注册,包括在sysfs文件系统中创建目录和文件、向用户层发送uevent消息以及尝试匹配驱动程序等功能,来看一下其中关心部分的代码:

parent = get_device(dev->parent);kobj = get_device_parent(dev, parent);if (kobj)dev->kobj.parent = kobj;....../* first, register with generic layer. *//* we require the name to be set before, and pass NULL */error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);if (error)goto Error;

此处首先设置demo设备kobject结构parent指针为它父设备的的kobject,就是前面platform_bus的kobject。由于kobject在sysfs系统中的体现就是目录(目录名为kobject的name,父目录就是parent->kobject的name),所以在kobject_add被调用后,sysfs文件系统的/sys/devices/platform目录下生成demo_device目录:

bash-4.3# ls/sys/devices/platform | grep demo

demo_device

error = device_create_file(dev, &dev_attr_uevent);if (error)goto attrError;
这里会在/sys/devices/platform/demo_device/目录下创建一个名为uevent的文件,dev_attr_uevent是一个device_attribute结构体,它使用宏生成,所以这里比较难找,分别在core.c、device.h和sysfs.h中有:

static DEVICE_ATTR_RW(uevent);#define DEVICE_ATTR_RW(_name) \struct device_attribute dev_attr_##_name = __ATTR_RW(_name)#define __ATTR_RW(_name) __ATTR(_name, (S_IWUSR | S_IRUGO),\ _name##_show, _name##_store)#define __ATTR(_name, _mode, _show, _store) {\.attr = {.name = __stringify(_name), .mode = _mode },\.show= _show,\.store= _store,\}

将这个宏展开后形式如下:

struct device_attribute dev_attr_uevent = {.attr = {.name = “uevent”,.mode = S_IWUSR | S_IRUGO;}.show = uevent_show,.store = uevent_store,};
注意,这里的_uevent_store函数就是前面“冷”加载设备后还能再次触发uevent并加载驱动的关键所在。
device_create_file函数调用sysfs_create_file在sysfs文件系统的demo设备的kobject之时的目录下(即/sys/devices/platform/demo_device/)创建了名为uevent的文件:

bash-4.3# ls/sys/devices/platform/demo_device/ueve*

/sys/devices/platform/demo_device/uevent

if (MAJOR(dev->devt)) {error = device_create_file(dev, &dev_attr_dev);if (error)goto ueventattrError;error = device_create_sys_dev_entry(dev);if (error)goto devtattrError;devtmpfs_create_node(dev);}

回到device_add函数中,这里如果设置了设备的主设备号,则为该设备在/sysfs/dev/char目录下创建一个符号链接,同时如果内核启用了devtmpfs则会在/dev目录下创建设备节点。显然现在这个分支并不会走到,但是最终加载完驱动后是会走进去执行的。

kobject_uevent(&dev->kobj, KOBJ_ADD);bus_probe_device(dev);
这里kobject_uevent函数会向用户空间发送KOBJ_ADD类型的uevent消息,执行如下流程:kobject_uevent(&dev->kobj, KOBJ_ADD)->kobject_uevent_env(kobj,action, NULL),进入kobject_uevent_env()函数后,执行如下主要流程:
top_kobj = kobj;while (!top_kobj->kset && top_kobj->parent)top_kobj = top_kobj->parent;kset = top_kobj->kset;uevent_ops = kset->uevent_ops;
kobject_uevent_env()函数首先找到device->kobject顶层kobject的kset,然后获取其中的uevent_ops,前面已经看到这里的kest即devices_kset,devices_kset结构在内核初始化时调用devices_init(void)函数初始化并绑定了uevent_ops为device_uevent_ops:
static const struct kset_uevent_ops device_uevent_ops = {.filter =dev_uevent_filter,.name =dev_uevent_name,.uevent =dev_uevent,};

这里的dev_uevent函数会往发向用户空间的uevent消息中添加和设备驱动相关的变量。继续往下分析:

/* environment buffer */env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);if (!env)return -ENOMEM;....../* default keys */retval = add_uevent_var(env, "ACTION=%s", action_string);if (retval)goto exit;retval = add_uevent_var(env, "DEVPATH=%s", devpath);if (retval)goto exit;retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);if (retval)goto exit;

kobject_uevent_env()函数接下来为uevent创建分配buffer,然后往其中添加3个默认变量:ACTION、DEVPATH和SUBSYSTEM。这里的ACTION是add,DEVPATH是/devices/platform/demo_device/,SUBSYSTEM是所在的总线platform。

if (uevent_ops && uevent_ops->uevent) {retval = uevent_ops->uevent(kset, kobj, env);if (retval) {pr_debug("kobject: '%s' (%p): %s: uevent() returned " "%d\n", kobject_name(kobj), kobj, __func__, retval);goto exit;}}

这里就让kset的uevent_ops->uevent函数继续添加它所需要的uevent变量,这里就调用前面的dev_uevent函数,来看一下:

/* add device node properties if present */if (MAJOR(dev->devt)) {const char *tmp;const char *name;umode_t mode = 0;kuid_t uid = GLOBAL_ROOT_UID;kgid_t gid = GLOBAL_ROOT_GID;add_uevent_var(env, "MAJOR=%u", MAJOR(dev->devt));add_uevent_var(env, "MINOR=%u", MINOR(dev->devt));name = device_get_devnode(dev, &mode, &uid, &gid, &tmp);if (name) {add_uevent_var(env, "DEVNAME=%s", name);if (mode)add_uevent_var(env, "DEVMODE=%#o", mode & 0777);if (!uid_eq(uid, GLOBAL_ROOT_UID))add_uevent_var(env, "DEVUID=%u", from_kuid(&init_user_ns, uid));if (!gid_eq(gid, GLOBAL_ROOT_GID))add_uevent_var(env, "DEVGID=%u", from_kgid(&init_user_ns, gid));kfree(tmp);}}......if (dev->driver)add_uevent_var(env, "DRIVER=%s", dev->driver->name);....../* have the bus specific function add its stuff */if (dev->bus && dev->bus->uevent) {retval = dev->bus->uevent(dev, env);if (retval)pr_debug("device: '%s': %s: bus uevent() returned %d\n", dev_name(dev), __func__, retval);}/* have the class specific function add its stuff */if (dev->class && dev->class->dev_uevent) {retval = dev->class->dev_uevent(dev, env);if (retval)pr_debug("device: '%s': %s: class uevent() " "returned %d\n", dev_name(dev), __func__, retval);}/* have the device type specific function add its stuff */if (dev->type && dev->type->uevent) {retval = dev->type->uevent(dev, env);if (retval)pr_debug("device: '%s': %s: dev_type uevent() " "returned %d\n", dev_name(dev), __func__, retval);}
这里的if分支现在不会执行到,在设备拥有主设备号后会添加MAJOR、MINOR、DEVNAME等uevent变量,然后如果已经绑定了设备驱动则会添加DRIVER变量,最后如果设备有指定总线bus_type和class,则会调它们各自的uevent函数来继续追加uevent变量,这里已经绑定了bus_type为platform_bus_type,则调用的uevent函数就是platform_uevent:

static int platform_uevent(struct device *dev, struct kobj_uevent_env *env){struct platform_device*pdev = to_platform_device(dev);int rc;/* Some devices have extra OF data and an OF-style MODALIAS */rc = of_device_uevent_modalias(dev, env);if (rc != -ENODEV)return rc;add_uevent_var(env, "MODALIAS=%s%s", PLATFORM_MODULE_PREFIX,pdev->name);return 0;}
该函数向uevent消息中追加变量MODALIAS,那这里就是“MODALIAS=platform:demo_device”,这条先记住,后面会用到。
#if defined(CONFIG_NET)......skb = alloc_skb(len + env->buflen, GFP_KERNEL);......retval = netlink_broadcast_filtered(uevent_sock, skb,    0, 1, GFP_KERNEL,    kobj_bcast_filter,    kobj);

接着回到kobject_uevent_env()函数中,如果已经配置了NET条件编译选项,则内核使用netlink机制实现向用户空间的uevent消息发送,在本文最开始部分已经使能了这一配置,所以uevent就在这里发出了,内部的细节就不详细分析了。

到这里为止,demo_device设备就注册完成了。

2、udev接收设备注册的uevent消息并加载驱动

前面讲到demo_device设备注册完成后会向用户空间发送uevent消息。而在用户空间就由udev来接收和处理。udev工具非常强大,它能够加载驱动模块,规定如何给设备节点命名、建立符号链接,设备加载或删除时执行指定的程序等。udev工具的核心是服务进程udevd,当udevd进程启动后,它将默认从/lib/udev/rules.d目录下加载和分析规则文件(可以在编译时指定目录,用户自行定义的规则在/etc/udev/rules.d目录下),然后udevd进程监听来自内核空间的uevent消息,它会对uevent消息内部的每个变量进行识别和匹配,并按照规则文件做出相应的动作。

此时需要分两种情况来讨论:

(1)情况一:udevd服务进程已经启动并接收到了内核发出的uevent消息。

这种情况下,udevd进程会按照规则文件来处理uevent消息,在前文中搭建环境时我往rules.d拷贝了规则文件80-drivers.rules,udevd就会按照它来加载驱动先来看一下80-drivers.rules:

ACTION=="remove", GOTO="drivers_end"DRIVER!="?*", ENV{MODALIAS}=="?*", RUN+="/sbin/modprobe -bv $env{MODALIAS}"......LABEL="drivers_end"

这里如果uevent的ACTION类型为remove则直接退出,什么都不执行,如果uevent中的DRIVER环境变量为空且MODALIAS不为空,则执行以下命令:
/sbin/modprobe –bv $env{MODALIAS}
在前文加载demo_device时,已经看到他的uevent消息包含了MODALIAS变量,内容就是“platform:demo_device”,那么这里执行的命令就转换为如下形式:
/sbin/modprobe –bvplatform:demo_device
但是这platform:demo_device又是什么东西,其实它就是驱动别名,来看demo_driver.c中的最后一行:
 MODULE_ALIAS("platform:demo_device");
通过MODULE_ALIAS指定了驱动的别名(对于那种支持多种设备的驱动程序则一般包含一个device_table,通过MODULE_DEVICE_TABLE宏来指定一组驱动别名,见后面参考文献)。这其实就可以理解为一种将驱动别名和驱动模块的“绑定”。
前文中已经看到在/lib/modules/3.12.35/modules.alia文件中包含了:
alias platform:demo_device demo_driver
这里调用modprobe加载 platform:demo_device其实就是加载demo_driver.ko。
至此,在udevd服务进程已经启动的情况下,设备驱动程序被正确的加载了。
(2)情况二:udevd服务进程没有启动,错过了该消息。
有如下一种场景:内核启动时会枚举存储设备,然后挂载initramfs,然而此时的udevd进程尚未启动,也就无法接受uevent消息而加载存储设备的驱动,最终导致启动失败。针对这一情况,除了事先指定以外又该如何加载合适的驱动程序呢。
其实此时udevadm工具就派上用场了,前文的示例中,在中启动udevd服务进程后手动执行如下命令来试内核再次向用户空间发送KOBJ_ADD类型的uevent消息:
udevadm trigger--action=add
这条命令其实会遍历sysfs文件系统devices下的uevent文件(/sys/devices/***/uevent),然后写入字符串“add”(手动执行echoadd > /sys/devices/***/uevent也可以达到同样的效果)。

这里会向/sys/devices/platform/demo_device/uevent文件写入“add”,前文中已经看到这个文件是在注册demo_devices是调用platform_device_register()->platform_device_add()->device_add()->device_create_file()->sysfs_create_file创建的。另外已经绑定demo_device ->device->kobject->ktype 为device_ktype:

static struct kobj_type device_ktype = {.release= device_release,.sysfs_ops= &dev_sysfs_ops,.namespace= device_namespace,};
在向/sys/devices/platform/demo_device/uevent文件写入“add”时会首先open这个文件,经过系统调用后将调用到sysfs文件系统的open接口sysfs_open_file():

/* every kobject with an attribute needs a ktype assigned */if (kobj->ktype && kobj->ktype->sysfs_ops)ops = kobj->ktype->sysfs_ops;buffer = kzalloc(sizeof(struct sysfs_buffer), GFP_KERNEL);if (!buffer)goto err_out;buffer->ops = ops;file->private_data = buffer;
sysfs_open_file()函数首先找到kobject->ktype(这里即device_ktype),找到sysfs_ops(这里即dev_sysfs_ops);然后为sysfs_buffer指针buffer分配空间并将刚才找到的sysfs_ops保存到buffer->ops中,最后将整个buffer保存到file结构体中(内核会为每一个打开的文件动态生成一个文件描述符fd和file结构体),以后对这个文件的read、write系统调用就会调用file->private_data->ops中的store函数指针,这里就是dev_attr_store(),来看一下这个函数:

static ssize_t dev_attr_store(struct kobject *kobj, struct attribute *attr,      const char *buf, size_t count){struct device_attribute *dev_attr = to_dev_attr(attr);struct device *dev = kobj_to_dev(kobj);ssize_t ret = -EIO;if (dev_attr->store)ret = dev_attr->store(dev, dev_attr, buf, count);return ret;}

这里找到attr属性文件所属的device_attribute中指定的store接口,前文中创建uevent属性文件是就已定义为uuevent_store:

static ssize_t uevent_store(struct device *dev, struct device_attribute *attr,    const char *buf, size_t count){enum kobject_action action;if (kobject_action_type(buf, count, &action) == 0)kobject_uevent(&dev->kobj, action);elsedev_err(dev, "uevent: unknown action-string\n");return count;}
这里通过kobject_action_type对输入的“add”进行验证与匹配,最终匹配到的action为KOBJ_ADD,于是调用kobject_uevent(&dev->kobj, KOBJ_ADD);发送KOBJ_ADD类型的uevent事件。后续的工作就同“情况一”一样了。
到这里为止,demo_device设备的驱动模块demo_driver.ko就被正确的识别并加载了。
 
3、驱动匹配并创建设备节点。
在驱动模块demo_driver.ko加载后,驱动模块执行了哪些工作来确保加载的驱动模块就能和demo_device设备匹配,如果能够匹配又该如何创建设备节点,向用户空间提供操作驱动的接口。下面来分析demo_driver.c:
在驱动模块的init函数中调用platform_driver_register函数(其实是一个宏)加载驱动,来看一下这个函数:
int __platform_driver_register(struct platform_driver *drv,struct module *owner){drv->driver.owner = owner;drv->driver.bus = &platform_bus_type;if (drv->probe)drv->driver.probe = platform_drv_probe;if (drv->remove)drv->driver.remove = platform_drv_remove;if (drv->shutdown)drv->driver.shutdown = platform_drv_shutdown;return driver_register(&drv->driver);}EXPORT_SYMBOL_GPL(__platform_driver_register);
该函数首先设置driver->bus_type为platform_bus_type,然后记下driver结构中的几个函数指针,其中probe函数将在platform设备和platform驱动匹配后被调用。然后调用driver_register继续完成注册:
other = driver_find(drv->name, drv->bus);if (other) {printk(KERN_ERR "Error: Driver '%s' is already registered, ""aborting...\n", drv->name);return -EBUSY;}ret = bus_add_driver(drv);if (ret)return ret;

这个函数中首先查找总线上是否有相同名字的driver以防止重复注册驱动,然后主要关注bus_add_driver,它完成了核心部分的注册和与设备的匹配工作:

if (drv->bus->p->drivers_autoprobe) {error = driver_attach(drv);if (error)goto out_unregister;}

如果drivers_autoprobe置位则会调用driver_attach进行匹配工作,这里platform_bus_type在初始化时已经置位了,进入driver_attach():

int driver_attach(struct device_driver *drv){return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);}
这将轮询总线上挂载的设备然后调用__driver_attach()函数:
static inline int driver_match_device(struct device_driver *drv,      struct device *dev){return drv->bus->match ? drv->bus->match(dev, drv) : 1;}
__driver_attach()函数函数首先调用driver_match_devices()进行匹配工作:
static int platform_match(struct device *dev, struct device_driver *drv){struct platform_device *pdev = to_platform_device(dev);struct platform_driver *pdrv = to_platform_driver(drv);/* Attempt an OF style match first */if (of_driver_match_device(dev, drv))return 1;/* Then try ACPI style match */if (acpi_driver_match_device(dev, drv))return 1;/* Then try to match against the id table */if (pdrv->id_table)return platform_match_id(pdrv->id_table, pdev) != NULL;/* fall-back to driver name match */return (strcmp(pdev->name, drv->name) == 0);}

这里会尝试使用多种匹配方式,我这里使用最“古老”的方式,那就是设备和驱动的名字,由于demo_device和demo_driver的名字都是“demo_device”,所以匹配成功。和设备匹配成功后所要做的第一件事情就是调用驱动初始化函数对设备进行初始化工作。

if (!dev->driver)driver_probe_device(drv, dev);

回到__driver_attach()函数,接着由于设备尚未绑定驱动,所以调用driver_probe_device->really_probe()来绑定设备并调用驱动的probe函数,完成驱动的初始化:

dev->driver = drv;......if (dev->bus->probe) {ret = dev->bus->probe(dev);if (ret)goto probe_failed;} else if (drv->probe) {ret = drv->probe(dev);if (ret)goto probe_failed;}

really_probe()首先将driver和devices绑定,然后就可以看到这里调用了driver->probe函数,于是demo_driver.c中的demo_driver_probe()函数被调用了。函数返回后,bus_add_driver()函数进行剩余的初始化工作,platform_driver_register就将demo_driver注册成功了,同时也对设备进行了绑定。

下面回到demo_driver.c的init函数中,这里除了注册驱动以外,其实还做了一件事,那就是创建了一个class,这是在后面为probe函数中调用device_create创建设备节点做准备工作。class是一种对devices更高层次的抽象,用来作为同类功能设备的一个容器(由此可见Linux设备驱动模型已经将C语言实现面向对象的思想发挥的淋漓尽致)。这里并不是重点,来简单看一下就可以了:

#define class_create(owner, name)\({\static struct lock_class_key __key;\__class_create(owner, name, &__key);\})
这里调用class来生成类对象demo_class,

cls = kzalloc(sizeof(*cls), GFP_KERNEL);if (!cls) {retval = -ENOMEM;goto error;}cls->name = name;cls->owner = owner;cls->class_release = class_create_release;retval = __class_register(cls, key);if (retval)goto error;

首先为class对象分配空间,然后为name、owner等字段赋值,接着调用__class_register开始向内核注册:

cp = kzalloc(sizeof(*cp), GFP_KERNEL);if (!cp)return -ENOMEM;......error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);
首先为私有数据对象subsys_private分配空间,然后设置kobj的name字段为class的nema,这里就是“demo_dev”。
cp->subsys.kobj.kset = class_kset;        ......cp->subsys.kobj.ktype = &class_ktype;cp->class = cls;cls->p = cp;

然后为kobj指定kset为class_kset(在内核初始化时调用class_init创建),同时指定ktype为class_ktype。

error = kset_register(&cp->subsys);if (error) {kfree(cp);return error;}
这里注册kset,至此将在/sys/class目录下生成新创建的demo_dev目录。
bash-4.3# ls -l/sys/class/
drwxr-xr-x 2 0 0 0 Jan 1 02:14 demo_dev

回到demo_driver.c程序中,前面在注册驱动程序的过程中,已经调用了驱动的probe函数demo_driver_probe(),在该函数中创建了一个字符设备,它的主设备号由内核分配,然后调用device_create()函数,该函数将通过udev或devtmpfs动态创建设备节点,来看一下这个函数:

struct device *device_create(struct class *class, struct device *parent,     dev_t devt, void *drvdata, const char *fmt, ...){va_list vargs;struct device *dev;va_start(vargs, fmt);dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);va_end(vargs);return dev;}

该函数入参class指针指定为刚才在init函数中创建的demo_class,parent指针和drvdata指针为空,devt为字符设备的主设备号,最后fmt为字符设备名“demo_dev”。继续进入device_create_vargs()->device_create_groups_vargs():

struct device *dev = NULL;int retval = -ENODEV;if (class == NULL || IS_ERR(class))goto error;dev = kzalloc(sizeof(*dev), GFP_KERNEL);if (!dev) {retval = -ENOMEM;goto error;}dev->devt = devt;dev->class = class;dev->parent = parent;dev->groups = groups;dev->release = device_create_release;dev_set_drvdata(dev, drvdata);
这里首先为新创建的devices结构分配内存,然后初始化变量,包括设置主设备号、class、parent等等。
retval = kobject_set_name_vargs(&dev->kobj, fmt, args);if (retval)goto error;retval = device_register(dev);if (retval)goto error;return dev;

这里首先为新分配的device->kobj添加名字,然后调用device_register注册该新生成的device结构:

int device_register(struct device *dev){device_initialize(dev);return device_add(dev);}EXPORT_SYMBOL_GPL(device_register);
这里的两个函数前面已经看到过了,其中device_initialize就不再分析了,但是device还要再进入看一下,这里有3点需要注意:(1)device没有挂载到任何的总线上,所以他的bus指针为空,(2)device的父设备没有指定,所以他的parent指针也为空。(3)device指定了class,因此这里将会走另一个分支。
parent = get_device(dev->parent);kobj = get_device_parent(dev, parent);if (kobj)dev->kobj.parent = kobj;

这里的parent指针为空,而kobj的父指针会在get_device_parent()中指定,而他里面会根据parent、bus和class的有无来走不同的分支,在前文中注册demo_device时由于已经执行了parent所以直接返回parent的kobj,但是这里只有class非空,这里会走入以下分支:

if (dev->class) {struct kobject *kobj = NULL;struct kobject *parent_kobj;struct kobject *k;......if (parent == NULL)parent_kobj = virtual_device_parent(dev);....../* find our class-directory at the parent and reference it */spin_lock(&dev->class->p->glue_dirs.list_lock);list_for_each_entry(k, &dev->class->p->glue_dirs.list, entry)if (k->parent == parent_kobj) {kobj = kobject_get(k);break;}spin_unlock(&dev->class->p->glue_dirs.list_lock);if (kobj) {mutex_unlock(&gdp_mutex);return kobj;}/* or create a new class-directory at the parent device */k = class_dir_create_and_add(dev->class, parent_kobj);/* do not emit an uevent for this simple "glue" directory */mutex_unlock(&gdp_mutex);return k;

这里会为parent_kobj创建出一个名为“virtual”kobject(如果已经有了则直接返回赋值),它的父指针为devices_kset->kobj。然后开始遍历class->p->glue_dirs.list查找是否已经add了父设备为“virtual”的kobject,由于class是新建的,因此这里当然不会找到,这里的调用class_dir_create_and_add创建之,就会在/sys/devices/virtual/目录下创建demo_dev目录。

error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);if (error)goto Error;

回到device_add函数,在选择好了父kobject后,这里会在/sys/devices/virtual/demo_dev/创建demo_dev目录。

error = device_create_file(dev, &dev_attr_uevent);if (error)goto attrError;
这里在/sys/devices/virtual/demo_dev/demo_dev/目录下创建了uevent属性文件,同样绑定读写函数为uevent_show和uevent_store。如果cat这个uevent文件会打印出uevent消息的各个变量:
bash-4.3# cat uevent
MAJOR=248
MINOR=0
DEVNAME=demo_dev
可见,我环境中内核自动分配的主设备号位248。
if (MAJOR(dev->devt)) {error = device_create_file(dev, &dev_attr_dev);if (error)goto ueventattrError;error = device_create_sys_dev_entry(dev);if (error)goto devtattrError;devtmpfs_create_node(dev);}
继续device_add()往下分析,这时由于已经分配了主设备号,所以这个分支就可以去了,首先会在/sys/devices/virtual/demo_dev/demo_dev目录下创建dev属性文件,并且绑定读函数为dev_show(只读属性文件,没有绑定写函数):
bash-4.3# ls -ldev

-r--r--r-- 1 0 0 4096 Jan  1 03:32 dev

static ssize_t dev_show(struct device *dev, struct device_attribute *attr,char *buf){return print_dev_t(buf, dev->devt);}
这个函数就是打印出设备的主次设备号:
bash-4.3# cat dev
248:0

接着如果内核已经支持了devtmpfs就会穿件设备节点,但是后面可以看到udevd也会创建设备节点,其实devtmpfs和udev并不冲突,udev可以在devtmpfs上进行用户空间的各种修饰。

kobject_uevent(&dev->kobj, KOBJ_ADD);
继续device_add()往下分析,又看到了kobject_uevent函数,这次它向用户空间发送的uevent消息就和前面的“demo_devices”有所不同了。
/* search the kset we belong to */top_kobj = kobj;while (!top_kobj->kset && top_kobj->parent)top_kobj = top_kobj->parent;......kset = top_kobj->kset;uevent_ops = kset->uevent_ops;

这里同样是找到kobj所属的kset和其中的uevent_ops,此时的kset也是devices_kset,参照前文的流程分析,这里调用dev_uevent后会进入下面的分支:

/* add device node properties if present */if (MAJOR(dev->devt)) {const char *tmp;const char *name;umode_t mode = 0;kuid_t uid = GLOBAL_ROOT_UID;kgid_t gid = GLOBAL_ROOT_GID;add_uevent_var(env, "MAJOR=%u", MAJOR(dev->devt));add_uevent_var(env, "MINOR=%u", MINOR(dev->devt));name = device_get_devnode(dev, &mode, &uid, &gid, &tmp);if (name) {add_uevent_var(env, "DEVNAME=%s", name);if (mode)add_uevent_var(env, "DEVMODE=%#o", mode & 0777);if (!uid_eq(uid, GLOBAL_ROOT_UID))add_uevent_var(env, "DEVUID=%u", from_kuid(&init_user_ns, uid));if (!gid_eq(gid, GLOBAL_ROOT_GID))add_uevent_var(env, "DEVGID=%u", from_kgid(&init_user_ns, gid));kfree(tmp);}}

为uevent消息添加MAJOR和MINOR变量(前面都uevent文件时已经看到了),结束了之后依然是通过netlink向用户空间udevd服务程序发送socket消息。udev在解析了之后就发现uevent变量中包含了设备号,则通过系统调用mknod创建设备节点。至此设备文件就可以正常操作,用户可以通过它和设备驱动进行交互了。

 

参考文献:
1.《深入Linux设备驱动内核机制》

2.《深度探索Linux操作系统——系统构建和原理解析》


0 0
原创粉丝点击