1 概述
第一部分先简单介绍下字符型设备 + platform总线 + sysfs设备模型的关系。
1.1 . 字符设备驱动
Linux设备驱动分三种,包括字符设备驱动、块设备驱动和网络设备驱动。字符设备只能按字节流先后顺序访问设备内存,不能随机访问。鼠标、触摸屏、LCD等是字符设备的代表。块设备可以随机访问设备内存的任意地址,硬盘、SD卡、NAND FLASH是块设备的代表。网络设备指的是网卡一类使用socket套接字进行通信的设备。
字符设备 驱动 纵向关系 应用层访问设备驱动即是通过open接口来最终获得设备驱动的操作接口集struct file_opertions.而open接口传入的参数是/dev目录下的设备名。而从可以知道,设备名对应的设备文件节点inode会存储设备号,而驱动框架中的**全局数组cdev_map(msm-kernel/drivers/base/map.c)**则维护设备号和file_opertions的关系。
即应用层到底层的关系主要是: 设备名-->设备号-->file_opertions Open函数返回的局部fd和file_opertions的关系(忽略进程数据结构)如下: fd-->file(当前进程数据结构成员)-> file_opertions
这样,通过 fd 即可以获得file_opertions,即可以通过read、write等接口来调用驱动的读操作函数和写操作函数、ioctl函数等。
字符设备 驱动 的任务 1)字符设备驱动最本质的任务应该是提供file_opertions的各个open、read、write、ioctl等接口的实现。
(这里和我们cat echo节点不太一样) 2)另外从以上的描述中,为了让应用层能够调用到底层的file_opertions还涉及到以下: i. 申请设备号,并将设备号和file_opertions注册(cdev_add接口)到驱动框架中的cdev_map数组。。
common-gki/fs/char_dev.c
ii. 在/dev目录中创建设备文件。
创建设备文件 一种方法就是用户在shell中使用mknod命令创建设备文件,同时传入设备名和设备号。这是人工的做法,很不科学。但它是一种演示的方法。 另外一种方法就是依赖设备模型来辅助创建设备文件。这也是设备模型的作用之一。
字符设备 驱动 编程流程 1)定义struct file_opertions my_fops并实现其中的各个接口,如open、read、write、ioctl等接口。 2)实现驱动的入口函数,如chardev_init。
3)module_init(chardev_init);//宏定义该初始化入口函数。卸载流程不做解释。 4)insmod加载这个module后,可以人工在shell命令行利用mknod创建设备文件。 5)应用层即可以用open来打开设备文件来进行访问了。
1.2. 设备驱动模型
我们主要谈及设备驱动模型在linux驱动中的作用和角色。设备驱动模型 侧重于内核对总线、设备和驱动的管理,并向应用层暴露这些管理的信息,而字符设备驱动 则侧重于设备驱动的功能实现。 设备驱动模型的作用 1)设备驱动模型实现uevent机制,调用应用层的medv来自动创建设备文件。。
2)设备驱动模型通过sysfs文件系统向用户层提供设备驱动视图,如下。
上图只是可视化的一种表达,用户可以通过命令窗口利用ls命名逐级访问/sys文件夹来获得各种总线、设备、驱动的信息和关系。 可以看出,在/sys顶级目录,有三个关键的子目录:总线、设备、设备类。 设备是具体的一个个设备,在/sys/devices/是创建了实际的文件节点。而其他目录,如设备类和总线以下的子目录中出现的设备都是用符号链接指向/sys/devices/目录下的文件。
设备类 是对/sys/devices/下的各种设备进行归类,以体现一类设备的公共属性,如鼠标和触摸屏都是属于input设备类。 总线目录是总线、设备、驱动模型的核心目录。因为设备和驱动都是依附在某种总线上的,如USB、PCI和平台总线等。设备和驱动正是依靠总线的管理功能才能找到对方,如设备注册到总线时去寻找驱动,而驱动注册的时候去寻找其能够支持的设备。
最重要的是,如果没有设备驱动模型,那应用层很难知晓驱动和设备的关系,因为字符设备驱动并没有提供这些信息,那对于设备驱动的管理者而言会非常麻烦。 事实上,内核中的总线bus(struct bus_type)、设备device(struct device)和驱动device_driver(struct device_driver)都不会将所有的信息暴露给用户层,例如这三个数据结构都有对应的private数据结构,它用于内核对上下级总线设备驱动的链表关系维护。
3)设备驱动模型提供各种对象实例的引用计数,防止对象被应用层误删。设备模型的所有数据结构均是继承kobject而来,而kobject就提供基础的计数功能。
4)设备驱动模型提供一种访问设备的方式给应用层,用户和内核可以通过sysfs进行交互,如通过修改/sys目录下设备的文件内容,即可以直接修改设备对应的参数。
设备 驱动 模型的核心接口
设备驱动模型是一种集合, *bus_register(struct bus_type bus) //注册总线 device_add(struct device dev ) //注册设备 driver_register(struct device_driverdrv) //注册驱动 class_create(owner, name) //创建设备类 等等
1.3. sysfs文件系统
1. sysfs文件系统和设备 驱动 模型的关系 Sysfs文件系统是设备驱动模型得以向用户暴露其管理信息的载体。它们之间的关系如下: 1)设备驱动模型的上下级关系(如子设备和所属父设备)通过sysfs文件系统的父目录和子目录来体现。 2)设备驱动模型的平级关系(如设备类管理的设备和具体的设备的关系)则通过sysfs文件系统的目录符号链接来实现。 3)设备驱动模型的属性(如设备的参数和设备名,设备号等)则通过sysfs文件系统的文件内容来记录实现。 4)设备驱动模型数据结构中的kobject对应于sysfs文件系统中的目录,而数据结构中的struct attribute成员则对应于sysfs文件系统中的文件。对应的意思是指含有struct kobject的device、device_driver和bus等在向系统注册的过程中会调用sysfs的create_dir 接口来创建对应的目录,而含有struct attribute成员属性的device、device_driver和bus等在向系统注册的过程中则会调用sysfs的sysfs_create_file接口来创建文件。
2. sysfs核心接口 sysfs_create_file(struct kobject * kobj,const struct attribute * attr)创建属性文件 sysfs_create_dir (struct kobject * kobj)创建目录 **int sysfs_open_file(struct inode inode,struct file file)打开sysfs文件系统格式的文件 ***sysfs_read_file(struct file file, char__user buf, size_t count, loff_t ppos) 读操作 ***sysfs_write_file(struct file file, constchar __user buf, size_t count, loff_t ppos) 写操作
3. sysfs文件系统与属性文件读写 sysfs_read_file是sysfs文件系统的读写入口,但是驱动需要向系统提供真正的读写操作,也即是struct sysfs_ops数据结构中的show和store接口。
Sysfs是基于内存的文件系统,掉电即消失,sysfs所有的操作接口均是对内存中的内核数据结构进行访问操作。 假如用户用cat命令去读取一个属性文件(如dev)的内容,那么会产生以下流程: 1)fd=open("dev")->vfs_open("dev")->sysfs_open("dev")获取该文件的句柄 2)read()->vfs_read()->sysfs_read_file()->sysfs_ops->show()该show接口即是设备在注册时产生属性文件,并向系统提供该文件的读接口。而读接口的实现中自然是对该属性参数的读访问。 /sys挂载了sysfs文件系统,因此所有对/sys目录下的文件或者目录的操作都会通过sysfs文件的接口进行访问。
1.4. platform平台设备驱动
平台设备驱动中的"平台"指的是平台总线,即platform_bus_type,是linux众多总线中的一种,如USB总线、PCI总线、I2C总线等等。只不过平台总线是一种虚拟的总线,专门用来管理SOC上的控制器(如看门狗、LCD、RTC等等),它们都是CPU的总线上直接取址访问设备的。而USB、PCI等设备都有通过特定的时序来访问SOC芯片以外的设备。平台设备驱动体现的关系是设备驱动模型上的一个子集,将平台视为一种总线的概念,那两者的关系就会容易理解。
1. 平台设备 驱动 和设备驱动模型的关系 1)平台设备驱动接口在设备驱动模型视图上创建了相关的平台设备类(/sys/class)、平台总线(/sys/bus/platform)、平台设备(/sys/devices/). 2)平台设备(platform_device)和平台设备驱动(platform_driver)均注册到平台总线上 ,即在/sys/bus/platform/目录下创建相应的设备和驱动目录。 3)平台总线负责匹配注册到其上面的设备和驱动,匹配成功后回调用驱动的probe接口。 4)平台设备驱动利用设备驱动模型接口来辅助 创建对应的设备文件(位于/dev/目录下)。(辅助,不一定真创建) 相关的接口包括: *platform_device_register(structplatform_device pdev) 注册平台设备 *platform_driver_register(structplatform_driver drv) 注册平台设备驱动
2. 平台设备 驱动 和字符设备驱动的关系 我们假设这个平台设备是字符设备。 平台设备驱动和字符设备驱动的关系始于驱动的probe接口,即在probe接口中实现字符设备驱动所要完成的任务,即通过alloc_chrdev_region申请设备号和通过cdev_add注册驱动的struct file_opertions.另外为了自动创建应用层访问的设备文件,以及设备名设备号添加到proc里
还要调用1,class_create和2,device_create接口在平台设备类下创建对应的设备类和设备,并发出uevent事件,调用mdev来创建设备文件。
mdev是busybox提供的一个工具,在嵌入式系统中,相当于简化版的udev,作用是:在系统启动、热插拔和动态加载驱动程序时,自动创建设备节点。文件系统中的/dev目录下的设备节点都是由mdev创建的。在加载驱动过程中,根据驱动程序,在/dev下自动创建设备节点。
创建节点最后还是调用mknod
,当然在class_create和device_create自动创建设备节点时,也会在/sys/class下自动创建和删除相关设备类和设备,这是sysfs的驱动内容。
3. 平台设备 驱动 的开发流程 1)将字符设备驱动的char_init函数的实现搬到platform_driver的probe接口中。 2)在char_init中调用platform_device_register和platform_driver_register分别注册设备和驱动。现在内核版本都带着设备树,所以platform_device_register是在linux启动的过程中完成的。因此char_init一般只有platform_driver_register注册驱动。
2 platform 虚拟总线驱动
当我们向系统注册一个驱动的时候,总线就会在设备列表中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动列表中查找看有没有与之匹配的设备,有的话也联系起来。驱动和设备之间的匹配就是依靠总线bus的match函数进行匹配的。
但是在 SOC 中有些外设是没有总线这个概念的,但是又要使用总线、 Linux 提出了 platform 这个虚拟总线,相应的就有 platform_driver 和 platform_device。
struct bus_type{
const char *name;//总线类型名称
const char *dev_name;//该总线下的设备节点名称
struct device *dev_root;//该总线下的根设备节点
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);//用于检查设备是否匹配总线类型的函数
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);//发送uevent消息的函数
int (*probe )(struct device *dev);//在设备被添加到总线上时调用的函数
int (*remove)(struct device *dev);//当设备被移除时调用的函数
void (*shutdown)(struct device *dev);//在系统关机时调用的函数
......
match 函数是完成设备和驱动之间匹配的,总线就是使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。 match 函数有两个参数: dev 和 drv,这两个参数分别为 device 和 device_driver 类型,也就是设备和驱动。platform 总线是 bus_type 的一个具体实例,定义在文件 drivers/base/platform.c
2.1 platform总线的注册
struct bus_type platform_bus_type= {
.name = "platform",//设备名称
.dev_groups = platform_dev_groups,//设备属性、含获取sys文件名,该总线会放在/sys/bus下
.match = platform_match,//匹配设备和驱动,匹配成功就调用driver的.probe函数
.uevent = platform_uevent,//消息传递,比如热插拔操作
.pm = &platform_dev_pm_ops,
};
platform总线初始化过程
发生在系统启动过程中。
1,在do_basic_setup中会通过driver_init调用platform_bus_init()函数初始化platform总线:
2,通过函数device_add()把已经初始化完成的设备(设备树中的设备)添加到相对应的总线devices链表 下,在带设备树的内核版本中platform_device描述的设备信息都被放到了设备树中。所以platform_device的驱动就不用写了。如果使用platform的设备树的匹配的方式,只需要填写设备树即可。
3,当完成了platform_bus设备的注册后,platform_bus_init()函数会执行bus_register()函数将platform总线注册到系统中,其实就是创建platform目录下的一些 目录 和 属性文件 信息。
其他总线也一样
2.2 platform驱动的添加
设备树 里有点节点比如i2c设备是在真正的实际总线上,比如i2c总线。但是有的设备树节点没有,比如下面的串口设备,也需要实现设备驱动分离,于是就适用与platform总线。
可以看看platform驱动下都有什么,
platform_driver
platform_driver----include/linux/platform_device.h
struct platform_driver {
//当驱动和硬件信息匹配成功之后,就会调用probe函数,驱动所有的资源的注册和初始化全部放在probe函数中
int (*probe)(struct platform_device *);
//硬件信息被移除了,或者驱动被卸载了,全部要释放,释放资源的操作就放在该函数中
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
//内核维护的所有的驱动必须包含该成员,通常driver->name用于和设备进行匹配
struct device_driver driver;
//往往一个驱动可能能同时支持多个硬件,这些硬件的名字都放在该结构体数组中
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
。。。。。
driver 成员,为 device_driver 结构体变量, Linux 内核里面大量使用到了面向对象的思维, device_driver 相当于基类,提供了最基础的驱动框架:
struct device_driver {
const char *name; //用于和硬件进行匹配。
struct bus_type *bus; //指向总线描述符的指针,总线连接所支持的设备。
struct module *owner;//设备驱动的owner,通常为THIS_MODULE
const char *mod_name; /* used for built-in modules */
// 通过sysfs操作设备驱动的bind/unbind,用来使能/关闭设备与驱动的自动匹配
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
const struct of_device_id*of_match_table;//device_tree中使用,用于匹配设备。
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);//当设备匹配/移除的时候,会调用设备驱动的probe/remove函数。
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;
struct driver_private *p;
};
platform_driver_register
当我们添加一个platform驱动时,
定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用platform_driver_register 函数向 Linux 内核注册一个 platform 驱动, platform_driver_register 函数原型如下所示:
#define platform_driver_register(drv) \
__platform_driver_register(drv, THIS_MODULE)
extern int __platform_driver_register(struct platform_driver *,
struct module *);
同样也是既有目录也有属性
msm-kernel/drivers/base/platform.c
当向系统中注册一个platform驱动时,同样会调用**__driver_attach**函数进行设备的匹配,匹配完成后会执行probe函数。
int driver_register(struct device_driver *drv)
{
int ret;
struct device_driver *other;
BUG_ON(!drv->bus->p);//driver的总线必须要有自己的subsys,因为这个才是整个bus连接device和driver的核心
/* driver和bus两种都实现了下面函数,而实际最只能执行一个,所以告警说重复 */
if ((drv->bus->probe && drv->probe) ||
(drv->bus->remove && drv->remove) ||
(drv->bus->shutdown && drv->shutdown))
printk(KERN_WARNING "Driver '%s' needs updating - please use "
"bus_type methods\n", drv->name);
/* 查找驱动是否已经装载注册,已经装载的则直接返回 */
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);//添加bus/platform/drivers添加目录
if (ret)
return ret;
ret = driver_add_groups(drv, drv->groups);//把驱动加入驱动的group中,添加属性组,driver_add_groups()在drv目录下添加属性集合,调用sysfs_create_groups()实现。
if (ret) {
bus_remove_driver(drv);
return ret;
}
//将事件发送到用户空间,调用kobject_uevent()向用户空间发布KOBJ_ADD消息。
kobject_uevent(&drv->p->kobj, KOBJ_ADD);
return ret;
}
int bus_add_driver(struct device_driver *drv)
{
struct bus_type *bus;
struct driver_private *priv;
int error = 0;
bus = bus_get(drv->bus);//拿到 driver 所属的总线
if (!bus)
return -EINVAL;
pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);
/* bus有自己的private,device有自己的private,driver也有,功能就是负责连接对方 */
priv = kzalloc(sizeof(*priv), GFP_KERNEL);
if (!priv) {
error = -ENOMEM;
goto out_put_bus;
}
//初始化klist,以及填充driver的private里面的内容
klist_init(&priv->klist_devices, NULL, NULL);
priv->driver = drv;
drv->p = priv;
priv->kobj.kset = bus->p->drivers_kset;
//driver绑定bus(通过各自里面的privte)//由于是在sys下添加节点,这里的kset就是platform kset,例如在/sys/bus/platform/drivers下添加qcom_geni_serial。这里是在节点路径添加目录,下面是添加到drivers链表里。
调用kobject_init_and_add()将drv加入sysfs,之前只是设置了priv->obj.kset为bus->p->drivers_kset,所以drv目录会出现在bus目录的drivers子目录中。如果总线允许自动probe,就会调用driver_attach()将驱动和总线上的设备进行匹配
error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,
"%s", drv->name);
if (error)
goto out_unregister;
/*把 driver 在bus的节点,加入到bus的driver链表的最后一个*/
klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
if (drv->bus->p->drivers_autoprobe) {
if (driver_allows_async_probing(drv)) {
pr_debug("bus: '%s': probing driver %s asynchronously\n",
drv->bus->name, drv->name);
async_schedule(driver_attach_async, drv);
} else {
error =driver_attach(drv);//match driver 匹配device
if (error)
goto out_unregister;
}
}
module_add_driver(drv->owner, drv);
// 添加driver的属性,driver_create_file()创建drv下的属性文件,调用sysfs_create_file()实现。
error = driver_create_file(drv, &driver_attr_uevent);
if (error) {
printk(KERN_ERR "%s: uevent attr (%s) failed\n",
func, drv->name);
}
error = driver_add_groups(drv, bus->drv_groups);//添加/sys/bus/platform/drivers/qcom_geni_serial下的属性组,driver_add_groups()在drv目录下添加属性集合,调用sysfs_create_groups()实现,就是批量添加属性。
if (error) {
/* How the hell do we get out of this pickle? Give up */
printk(KERN_ERR "%s: driver_create_groups(%s) failed\n",
func, drv->name);
}
if (!drv->suppress_bind_attrs) {
error = add_bind_files(drv);
if (error) {
/* Ditto */
printk(KERN_ERR "%s: add_bind_files(%s) failed\n",
func, drv->name);
}
}
return 0;
out_unregister:
kobject_put(&priv->kobj);
kfree(drv->p);
drv->p = NULL;
out_put_bus:
bus_put(bus);
return error;
}
int driver_attach(struct device_driver *drv)
{
return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
}
总线会管理两个链表,一个是驱动链表platform_driver,一个是设备链表platfoem_devie。 假设 当前注册一个设备进入到设备链表中 ,注册完成之后就会与驱动链表中的驱动进行比较。根据platform_match函数源码
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);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Then try to match against the id table */
if (pdrv->id_table)//在驱动中定义id_table,在id_table结构体中定义多个设备名,利用函数比较设备的id_table
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
匹配成功就会调用驱动结构体platform_driver中的probe函数
匹配成功就会调用驱动结构体platform_driver中的probe函数.
probe函数的参数为设备结构体platform_device ,在设备结构体中通常包含所有的资源的详细信息,因此总线设备驱动的一些对于硬件寄存器的相关操作 通常在驱动结构体的probe函数指针所指函数中进行,该函数调用后通常会分配设置注册fileoperation结构体 ,并根据platform_device确定硬件相关寄存器使用ioreamp来映射寄存器等。反之,如果先对驱动进行注册,当驱动被注册进总线中的驱动链表 中之后,就会对设备链表中的设备进行比较,后面的操作同上。
这样一来就实现了驱动和设备分离。将传统驱动框架中的file_operation中对于硬件寄存器的一些操作通过驱动结构体中probe函数指针指向的函数中进行probe函数将与之相匹配的设备结点作为参数。设备结构体中通常包含一些硬件资源相关的信息。
需要了解的是,当注册一个platform_drv之后就会与设备链表上的所有设备进行一一比较,但是当注册platform_driver时,通过__device_attach_driver去匹配驱动时,匹配成功之后就立即调用probe函数。
2.3 platform设备
内核支持设备树的话就不要再使用 platform_device 来描述设备了,因为改用设备树去描述了。
3 sysfs 设备模型结构
3.1 设备驱动模型结构
在Linux设备驱动模型中,设备驱动模型在内核中的关系 用kobject结构体来表示。在用户空间的关系用sysfs文件系统的结构来表示。如下图,左边是bus子系统在内核中的关系,使用kobject结构体来组织。右边是sysfs文件系统的结构关系,使用目录和文件来表示。左边的kobject和右边的目录或者文件是一一对应的关系,如果左边有一个kobject对象,那么右边就对应一个目录。文件表示该kobject的属性,并不与kobejct相对应。
sysfs文件系统
sysfs文件系统是Linux众多文件系统中的一个。在Linux系统中,每个文件系统都有其特殊的用途。例如ext2用于快速读写存储文件;ext3用来记录日志文件。
Linux设备驱动模型由大量的数据结构和算法组成。这些数据结构之间的关系非常的复杂,多数结构之间通过指针相互关联,构成树形或者网状关系。显示这种关系的最好方法是利用一种树形的文件系统,但是这种文件系统需要具有其他文件系统没有的功能,例如显示内核中的一些关于设备、驱动和总线的信息。为了达到这个目的,Linux内核开发者创建了sysfs文件系统。
1.sys概述
sysfs文件系统是Linux2.6内核的一个新特性,其是一个只存在于内存中的文件系统。内核通过这个文件系统将信息导出到用户空间中。sysfs文件系统的各目录与文件之间既有树形结构,又有目录关系 。
在内核中,这种关系由设备驱动模型来表示。在sysfs文件系统中产生的文件大多数是ASCII文件,通常每个文件有一个值,也可叫属性文件。文件的ASCII码特性保证了被导出信息的准确性,而且易于访问。
2.sysfs文件系统与内核结构的关系
sysfs文件系统是内核对象(kobject)、属性(kobj_type)及它们的相互关系的一种表现机制。用户可以从sysfs文件系统中读出内核的数据,也可以将用户空间的数据写入内核中。这是sysfs文件系统非常重要的特性,通过这个特性,用户空间的数据就能够传送到内核空间中,从而设置驱动程序的属性和状态。
sysfs文件系统中包含了一些重要的目录,这些目录中包含了与设备和驱动等相关的信息:
1.sysfs文件系统的目录
sysfs文件系统与其他文件系统一样,由目录、文件、链接组成 。与其他文件系统不同的是,sysfs文件系统表示的内容与其他文件系统中的内容不同。另外,sysfs文件系统只存在于内存中,动态的表示着内核的数据结构。
sysfs文件系统挂接了一些子目录,这些目录代表了注册sysfs中的主要子系统。
要查看这些子目录和文件,可以使用ls命令 ,命令当设备启动时,设备驱动模型会注册kobject对象,并在sysfs文件系统中产生以上的目录。现对其中的主要目录所包含的信息进行说明。
2. block目录
块目录包含了在系统中发现的每个块设备的子目录,每个块设备对应一个子目录。每个块设备的目录中有各种属性,描述了设备的各种信息。例如设备的大小、设备号等。块设备目录中有一个表示I/O调度器的目录,这个目录中提供了一些属性文件。它们是关于设备请求队列信息和一些可调整的特性。块设备的每个分区表示为块设备的子目录,这些目录中包含了分区的读写属性。
3. bus目录
总线目录包含了在内核中注册而得到支持的每个物理总线的子目录,例如ide、pci、scsi、i2c和pnp总线等。使用ls命令可以查看bus目录的结构信息,如下所示:
platform目录中包含了devices和drivers
目录。devices
目录包含了总线下所有设备的列表,这些列表实际上是指向设备目录中相应设备的符号链接。
4.class目录
类目录中的子目录表示每一个注册到内核中的设备类。例如固件类(firmware
)、混杂设备类(misc
)、图形类(graphics
)、声音类(sound
)和输入类(input
)等。这些类如下所示。
类对象只包含一些设备的总称,例如网络类包含一切的网络设备,集中在/sys/class/net
目录下。输入设备类包含一切的输入设备,如鼠标、键盘和触摸板等,它们集中在/sys/class/input
目录下。关于类的详细概述将在后面讲述。
3.2 设备 驱动 模型的核心数据结构
/home/docker/txwork1/ap8650/kernel_platform/msm-kernel/include/linux/kobject.h
3.2.1 struct kobject {
const char *name; /*kobject的名称该名称将显示在sysfs
文件系统中,作为一个目录的名字*/
struct list_head entry; /*连接下一个kobject结构*/
struct kobject *parent; /*指向父kobject结构体,如果存在父对象*/
struct kset *kset; /*指向kset集合*/
const struct kobj_type *ktype; /*指向kobject的类型描述符,可以将属性看成sysfs
中的一个属性文件*/
struct kernfs_node *sd; /*对应sysfs的文件目录*/
struct kref kref; /*kobject的引用计数*/
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
unsigned int state_initialized:1; /*该kobject对象是否初始化的位*/
unsigned int state_in_sysfs:1; /*表示kobject
是否已经注册到sysfs
文件系统中*/
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
ANDROID_KABI_RESERVE(3);
ANDROID_KABI_RESERVE(4);
};
kobj_type代表的kobject的属性,可以将属性看成sysfs中的一个属性文件。每个对象都有属性。需要注意的是,对于sysfs中的普通文件读写操作都是都是由kobject->ktype->sysfs_ops指针来完成的。对于kobj_type的详细说明将在后面列出。
kref
字段表示该对象引用的计数 ,内核通过kref
实现对象引用计数管理。内核提供两个函数kobject_get()、kobject_put()
分别用于增加和减少引用计数,当引用计数为0时,所有该对象使用的资源被释放。
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
char *err_str; /*出错时,保存错误字符串提示*/
if(!kobj){
err_str = "invaild kobject pointer!"; /*kobjetc为无效指针*/
goto error;
}
if(!ktype){
err_str = "must have a ktype to be initialized properly!\n";
goto error;
}
if(kobj->state_initialized){ /*如果kobject已经初始化,则出错*/
/*打印错误信息,有时候可以恢复到正常状态*/
printk(KERN_ERR "kobject (%p):tired to init an initialized"
"object, something is seriously wrong.\n",kobj);
dump_stack(); /*以堆栈方式追溯出错信息*/
}
kobject_init_internel(kobj); /*初始化kobj
结构体的内部成员,*/
kobj->ktype = ktype; /*为kobject绑定一个ktype属性*/
return;
error:
printk(KERN_ERR"kobject (%p): %s\n", kobj, err_str);
dump_stack();
}
调用kobject_add_internal()函数向设备驱动模型中添加kobject结构体。
static void kobject_init_internal(struct kobject *kobj)
{
if(!kobj) /*如果kobj为空,则出错退出*/
return
kref_init(&kobj->kref); /*增加kobjetc的引用计数*/
INIT_LIST_HEAD(&kobj->entry); /*初始化kobject的链表*/
kobj->state_in_sysfs = 0; /*表示kobject还没注册到sysfs中*/
kobj->state_add_uevent_sent = 0;/*始终初始化为0*/
kobj->state_remove_uevent_sent =0; /*始终初始化为0*/
kobj->state_initialized = 1; /*表示该结构体已经初始化了*/
}
3.2.2 设备属性kobj_type
每个kobject对象都有一些属性,这些属性由kobj_type结构体表示。kobject中有指向kobj_type的指针,如下图所示。
当创建kobject结构体的时候,会给kobject一些默认的属性。这些属性保存在kobj_type结构体中,该结构体定义如下:
struct kobj_type {
void (*release)(struct kobject *kobj);/*释放kobject和其占用资源的函数*/
const struct sysfs_ops *sysfs_ops;/*操作下一个属性数组的方法*/
const struct attribute_group **default_groups;/*属性数组*/
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
ANDROID_KABI_RESERVE(1);
};
kobj_type的default_atrrs成员保存了属性数组,每一个kobject对象可以有一个或者多个属性。属性结构体如下:
struct attribute{
const char *name; /*属性的名称*/
struct module *owner; /*指向拥有该属性的模块,已经不常使用*/
mode_t mode; /*属性的读写权限*/
}
在这个结构体中,name
是属性的名字,对应某个目录下的一个文件的名字。owner
指向实现这个属性的模块指针,就是驱动模块的指针。
kobject始终代表sysfs文件系统中的一个目录 ,而不是文件。对kobject_add()函数的调用将在sysfs文件系统中创建一个目录。最底层目录对应于系统中的一个设备、驱动或者其他内容。通常一个目录中包含一个或者多个属性,以文件的方式表示,属性由ktype指向。
kobject对象的成员name是sysfs文件系统中的目录名。通常使用kobject_set_name()函数来设置。在同一个目录下,不能有相同的目录名。
kobject在sysfs文件系统中的位置由parent指针指定。parent指针指向一个kobejct结构体,kobject对应一个目录。
kobj_type是kobject的属性。一个kobject可以有一个或者多个属性。属性用文件来表示,放在kobejct对应的目录下。(比如的qcom-battery的父对象就是class,且qcom-battery下面有许多)
atrribute表示一个属性,其具体定义将在下面介绍。
sysfs_ops表示对属性的操作函数。一个属性只有两种操作,一种是读操作,一种是写操作。
kobject_add ()函数的调用将在sysfs文件系统中创建一个目录。
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...)
{
va_list args;
int retval;
if (!kobj)
return -EINVAL;
if (!kobj->state_initialized) {
pr_err("kobject '%s' (%p): tried to add an uninitialized object, something is seriously wrong.\n",
kobject_name(kobj), kobj);
dump_stack();
return -EINVAL;
}
va_start(args, fmt);
retval = kobject_add_varg(kobj, parent, fmt, args);
va_end(args);
return retval;
}
EXPORT_SYMBOL(kobject_add);
3.2.3 kset集合
kobject通过kset组织成层次化的结构。kset是具有相同类型的kobejct集合 ,像驱动程序一样放在/sys/drivers目录下,目录drivers是一个kset对象,包含系统中的驱动程序对应的目录,驱动程序的目录由kobject表示。
kset结构体的定义如下:
struct kset{
struct list_head list; /*连接所包含的kobject对象的链表首部*/
spinlock_t list_lock; /*维护list链表的自旋锁*/
struct kobject kobj; /*内嵌的kobject结构体,说明kset本身也是一个目录*/
struct kset_uevent_ops *uevent_ops; /*热插拔事件*/
}
内嵌的kobject
对象。所有属于这个kset
集合的kobejct
对象的parent
指针,均指向这个内嵌的kobject
对象。另外kset
的引用计数就是内嵌的kobject
对象的引用计数。
举个栗子,/sys/drivers drivers是父kobject ,下面的kobject 都指向他
kset与kobject的关系
kset
是kobject
的一个集合,用来与kobject
建立层次关系。内核可以将相似的kobject
结构连接在kset
集合中,这些相似的kobject
可能有相似的属性,使用统一的kset
来表示。下图显示了kset
集合和kobject
之间的关系。
kset集合包含了属于其的kobject结构体,kset.list链表用来连接 第一个和最后一个kobject对象。第一个kobject使用entry连接kset集合和第二个kobejct对象。第二个kobject对象使用entry连接第一个kobject对象和第三个kobject对象,依次类推,最终形成一个kobject对象的链表。
- 所有
kobject
结构的parent
指针指向kset
包含的kobejct
对象,构成一个父子层次关系。
4 cdev字符设备 数据结构 驱动文件位置
字符设备是 3 大类设备(字符设备、块设备和网络设备)中的一类,Linux系统将设备分别抽象为struct cdev, struct block_device,struct net_devce三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作。
Linux内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象, cdev记录了字符设备的相关信息(设备号 、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations ),其驱动程序完成的主要工作是初始化、添加和删除 cdev 结构体,申请和释放设备号,以及填充 file_operations 结构体中的操作函数,实现file_operations 结构体中的 read()、write()和 ioctl()等函数是驱动设计的主体工作。 在我们想要添加一个字符设备时,就是将这个对象注册到内核中( proc/devices下) ,通过创建一个设备文件( 设备节点 dev 下 )。
例如想要读写 ttyMSM0串口设备,直接对/dev/ttyMSM0进行读写操作即可。 相当于/dev/ttyMSM0这个文件是 ttyMSM0设备在用户空间中的实现。当我们对这个文件(例如dev/ttyMSM0)进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。
字符设备驱动框架,如下图所示:
设备号会与字符设备驱动程序中的 file_operations 结构体关联,
4.1 struct cdev
在当前的 liux 内核中每一个字符设备都有一个 cdev 描述,即一个 cdev 结构表示一个字符设备。
在cdev 中有两个重要的成员变量:ops 和dev,这两个就是字符设备文件操作函数集合files_operations 以及设备号dev_t。编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就是表示一个字符设备。
/home/docker/txwork1/ap8650/kernel_platform/msm-kernel/include/linux/cdev.h
struct cdev {
struct kobject kobj; /* 内嵌的 kobject 对象,kobject结构体,用于表示cdev对象在内核中的内存管理等方面的信息。 */
struct module *owner; /* 所属模块 */
struct file_operations *ops; /* 指向字符设备驱动的file_operations结构体。 */
struct list_head list;链表节点,用于将多个cdev结构体链接在一起。
dev_t dev; /* 添加该设备的设备号 dev_t,用于关联字符设备 */
unsigned int count;用于表示同一设备实例的数量,通常为1。
};
4.2 file_operations
该结构体是系统调用与驱动连接的桥梁,当我们在应用层使用 open 函数打开一个设备的时候,内核会创建一个 file 结构并关联 file_operations 中的一组函数,最终会调用到驱动中关联的 file_operations 结构体实例中 open 函数。而 file_operations 定义了一组操作函数,我们不一定全部用到,通常用到什么函数就关联什么函数。
这里要区别开针对属性节点的 cat 和 echo 操作。
/home/docker/txwork1/ap8650/kernel_platform/msm-kernel/include/linux/fs.h
struct file_operations {
struct module *owner ;// 一个指向拥有这个结构的模块的指针,内核使用这个字段以避免在模块的操作正在被使用时卸载该模块,几乎所有情况被初始化为THIS_MODULE。
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
unsigned int poll_flags);
} __randomize_layout;
int (*open) (struct inode *, struct file *);
对设备文件进行的第一个操作,如果这个函数没有实现,当用户调用 open() 时,一直显示成功,但是你的驱动不会得到通知。open 函数提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。大部分驱动程序中应当完成下面工作。
|----------|-------------------------------------------------------------------|---|
| 函数接口 | int (*open) (struct inode *inode , struct file *filp); | |
| 函数参数 | 参数含义 | |
| inode | 为文件节点(详细见前面inode结构) | |
| filp | 指向内核创建的文件结构(详细见前面file结构) | |
-
检测设备特定的错误(注入设备未就绪或类似的硬件问题)
-
如果设备是首次打开,则对其进行初始化。
-
如果有必要,更新 fop 指针
-
分配并填写置于 filp->private_date 里的数据结
ssize_t (* read) (struct file *, char __user *, size_t, loff_t *);
用来从设备读取数据,成功时函数返回读取的字节数,返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型,出错时返回一个负值,用户调用 read() 时如果此函数未实现,将得到 -EINVAL 的返回值,它与用户空间的 fread() 函数对应。
|----------|-----------------------------------------------------------------------------------------------|---|
| 函数接口 | ssize_t (*read) (struct file *filp, char __user *buffer, size_t size , loff_t *ppos); | |
| 函数参数 | 参数含义 | |
| filp | 指向内核创建的文件结构 | |
| buffer | 数据返回给用户空间的内存地址 | |
| size | 为要读取的信息长度,以字节为单位 | |
| ppos | 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值 | |
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
向设备发送数据,成功时返回写入的字节数,如果此函数未实现,当用户调用 write() 时,将得到 -EINVAL 的返回值,它与用户空间的 fwrite() 函数对应
注:这个操作和上面的对文件进行读的操作均为阻塞操作
|----------|-------------------------------------------------------------------------------------------------------|---|
| 函数接口 | ssize_t (*write) (struct file * filp, const char __user *buffer, size_t size, loff_t * ppos); | |
| 函数参数 | 参数含义 | |
| filp | 指向系统open时内核创建的文件结构 | |
| buffe | 用户要写入文件的信息缓冲区 | |
| size | 要写入信息的长度 | |
| ppos | 当前的读/写位置,这个值通常是用来判断写文件是否越界 | |
c: 表示这是一个字符设备文件。
rw-rw----: 文件权限,代表文件所有者和所属组有读写权限,其他用户没有任何权限。
1: 确定这个设备文件的硬链接数。
root: 文件所有者为root。
root: 文件所属root组。
237, 0: 设备号,代表这是主设备号为 29,次设备号为 0 的设备。
4月 16 10:30: 文件的最后修改时间。
ttyMSM0: 设备文件名,代表此文件是第一个帧缓冲设备的字符设备文件。
4.3 静态注册cdev设备(调试时候用)
静态分配设备号需要开发者手动指定设备号,保证每个设备号都唯一。在代码中通过调用 result = register_chrdev_region(devno, 4, "chrdev");//静态的申请和注册设备号
函数进行注册。这个函数的第一个参数是主设备号,第二个参数是设备数量,第三个参数是设备名。当驱动的主、次设备号申请成功后,/proc/devices
里将会出现该设备,但是/dev路径下并不会创建该设备文件,需要我们通过mknod去创建设备节点。可以通过例如,mknod /dev/chrdevbase c 251 0 ,根据设备号和设备名字,申请设备节点。例如/dev/ttyMSM0。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
-
dev:用于返回分配的设备号。
-
firstminor:要分配的第一个次设备号,它常常是0。
-
count:要分配的设备号数量。
-
name:设备名字
alloc_chrdev_region在/proc/devices
下创建了主设备号,cdev_add会在proc/devices下添加设备号和设备名字
4.4 自动注册设备文件(实际中使用)
在Linux中,可以通过udev(userspace dev)来实现自动创建设备节点。udev是Linux系统中的一个动态设备管理机制,它负责在系统启动时检测硬件设备的插拔并创建相应的设备节点。
关于udev可以参考:用户空间的uevent处理和驱动固件升级
当udev检测到新设备插入时,它会执行一系列规则(rules)来确定设备应该如何创建,这些规则定义了设备节点的名称、权限等信息,并使用mknod系统调用来创建设备节点。
class_create():在调用device_create()前要先用class_create()创建一个类。在/sys/class目录下创建对应的目录
-
owner:struct module结构体类型的指针,一般赋值为THIS_MODULE。
-
name:char类型的指针,类名。例如 sys/class/power_supply sys/class/ tty
device_create()能自动创建设备文件是依赖于udev这个应用程序. 在/ dev 目录下创建相应的设备节点,例如 dev/ttyMSM0。同时也会在sys/class/下创建出对应的设备文件,例如 sys/class/ tty /ttyMSM0
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:该设备依附的类
-
parent:父设备
-
devt:设备号(此处的设备号为主次设备号)添加该设备的设备号 dev_t,用于关联字符设备
-
drvdata:私有数据
-
fmt:设备 的名称,创建成功后,将出现 " dev /fmt" 已经 "/sys/class/xxx/fmt"
device_create能自动创建设备文件是依赖于udev这个应用程序,使用udev后,在/ dev 目录下 也是会调用mknod来创建设备节点
4.5 open dev下节点
file是实时建立的
struct file 结构与用户空间程序中的FILE结构没有任何关联,FILE结构在 C 库中定义不会出现在内核代码中,struct file 是一个内核结构,它不会出现在用户程序中。struct file 结构代表一个打开的文件(它不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的 file 结构。它由内核在open时创建,并且传递给在该文件上操作的所有函数,直到最后的close函数,在文件的所有实例都被关闭后,内核会释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 file 或 filp ,为了不和这个结构本身混淆,我们一致将指向该结构的指针称为 filp,file 则为结构本身。
/home/docker/txwork1/ap8650/kernel_platform/msm-kernel/include/linux/fs.h
struct file {
union {
struct llist_node f_llist;
struct rcu_head f_rcuhead;
unsigned int f_iocb_flags;
};
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;//与文件相关的操作。内核在执行open操作时对这个指针赋值,以后需要处理这个操作时就读取这个指针。
/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;//文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC,检查用户的请求是否是非阻塞式的操作,驱动程序需要检查O_NONBLOCK标志,而其他标志很少用到。注意:检测读写权限应该使用f_mode而不是f_flags。所有这些标志都被定义在<linux/fcntl.h>中
fmode_t f_mode; /* 文件读/写模式,FMODE_READ和FMODE_WRITE ,文件打开是已经做了判断,基本用不着 */
struct mutex f_pos_lock;
loff_t f_pos; /* 当前读写位置。loff_t有64位,驱动程序要知道文件中的当前位置,可以读取这个值,但不要去修改它。read/write会使用他们接收到的最后那个指针参数来更新这一位置,而不是直接针对filp->f_pos进行操作。这一规则的一个例外是llseek方法,该方法的目的本身就是为了修改文件位置**/*
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;/* 文件私有数据 */
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
} __randomize_layout
attribute((aligned(4))); /* lest something weird decides that 2 is OK */
struct inode
内核用 inode 结构在内部表示文件,因此它和 file 结构不同,后者表示打开的文件描述。对单个文件可能有多个打开的 file 文件描述(上层可以多次 open 一个文件),但他们都指向同一个 inode 文件。inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等大量文件信息。部分数据结构如下:
其中驱动对驱动编程有用的成员变量只有两个如下:
dev_t i_rdev当 inode 结构描述的文件为设备文件时,表示它的设备号
struct cdev *i_cdev 当 inode 指向一个字符设备文件时,i_cdev 为其对应的 cdev 结构体指针
在驱动中也可通过i_rdev获取设备号,内核提供了下面两个函数来获取 inode 结构中 i_rdev 字段中的设备号:
unsigned int imajor(struct inode* inode); //获取主设备号 unsigned int iminor(struct inode* inode); //获取次设备号
struct inode {
umode_t i_mode;/* inode 的权限 */
unsigned short i_opflags;
kuid_t i_uid;/* inode 拥有者的 id */
kgid_t i_gid;/* inode 所属的群组 id */
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;/* 若是设备文件,此字段将记录设备的 设备号 */
loff_t i_size;/* inode 所代表的文件大小 */
struct timespec i_atime; /* inode 最近一次的存取时间 */
struct timespec i_mtime; /* inode 最近一次的修改时间 */
struct timespec i_ctime; /* inode 的产生时间 */
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
u8 i_blkbits;
u8 i_write_hint;
blkcnt_t i_blocks;/* inode 所使用的 bloc k数,一个block为 512 字节 */
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct rw_semaphore i_rwsem;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct bdi_writeback *i_wb; /* the associated cgroup wb */
/* foreign inode detection, see wbc_detach_inode() */
int i_wb_frn_winner;
u16 i_wb_frn_avg_time;
u16 i_wb_frn_history;
#endif
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
struct list_head i_wb_list; /* backing dev writeback list */
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
atomic64_t i_version;
atomic64_t i_sequence; /* see futex */
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
atomic_t i_readcount; /* struct files open RO */
#endif
union {
const struct file_operations*i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct cdev *i_cdev;/* 若是字符设备,为其对应的 c dev 结构体指针 。 若是块设备,为其对应的 block_device 结构体指针*/
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct fsnotify_mark_connector __rcu *i_fsnotify_marks;
#endif
#ifdef CONFIG_FS_ENCRYPTION
struct fscrypt_info *i_crypt_info;
#endif
#ifdef CONFIG_FS_VERITY
struct fsverity_info *i_verity_info;
#endif
void *i_private; /* fs or device private pointer */
} __randomize_layout;
5.sys/class/qcom-battery 为例:
下面主要介绍qcom-battery的路径添加以及 sys/class/qcom-battery下面的节点批量添加属性
例如qcom-battery的父kobject为class fake_cyclet为qcom-battery的属性。
qcom-battery作为platform驱动,device driver匹配后执行probe函数。不过并没有实质性的内容
5.1 qcom-battery路径创建
rc = class_register(&bcdev->battery_class);会在sys/class/下注册qcom-battery
int __class_register(struct class *cls, struct lock_class_key *key)
{。。。
error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);name为 "qcom-battery",设置节点名字了
cp->subsys.kobj.kset = class_kset;//设置kset为class_kset 即出现在 /sys/class/目录下 (一般是不会设置class的父kobj的,因此默认使用kset作为父kobj)
#endif
error = kset_register(&cp->subsys); //注册kset创建对应的class节点, /sys/class/xxx
调用
kobject_add_internal()
函数负责向设备驱动模型中添加kobject
结构体,并在sysfs
文件系统中创建一个目录。该函数的代码如下:
static int kobject_add_internal(struct kobject *kobj)
{
int error = 0;
struct kobject *parent;
if(!obj) /*为空,则失败,表示没有需要添加的kobject*/
return -ENOENT;
if(!kobj->name | !kobj->name[0]) {
/*kobject没有名字,不能注册到设备驱动模型中*/
WARN(1, "kobject: (%p): attempted to be registered with empty"
"name!\n", kobj);
return -EINVAL;
}
parent = kobject_get(kobj->parent ); /*增加父目录的引用计数*/
if(kobj->kset){ /*是否属于一个kset集合*/
if(!parent) /*如果kobject本身没有父kobject,则使用kset的kobject作为kobject的父节点*/
parent = kobject_get(&kobj->kset->kobj); /*增加引用计数*/
kobj_kset_join(kobj);
kobj->parent = parent; /*设置父kobject结构*/
}
/*打印调试信息:kobject名字、对象地址、该函数名;父kobject名字;kset集合名字*/
pr_debug("kobject:'%s' (%p): %s : parent: %s , set :'%s'\n",
kobject_name(kobj), kobj, func,
parent? kobject_name(parent): "<NULL>");
error = create_dir(kobj); /*创建一个sysfs目录,该目录名字为kobj_name 这里是 qcom-battery*/
if(error){ /*以下为创建目录失败的函数*/
kobj_kset_leave(kobj);
kobject_put(parent);
kobj->parent = NULL;
/*be noisy on error issues*/
if(error == -EEXIST)
peintk(KERNEL_ERR "%s failed for %s with"
"-EEXIST, don't try to register things with"
"the same name in the same directory.\n",
func, kobject_name(kobj));
else
printk(KERN_ERR "%s failed for %s (%d)\n",
func, kobject_name(kobj), error);
dump_stack();
}else
kobj->state_in_sysfs = 1; /*创建成功,表示kobject在sysfs中*/
return error;
}
5.2 qcom-battery attribute_group 属性组创建
msm-kernel/include/linux/device/class.h
msm-kernel/include/linux/sysfs.h
struct attribute_group {
const char *name;
umode_t (*is_visible)(struct kobject *,
struct attribute *, int);
umode_t (*is_bin_visible)(struct kobject *,
struct bin_attribute *, int);
struct attribute**attrs;
struct bin_attribute **bin_attrs;
};
attribute_group 注册
msm-kernel/drivers/base/class.c
int __class_register(struct class *cls, struct lock_class_key *key)
{
。。。。。。
kset_init(&cp->glue_dirs); kset是啥来着,一般一组节点的需要kset
__mutex_init(&cp->mutex, "subsys mutex", key);
error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);name为 "qcom-battery",设置节点名字了
。。。。。
error = class_add_groups (class_get( cls ), cls->class_groups); 那这里就是添加属性组的地方了
class_put(cls);
if (error) {
kobject_del(&cp->subsys.kobj);
kfree_const(cp->subsys.kobj.name);
kfree(cp);
}
return error;
}
EXPORT_SYMBOL_GPL(__class_register);
static int internal_create_group(struct kobject *kobj, int update,
这里就把qcom-battery下的属性都添加了
5.3 qcom-battery里属性的show store函数是怎么被加进去的
操作结构体sysfs_ops
kobj_type结构的字段default_attrs数组说明了一个kobject都有那些属性,但是并没有说明如何操作这些属性。这个任务要使用kobj_type->sysfs_ops成员来完成,sysfs_ops结构体的定义如下:
struct sysfs_ops{
ssize_t (*show)(struct kobject *, struct attribute *, char *);
/*读属性操作函数*/
ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
/*写属性操作函数*/
};
show()函数用于读取一个属性(节点值?)到用户空间。函数的第1个参数是要读取的kobject的指针,它对应要读的目录;第2个参数是要读的属性;第3个参数是存放读到的属性的缓存区。当函数调用成功后,会返回实际读取的数据长度,这个长度不能超过PAGE_SIZE个自己的大小。将kobject
的名字赋给buf
,并返回给用户空间。例如在用户空间使用cat
命令查看属性文件时,会调用kobejct_test_show()
函数,并显示kobject()
的名字。
store()函数将(节点值?)属性写入内核。函数的第一个参数是与写相关的kobject的指针,它对应要写的目录;第2个参数是要写的属性;第3个参数是要写入的数据;第4个参数是要写入的参数长度。这个长度不能超过PAGE_SIZE个字节大小。只有当拥有属性有写权限时,才能调用store()函数。用于将来自用户空间的buf
数据写入内核,此处并没有实际的写入操作,可以根据具体的情况写入一些需要的数据。
说明:sysfs文件系统约定一个属性不能太长,一般一至两行左右,如果太长,需要把它分为多个属性。
比如sys/class/qcom-battery/下的这些节点 都是属性attribute 或者最底下一层是属性
qcom-battery目录下的属性例如apdo max,绑定了一个操作函数,apdo_max_show
#define CLASS_ATTR_RW(_name) \
struct class_attribute class_attr_##_name = __ATTR_RW(_name)
msm-kernel/include/ linux /sysfs.h
#define __ATTR_RW(_name) __ATTR(_name, 0644, _name##_show, _name##_store)
#define __ATTR (_name, _mode, _show, _store) { \
.attr = {.name = __stringify(_name), \
.mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \
.show = _show, \
.store = _store, \
}
在写程序时,可以使用宏DEVICE_ATTR定义attribute结构,这个宏的定义如下:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode. _show, _store)
该宏使用dev_attr_作为前缀构造属性名,并传递属性的读写模式,读函数和写函数。另外可以使用下面两个函数对属性文件进行实际的处理。
5.4 platform/devices/soc:qcom,pmic_glink:qcom,battery_charger
soc:qcom,pmic_glink:qcom,battery_charger
这个是在注册platform总线时候根据设备树添加的设备
从这个也可以看到,设备树里并没有实际内容。应该为了加载ko后运行probe函数
5.5 platform/drivers/qti_battery_charger
6.ttyMSM串口驱动为例
ttyMSM设备比较复杂,注册了一堆节点
1,ttyMSM0驱动添加了platform的device和driver驱动,所以在sys/bus/platform(device driver)下都有对应的目录。
2,在class目录下有tty类的目录,tty是设备终端类,串口ttyMSM0是其中一个。
3,同时也注册了cdev设备,在/dev下有ttyMSM0设备
关于高通串口驱动可以参考这篇飞书文档
platform总线下的devices sys/bus/devices
sys/bus/driver 下面就直接是qcom-geni-serial了
6.1 proc/devices/ttyMSM 的注册
/home/docker/txwork1/ap8650/kernel_platform/msm-kernel/drivers/tty/tty_io.c
首先我们找到驱动的入口函数module_init (qcom_geni_serial_init),在函数qcom_geni_serial_init中调用uart_register_driver向内核注册了一个驱动.
驱动模块加载和卸载
Linux 驱动有两种运行方式:
① 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
② 将驱动编译成模块(Linux 下模块扩展名为. ko),在Linux 内核启动以后使用"insmod"命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用"insmod"命令加载驱动的时候,xxx_init 这个函数就会被调用。
module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
static int __init console_register(struct uart_driver *drv)
{
return uart_register_driver(drv);
}
uart_register_driver调用了tty_register_driver来注册cdev设备。
uart_driver的注册,实际上就是tty_driver的注册,都是将uart的参数传递给tty_driver,后注册字符设备、分配设备文件、将驱动注册到tty_driver链表中。与用户空间打交道的工作完全交给tty_driver。
int uart_register_driver(struct uart_driver *drv)
{
....
normal->driver_name = drv->driver_name;//设置驱动名称driver_name;
normal->name = drv->dev_name;//设置设备名称name;
normal->major = drv->major;//设置主设备号major;
....
tty_set_operations(normal, &uart_ops);//调用tty_set_operations将uart_ops这一个 tty 设备的操作函数ops集设置到了tty 驱动 中//设置tty驱动操作集合ops为uart_ops,类型为struct tty_operations;//操作tty设备的函数
......
retval = tty_register_driver (normal);/*注册struct tty_driver,向内核注册了 tty 驱动 */// 注册tty驱动
.......
int tty_register_driver(struct tty_driver *driver)/*注册struct tty_driver,最终要注册的实体*/
{
int error;
int i;
dev_t dev;
struct device *d;
if (!driver->major) {/* 如果没有主设备号则申请 */ // 动态申请设备号 // dev : 动态获取的设备号 // baseminor: 次设备号的起始地址 // cont: 要申请的设备数量 // name: 名称
error = alloc_chrdev_region(&dev, driver->minor_start,
driver->num, driver->name);
if (!error) {
driver->major = MAJOR(dev);// 得到主 设备号
driver->minor_start = MINOR(dev);// 得到次 设备号
}
} else {
dev = MKDEV(driver->major, driver->minor_start);
error = register_chrdev_region(dev, driver->num, driver->name);
}/*将char_device_struct变量注册到内核*/
if (error < 0)
goto err;
if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC) {
error = tty_cdev_add(driver, dev, 0, driver->num);/* 创建添加字符设备,使用 tty_fops , cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用cdev_init 函数完成对cdev 结构体变量的初始化,然后使用 cdev_add会在proc/devices下添加设备号和设备名字 。
*/
static int tty_cdev_add(struct tty_driver *driver, dev_t dev,
unsigned int index, unsigned int count)
{
int err;
/* init here, since reused cdevs cause crashes */
driver->cdevs[index] = cdev_alloc();
if (!driver->cdevs[index])
return -ENOMEM;
driver->cdevs[index]->ops = &tty_fops;
driver->cdevs[index]->owner = driver->owner;
err = cdev_add (driver->cdevs[index], dev , count);//cdev_add会在proc/devices下添加设备号和设备名字
if (err)
kobject_put(&driver->cdevs[index]->kobj);
return err;
}
/home/docker/txwork1/ap8650/kernel_platform/msm-kernel/fs/char_dev.c
申请设备号,并将设备号和file_opertions注册(cdev_add接口)到 驱动 框架中的cdev_map数组。
if (error)
goto err_unreg_char;
}
mutex_lock(&tty_mutex);
list_add(&driver->tty_drivers, &tty_drivers);/* 将该 driver->tty_drivers 添加到全局链表 tty_drivers */
mutex_unlock(&tty_mutex);
if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {
for (i = 0; i < driver->num; i++) {
d = tty_register_device(driver, i, NULL);
if (IS_ERR(d)) {
error = PTR_ERR(d);
goto err_unreg_devs;
}
}
}
proc_tty_register_driver(driver); /* proc 文件系统注册driver */
。。。。。
6.2 sys/class/tty/ttyMSM
platform_driver_register(&qcom_geni_serial_platform_driver);注册platform驱动,和platform 设备匹配后,执行
qcom_geni_serial_probe函数。
uart_add_one_port
tty_port_register_device_attr_serdev
tty_register_device_attr device_register
device_add
msm-kernel/drivers/base/core.c
int device_add(struct device *dev)
{
struct device *parent;
struct kobject *kobj;
struct class_interface *class_intf;
int error = -EINVAL;
struct kobject *glue_dir = NULL;
。。。
if (dev->init_name) {
dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL;
}
。。。。
parent = get_device(dev->parent);
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) {
error = PTR_ERR(kobj);
goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj;
/* use parent numa_node */
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));
/* 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);//父目录为 tty ,注册ttyMSM目录,这里没有 class_create .
if (error) {
glue_dir = kobj;
goto Error;
}
/* notify platform of device entry */
device_platform_notify(dev);
error = device_create_file(dev, &dev_attr_uevent);
if (error)
goto attrError;
error = device_add_class_symlinks(dev);
if (error)
goto SymlinkError;
error = device_add_attrs(dev);//添加属性组,dev->group
.......
}
EXPORT_SYMBOL_GPL(device_add);
添加class/tty/ttyMSM0的属性组
在 uart_add_one_port 里面添加了ttyMSM下的属性组,
6.3 sys/bus/platform/devices
设备树的内核版本中platform_device描述的设备信息都被放到了设备树中。所以platform_device的驱动就不用写了。如果使用platform的设备树的匹配的方式,只需要填写设备树即可。
设备树节点和sys/bus/platform/devices的devices名字对应
6.4 sys/bus/platform/driver
"qcom_geni_serial"和sys/bus/platform/drivers下是对应的。
ko模块加载后,会执行qcom_geni_serial_init
platform_driver_register,匹配后
qcom_geni_serial_probe
msm-kernel/drivers/base/platform.c
msm-kernel/drivers/base/driver.c
int driver_register(struct device_driver *drv)
{
int ret;
struct device_driver *other;
.......
ret = bus_add_driver(drv);//添加qcom_geni_serial目录
if (ret)
return ret;
ret = driver_add_groups(drv, drv->groups);//添加qcom_geni_serial目录下的属性组
......
注册platform driver会调用match函数,匹配后,调用qcom_geni_serial_probe函数
参考链接:
https://blog.csdn.net/yangjizhen1533/article/details/110520538
链接:字符设备驱动、平台设备驱动、设备驱动模型、sysfs的比较和关联
原文链接:https://blog.csdn.net/HuangChen666/article/details/132849010
原文链接:https://blog.csdn.net/weixin_42482191/article/details/130921025
原文链接:https://blog.csdn.net/cha1290878789/article/details/121458261
设备驱动模型
原文链接:https://blog.csdn.net/m0_56145255/article/details/131731248
链接:不太行 一张图掌握 Linux 字符设备驱动架构!
原文链接:https://blog.csdn.net/qq_16504163/article/details/118307301
链接:https://blog.csdn.net/qq_52836452/article/details/130195629
链接:linux 驱动基础 - 一、字符设备
原文链接:https://blog.csdn.net/sty01z/article/details/131363839
节点 字符类设备
原文链接:https://blog.csdn.net/lengyuefeng212/article/details/118719335
kobject简介 原文链接:https://blog.csdn.net/vertor11/article/details/103748424
insmod 原文链接:https://blog.csdn.net/qq_21438461/article/details/131434410
原文链接:https://blog.csdn.net/dengjin20104042056/article/details/138501685