• linux设备模型:设备及设备节点创建过程分析


    上一篇<>中分析了devtmpfs虚拟设备文件系统的初始化及执行过程,devtmpfs中的所有内容都是临时的,devtmpfs的根路径在/dev,它通过文件系统上下文创建mount(挂载)对象,使得用户层可以访问。devtmpfs通过devtmpfsd线程函数,分配新的命名空间代理(nsproxy)对象,并分配、关联多类命名空间,如mnt、uts、ipc、pid、cgroup、net、time等,紧接着mount(挂载)文件系统,初始工作完成后,进入while循环函数(设备处理函数)。届时,devtmpfs进入工作状态,设备可以正常注册或移除。

    本篇分析设备的初始化及注册过程,包括设备与驱动绑定,设备与电源管理之间的联系、中断域的储存及物理设备之间的关系等等。这一篇内容较多,结合kobject、kset、class、bus等众多概念,也是从理论阶段转换为实际使用阶段的重要过程。

    设备初始化阶段,创建kset容器结构对象devices_kset,用于设备的uevent(用户事件通知)操作。dev_kobj表示设备根对象,在它的基础上扩展(延伸)sysfs_dev_block_kobj(“block”) 块设备根对象和sysfs_dev_char_kobj(“char”)字符设备跟对象。现在我们可以得到一个思构图,是一个树形状图:

    							devices_kset (kset容器结构对象)
    										|
    						————	dev_kobj (设备根对象)	————
    						|									|
    		sysfs_dev_block_kobj("block") (块设备根对象)		sysfs_dev_char_kobj("char") (字符设备根对象)
    				|					|							|					|
    		块设备(类型1)	  块设备(类型...)	  			字符设备(类型1)		字符设备(类型...)
    			|					|							|					|
    		块设备节点...		块设备节点...				字符设备节点...		字符设备节点...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    通过分析源码,我们可以了解到设备(属于)由根kobj为基础扩展出的管理框架,它根据不同的type(device_type)可以表示不同的设备类型,它根据不同的名称表示不同的设备(如块设备和字符设备),设备的意义用于管理硬件(物理设备)、维护软件之间的关系,如通过电源管理硬件的唤醒或休眠,通过映射bar对设备物理内存的访问,通过irq_domain(中断域)等获取中断事件的触发等(平台设备常见)。

    设备结构对象初始化
      关联devices_kset容器,关联device_ktype,初始化dma链表(如果存在dma),初始化设备互斥锁,设置lockdep_map对象的各项参数,如果subclass非0,分配一个新的锁类并将其添加到散列中,初始化raw_spinlock_t结构对象,初始化设备资源链表,初始化设备电源操作结构,包括初始化与系统挂起相关的设备字段、初始化给定设备对象中的运行时pm字段,设置numa节点(不指定numa),初始化厂商设备链接列表,初始化消费者设备的链接列表,挂钩到已延迟sync_state的设备的全局列表,关联默认的io tlb内存池描述结构等等。

    提交设备节点添加请求过程
      通过设备的根kobj,通过container_of偏移到设备对象,初始化设备的私有区域的klist链表,绑定klist_children_get和klist_children_put函数,用于访问私有区域关联的设备(通过klist遍历设备节点链表,然后通过这两个函数访问设备),初始化deferred_probe链表,用于重试无法获得设备所需的所有资源的驱动程序的绑定(通常是因为它取决于另一个驱动程序首先被探测),对于静态分配的设备,我们需要初始化名称,子系统可以指定简单的设备枚举。
      获取设备的父级结构,如果设备类存在:指定了块类型,得到块设备的根kobj,或设备私有结构的子系统根kobj,虚拟设备则以非类设备为父级的类设备位于“粘合”目录中,以防止命名空间冲突,真实设备则分配class_dir对象,目录kobj初始化,并关联class_dir_ktype,关联私有结构的目录kset容器,目录kobj关联父级kobj,形成树形遍历表,如果设备类不存在:父级设备存在,获取父级设备的根kobj,父级设备不存在,返回总线根设备的根kobj(总线已注册并且根设备已注册),关联设备根kobj的父级,形成树形设备目录结构。
      使用父级numa节点,注册通用层(如果需要),通知平台设备进入,设备关联电源管理设备,并设置相关的唤醒方式,设备与软件节点互作软连接,并为通知平台预留了接口函数,为设备创建sysfs属性文件,设备软链接到子系统,“subsystem”,如果设备不属于分区类型"partition",设备软链接到父设备,“device”,为设备的kobj增加到class的属性组中,type的属性组中,设备物理位置属性组中,如果设备的固件节点存在,解码由_PLD方法返回的位包装缓冲区到一个对ACPI驱动程序更有用的本地结构(描述设备连接点在系统外壳中的物理位置),总线(bus)与设备之间的软连接,设备私有结构的knode_bus 加入 bus私有结构的klist_devices链表中,设备添加到电源管理相关的属性组中,设备添加到电源管理核心的活动设备列表中,设备创建 主版本号:子版本号链接,如 “1: 80” ,devtmpfs虚拟文件系统提交设备节点请求。
      调用阻塞通知链中的函数,表示设备增加(通过发送uevent通知用户空间),新设备探测驱动程序,如果总线允许,自动探测驱动程序,如果所有的驱动程序注册都完成了,并且新添加的设备与任何驱动程序都不匹配,那么不要阻止它的消费者进行探测,以防消费者设备能够在没有这个供应商的情况下运行,如果设备类存在,通知所有接口设备在此。


    目录


    1. 函数分析

    1.1 devices_init

    1.2 device_register

    2. 源码结构

    3. 部分结构定义

    4. 扩展函数/变量

    目录预览


    1. 函数分析

    1.1 devices_init

      创建kset容器结构对象devices_kset,用于设备的uevent操作

      分配kobj对象dev_kobj(“dev”)表示根设备对象,
      分配kobj对象sysfs_dev_block_kobj(“block”)表示 块 的根设备对象
      分配kobj对象sysfs_dev_char_kobj(“char”)表示 字符 的根设备对象

    int __init devices_init(void)
    {
            devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL); // 创建kset容器结构对象devices_kset
            // 设置根kobj名称,关联kobjs的uevent操作(如果存在)等,
            // 然后通过kset_register函数 初始化sysfs对象的引用计数、链表对象及一些变量标志,
            // kobj对象关联父指针(这里它是根节点(NULL),通过发送uevent通知用户空间(如果设置了uevent),
            // kobj向用户空间发送环境缓冲内容
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    device_uevent_ops

    dev_kobj = kobject_create_and_add("dev", NULL); // 分配并初始化sysfs对象(kobj对象)
    // 关联kobj_ktype,为kobj对象赋值父对象指针,并加入到kset容器(链表)中,
    // 然后通create_dir 获取kobj对象的sysfs所有权数据,分配kernfs节点…
    // static struct kobject *dev_kobj; 全局dev_kobj对象
    
    sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);  // 分配并初始化kobj对象sysfs_dev_block_kobj,关联父级dev_kobj
    // block,表示块设备
    
    sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj); // 分配并初始化kobj对象sysfs_dev_char_kobj ,关联父级dev_kobj
    // char,表示字符设备
    
    return 0;
    ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.2 device_register

      注册新的设备

    int device_register(struct device *dev)
    {
            device_initialize(dev); // 设备结构对象初始化
            // 关联devices_kset容器,关联device_ktype,初始化dma链表,初始化设备互斥锁
            // 设置lockdep_map对象的各项参数,如果subclass非0,分配一个新的锁类并将其添加到散列中
            // 初始化raw_spinlock_t结构对象,初始化设备资源链表,初始化设备电源操作结构,
            // 包括初始化与系统挂起相关的设备字段、初始化给定设备对象中的运行时pm字段
            // 设置numa节点(不指定numa),初始化厂商设备链接列表,初始化消费者设备的链接列表
            // 挂钩到已延迟sync_state的设备的全局列表,关联默认的io tlb内存池描述结构
            
            return device_add(dev); // devtmpfs虚拟文件系统提交设备节点添加请求
    }
    EXPORT_SYMBOL_GPL(device_register);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    device_initialize
    device_add


    2. 源码结构

      device_uevent_ops kset与它相关的kobjects的uevent操作

    static const struct kset_uevent_ops device_uevent_ops = {
            .filter =       dev_uevent_filter, // 设备用户事件过滤函数
            // 允许kset阻止一个特定kobject的uevent被发送到用户空间
    		// 如果该函数返回0,该uevent将不会被发送出去
    
            .name =         dev_uevent_name, // 获取用户事件名称
            // 如果关联了bus,返回bus名称
            // 如果关联了class,返回class名称
            
            .uevent =       dev_uevent, // 当uevent即将被发送至用户空间时,uevent函数将被调用
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    dev_uevent_filter

      device_ktype 设备动态注册的sysfs的ktype(用于释放对象,访问sysfs文件等等)

    static struct kobj_type device_ktype = {
            .release        = device_release, // 释放设备函数,包括释放设备私有结构、设备节点等等
            .sysfs_ops      = &dev_sysfs_ops, // // 读取(显示)、写入(存储)函数
            .namespace      = device_namespace, // 设备关联的class(关联)的命名空间
            .get_ownership  = device_get_ownership, // // 允许类为属于该类的设备指定sysfs目录的uid/gid
            // 通常绑定到设备的命名空间
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      block_class 块类

    struct class block_class = {
    	.name		= "block",
    	.dev_uevent	= block_uevent, // block_uevent 块用户事件函数,向环境缓存区写入diskseq信息
    };
    
    • 1
    • 2
    • 3
    • 4

      class_dir_ktype 类目录操作

    static struct kobj_type class_dir_ktype = {
    	.release	= class_dir_release,  // 类目录释放函数
    	.sysfs_ops	= &kobj_sysfs_ops, // sysfs操作结构对象
    	// 读取(显示,kobj_attr_show)、写入(存储,kobj_attr_store)默认函数
    	
    	.child_ns_type	= class_dir_child_ns_type // 回调使sysfs可以确定名称空间
    	// 通过kobj对象指针经过偏移计算,获得指向的class对象指针指向的命令空间类型指针
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    class_dir
    class_dir_child_ns_type

      pm_wakeup_attr_group 电源唤醒(记录)属性组

    static const struct attribute_group pm_wakeup_attr_group = {
    	.name	= power_group_name,
    	.attrs	= wakeup_attrs, // wakeup_attrs[],包含多项属性描述结构
    };
    
    • 1
    • 2
    • 3
    • 4

      dev_attr_physical_location_group 设备物理位置属性组

    const struct attribute_group dev_attr_physical_location_group = {
    	.name = "physical_location",
    	.attrs = dev_attr_physical_location,
    };
    
    • 1
    • 2
    • 3
    • 4

    3. 部分结构定义

      class_dir 类目录结构

    struct class_dir {
    	struct kobject kobj; // 根kobj
    	struct class *class; // 类
    };
    
    • 1
    • 2
    • 3
    • 4

      kobj_ns_type_operations kobj命名空间类型操作

    struct kobj_ns_type_operations {
    	enum kobj_ns_type type; // 用于标记kobjects和sysfs条目的命名空间类型
    	bool (*current_may_mount)(void); // 如果当前任务(想要的用户名称空间)具有当前可用的给定高级功能,则返回true
    	// 如果在假设即将使用该功能的情况下该功能可用,则会在任务上设置PF_SUPERPRIV
    	
    	void *(*grab_current_ns)(void); // 返回对调用任务命名空间的新引用
    	const void *(*netlink_ns)(struct sock *sk); // 返回sock所属的命名空间
    	const void *(*initial_ns)(void); // 返回初始名称空间
    	void (*drop_ns)(void *); // 删除对命名空间的引用
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

      fwnode_operations fwnode接口的操作

    struct fwnode_operations {
    	struct fwnode_handle *(*get)(struct fwnode_handle *fwnode); // 获取对fwnode的引用
    	
    	void (*put)(struct fwnode_handle *fwnode); // 将引用放在fwnode上
    	
    	bool (*device_is_available)(const struct fwnode_handle *fwnode); // 如果设备可用,则返回true
    	
    	const void *(*device_get_match_data)(const struct fwnode_handle *fwnode,
    					     const struct device *dev); // 返回设备驱动程序匹配数据
    					     
    	bool (*device_dma_supported)(const struct fwnode_handle *fwnode);  // 是否支持DMA
    	
    	enum dev_dma_attr
    	(*device_get_dma_attr)(const struct fwnode_handle *fwnode); // 检查DMA设备是否一致
    	
    	bool (*property_present)(const struct fwnode_handle *fwnode,
    				 const char *propname); // 如果存在属性,则返回true
    				 
    	int (*property_read_int_array)(const struct fwnode_handle *fwnode,
    				       const char *propname,
    				       unsigned int elem_size, void *val,
    				       size_t nval); // 读取整数属性数组,成功时返回零
    				       
    	int
    	(*property_read_string_array)(const struct fwnode_handle *fwnode_handle,
    				      const char *propname, const char **val,
    				      size_t nval); // 读取字符串属性数组,成功时返回零
    				      
    	const char *(*get_name)(const struct fwnode_handle *fwnode); // 返回fwnode的名称
    	
    	const char *(*get_name_prefix)(const struct fwnode_handle *fwnode); // 获取节点的前缀(用于打印)
    	
    	struct fwnode_handle *(*get_parent)(const struct fwnode_handle *fwnode); // 返回fwnode的父级
    	
    	struct fwnode_handle *
    	(*get_next_child_node)(const struct fwnode_handle *fwnode,
    			       struct fwnode_handle *child); // 返回迭代中的下一个子节点
    			       
    	struct fwnode_handle *
    	(*get_named_child_node)(const struct fwnode_handle *fwnode,
    				const char *name); // 返回具有给定名称的子节点
    				
    	int (*get_reference_args)(const struct fwnode_handle *fwnode,
    				  const char *prop, const char *nargs_prop,
    				  unsigned int nargs, unsigned int index,
    				  struct fwnode_reference_args *args); // 返回属性指向的引用,带参数
    				  
    	struct fwnode_handle *
    	(*graph_get_next_endpoint)(const struct fwnode_handle *fwnode,
    				   struct fwnode_handle *prev); // 返回迭代中的端点节点
    				   
    	struct fwnode_handle *
    	(*graph_get_remote_endpoint)(const struct fwnode_handle *fwnode); // 返回本地端点节点的远程端点节点
    	
    	struct fwnode_handle * 
    	(*graph_get_port_parent)(struct fwnode_handle *fwnode); // 返回端口节点的父节点
    	
    	int (*graph_parse_endpoint)(const struct fwnode_handle *fwnode,
    				    struct fwnode_endpoint *endpoint); // 解析端口和端点id的端点
    				    
    	void __iomem *(*iomap)(struct fwnode_handle *fwnode, int index); // 映射物理地址到虚拟内存
    	
    	int (*irq_get)(const struct fwnode_handle *fwnode, unsigned int index); // 获取irq编号(中断号)
    	
    	int (*add_links)(struct fwnode_handle *fwnode); // 创建指向fwnode所有厂商的fwnode链接
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

      acpi_bus_type 表示一种电源管理总线的类型(不包括pci设备)

      pci设备通过pci总线类型 关联的电源管理操作结构

    struct acpi_bus_type {
    	struct list_head list; // 链表
    	const char *name; // 名称
    	bool (*match)(struct device *dev);  // 匹配函数
    	struct acpi_device * (*find_companion)(struct device *); // 找同类
    	void (*setup)(struct device *); // 设置
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      acpi_device_wakeup_context 电源管理设备唤醒上下文

    struct acpi_device_wakeup_context {
    	void (*func)(struct acpi_device_wakeup_context *context);
    	struct device *dev;
    };
    
    • 1
    • 2
    • 3
    • 4

      swait_queue 任务(进程)简单等待队列

      简单等待队列在语义上与常规等待队列(wait.h)非常不同
      最重要的区别是,简单的等待队列允许确定性行为——IOW它具有严格限制的IRQ和锁保持时间

    struct swait_queue {
    	struct task_struct	*task; // 任务(进程)
    	struct list_head	task_list; 任务列表
    };
    
    • 1
    • 2
    • 3
    • 4

      irq_domain 硬件中断号转换对象(中断域)

    struct irq_domain {
    	struct list_head link; // 链表
    	const char *name; // 中断域名称
    	const struct irq_domain_ops *ops; // 中断域操作
    	void *host_data;  // 供所有者使用的私有数据指针。irq_domain核心代码没有触及
    	unsigned int flags; // 每个主机的中断域标志
    	unsigned int mapcount; // 映射的中断数
    
    	/* 可选的数据 */
    	struct fwnode_handle *fwnode; // 指向与中断域关联的固件节点的指针
    	enum irq_domain_bus_token bus_token; // 中断域总线的标志
    	// 如果几个域具有相同的设备节点,但服务于不同的目的(例如,一个域用于PCI/MSI,另一个用于有线irq),
    	// 则可以使用特定于总线的标志来区分它们
    	// 大多数域期望只携带DOMAIN_BUS_ANY
    
    	struct irq_domain_chip_generic *gc; // 指向通用芯片列表的指针
    	struct device *dev; // 指向域所代表的设备,该设备将用于电源管理
    #ifdef	CONFIG_IRQ_DOMAIN_HIERARCHY
    	struct irq_domain *parent; // 指向父irq_domain以支持层次结构irq_ddomains的指针
    #endif
    
    	/* 反向映射数据,线性映射被附加到irq_domain */
    	irq_hw_number_t hwirq_max;
    
    	/* revmap数据,由irq_domain内部使用 */
    	unsigned int revmap_size; // revmap[] 线性映射表的大小 
    	struct radix_tree_root revmap_tree; // 不适合线性映射的hwirq的基数映射树
    	struct mutex revmap_mutex; // 锁定revmap
    	struct irq_data __rcu *revmap[]; // irq_data指针的线性表
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

      acpi_table_iort 电源管理 IO重新映射表

      符合《ARM平台IO重映射表系统软件》,文件编号:ARM DEN 0049E.d, 2022年2月

    struct acpi_table_iort {
    	struct acpi_table_header header; // 通用电源管理表头
    	u32 node_count;
    	u32 node_offset;
    	u32 reserved;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    acpi_table_header

      acpi_table_header 通用电源管理表头

    struct acpi_table_header {
    	char signature[ACPI_NAMESEG_SIZE];	/* ASCII表签名 */
    	u32 length;		/* 表的长度(以字节为单位),包括此标头 */
    	u8 revision;		/* ACPI规范次要版本号 */
    	u8 checksum;		/* 使整个表的总和 == 0 */
    	char oem_id[ACPI_OEM_ID_SIZE];	/* ASCII OEM识别 */
    	char oem_table_id[ACPI_OEM_TABLE_ID_SIZE];	/* ASCII OEM表识别*/
    	u32 oem_revision;	/* OEM版本号 */
    	char asl_compiler_id[ACPI_NAMESEG_SIZE];	/* ASCII ASL编译器供应商ID */
    	u32 asl_compiler_revision;	/* ASL编译器版本 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      irq_fwspec 通用 irq说明符结构

    struct irq_fwspec {
    	struct fwnode_handle *fwnode; // 指向固件特定描述符的指针
    	int param_count; // 设备特定参数的数量
    	u32 param[IRQ_DOMAIN_IRQ_SPEC_PARAMS]; // 设备特定参数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

      dev_msi_info 与MSI相关的设备数据

      message signal interrupt,消息信号中断

    struct dev_msi_info {
    #ifdef CONFIG_GENERIC_MSI_IRQ_DOMAIN
    	struct irq_domain	*domain;  // 与设备相关联的MSI中断域
    #endif
    #ifdef CONFIG_GENERIC_MSI_IRQ
    	struct msi_device_data	*data; // 指向MSI设备数据的指针
    #endif
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4. 扩展函数/变量


      dev_uevent_filter 设备用户事件过滤函数

    static int dev_uevent_filter(struct kobject *kobj)
    {
            const struct kobj_type *ktype = get_ktype(kobj);
    
            if (ktype == &device_ktype) { // 如果是相同的ktype
                    struct device *dev = kobj_to_dev(kobj);
                    if (dev->bus) // 如果设备关联了bus
                            return 1;
                    if (dev->class) // 如果设备关联了class
                            return 1;
            }
            return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    device_ktype

      device_initialize 设备结构对象初始化

      关联devices_kset容器,关联device_ktype,初始化dma链表,初始化设备互斥锁
      设置lockdep_map对象的各项参数,如果subclass非0,分配一个新的锁类并将其添加到散列中
      初始化raw_spinlock_t结构对象,初始化设备资源链表,初始化设备电源操作结构,
      包括初始化与系统挂起相关的设备字段、初始化给定设备对象中的运行时pm字段
      设置numa节点(不指定numa),初始化厂商设备链接列表,初始化消费者设备的链接列表
      挂钩到已延迟sync_state的设备的全局列表,关联默认的io tlb内存池描述结构

    void device_initialize(struct device *dev)
    {
            dev->kobj.kset = devices_kset; // 关联devices_kset容器
            kobject_init(&dev->kobj, &device_ktype); // 初始化kobj对象,并关联设备动态注册的sysfs的ktype
            INIT_LIST_HEAD(&dev->dma_pools); // 初始化dma链表
            mutex_init(&dev->mutex); // 初始化设备互斥锁
            lockdep_set_novalidate_class(&dev->mutex); // 设置lockdep_map对象的各项参数,如果subclass非0,分配一个新的锁类并将其添加到散列中
            spin_lock_init(&dev->devres_lock); // 初始化raw_spinlock_t结构对象
            INIT_LIST_HEAD(&dev->devres_head); // 初始化设备资源链表
            device_pm_init(dev); // 初始化设备电源操作结构,包括初始化与系统挂起相关的设备字段、初始化给定设备对象中的运行时pm字段
            set_dev_node(dev, NUMA_NO_NODE); // 设置numa节点(不指定numa)
            INIT_LIST_HEAD(&dev->links.consumers); // 初始化厂商设备链接列表
            INIT_LIST_HEAD(&dev->links.suppliers); // 初始化消费者设备的链接列表
            INIT_LIST_HEAD(&dev->links.defer_sync); // 挂钩到已延迟sync_state的设备的全局列表
            dev->links.status = DL_DEV_NO_DRIVER; // 驱动状态信息
            
    #if defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_DEVICE) || \
        defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU) || \
        defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU_ALL) // 默认配置dma这三项没定义
    	dev->dma_coherent = dma_default_coherent;
    #endif
    #ifdef CONFIG_SWIOTLB
    	dev->dma_io_tlb_mem = &io_tlb_default_mem; // 默认的io tlb内存池描述符
    #endif
    }
    EXPORT_SYMBOL_GPL(device_initialize);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    lockdep_set_novalidate_class

      lockdep_set_novalidate_class 分配一个新的锁类并将其添加到散列中

      设置lockdep_map对象的各项参数,如果subclass非0,分配一个新的锁类并将其添加到散列中

    #define lockdep_set_novalidate_class(mutex) \
            lockdep_set_class_and_name(mutex, &__lockdep_no_validate__, "mutex") // struct lock_class_key __lockdep_no_validate__;
            ||
            \/
            lockdep_init_map_type(&(mutex)->dep_map, "mutex", key, 0,   \
                                  (mutex)->dep_map.wait_type_inner,  \
                                  (mutex)->dep_map.wait_type_outer,  \
                                  (mutex)->dep_map.lock_type)
    		||
    		\/
    void lockdep_init_map_type(struct lockdep_map *lock, const char *name,
                                struct lock_class_key *key, int subclass,
                                u8 inner, u8 outer, u8 lock_type)
    {
            int i;
    
            for (i = 0; i < NR_LOCKDEP_CACHING_CLASSES; i++)  // 2个锁类
                    lock->class_cache[i] = NULL;
    
    #ifdef CONFIG_LOCK_STAT
            lock->cpu = raw_smp_processor_id(); // 得到当前cpu id
    #endif
    
    		lock->name = name; // 锁名称
    
            lock->wait_type_outer = outer; // 锁等待类型外部的
            lock->wait_type_inner = inner; // 内部的
            lock->lock_type = lock_type; // 锁类型
    
    		 /* 完整性检查,锁类密钥必须是静态分配或者是动态注册 */
    		 if (!static_obj(key) && !is_dynamic_key(key)) {
                    if (debug_locks)
                            printk(KERN_ERR "BUG: key %px has not been registered!\n", key);
                    DEBUG_LOCKS_WARN_ON(1);
                    return;
            }
            lock->key = key;
    
    		/* 我们希望通过全局标志同时打开/关闭所有锁调试工具
    		 * 一旦检测到并报告了一个错误,就可能会出现一连串的后续错误,
    		 * 这些错误只会搅乱日志。所以我们只报告第一起错误
    		 * /
    		if (unlikely(!debug_locks))
                    return;
    
    		if (subclass) { // 这里为0
                    unsigned long flags;
    
                    if (DEBUG_LOCKS_WARN_ON(!lockdep_enabled())) // 如果没有打开debug锁选项 或 每CPU lockdep版本存在 或 当前任务的 lockdep版本存在,打印警告信息
                            return; 
    
                    raw_local_irq_save(flags); // 禁用CPU上的硬中断
    				lockdep_recursion_inc(); // 每CPU lockdep版本加1
                    register_lock_class(lock, subclass, 1); // 分配一个新的锁类并将其添加到散列中
                    lockdep_recursion_finish(); // 每CPU lockdep版本写0
    				raw_local_irq_restore(flags); // 恢复CPU上的硬中断
    	}
    }
    EXPORT_SYMBOL_GPL(lockdep_init_map_type);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    register_lock_class

      register_lock_class 分配一个新的锁类并将其添加到散列中

    static struct lock_class *
    register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
    {
    	struct lockdep_subclass_key *key;
    	struct hlist_head *hash_head;
    	struct lock_class *class;
    	int idx;
    
    	DEBUG_LOCKS_WARN_ON(!irqs_disabled());
    
    	class = look_up_lock_class(lock, subclass); // 查找锁类是否存在
    	if (likely(class))
    		goto out_set_class_cache;
    
    	key = lock->key->subkeys + subclass; // subclass 0
    	hash_head = classhashentry(key); // 计算哈希链表地址
    	// #define classhashentry(key)	(classhash_table + __classhashfn((key))) 哈希链表头地址 + key的哈希值
    	//	64位处理器的class哈希运算,val * 大的奇数(0x61C8864680B583EBull),然后右移52位
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    look_up_lock_class
    __classhashfn

    	/* 为了避免与另一个CPU竞争,我们不得不再次进行哈希值比较 */
    	hlist_for_each_entry_rcu(class, hash_head, hash_entry) {
    		if (class->key == key)
    			goto out_unlock_set;
    	}
    
    	init_data_structures_once(); // 初始化lock_classes[]数组元素、free_lock_classes列表以及delayed_free结构
    	// 初始化块链表
    
    	/* 分配一个新的锁类并将其添加到散列中 */
    	class = list_first_entry_or_null(&free_lock_classes, typeof(*class),
    					 lock_entry);
    
    	nr_lock_classes++; // class空间锁+1
    	__set_bit(class - lock_classes, lock_classes_in_use); // 向lock_classes_in_use数组中写入偏移后的地址
    	debug_atomic_inc(nr_unused_locks);
    	class->key = key;
    	class->name = lock->name;
    	class->subclass = subclass;
    	WARN_ON_ONCE(!list_empty(&class->locks_before));
    	WARN_ON_ONCE(!list_empty(&class->locks_after));
    	class->name_version = count_matching_names(class);
    	class->wait_type_inner = lock->wait_type_inner;
    	class->wait_type_outer = lock->wait_type_outer;
    	class->lock_type = lock->lock_type;
    	/*
    	 * 我们使用RCU的safe list-add方法来实现哈希表的并行遍历:
    	 */
    	hlist_add_head_rcu(&class->hash_entry, hash_head);
    
    	/*
    	 * 从空闲列表中删除类,并将其添加到类的全局列表中
    	 */
    	list_move_tail(&class->lock_entry, &all_lock_classes);
    	idx = class - lock_classes; // id索引值
    	if (idx > max_lock_class_idx) // 如果索引值大于默认最大值,索引值设置为最大值
    		max_lock_class_idx = idx;
    	
    	...
    	}
    out_unlock_set:
    	graph_unlock(); // lockdep_unlock();
    
    out_set_class_cache:
    	if (!subclass || force)
    		lock->class_cache[0] = class;
    	else if (subclass < NR_LOCKDEP_CACHING_CLASSES)
    		lock->class_cache[subclass] = class;
    
    	/*
    	 * Hash collision, did we smoke some? We found a class with a matching
    	 * hash but the subclass -- which is hashed in -- didn't match.
    	 */
    	if (DEBUG_LOCKS_WARN_ON(class->subclass != subclass))
    		return NULL;
    
    	return class;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

      look_up_lock_class 查找锁类是否存在

    static noinstr struct lock_class *
    look_up_lock_class(const struct lockdep_map *lock, unsigned int subclass)
    {
    	struct lockdep_subclass_key *key;
    	struct hlist_head *hash_head;
    	struct lock_class *class;
    
    	if (unlikely(subclass >= MAX_LOCKDEP_SUBCLASSES)) {
    		instrumentation_begin(); // 安全区域的开始
    		// 在.discard.instr_begin 段添加一个长整形计数,从0开始,每次调用加1
    		
    		debug_locks_off(); // 如果关闭"所有锁调试"功能,控制台输出等级调整为CONSOLE_LOGLEVEL_MOTORMOUTH 15
    		printk(KERN_ERR
    			"BUG: looking up invalid subclass: %u\n", subclass);
    		printk(KERN_ERR
    			"turning off the locking correctness validator.\n");
    		dump_stack(); // 转储当前任务信息及其堆栈跟踪
    		instrumentation_end(); // 安全区域的结束
    		return NULL;
    	}
    
    	/* 如果它没有初始化,那么它从未被锁定,因此它不会出现在哈希表中 */
    	if (unlikely(!lock->key))
    		return NULL;
    
    	/*
    	 * 类密钥必须是唯一的
    	 * 对于动态锁,静态lock_class_key变量通过mutex_init() 或spin_lock_init() 调用传入,该调用充当密钥
    	 * 对于静态锁,我们使用锁对象本身作为密钥
    	 * /
    	if (DEBUG_LOCKS_WARN_ON(!irqs_disabled()))
    		return NULL;
    
    	hlist_for_each_entry_rcu_notrace(class, hash_head, hash_entry) {
    		if (class->key == key) {
    			/*
    			 * Huh! same key, different name? Did someone trample
    			 * on some memory? We're most confused.
    			 */
    			WARN_ONCE(class->name != lock->name &&
    				  lock->key != &__lockdep_no_validate__,
    				  "Looking for class \"%s\" with key %ps, but found a different class \"%s\" with the same key\n",
    				  lock->name, lock->key, class->name);
    			return class;
    		}
    	}
    
    	return NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    instrumentation_begin

      instrumentation_begin 在.discard.instr_begin 段添加一个长整形计数,从0开始,每次调用加1

    #define instrumentation_begin() __instrumentation_begin(__COUNTER__) // __COUNTER__ 是一个计数器,从0开始,每次调用加1
    ||
    \/
    #define __instrumentation_begin(0) ({					\
    	asm volatile(__stringify(0) ": nop\n\t"				\  
    		     ".pushsection .discard.instr_begin\n\t"		\   
    		     ".long " __stringify(0) "b - .\n\t"		\
    		     ".popsection\n\t" : : "i" (0));			\
    })
    ||
    \/ // 扩展成汇编代码
     		0: nop 
            .pushsection .discard.instr_begin // .pushsection 在目标文件中插入一个 .discard.instr_begin 段
            .long 0b - . // 填充一个长整型数字
            .popsection // .pushsection 与 .popsection 成对使用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

      __classhashfn 64位处理器的class哈希运算,val * 大的奇数(0x61C8864680B583EBull),然后右移52位

    #define __classhashfn(key)	hash_long((unsigned long)key, CLASSHASH_BITS) // CLASSHASH_BITS 12
    ||
    \/
    static __always_inline u32 hash_64_generic(u64 val, unsigned int bits)
    {
    	/* 64x64位乘法在所有64位处理器上都是有效的 */
    	return val * GOLDEN_RATIO_64 >> (64 - bits);
    	||
    	\/
    	return val * 0x61C8864680B583EBull >> (64 - 12);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      device_add devtmpfs虚拟文件系统提交设备节点添加请求

      通过设备的根kobj,通过container_of偏移到设备对象
      初始化设备的私有区域的klist链表,绑定klist_children_get和klist_children_put函数,
      用于访问私有区域关联的设备(通过klist遍历设备节点链表,然后通过这两个函数访问设备)

      初始化deferred_probe链表,用于重试无法获得设备所需的所有资源的驱动程序的绑定
      通常是因为它取决于另一个驱动程序首先被探测

      对于静态分配的设备(它们总有一天会被转换),我们需要初始化名称
      子系统可以指定简单的设备枚举

      获取设备的父级结构
      如果设备类存在:指定了块类型,得到块设备的根kobj,或设备私有结构的子系统根kobj;
      虚拟设备则以非类设备为父级的类设备位于“粘合”目录中,以防止命名空间冲突;
      真实设备则分配class_dir对象,目录kobj初始化,并关联class_dir_ktype,
      关联私有结构的目录kset容器,目录kobj关联父级kobj,形成树形遍历表;
      如果设备类不存在:父级设备存在,获取父级设备的根kobj;
      父级设备不存在,返回总线根设备的根kobj(总线已注册并且根设备已注册);
      关联设备根kobj的父级,形成树形设备目录结构

      使用父级numa节点,注册通用层(如果需要),通知平台设备进入
      设备关联电源管理设备,并设置相关的唤醒方式
      设备与软件节点互作软连接,并为通知平台预留了接口函数

      为设备创建sysfs属性文件,设备软链接到子系统,“subsystem”
      如果设备不属于分区类型"partition",设备软链接到父设备,“device”

      为设备的kobj增加到class的属性组中,type的属性组中,设备物理位置属性组中
      如果设备的固件节点存在,解码由_PLD方法返回的位包装缓冲区到一个对ACPI驱动程序更有用的本地结构(描述设备连接点在系统外壳中的物理位置)

      总线(bus)与设备之间的软连接
      设备私有结构的knode_bus 加入 bus私有结构的klist_devices链表中

      设备添加到电源管理相关的属性组中
      设备添加到电源管理核心的活动设备列表中

      设备创建 主版本号:子版本号链接,如 “1: 80”
      devtmpfs虚拟文件系统提交设备节点请求

      调用阻塞通知链中的函数,表示设备增加
      通过发送uevent通知用户空间

      新设备探测驱动程序,如果总线允许,自动探测驱动程序

      如果所有的驱动程序注册都完成了,并且新添加的设备与任何驱动程序都不匹配,
      那么不要阻止它的消费者进行探测,以防消费者设备能够在没有这个供应商的情况下运行

      如果设备类存在,通知所有接口设备在此

    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;
    
    	dev = get_device(dev); // 通过设备的根kobj,通过container_of偏移到设备对象
    
    	if (!dev->p) {
    		error = device_private_init(dev); // 设备私有区域关联这个设备对象
    		// 初始化私有区域的klist链表,绑定klist_children_get和klist_children_put函数,用于访问私有区域关联的设备(通过klist遍历设备节点链表,然后通过这两个函数访问设备)
    		// 初始化deferred_probe链表,用于重试无法获得设备所需的所有资源的驱动程序的绑定
    		// 通常是因为它取决于另一个驱动程序首先被探测
    		
    		if (error)
    			goto done;
    	}
    
    	/*
    	 * 对于静态分配的设备(它们总有一天会被转换),我们需要初始化名称
    	 * 我们阻止回读名称,并强制使用dev_name()
    	 * /
    	if (dev->init_name) { 
    		dev_set_name(dev, "%s", dev->init_name);
    		dev->init_name = NULL;
    	}
    
    	/* 子系统可以指定简单的设备枚举 */
    	if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
    		dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
    
    	pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
    
    	parent = get_device(dev->parent); // 获取设备父级
    	kobj = get_device_parent(dev, parent); // 获取设备的父级结构
    	// 如果设备类存在:指定了块类型,得到块设备的根kobj,或设备私有结构的子系统根kobj;
    	// 虚拟设备则以非类设备为父级的类设备位于“粘合”目录中,以防止命名空间冲突;
    	// 真实设备则分配class_dir对象,目录kobj初始化,并关联class_dir_ktype,
    	// 关联私有结构的目录kset容器,目录kobj关联父级kobj,形成树形遍历表;
    	// 如果设备类不存在:父级设备存在,获取父级设备的根kobj;
    	// 父级设备不存在,返回总线根设备的根kobj(总线已注册并且根设备已注册);
    
    	if (kobj)
    		dev->kobj.parent = kobj; // 关联设备根kobj的父级,形成树形设备目录结构
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    get_device_parent

    	/* 使用父级numa节点 */
    	if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
    		set_dev_node(dev, dev_to_node(parent));
    
    	/* 首先,注册通用层 */
    	/* 我们要求在之前设置名称,并传递NULL */
    	error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
    	if (error) {
    		glue_dir = get_glue_dir(dev);
    		goto Error;
    	}
    
    	/* 通知平台设备进入 */
    	device_platform_notify(dev);  // 设备关联电源管理设备,并设置相关的唤醒方式
    	// 设备与软件节点互作软连接,并为通知平台预留了接口函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    device_platform_notify

    error = device_create_file(dev, &dev_attr_uevent); // 为设备创建sysfs属性文件
    error = device_add_class_symlinks(dev); // 设备软链接到子系统,"subsystem"
    // 如果设备不属于分区类型"partition",设备软链接到父设备,"device"
    
    error = device_add_attrs(dev); // 为设备的kobj增加到class的属性组中,type的属性组中,设备物理位置属性组中
    // 如果设备的固件节点存在,解码由_PLD方法返回的位包装缓冲区到一个对ACPI驱动程序更有用的本地结构(描述设备连接点在系统外壳中的物理位置)
    
    error = bus_add_device(dev); // 总线(bus)与设备之间的软连接
    // 设备私有结构的knode_bus 加入 bus私有结构的klist_devices链表中 
    
    error = dpm_sysfs_add(dev); // 设备添加到电源管理相关的属性组中
    
    device_pm_add(dev); // 设备添加到电源管理核心的活动设备列表中
    
    if (MAJOR(dev->devt)) {
    		error = device_create_file(dev, &dev_attr_dev); // 为设备创建sysfs属性文件
    		if (error)
    			goto DevAttrError;
    
    		error = device_create_sys_dev_entry(dev); // 设备创建 主版本号:子版本号链接,如 "5: 50" 
    		if (error)
    			goto SysEntryError;
    
    		devtmpfs_create_node(dev); // devtmpfs虚拟文件系统提交设备节点请求
    	}
    
    	/* 
    	 * 通知客户端设备的添加
    	 * 这个调用必须在dpm_sysfs_add()之后和kobject_uevent()之前进行
    	 */
    	if (dev->bus)
    		blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
    					     BUS_NOTIFY_ADD_DEVICE, dev); // 调用阻塞通知链中的函数,表示设备增加
    
    	kobject_uevent(&dev->kobj, KOBJ_ADD); // 通过发送uevent通知用户空间
    
    	/*
    	 * 检查是否有任何其他设备(消费者)一直在等待该设备(供应商)被添加,
    	 * 以便它们可以创建到该设备的设备链接
    	 * 
         * 这需要发生在device_pm_add()之后,因为device_link_add()要求在调用供应商之前注册它
         * 
         * 但这也需要发生在bus_probe_device()之前,
         * 以确保等待的消费者可以在驱动程序绑定到设备和为该设备调用驱动程序sync_state回调之前链接到它
         * /
    	if (dev->fwnode && !dev->fwnode->dev) {
    		dev->fwnode->dev = dev;
    		fw_devlink_link_device(dev);
    	}
    
    	bus_probe_device(dev); // 新设备探测驱动程序
    	// 如果总线允许,自动探测驱动程序
    
    	/*
    	 * 如果所有的驱动程序注册都完成了,并且新添加的设备与任何驱动程序都不匹配,
    	 * 那么不要阻止它的消费者进行探测,以防消费者设备能够在没有这个供应商的情况下运行
    	 */
    	if (dev->fwnode && fw_devlink_drv_reg_done && !dev->can_match)
    		fw_devlink_unblock_consumers(dev);
    
    	if (parent)
    		klist_add_tail(&dev->p->knode_parent,
    			       &parent->p->klist_children); // 设备私有结构的knode_parent 添加到父级私有结构的klist_children链表中
    			     
    	if (dev->class) { // 如果设备类存在
    		mutex_lock(&dev->class->p->mutex);
    		/* tie the class to the device */
    		klist_add_tail(&dev->p->knode_class,
    			       &dev->class->p->klist_devices); //  设备私有结构的knode_class 添加到 设备类私有结构的klist_devices
    
    		/* 通知所有接口设备在此 */
    		list_for_each_entry(class_intf,
    				    &dev->class->p->interfaces, node)
    			if (class_intf->add_dev)
    				class_intf->add_dev(dev, class_intf);
    		mutex_unlock(&dev->class->p->mutex);
    	}
    EXPORT_SYMBOL_GPL(device_add);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    dev_attr_physical_location_group
    dev_add_physical_location

      get_device_parent 获取设备的父级结构

      如果设备类存在:指定了块类型,得到块设备的根kobj,或设备私有结构的子系统根kobj;
      虚拟设备则以非类设备为父级的类设备位于“粘合”目录中,以防止命名空间冲突;
      真实设备则分配class_dir对象,目录kobj初始化,并关联class_dir_ktype,
      关联私有结构的目录kset容器,目录kobj关联父级kobj,形成树形遍历表;

      如果设备类不存在:父级设备存在,获取父级设备的根kobj;
      父级设备不存在,返回总线根设备的根kobj(总线已注册并且根设备已注册);

    static struct kobject *get_device_parent(struct device *dev,
    					 struct device *parent)
    {
    	if (dev->class) { // 如果设备类存在
    		struct kobject *kobj = NULL;
    		struct kobject *parent_kobj;
    		struct kobject *k;
    
    #ifdef CONFIG_BLOCK // 块设备
    		/* 块磁盘显示在/sys/block中 */
    		if (sysfs_deprecated && dev->class == &block_class) {  // 块类
    			if (parent && parent->class == &block_class)
    				return &parent->kobj;
    			return &block_class.p->subsys.kobj; // 子系统根kobj
    		}
    #endif
    
    		/* 
    		 * 如果没有父类,我们就生活在“虚拟”中
    		 * 以非类设备为父级的类设备位于“粘合”目录中,以防止命名空间冲突
    		 * /
    		if (parent == NULL)
    			parent_kobj = virtual_device_parent(dev);
    		else if (parent->class && !dev->class->ns_type)
    			return &parent->kobj;
    		else
    			parent_kobj = &parent->kobj;
    
    		mutex_lock(&gdp_mutex); // gbp互斥锁
    		
    		/* 在父级找到类目录并引用它 */
    		spin_lock(&dev->class->p->glue_dirs.list_lock);
    		list_for_each_entry(k, &dev->class->p->glue_dirs.list, entry)
    			if (k->parent == parent_kobj) {
    				kobj = kobject_get(k);
    				break;
    			}
    		spin_unlock(&dev->class->p->glue_dirs.list_lock);
    		if (kobj) {
    			mutex_unlock(&gdp_mutex);
    			return kobj;
    		}
    
    		/* 或在父设备上创建新的类目录 */
    		k = class_dir_create_and_add(dev->class, parent_kobj); // 分配class_dir对象,目录kobj初始化,并关联class_dir_ktype
    		// 关联私有结构的目录kset容器,目录kobj关联父级kobj,形成树形遍历表
    		
    		/* 不要为这个简单的“粘合”目录发出uevent */
    		mutex_unlock(&gdp_mutex);
    		return k;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    block_class
    class_dir_create_and_add

    	/* 子系统可以为其设备指定默认根目录 */
    	if (!parent && dev->bus && dev->bus->dev_root)
    		return &dev->bus->dev_root->kobj;
    
    	if (parent)
    		return &parent->kobj;
    	return NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

      class_dir_create_and_add 分配class_dir对象,目录kobj初始化,并关联class_dir_ktype

      关联私有结构的目录kset容器,目录kobj关联父级kobj,形成树形遍历表

    static struct kobject *
    class_dir_create_and_add(struct class *class, struct kobject *parent_kobj)
    {
    	struct class_dir *dir;
    	int retval;
    
    	dir = kzalloc(sizeof(*dir), GFP_KERNEL); // 分配class_dir对象
    	if (!dir)
    		return ERR_PTR(-ENOMEM);
    
    	dir->class = class; // 关联类
    	kobject_init(&dir->kobj, &class_dir_ktype); // 目录kobj初始化,并关联class_dir_ktype
    
    	dir->kobj.kset = &class->p->glue_dirs; // 关联私有结构的目录kset容器
    
    	retval = kobject_add(&dir->kobj, parent_kobj, "%s", class->name); // 目录kobj关联父级kobj
    	if (retval < 0) {
    		kobject_put(&dir->kobj);
    		return ERR_PTR(retval);
    	}
    	return &dir->kobj;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    class_dir_ktype

      device_platform_notify 设备关联电源管理设备,并设置相关的唤醒方式

      设备与软件节点互作软连接,并为通知平台预留了接口函数

    static void device_platform_notify(struct device *dev)
    {
    	acpi_device_notify(dev); // 设备关联电源管理设备,并设置相关的唤醒方式
    
    	software_node_notify(dev); // 设备与软件节点互作软连接
    	// "software_node"
    
    	if (platform_notify) // 为通知平台预留的接口函数
    		platform_notify(dev);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    acpi_device_notify

      acpi_device_notify 设备关联电源管理设备,并设置相关的唤醒方式

      设备关联电源管理设备,设置/重置设备唤醒能力标志

      如果不是pci设备,从bus_type_list链表中遍历节点,
      (内部)通过container_of偏移到acpi_bus_type结构(电源管理总线类型)
      然后电源设备关联到例如usb4_port的端口对象;

      pci设备通过pci_bus_type获取 pci_dev_pm_ops,
      设置pci设备或桥的唤醒方式

      平台设备,设备总线等于platform_bus_type
      电源配置设备关联msi域(消息信号中断域)

      最后,如果acpi_scan_handler结构对象关联了bind函数,设备与物理设备绑定

    void acpi_device_notify(struct device *dev)
    {
    	struct acpi_device *adev;
    	int ret;
    
    	ret = acpi_bind_one(dev, NULL); // 设备关联电源管理设备,设置/重置设备唤醒能力标志
    	// 电源管理设备属于固件节点(fwnode成员)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    acpi_bind_one

    if (ret) { // 如果关联电源管理设备失败
    // 电源管理总线类型用于pci之外的设备,pci有自己的设备电源管理
    
    		struct acpi_bus_type *type = acpi_get_bus_type(dev); // 从bus_type_list链表中遍历节点,(内部)通过container_of偏移到acpi_bus_type结构(电源管理总线类型)
    		// bus_type_list属于电源管理总线类型 链表
    		
    		adev = type->find_companion(dev); // 在设备匹配的电源管理总线类型中找到电源管理设备(节点)
    		// 这里我们可以考虑一个概念,如usb设备,它通过usb总线类型供电,而pci和pcie也有自己的总线类型供电
    
    		ret = acpi_bind_one(dev, adev);  // 设备关联电源管理设备,设置/重置设备唤醒能力标志
    		if (ret)
    			goto err;
    
    		if (type->setup) { // 目前只在thunderbolt中找到了setup函数实现
    			type->setup(dev); // 电源设备关联到 usb4_port
    			goto done;
    		}
    	} else { // 关联电源管理设备成功
    	// pci电源管理不通过电源管理总线类型,它通过pci_bus_type获取 pci_dev_pm_ops
    	
    		adev = ACPI_COMPANION(dev); // to_acpi_device_node((dev)->fwnode), 固件节点(电源管理设备)
    
    		if (dev_is_pci(dev)) { // pci设备或桥,设备总线等于pci_bus_type
    			pci_acpi_setup(dev, adev); // 设置pci设备或桥的唤醒方式
    			goto done;
    		} else if (dev_is_platform(dev)) { // 平台设备,设备总线等于platform_bus_type
    			acpi_configure_pmsi_domain(dev); // 电源配置设备关联msi域(消息信号中断域)
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    acpi_bus_type
    pci_acpi_setup
    acpi_configure_pmsi_domain

    if (adev->handler && adev->handler->bind) // 如果acpi_scan_handler结构对象关联了bind函数
    		adev->handler->bind(dev); // 设备与物理设备绑定
    
    done:
    	acpi_handle_debug(ACPI_HANDLE(dev), "Bound to device %s\n",
    			  dev_name(dev));
    
    	return;
    
    err:
    	dev_dbg(dev, "No ACPI support\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

      acpi_bind_one 设备关联电源管理设备,设置/重置设备唤醒能力标志

      创建物理节点链接和固件节点链接,通过发送uevent通知用户空间,变更状态通知

    int acpi_bind_one(struct device *dev, struct acpi_device *acpi_dev)
    {
    	struct acpi_device_physical_node *physical_node, *pn;
    	char physical_node_name[PHYSICAL_NODE_NAME_SIZE];
    	struct list_head *physnode_list;
    	unsigned int node_id;
    	int retval = -EINVAL;
    
    	if (has_acpi_companion(dev)) { // 如果固件设备节点是电源管理操作(acpi_device_fwnode_ops)
    		if (acpi_dev) {
    			dev_warn(dev, "ACPI companion already set\n");
    			return -EINVAL;
    		} else {
    			acpi_dev = ACPI_COMPANION(dev); // to_acpi_device_node((dev)->fwnode), 固件节点(电源管理设备)
    		}
    	}
    
    	acpi_dev_get(acpi_dev); // 获取电源管理设备
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    fwnode_operations

    	get_device(dev); // 获取设备
    	physical_node = kzalloc(sizeof(*physical_node), GFP_KERNEL); // 分配物理节点
    
    	/*
    	 * 按照node_id对列表进行排序,以便于回收被删除节点的id
    	 */
    	physnode_list = &acpi_dev->physical_node_list; // 物理节点链表
    	node_id = 0;
    
    	list_for_each_entry(pn, &acpi_dev->physical_node_list, node) { // 遍历到未使用的node_id,并获取物理节点链表
    		/* 完整性检查 */
    		if (pn->dev == dev) { // 已与电源管理节点关联
    			mutex_unlock(&acpi_dev->physical_node_lock);
    
    			dev_warn(dev, "Already associated with ACPI node\n");
    			kfree(physical_node); // 释放物理节点
    			if (ACPI_COMPANION(dev) != acpi_dev)
    				goto err;
    
    			put_device(dev); // 递减引用计数(设备根kobj)
    			acpi_dev_put(acpi_dev); // 递减引用计数(设备根kobj)
    			return 0;
    		}
    		if (pn->node_id == node_id) {
    			physnode_list = &pn->node;
    			node_id++;
    		}
    	}
    
    	physical_node->node_id = node_id; // 物理节点id(node_id)
    	physical_node->dev = dev; // 物理节点设备
    	list_add(&physical_node->node, physnode_list); // 物理节点放入到物理节点链表
    	acpi_dev->physical_node_count++; // 物理节点计数增加
    
    	if (!has_acpi_companion(dev)) // 如果不是固件管理的节点
    		ACPI_COMPANION_SET(dev, acpi_dev); // 更改设备的主固件节点
    
    	acpi_physnode_link_name(physical_node_name, node_id); // 物理节点链接名称,如"physical_node*"
    	retval = sysfs_create_link(&acpi_dev->dev.kobj, &dev->kobj,
    				   physical_node_name);  // 在两个对象之间创建符号链接,电源管理设备 链接到 设备
    	// &acpi_dev->dev.kobj -> &dev->kobj
    
    	retval = sysfs_create_link(&dev->kobj, &acpi_dev->dev.kobj,
    				   "firmware_node"); // 设备 链接到 电源管理设备, 链接名称"firmware_node"
    
    	if (acpi_dev->wakeup.flags.valid)
    		device_set_wakeup_capable(dev, true); // 设置/重置设备唤醒能力标志
    
    	return 0;
    	...
    }
    EXPORT_SYMBOL_GPL(acpi_bind_one);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    wakeup_sysfs_add

      wakeup_sysfs_add 将文件合并到一个预先存在的属性组中,发送uevent通知用户空间

    int wakeup_sysfs_add(struct device *dev)
    {
    	int ret = sysfs_merge_group(&dev->kobj, &pm_wakeup_attr_group); // 将文件合并到一个预先存在的属性组中
    
    	if (!ret)
    		kobject_uevent(&dev->kobj, KOBJ_CHANGE); // 通过发送uevent通知用户空间,变更状态通知
    
    	return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    pm_wakeup_attr_group

      pci_acpi_setup 设置pci设备的唤醒方式

      对于能够实现D3的网桥,我们会自动启用唤醒(就像我们在这种情况下对电源管理本身所做的那样)
      另一种情况通过电源管理唤醒设备

    void pci_acpi_setup(struct device *dev, struct acpi_device *adev)
    {
    	struct pci_dev *pci_dev = to_pci_dev(dev); // 通过container_of偏移到pci设备
    
    	pci_acpi_optimize_delay(pci_dev, adev->handle); // 从电源管理设备优化pci D3和D3冷延迟
    	// pci_dev 延迟要更新的PCI设备
    	// handle 此设备的ACPI句柄
    	// 从设备本身或PCI主机桥的ACPI _DSM控制方法更新PCI设备的d3hot_delay和d3cold_delay
    	// 这些_DSM函数由2014年1月28日的ECN草案定义,标题为“FW延迟优化的ACPI添加”
    	// 这些设置参数记录在 union acpi_object params[4]
    	
    	pci_acpi_set_external_facing(pci_dev); // 来自平台的信息,例如ACPI或设备树,可能会将设备标记为“面向外部”
    	// 面向外部的设备本身是内部的,但它的下游设备是外部的。读取方法如下:
    	// 如果读取到以下u8属性数据 设置dev->external_facing = 1;
    	// u8 val; device_property_read_u8(&dev->dev, "ExternalFacingPort", &val)  这部分知识延伸到固件操作结构
    	// 函数从设备固件(fwnode 固件节点)描述中读取带有"ExternalFacingPort"的u8属性数组,如果找到,将其存储到val
    	
    	pci_acpi_add_edr_notifier(pci_dev); // 在ACPI设备、thermal_zone或Processor对象上安装通知处理程序
    	// 注意:根命名空间对象对于每种类型的通知(系统/设备)只能有一个处理程序
    	// 设备/Thermal/处理器对象可以有一个设备通知处理程序和多个系统通知处理程序
    
    	pci_acpi_add_pm_notifier(adev, pci_dev); // 为给定PCI设备注册电源通知器
    	// 触发函数pci_acpi_wake_dev,如果设备已生成PME,唤醒设备
    	
    	if (!adev->wakeup.flags.valid)
    		return;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    pci_acpi_wake_dev

    	device_set_wakeup_capable(dev, true); // 设置/重置设备唤醒能力标志
    
    	/*
    	 * 对于能够实现D3的网桥,我们会自动启用唤醒(就像我们在这种情况下对电源管理本身所做的那样)
    	 * 原因是桥可能有需要调用的额外方法,如_DSW
    	 */
    	if (pci_dev->bridge_d3)
    		device_wakeup_enable(dev); 
    
    	acpi_pci_wakeup(pci_dev, false); // 电源配置禁用唤醒
    
    	acpi_device_power_add_dependent(adev, dev); // 添加该电源配置设备的从属设备
    
    	if (pci_is_bridge(pci_dev)) // 检查PCI设备是否是桥
    	// 如果PCI设备是桥,无论它是否有从属设备,返回true
    		acpi_dev_power_up_children_with_adr(adev); // 更改adev在D3cold中的直接子节点的电源状态
    		// 并将有效的_ADR对象保持为D0,以便允许总线(例如PCI)枚举代码访问它们
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

      pci_acpi_wake_dev

    static void pci_acpi_wake_dev(struct acpi_device_wakeup_context *context)
    {
    	struct pci_dev *pci_dev;
    
    	pci_dev = to_pci_dev(context->dev); // 从电源管理设备唤醒上下文中获取设备
    
    	if (pci_dev->pme_poll) // 轮询设备的PME状态位
    		pci_dev->pme_poll = false;
    
    	if (pci_dev->current_state == PCI_D3cold) { // D3冷延迟
    		pci_wakeup_event(pci_dev); // 通知pm核心唤醒事件(等待100毫秒)
    		// 最后调用wake_up_process,唤醒任务
    		pm_request_resume(&pci_dev->dev); // 对设备的运行时恢复进行排队
    		return;
    	}
    
    	/* 如果设置,清除PME状态 */
    	if (pci_dev->pme_support)
    		pci_check_pme_status(pci_dev);
    
    	pci_wakeup_event(pci_dev); // 唤醒任务(通过源唤醒设备)
    	pm_request_resume(&pci_dev->dev); // 对设备的运行时恢复进行排队
    
    	pci_pme_wakeup_bus(pci_dev->subordinate); // 在总线上/下行走设备,调用回调(设备已生成PME,唤醒设备)
    	// 回调函数pci_pme_wakeup
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    acpi_device_wakeup_context
    swait_queue

      acpi_configure_pmsi_domain 电源配置设备关联msi域(消息信号中断域)

      找到设备关联的iort节点(IO重新映射表),然后找到它的msi父节点
      移动到ITS特定数据,然后通过ID查找到固件节点描述,返回对应中断域
      如果msi域存在,设备关联msi域

    void acpi_configure_pmsi_domain(struct device *dev)
    {
    	struct irq_domain *msi_domain;
    
    	msi_domain = iort_get_platform_device_domain(dev); // 获取平台设备的IO重新映射表对应的中断域
    	// 找到设备关联的iort节点,然后找到它的msi父节点
    	// 移动到ITS特定数据,然后通过ID查找到固件节点描述,返回对应中断域
    
    	if (msi_domain) // msi域存在
    		dev_set_msi_domain(dev, msi_domain); // 设备关联msi域
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    irq_domain
    iort_get_platform_device_domain
    dev_msi_info

      iort_get_platform_device_domain 获取平台设备的IO重新映射表对应的中断域

      找到设备关联的iort节点,然后找到它的msi父节点
      移动到ITS特定数据,然后通过ID查找到固件节点描述,返回对应中断域

    static struct irq_domain *iort_get_platform_device_domain(struct device *dev)
    {
    	struct acpi_iort_node *node, *msi_parent = NULL;
    	struct fwnode_handle *iort_fwnode;
    	struct acpi_iort_its_group *its;
    	int i;
    
    	/* 找到其关联的iort节点 */
    	node = iort_scan_node(ACPI_IORT_NODE_NAMED_COMPONENT,
    			      iort_match_node_callback, dev);
    
    	/* 然后找到它的msi父节点 */
    	for (i = 0; i < node->mapping_count; i++) {
    		msi_parent = iort_node_map_platform_id(node, NULL,
    						       IORT_MSI_TYPE, i);
    		if (msi_parent)
    			break;
    	}
    
    	/* 移动到ITS特定数据 */
    	its = (struct acpi_iort_its_group *)msi_parent->node_data;
    
    	iort_fwnode = iort_find_domain_token(its->identifiers[0]); // 根据给定的ITS ID查找域token(固件节点描述,句柄)
    	// 通过 iort_msi_chip_list列表
    
    	return irq_find_matching_fwnode(iort_fwnode, DOMAIN_BUS_PLATFORM_MSI); // 为给定的fwspec查找域
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    acpi_table_iort
    irq_fwspec

      dev_add_physical_location 如果设备的固件节点存在,解码由_PLD方法返回的位包装缓冲区到一个对ACPI驱动程序更有用的本地结构(描述设备连接点在系统外壳中的物理位置)

    bool dev_add_physical_location(struct device *dev)
    {
    	struct acpi_pld_info *pld;
    	acpi_status status;
    
    	if (!has_acpi_companion(dev)) // 设备的固件节点存在
    		return false;
    
    	status = acpi_get_physical_device_location(ACPI_HANDLE(dev), &pld);  // 解码由_PLD方法返回的位包装缓冲区到一个对ACPI驱动程序更有用的本地结构
    
    	dev->physical_location =
    		kzalloc(sizeof(*dev->physical_location), GFP_KERNEL); 
    	if (!dev->physical_location)
    		return false;
    	dev->physical_location->panel = pld->panel;
    	dev->physical_location->vertical_position = pld->vertical_position;
    	dev->physical_location->horizontal_position = pld->horizontal_position;
    	dev->physical_location->dock = pld->dock;
    	dev->physical_location->lid = pld->lid;
    
    	ACPI_FREE(pld);
    	return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    目录预览

    <>
    <>
    <>
    <>

  • 相关阅读:
    连接数据库
    循环神经网络(RNN)简单介绍及实现(基于表面肌电信号)
    【设计模式】访问者模式
    抛弃chatgpt,使用微软的Cursor提升coding效率
    JavaEE初阶:文件操作 和 IO
    MR案例 - 计算总成绩
    git能pink成功,为什么一直克隆超时啊
    文本相似度 Text Similarity
    Vue rules校验规则详解
    Spring学习|使用JavaConfig实现bean配置、代理模式:静态代理模式、动态代理模式(通俗易懂)
  • 原文地址:https://blog.csdn.net/a29562268/article/details/127796087