Linux设备驱动模型

转自:zhuanlan.zhihu.com/p/410017057

1.设备驱动模型
在前面写的驱动中,我们发现编写驱动有个固定的模式只有往里面套代码就可以了,它们之间的大致流程可以总结如下:

  • 实现入口函数xxx_init()和卸载函数xxx_exit()
  • 申请设备号 register_chrdev_region()
  • 初始化字符设备,cdev_init函数、cdev_add函数
  • 硬件初始化,如时钟寄存器配置使能,GPIO设置为输入输出模式等。
  • 构建file_operation结构体内容,实现硬件各个相关的操作
  • 在终端上使用mknod根据设备号来进行创建设备文件(节点)
    (也可以在驱动使用class_create创建设备类、在类的下面device_create创建设备节点)

因此,在Linux开发驱动,只要能够掌握了这些“套路”,开发一个驱动便不是难事。但是,如果我们将硬件的信息都写进了驱动里了, 根据某个硬件编写的驱动只要修改了一下引脚接口,这个驱动代码就得重新修改才能使用,这显然是不合理的。

那有没有合适的解决方案呢?答案是肯定的:

Linux引入了设备驱动模型分层的概念, 将我们编写的驱动代码分成:设备与驱动。

  • 设备负责提供硬件资源
  • 驱动代码负责去使用这些设备提供的硬件资源
  • 总线将它们联系起来

这样子就构成以下图形中的关系:
在这里插入图片描述
在实际操作上:

  • 每次有新设备device添加时,bus就会去匹配合适的驱动
  • 每次有新驱动driver添加时,bus就会去匹配合适的设备
  • 这样device代表的硬件就和driver代表的软件解耦了

当然实际中,同一总线下的设备有很多,驱动也有很多,在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动, 同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。
在这里插入图片描述
在插入的同时总线会执行一个匹配方法对新插入的设备/驱动进行匹配,在匹配成功的时候会调用驱动中的初始化方法,在移除设备或驱动时会调用注销方法。

以上只是设备驱动模型的 机制 。

看到这里是对设备驱动模型的一个粗略的介绍,接下来我们结合代码看一下实现的细节。

2.总线
总线要干的核心工作就是:关联和匹配(match)

  • 关联设备链表和驱动链表
  • 为设备匹配合适的驱动
  • 为驱动匹配合适的设备

总线数据结构:

总线的类型也有很多种,在内核中使用结构体bus_type来表示总线,如下所示:

/* (内核源码/include/linux/device.h)*/
struct bus_type {
     const char              *name;
     const struct attribute_group **bus_groups; // 为bus目录创建属性
     const struct attribute_group **dev_groups; // 为device目录创建属性
     const struct attribute_group **drv_groups; // 为driver目录创建属性int (*match)(struct device *dev, struct device_driver *drv); // 匹配函数
     int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
     int (*probe)(struct device *dev);
     int (*remove)(struct device *dev);int (*suspend)(struct device *dev, pm_message_t state);
     int (*resume)(struct device *dev);const struct dev_pm_ops *pm;struct subsys_private *p; // 私有数据
};
  • name :指定总线的名称,当新注册一种总线类型时,会在/sys/bus目录创建一个新的目录,目录名就是该参数的值;
  • drv_groups、dev_groups、bus_groups
    :分别表示驱动、设备以及总线的属性。这些属性可以是内部变量、字符串等等。通常会在对应的/sys目录下在以文件的形式存在,对于驱动而言,在目录/sys/bus//driver/存放了设备的默认属性;设备则在目录/sys/bus//devices/中。这些文件一般是可读写的,用户可以通过读写操作来获取和设置这些attribute的值。
  • match:当向总线注册一个新的设备或者是新的驱动时,会调用该回调函数。该回调函数主要负责判断是否有注册了的驱动适合新的设备,或者新的驱动能否驱动总线上已注册但没有驱动匹配的设备;
  • uevent :总线上的设备发生添加、移除或者其它动作时,就会调用该函数,来通知驱动做出相应的对策。
  • probe :当总线将设备以及驱动相匹配之后,执行该回调函数,最终会调用驱动提供的probe函数。
  • remove :当设备从总线移除时,调用该回调函数;
  • suspend、resume:电源管理的相关函数,当总线进入睡眠模式时,会调用suspend回调函数;而resume回调函数则是在唤醒总线的状态下执行;
  • pm :电源管理的结构体,存放了一系列跟总线电源管理有关的函数,与device_driver结构体中的pm_ops有关;
  • p :该结构体用于存放特定的私有数据,其成员klist_devices和klist_drivers记录了挂载在该总线的设备和驱动;

