Linux 字符型设备 + platform总线 + sysfs设备模型

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的关系

ksetkobject的一个集合,用来与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结构) | |

  1. 检测设备特定的错误(注入设备未就绪或类似的硬件问题)

  2. 如果设备是首次打开,则对其进行初始化。

  3. 如果有必要,更新 fop 指针

  4. 分配并填写置于 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

相关推荐
夏天Aileft2 分钟前
Jenkins通过Squid代理服务器添加局域网节点机器
linux·运维·jenkins
修炼之路16 分钟前
使用label-studio对OCR数据进行预标注
网络·人工智能·ocr
陶然同学17 分钟前
【Nginx】源码安装
linux·服务器·nginx
志凌海纳SmartX25 分钟前
金融行业专题|某头部期货基于 K8s 原生存储构建自服务数据库云平台
网络·数据库·云原生·架构·kubernetes·存储
橙子味冰可乐1 小时前
input()函数——输入
linux·服务器·windows·python·tcp/ip·sqlite
状元岐1 小时前
c#文件操作
java·服务器·c#
@逝水流年轻染尘@1 小时前
【Python3的内置函数和使用方法】
服务器·python·青少年编程·pip
为祖国添砖爪哇1 小时前
【计算机网络】期末复习(1)模拟卷
网络·网络协议·计算机网络
小豆同学1985081 小时前
在本地和Linux之间传输文件
linux·运维·服务器
琢瑜1 小时前
Linux-进程间通信(IPC)
linux