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;/* 若是字符设备,为其对应的 cdev 结构体指针。 若是块设备,为其对应的 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/34410.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Crypto++ 入门

一、简介 Crypto&#xff08;也称为CryptoPP、libcrypto或cryptlib&#xff09;是一个免费的开源C库&#xff0c;提供了多种加密方案。它由Wei Dai开发和维护&#xff0c;广泛应用于需要强大加密安全的各种应用程序中。该库提供了广泛的加密算法和协议的实现&#xff0c;包括&…

Spring循环依赖问题——从源码画流程图

文章目录 关键代码相关知识为什么要使用二级缓存为什么要使用三级缓存只使用两个缓存的问题不能解决构造器循环依赖为什么多例bean不能解决循环依赖问题初始化后代理对象赋值给原始对象解决循环依赖SpringBoot开启循环依赖 循环依赖 在线流程图 关键代码 从缓存中查询getSingl…

【贪心算法初级训练】在花坛上是否能种下n朵花、碰撞后剩余的行星

1、在花坛上是否能种下n多花 一个很长的花坛&#xff0c;一部分地已经种植了花&#xff0c;另一部分却没有&#xff0c;花不能种植在相邻的地块上否则它们会争夺水源&#xff0c;两者都会死去。给你一个整数数组表示花坛&#xff0c;由若干个0和1组成&#xff0c;0表示没种植花…

51单片机STC89C52RC——7.1 串口通信

目的/效果 实现单片机串口与电脑串口工具进行数据通讯&#xff0c; 1&#xff1a;设备向电脑串口发送HEX 2&#xff1a;让电脑串口工具控制单片机LED亮灭。同时让单片机反馈控制的结果。 一&#xff0c;STC单片机模块 二&#xff0c;串口通讯 2.1 串行通信与并行通信 &…

axios全局封装AbortController取消重复请求

为什么&#xff1f; 问题&#xff1a;为什么axios要配置AbortController&#xff1f;防抖节流不行吗&#xff1f; 分析&#xff1a; 防抖节流本质上是用延时器来操作请求的。防抖是判断延时器是否存在&#xff0c;如果存在&#xff0c;清除延时器&#xff0c;重新开启一个延…

win10改远程桌面端口,Windows 10 修改远程桌面端口号的专业指南

在Windows 10系统中&#xff0c;远程桌面&#xff08;Remote Desktop&#xff09;功能允许用户从一台计算机远程访问和控制另一台计算机。为了增加远程连接的安全性&#xff0c;减少潜在的安全风险&#xff0c;修改默认的远程桌面端口号是一个常见的安全措施。以下是在Windows …

k8s学习--YAML资源清单文件托管服务nginx

文章目录 前言应用环境具体实现步骤1.安装源码nginx及相关模块2.修改nginx配置文件3.启动验证4.测试 总结 前言 nginx 是一个开源的高性能 HTTP 和反向代理服务器&#xff0c;也是一个 IMAP/POP3/SMTP 代理服务器。在容器和 Kubernetes 的背景下&#xff0c;nginx 经常被用作静…

决策树算法原理

目录 一&#xff1a;介绍 二&#xff1a;算法原理 1.熵和信息熵 2.信息增益 三决策树分裂指标 1.信息熵分裂&#xff1a; 2.Gini系数&#xff08;CART&#xff09; 3.信息增益率 一&#xff1a;介绍 决策树( Decision Tree) 又称为判定树&#xff0c;是数据挖掘技术中的…

你如何看待市场波动性的?

实际上&#xff0c;波动性并不总是负面的&#xff0c;它有时也孕育着快速获利的机会。 对于长期投资者而言&#xff0c;市场波动&#xff08;尤其与熊市相伴时&#xff09;往往是一个优势。它允许投资者拓展并多样化投资组合&#xff0c;以较低的价格购入投资工具&#xff0c;…

【嵌入式Linux】<总览> 多进程(更新中)

文章目录 前言 一、进程的概念与结构 1. 相关概念 2. 内核区中的进程结构 3. 进程的状态 4. 获取进程ID函数 二、进程创建 1. fork和vfork函数 2. 额外注意点 3. 构建进程链 4.构建进程扇 三、进程终止 1. C程序的启动过程 2. 进程终止方式 四、特殊的进程 1. 僵…

免费体验软件开发生产线 CodeArts

软件开发生产线 CodeArts 一站式、全流程、安全可信的软件开发生产线&#xff0c;开箱即用&#xff0c;内置华为多年研发最佳实践&#xff0c;助力效能倍增和数字化转型 免费试用体验版套餐&#xff0c;50人内免费试用 功能特性 Scrum和看板需求模型 代码托管 代码检查&am…

GIS开发如何高质量就业?这几点是关键!

高质量就业&#xff0c;包含薪资和其他福利待遇&#xff0c;在讨论如何高质量就业之前&#xff0c;我们先来看下GIS开发岗位的前景、薪资水平如何&#xff1f;最后讨论一下GIS开发工程师到底需要学习哪些技术&#xff1f; 01 GIS开发岗位呈持续上升趋势 从GIS开发岗位趋势也可…

Java知识点整理 11— 后端 Spring Boot 万用初始化模板使用

一. 模块简介 annotation&#xff1a;自定义注解aop&#xff1a;请求日志和权限校验common&#xff1a;通用类config&#xff1a;配置类constant&#xff1a;常量 controller&#xff1a;控制层esdao&#xff1a;方便操作ESexception&#xff1a;异常类job&#xff1a;定时任务…

Facebook广告投放的6个误区,老手也会犯

一、没有目标 无论是投放哪种产品&#xff0c;我们始终都需要明确&#xff0c;广告的目标是什么。 因为Facebook广告的形式和类型&#xff0c;也经常会有变化&#xff0c;例如近期Facebook推出的360视频广告&#xff0c;以及之后即将推出的LIVE&#xff0c;Mid-Roll视频插播广…

美国电商选品、大促、趋势、案例,掌慧科技首期NewsBreak沙龙干货满满

今年第一季度&#xff0c;美国电商销售额达到了2681.2亿美元&#xff0c;相较上一年同期的2471.8亿美元增长8.5%。同时&#xff0c;该季度美国电商销售额在零售业总销售额中的占比为22.2%&#xff0c;高于上一年同期的21.2%。美国在2023年下半年通胀得到良好控制&#xff0c;20…

CleanMyMac2024破解版下载链接!你的Mac清洁利器!

嘿&#xff0c;亲爱的朋友们&#xff0c;今天我要跟大家分享一款我最近超级依赖的电脑清理神器—CleanMyMac2024破解版&#xff01;如果你还在为电脑运行缓慢、存储空间不够而烦恼&#xff0c;那你一定不能错过它&#xff01; &#x1f525; 为什么选择CleanMyMac2024破解版&am…

声波的种类

声波可以根据不同的特性进行分类&#xff0c;主要包括频率和传播方式两个方面&#xff1a; ### 按频率分类&#xff1a; 1. **次声波**&#xff1a;频率低于20Hz的机械波&#xff0c;这类波通常不能被人耳感知。 2. **可闻声波**&#xff1a;频率在20Hz至20kHz之间的机械波&am…

C++ | Leetcode C++题解之第160题相交链表

题目&#xff1a; 题解&#xff1a; class Solution { public:ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {if (headA nullptr || headB nullptr) {return nullptr;}ListNode *pA headA, *pB headB;while (pA ! pB) {pA pA nullptr ? headB : p…

【fiddler】fiddler抓取websocket

1.先了解websocket流 下载4.5版本以上的fiddler 如图所示&#xff1a;在rules--customize rules 里面插入以下代码&#xff1a; static function OnWebSocketMessage(oMsg: WebSocketMessage) { // Log Message to the LOG tab FiddlerApplication.Log.LogString(oMsg.ToStr…

鸿蒙开发下拉选项框在表单递交的处理

下拉选项框 <select name"identity"><option value"0">顾 客</option><option value"1">行 政</option><option value"2" >保 洁</option></select>在表单数据中没有找到identit…