P 指向了一个私有数据结构,我们看一下这个数据结构的定义:

/* (内核源码/include/linux/base.h)*/
struct subsys_private {
     struct kset subsys;         // bus 目录
     struct kset *devices_kset;  // device 目录
     struct list_head interfaces;
     struct mutex mutex;struct kset *drivers_kset;  // driver 目录
     struct klist klist_devices; // 设备链表头
     struct klist klist_drivers; // 驱动链表头
     struct blocking_notifier_head bus_notifier;
     unsigned int drivers_autoprobe:1;
     struct bus_type *bus;struct kset glue_dirs;
     struct class *class;
};
  • subsys:代表bus下的目录
  • devices_kset:指向xbus下的devices目录
  • drivers_kset:指向xbus下的drivers目录
  • klist_devices:链表头,指向设备链表
  • klist_drivers:链表头,指向驱动链表

两个关键的结构体,组成bus的基本数据结构:
在这里插入图片描述
总线操作:

总线不是凭空出来的,可以通过以下的函数注册新的总线和注销总线,原型如下:

3.设备
设备数据结构:

在内核使用device结构体来描述我们的物理设备,如下所示,

/* device结构体(内核源码/include/linux/device.h)*/
struct device {
     const char *init_name;
     struct device           *parent;
     struct kobject kobj;
     struct bus_type *bus;
     struct device_driver *driver;
     void            *platform_data;
     void            *driver_data;
     struct device_node      *of_node;
     dev_t                   devt;
     struct class            *class;
     void (*release)(struct device *dev);
     const struct attribute_group **groups;  /* optional groups */
     struct device_private   *p;
};
  • kobj:代表目录项
  • init_name:指定该设备的名称,总线匹配时,一般会根据比较名字来进行配对;
  • parent:表示该设备的父对象,前面提到过,旧版本的设备之间没有任何关联,引入Linux设备模型之后,设备之间呈树状结构,便于管理各种设备;
  • bus:表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。
  • of_node:存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table以及设备树的compatible属性进行比较之后,将匹配的节点保存到该变量。
  • platform_data:特定设备的私有数据,通常定义在板级文件中;
  • driver_data:同上,驱动层可通过dev_set/get_drvdata函数来获取该成员;
  • class:指向了该设备对应类,开篇我们提到的触摸,鼠标以及键盘等设备,对于计算机而言,他们都具有相同的功能,都归属于输入设备。我们可以在/sys/class目录下对应的类找到该设备,如input、leds、pwm等目录;
  • dev:dev_t类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys目录中导出对应的设备。
  • release:回调函数,当设备被注销时,会调用该函数。如果我们没定义该函数时,移除设备时,会提示“Device ‘xxxx’ does
    not have a release() function, it is broken and must be fixed”的错误。
  • group:指向struct attribute_group类型的指针,指定该设备的属性;
/* device_private 结构体(内核源码/include/linux/base.h) */
struct device_private {
     struct klist klist_children;
     struct klist_node knode_parent;
     struct klist_node knode_driver;
     struct klist_node knode_bus; // 链表头,关联总线的driver链表头
     struct list_head deferred_probe;
     struct device *device;
};

在这里插入图片描述
设备操作:

内核也提供相关的API来注册和注销设备,如下所示:

/* 内核注册/注销设备(内核源码/driver/base/core.c) */
int device_register(struct device *dev);
void device_unregister(struct device *dev);

在讲解总线的时候,我们说过,当成功注册总线时,会在/sys/bus目录下创建对应总线的目录,该目录下有两个子目录,分别是drivers和devices, 我们使用device_register注册的设备从属于某个总线时,该总线的devices目录下便会存在该设备文件。

4.驱动
前面两小节,已经大致介绍完总线以及设备。设备能否正常工作,取决于驱动。驱动需要告诉内核, 自己可以驱动哪些设备,如何初始化设备。

驱动数据结构:

在内核中,使用device_driver结构体来描述我们的驱动,如下所示:

