前言
现在来聊点原理性的东西——linux设备管理模型
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
行文目录
1. LDM数据结构介绍
linux
设备模型LDM
的上层依赖于总线、设备驱动程序、设备和类。
1.1 总线、设备、驱动关系
在LINUX驱动的世界里,所有的设备和驱动都是挂在总线上的,也就是总线来管理设备和驱动的,总线知道挂在它上边的所有驱动和设备的情况,由总线完成驱动和设备的匹配和探测。
总线上挂着驱动和设备,一个驱动可以管理多个设备,一个设备保存一个对应驱动的信息,一般在初始化的时候,总线先初始化,然后设备先注册,最后驱动去找设备,完成他们之间的衔接。
系统已经给我们准备好了我们所学要的总线。对于我们来说,就是去学好怎么在系统中添加设备以及相关的驱动就行了。
1.1.1 类
相关结构体:struct class
和 struct class_device
类发明来就是来管理设备的,是对设备的高级抽象,本质也是一个结构体,但是按照类的思想来组织成员的。运用class,可以让用户空间的程序根据自己要处理的事情来调用设备,而不是根据设备被接入到系统的方式或设备的工作原来调用。
一个struct class
结构体类型变量对应一个类,内核提供了class_create()
函数,可以用它来创建一个类,这个类存放于 sysfs 下面, 一旦创建了类,再调用 device_create() 函数在 /dev 目录下创建相应的设备节点。
1.1.2 设备
驱动中常写的struct device
是硬件设备在内核驱动框架中的抽象
使用device_register
函数向内核驱动框架注册一个设备,也可使用device_create
来创建,device_create
函数是对device_register
的封装。
通常device
不会单独使用,而是被包含在一个具体的设备结构体中
使用案例如下所示:
struct gpioled_dev {
dev_t devid;
struct cdev cdev;
struct class *class; // 定义一个class
struct device *device; // 定义一个device
int major;
int minor;
struct device_node *nd;
int led_gpio;
};
static int __init led_init(void)
{
......
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); // 先创建class
if (IS_ERR(newchrled.class)) {
return PTR_ERR(newchrled.class);
}
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME); // 再用class创建device
if (IS_ERR(newchrled.device)) {
return PTR_ERR(newchrled.device);
}
......
}
static void __exit led_exit(void)
{
......
device_destroy(newchrled.class, newchrled.devid); // 先释放device
class_destroy(newchrled.class); // 再释放class
}
1.1.3 驱动
struct device_driver
是驱动程序在内核驱动框架中的抽象
关键元素1:name
,驱动程序的名字,很重要,经常被用来作为驱动和设备的匹配依据
关键元素2:probe
,驱动程序的探测函数,用来检测一个设备是否可以被该驱动所管理
使用方式:
/*
* @description: 驱动程序的探测函数,检测设备是否被该驱动所管理,当驱动与设备匹配后此函数会执行
* @param-dev : platform设备
* @return : 0,成功;其他负值,失败
*/
static int led_probe(struct platform_device *dev)
{
int i = 0;
int ressize[5];
u32 val = 0;
struct resource *ledsource[5];
printk("led driver and device has matched!\r\n");
for (i = 0; i < 5; i++) {
ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
if (!ledsource[i]) {
dev_err(&dev->dev, "No MEM resource for always on\n");
return -ENXIO;
}
ressize[i] = resource_size(ledsource[i]);
}
IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]);
SW_MUX_GPIO1_IO3 = ioremap(ledsource[1]->start, ressize[1]);
SW_PAD_GPIO1_IO3 = ioremap(ledsource[2]->start, ressize[2]);
GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]);
GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
writel(5, SW_MUX_GPIO1_IO3);
writel(0x10b0, SW_PAD_GPIO1_IO3);
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
if (leddev.major) {
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
} else {
alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
leddev.major = MAJOR(leddev.devid);
leddev.minor = MINOR(leddev.devid);
}
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev, &led_fops);
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if (IS_ERR(leddev.class)){
return PTR_ERR(leddev.class);
}
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
if (IS_ERR(leddev.device)) {
return PTR_ERR(leddev.device);
}
return 0;
}
/*
* @description: remove函数,移除platform驱动的时候此函数会执行
* @param-dev : platfrom 设备
* @return : 0,成功;其他负值,失败
*/
static int led_remove(struct platform_device *dev)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO3);
iounmap(SW_PAD_GPIO1_IO3);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
cdev_del(&leddev.cdev);
unregister_chrdev_region(leddev.devid, LEDDEV_CNT);
device_destroy(leddev.class, leddev.devid);
class_destroy(leddev.class);
return 0;
}
static struct platform_driver led_driver = {
.driver = {
.name = "imx6ul-led2",
},
.probe = led_probe,
.remove = led_remove
};
1.2 一切皆文件
在Linux中,各种设备都是文件,不过此文件不对应磁盘上的数据文件,而是对应着存在内存当中的设备文件。实际上我们对设备文件进行操作就等于操作具体的设备。
Linux的设备驱动管理将面向对象的思想对各式各样的设备、总线、驱动进行管理:
总线(bus):负责管理挂载对应总线的设备以及驱动;
设备(device):挂载在某个总线的物理设备;
驱动(driver):与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
类(class):对于具有相同功能的设备,归结到一种类别,进行分类管理;
2. sysfs文件系统
2.1 sysfs介绍
sysfs是一种虚拟文件系统,旨在提供一种访问内核数据结构的方法,从而允许用户空间程序查看和控制系统的设备和资源。
sysfs文件系统通常被挂载在/sys目录下。sysfs提供了一种以树状结构组织的系统信息的方式,其中每个设备都有一个唯一的目录来表示它自己,其中包含有关设备的各种属性和状态信息的文件。这些文件通常是只读的,但有些也可以用于修改设备的某些设置。sysfs还提供了一个机制来通知用户空间程序有关设备状态更改的信息,从而使其能够对这些更改做出反应。sysfs文件系统被广泛用于Linux内核中,它为开发者提供了一种简单的方式来管理和控制系统中的各种设备和资源。
具体的理解如下所示:
kobject
和 kset
是构成 /sys
目录下的目录节点和文件节点的核心,也是层次化组织总线、设备、驱动的核心数据
结构,kobject、kset 数据结构都能表示一个目录或者文件节点。
2.2 kobjects
设备模型的核心部分是kobjects
,类似于java中object对象类,提供了诸如计数、名称、父指针等字段,可以创建对象的层次结构。其定义如下:
struct kobject {
const char *name; // 指向kobject名称,如果名称长度小于KOBJ_NAME_LEN,则存入name数组中,如果超过,则动态分配一个缓冲区存放,KOBJ_NAME_LEN是20个字节
struct list_head entry; // 一个Linux内核链表
struct kobject *parent; // 父对象,在内核中构造一个对象层次结构,并可以将多个对象间的关系表现出来
struct kset *kset; // 指向所属的kset
struct kobj_type *ktype;
struct kernfs_node *sd; // 指向sysfs文件系统目录项
struct kref kref; // 实现kobject的引用计数
unsigned int state_initialized:1; // 初始化状态
unsigned int state_in_sysfs:1; // 是否在sysfs中
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
};
name属性可以使用kobject_set_name(struct kobject* kobj,const char * name)
修改名称。
kobject通常是嵌入到其他结构体中的,其单独意义其实并不大。当kobject被嵌入到其他结构体中时,该结构体便拥有了kobject提供的标准功能。
kobject是设备模型的核心,在后台运行。它为内核带来类似于OO的编程风格,主要用于引用计数以及提供设备层次和它们之间的关系。kobject引入了通用对象属性(如使用引用计数)的封装概念。
每个内核设备直接或间接嵌入 kobject 属性。在添加到系统之前,必须使用 kobject_create()
函数分配kobject,该函数返回的空 kobject 必须用 kobject_init()
进行初始化,并将已经分配但尚未初始化的kobject指针及其 kobject_type
指针作为参数(知道有就行了,暂时用不到)。
struct kobject *kobject_create(void)
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
kobject_add()
函数用于添加 kobject 并将其链接到系统,同时根据其层次结构创建目录及其默认属性。功能与之相反的函数是kobject_del()
,也可以调用kobject_create_and_add
,它将内部调用kobject_create
和kobject_add
int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);
void kobject_del(struct kobject *kobj)
kobject * __must_check kobject_create_and_add(const char *name, struct kobject *parent);
2.2 ktype
ktype是为了描述一族kobject所具有的普遍特性。因此,不在需要每个kobject都分别定义自己的特性,而是将这些特性在ktype结构体中一次定义,然后所有“同类”的kobject都能共享一样的特性。
struct kobj_type {
void (*release)(struct kobject *);
struct sysfs_ops * sysfs_ops;
struct attribute ** default_attrs;
};
release指针指向在kobject引用计数减为零时要被调用的析构函数。该函数负责释放所有的kobject使用的内存和其他相关清理工作。
sysfs_ops变量指向sysfs_ops结构体。该结构体表述了sysfs文件读写是的特性。
default_attrs指向一个attribute结构体数组。这些结构体定义了该kobject相关的默认属性。属性描述了给定对象的特征,如果该kobject被导出到sysfs中,那么这些属性都将相应地作为文件而导出。数组中的最后一项必须为NULL。
2.3 kset
kset 是 kobject 对象的集合体。把它看成是一个容器,可将所有相关的 kobject 对象,比如“全部的块设备”置于一个位置。kset 把 kobject 集中到一个集合中,如下图所示:
kset结构体的定义如下所示:
struct kset {
struct list_head list; // 是一个linux内核链表,挂载kobject结构的链表
spinlock_t list_lock; // 保护链表访问的自旋锁
struct kobject kobj; // 自身包含一个kobject结构体
const struct kset_uevent_ops *uevent_ops;
};
每个注册的(添加到系统中的)kset对应于sysfs目录。可以使用kset_create_and_add()
函数创建和添加kset(知道有就行了,暂时用不到)。
struct kset *kset_create_and_add(const char *name, const struct kset_uevent_ops *uevent_ops, truct kobject *parent_kobj)
在这个目录下面的每一个子目录,其实都是相同类型的kobject集合。然后不同的kset组织成树状层次的结构,就构成了sysfs子系统。
3. 设备驱动模型
3.1 总线
3.1.1 总线介绍
总线不仅仅是组织设备和驱动的容器,还是同类设备的共有功能的抽象层。此总线可以是实际总线,如IIC,SPI等,也可以是虚拟总线,如platform总线。bus_type
结构体定义在include/linux/device.h
中。
struct bus_type {
const char *name; // 总线名称
const char *dev_name; // 总线对应设备名称
struct device *dev_root; // 该总线对应的device
struct device_attribute *dev_attrs; /* use dev_groups instead */
const struct attribute_group **bus_groups; // 总线属性
const struct attribute_group **dev_groups; // 设备属性
const struct attribute_group **drv_groups; // 驱动属性
int (*match)(struct device *dev, struct device_driver *drv); // match接口,用于进行device与driver的匹配
int (*uevent)(struct device *dev, struct kobj_uevent_env *env); // uevent接口,用于发送kobject event,供应用层mdev/udev使用
int (*probe)(struct device *dev); // 总线的probe接口,该接口会调用具体驱动的probe接口
int (*remove)(struct device *dev); // 总线的remove接口,一般该接口主要是调用具体驱动的remove接口
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p; // 该接口包含了该bus对应object、device对应的kset、driver对应的kset、链接所有设备、驱动的链表
struct lock_class_key lock_key;
};
struct bus
结构管理挂载在该总线下的struct device
和struct device_driver
,负责device和device_driver的匹配,调用probe等工作。
这其中有一个子系统叫subsys_private
,由管理struct device和struct device_driver的功能独立出来的。该结构管理该bus下的设备和驱动,还用于处理bus,device和device_driver的一些默认属性,uevent的事件等。
3.1.2 subsys_private
这是总线的驱动核心的私有数据,其数据结构下有两个链表,一个用于挂载device,另一个用于挂载device_dirver,从而实现总线对device和device_driver的管理。
struct subsys_private {
struct kset subsys; // 定义这个子系统的kset
struct kset *devices_kset; // 该总线的“设备”目录,包含所有的设备
struct list_head interfaces; // 总线相关接口的列表
struct mutex mutex; // 保护设备和接口列表
struct kset *drivers_kset; // 该总线的驱动目录,包含所有的驱动
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; // 指向这个结构所关联结构的指针
};
// 通过kobject找到对应的subsys_private
#define to_subsys_private(obj) container_of(obj, struct subsys_private, subsys.kobj)
整个bus_type结构如下所示:
3.2 设备
device结构体用于抽象驱动设备,系统下挂载的形形色色的设备都是通过struct device
结构体来描述,其中dts里定义的很多节点最终都会转为struct device
结构体,用于描述一个设备信息,管理设备用到的资源等。device结构体定义在"include/linux/device.h"
中:
struct device {
struct device *parent;
struct device_private *p; // 设备的私有数据
struct kobject kobj;
const char *init_name; // 设备初始化名字
const struct device_type *type;
struct mutex mutex; /* mutex to synchronize calls to
* its driver.
*/
struct bus_type *bus; // 指向设备设备所属总线
struct device_driver *driver; // 指向设备的驱动
void *platform_data; /* Platform specific data, device
core doesn't touch it */
void *driver_data; /* Driver data, set and get with
dev_set/get_drvdata */
struct dev_pm_info power;
struct dev_pm_domain *pm_domain;
...
};
device结构体下一个重要结构是device_private
,该结构体成员knode_bus就是用于挂载到上面提到的bus下的subsys_private结构体中的klist_devices。
struct device_private {
struct klist klist_children;
struct klist_node knode_parent;
struct klist_node knode_driver;
struct klist_node knode_bus;
struct list_head deferred_probe;
struct device *device;
};
3.3 驱动
device_driver
结构体用于描述对struct device
结构体描述的驱动方法,比如对于通信协议的实现,对控制器的操作等。这样设备和设备的驱动实现分离单独管理,而设备和驱动分离后两者的匹配工作就是bus完成的。device_driver
是用户需要编写的具体操作设备的方法和流程。
struct device_driver {
const char *name; // 驱动名称
struct bus_type *bus; // 该驱动所依附的总线
struct module *owner; // 该驱动所属module
const char *mod_name; // 模块的名称
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
const struct of_device_id *of_match_table; // 设备树使用的设备id
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev); // 该驱动的探测接口
int (*remove) (struct device *dev);
// shutdown、suspend、resume主要是对应电源管理方面的接口
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups; // 该驱动所相关的属性接口,主要用于与sysfs模型管理
const struct dev_pm_ops *pm; // 性能管理相关的内容
struct driver_private *p; // 该驱动模块相关的私有变量,主要包括驱动对应的kobject、所属模块的kobject等
};
同样struct device_driver
结构体下的driver_private
的knode_bus用于链接到struct bus
下subsys_private
结构体中的klist_drivers
。device_private
如下所示:
struct device_private {
struct klist klist_children;
struct klist_node knode_parent;
struct klist_node knode_driver;
struct klist_node knode_bus;
struct list_head deferred_probe;
struct device *device;
};
4. Linux内核中三者的关系
系统启动后,会调用buses_init()
函数创建/sys/bus
文件目录,这部分系统开机时已经帮我们准备好了。
接下来就是通过总线注册函数bus_register()
进行总线注册,注册完成后,在/sys/bus目录下生成device文件夹和driver文件夹,最后分别通过device_register()
和driver_register()
函数注册对应的设备和驱动。过程如下所示:
参考资料
[1] linux设备模型
[2] Linux设备模型
[3] Linux设备驱动模型
[4] device_create()、device_register()、deivce_add()区别