/* device_driver 结构体(内核源码/include/linux/device.h) */
struct device_driver {
     const char      *name;
     struct bus_type     *bus;struct module       *owner;
     const char      *mod_name;  /* used for built-in modules */
 ​
     bool suppress_bind_attrs;   /* disables bind/unbind via sysfs */
     enum probe_type probe_type;const struct of_device_id   *of_match_table;
     const struct acpi_device_id *acpi_match_table;int (*probe) (struct device *dev);
     int (*remove) (struct device *dev);
     void (*shutdown) (struct device *dev);
     int (*suspend) (struct device *dev, pm_message_t state);
     int (*resume) (struct device *dev);
     const struct attribute_group **groups;const struct dev_pm_ops *pm;
     void (*coredump) (struct device *dev);struct driver_private *p;
};
  • name :指定驱动名称,总线进行匹配时,利用该成员与设备名进行比较;
  • bus :表示该驱动依赖于哪个总线,内核需要保证在驱动执行之前,对应的总线能够正常工作;
  • owner :表示该驱动的拥有者,一般设置为THIS_MODULE;
  • suppress_bind_attrs:布尔量,用于指定是否通过sysfs导出bind与unbind文件,bind与unbind文件是驱动用于绑定/解绑关联的设备。
  • of_match_table:指定该驱动支持的设备类型。当内核使能设备树时,会利用该成员与设备树中的compatible属性进行比较。
  • probe:当驱动以及设备匹配后,会执行该回调函数,对设备进行初始化。通常的代码,都是以main函数开始执行的,但是在内核的驱动代码,都是从probe函数开始的。
  • remove:当设备从操作系统中拔出或者是系统重启时,会调用该回调函数;
  • group:指向struct attribute_group类型的指针,指定该驱动的属性;
  • p:指向驱动的私有数据结构

driver_private 结构如下:

/* driver_private 结构体(内核源码/include/linux/base.h) */
struct driver_private {
     struct kobject kobj;
     struct klist klist_devices;
     struct klist_node knode_bus; // 链表头,关联总线的driver链表头
     struct module_kobject *mkobj;
     struct device_driver *driver;
};

完整的数据结构连起来如下:
在这里插入图片描述
驱动操作:

内核提供了driver_register函数以及driver_unregister函数来注册/注销驱动,成功注册的驱动会记录在/sys/bus//drivers目录, 函数原型如下所示:

/* device_driver结构体(内核源码/include/linux/device.h) */
int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);

5.注册总线、设备、驱动
总线、设备、驱动基本的数据结构明白了之后,接下来我们要通过系统提供的接口,完成总线、设备、驱动的构建和关联,大致分步骤如下:
在这里插入图片描述
系统启动之后会调用buses_init函数创建/sys/bus文件目录,这部分系统在开机时已经帮我们准备好了, 接下去就是通过总线注册函数bus_register进行总线注册,注册完总线后在总线的目录下生成devices文件夹和drivers文件夹, 最后分别通过device_register以及driver_register函数注册相对应的设备和驱动。

①:总线初始化

系统启动之后会调用 buses_init() 函数创建 /sys/bus 这个文件目录,这部分操作在系统开机时已经帮我们准备好了。

②:总线注册

系统中不一定有你需要的总线,linux提供了一些函数来添加或注销总线;大部分情况下编写linux驱动模块时,内核已经为我们写好了大部分总线驱动,正常情况下我们一般不会去注册一个新的总线。

总线注册和注销的函数原型如下:

int bus_register(struct bus_type *bus);

以注册xbus总线为例:

  • 创建/sys/bus/xbus目录,目录名xbus为我们新注册的总线名。
  • 创建/sys/bus/xbus/devices目录,并创建属性文件
  • 创建/sys/bus/xbus/drivers目录,并创建属性文件
  • 初始化 priv->klist_devices链表头
  • 初始化priv->klist_drivers链表头

③:设备注册

添加设备,关联硬件相关代码

int device_register(struct device *dev)
  • 创建/sys/bus/xbus/devices/yyy目录
  • 加入bus-> priv->devices_kset链表
  • 加入bus-> priv->klist_devices链表
  • 遍历bus-> priv->klist_drivers,执行bus->match()寻找合适的drv
  • dev关联driv,执行drv->probe()

④:驱动注册

添加驱动,关联软件相关代码

int driver_register(struct device_driver *drv)
  • 创建/sys/bus/xbus/drivers/zzz目录
  • 加入bus-> priv->drivers_kset链表
  • 加入bus-> priv->klist_drivers链表
  • 遍历bus-> priv->klist_klist_devices链表,执行bus->match()寻找合适的dev
  • dev关联dev,执行drv->probe()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值