2 设备模型结构
如表2-1,Linux设备模型包含以下四个基本结构:
类型 | 所包含的内容 | 内核数据结构 | 对应/sys项 |
设备(Devices) | 设备是此模型中最基本的类型,以设备本身的连接按层次组织 | struct device | /sys/devices/*/*/.../ |
驱动(Drivers) | 在一个系统中安装多个相同设备,只需要一份驱动程序的支持 | struct device_driver | /sys/bus/pci/drivers/*/ |
总线 (Bus) | 在整个总线级别对此总线上连接的所有设备进行管理 | struct bus_type | /sys/bus/*/ |
类别(Classes) | 这是按照功能进行分类组织的设备层次树;如 USB 接口和 PS/2 接口的鼠标都是输入设备,都会出现在/sys/class/input/下 | struct class | /sys/class/*/ |
表2-1:设备模型基本结构
device、driver、bus、class是组成设备模型的基本数据结构。kobject是构成这些基本结构的核心,kset又是相同类型结构kobject的集合。kobject和kset共同组成了sysfs的底层数据体系。本节采用从最小数据结构到最终组成一个大的模型的思路来介绍。当然,阅读时也可先从Device、Driver、Bus、Class的介绍开始,先总体了解设备模型的构成,然后再回到kobject和kset,弄清它们是如何将Device、Driver、Bus、Class穿插链接在一起的,以及如何将这些映像成文件并最终形成一个sysfs文件系统。
kobject
kobject是组成device、driver、bus、class的基本结构。如果把前者看成基类,则后者均为它的派生产物。device、driver、bus、class构成了设备模型,而kobject内嵌于其中,将这些设备模型的部件组织起来,并形成了sysfs文件系统。kobject就是device、driver、bus、class在文件系统中的代表。在sysfs操作设备时,也必须通过kobject这个中间人来完成。kobject的主要功能如下:
对象的引用计数
通常一个内核对象被创建时,不可能知道该对象存活的时间。跟踪此对象生命周期的一个方法是使用引用计数。当内核中没有代码持有该对象的引用时,该对象将结束自己的有效生命周期,并且可以被删除。
sysfs表述
在sysfs中显示的每一个对象,都对应一个kobject,它被用来与内核交互并创建它的可见表述。
数据结构关联
从整体上看,设备模型是一个友好而复杂的数据结构,通过在其间的大量连接而构成一个多层次的体系结构。Kobject实现了该结构并把它们聚合在一起。
uevent事件处理
当系统中的硬件被热插拔时,在kobject子系统控制下,将产生事件以通知用户空间。
下面以2.6.29版本(本文涉及代码均为此版本)内核源码一一介绍kobject的功能。在kernel/include/linux/kobject.h中,kobject结构定义如下:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct sysfs_dirent *sd;
struct kref kref;
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
};
*name
kobject的名字,每个kobject都对应着sysfs下的一个文件夹,该名字也是对应的文件夹的名字。
entry
双向链表指针,用于将同一kset集合中的kobject链接到一起,便于访问。
*parent
kobject对应的父kobject节点,在sysfs表现为上一级目录。
*kset
kobject所在的集合的指针,kset概念将在kset一节中描述。
*ktype
kobject对象类型指针,随后将会介绍。
*sd
sd用于表示VFS文件系统的目录项,由此可见它是设备与文件之间的桥梁。在sysfs节会对此结构进行分析。
kref
对象引用计数器。引用计数器的作用前面已经讲过。
state_initialized
初始化标志位,在对象初始化时被置位。
state_in_sysfs
kobject对象在sysfs中的状态,创建则置1,否则为0。亦即kobject对应的目录在sysfs中是否被创建。
state_add_uevent_sent
添加设备的uevent事件是否发送标志,添加设备时会向用户空间发送uevent事件,请求新增设备。
state_remove_uevent_sent
删除设备的uevent事件是否发送标志,删除设备时会向用户空间发送uevent事件,请求卸载设备。
kset
kset是嵌入相同类型结构的kobject集合。我们可以认为它是kobject的顶层容器类。kset也是基于sysfs的,维系着设备、驱动等等分类与链接关系。图2-1(来自LDD3,但稍作修改)可清晰表示kset与kobject的关系。
图2-1:kset与kobject关系
下面我们来看一下kset的结构:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
struct kset_uevent_ops *uevent_ops;
};
从结构体中我们可以看到kset与kobject最大的不同就是多了kset_uevent_ops类型的成员。因此,我们可理解kset就是为了让一组kobject使用相同的uevent处理函数。
uevent知识请参考uevent和udev一章。
attribute
前面说过kobject对应sysfs中的文件夹,但作为一个文件系统,不可能没有文件。下面将要讲到的属性即对应这里的文件。我们先来看kobject中的一个重要成员*ktype,类型如下:
struct kobj_type {
void (*release)(struct kobject *kobj);
struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
};
*release
意如其名,即当kobject引用计数器为0时,用来释放kobject对象。
*sysfs_ops
根据default_attrs中的mode要求,提供方法操作指定属性文件。一般只有读写两个函数,如下:
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *,char *);
ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
};
**default_attrs
保存了属性列表,用于创建该类型的每一个kobject文件。结构如下:
struct attribute {
const char *name;
struct module *owner;
mode_t mode;
};
*name
属性名,对应于kobject的sysfs目录中的一个文件。
*owner
指向模块的指针,该模块负责实现这些属性。源码注释已明确指出该字段已不在使用,目前存在的原因就是为了保持向上兼容。
mode
指明该属性文件是只读只写还是可读可写,谁可写等等。
除了在初始化时指定属性外,我们还可以根据需要使用函数对属性进行增删,如下:
int sysfs_create_file(struct kobject * kobj, const struct attribute * attr);
void sysfs_remove_file(struct kobject * kobj, const struct attribute * attr);
请注意,属性可以任意增删,但方法sysfs_ops确是唯一的不可改变,所以必须确保该方法可以处理新的属性。
设备
具有特定功能的硬件,比如键盘、鼠标等。
数据结构
struct device {
struct klist klist_children;
struct klist_node knode_parent; /* node in sibling list */
struct klist_node knode_driver;
struct klist_node knode_bus;
struct device *parent;
struct kobject kobj;
char bus_id[BUS_ID_SIZE]; /* position on parent bus */
unsigned uevent_suppress:1;
const char *init_name; /* initial name of the device */
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this device */
void *driver_data; /* data private to the driver */
dev_t devt; /* dev_t, creates the sysfs "dev" */
struct klist_node knode_class;
struct class *class;
struct attribute_group **groups; /* optional groups */
void (*release)(struct device *dev);
/* 省略了部分成员 */
};
klist_children
子设备双向链表头,用于指向该设备所有子设备链表的表头。通过此表头可以查找所有子设备。操作函数如下:
int device_for_each_child(struct device *parent, void *data,
int (*fn)(struct device *dev, void *data));
struct device *device_find_child(struct device *parent, void *data,
int (*match)(struct device *dev, void *data));
这两个函数的区别是:前者遍历子设备,并作fn()处理,如果fn()返回非0值,则停止遍历,返回错误码;后者是遍历子设备,并作match()处理,如果match()返回非0,则调用get_device()获取对应的子设备(将对应的子设备参考值曾1),然后停止遍历,返回子设备。注意后者获取的子设备用完后,需要用put_device()去除子设备。
knode_parent
父设备的子设备集节点,用于链入父设备的klist_children链表中。
knode_driver
设备所属驱动的设备集节点,用于链入具有相同驱动的设备链表中。该链的表头在device_driver->p ->klist_devices。
knode_bus
设备所属总线的设备集节点,用于链入具有相同总线的设备链表中。该链的表头为bus_type->p -> klist_devices。
*parent
设备的父设备,即该设备所属的设备。在大多数情况下,一个父设备通常是某种总线或者是宿主控制器。
kobj
表示该设备并把该它连接到sysfs体系中的kobject。
bus_id[BUS_ID_SIZE]
在总线上唯一标识该设备的字符串。
uevent_suppress
过滤该设备的uevent事件标志位。为1,则过滤。
*init_name
设备初始化名称,device_add()里将该值拷贝给bus_id,因此也表示总线上该设备标识。注意,文件系统中显示的设备名称并非此处指定,而是由该设备对应的kobj中指定。
*bus
标识了该设备连接在何种类型的总线上。
*driver
管理该设备的驱动程序。在下一节中将介绍device_driver结构。
*driver_data
由设备驱动程序使用的私有数据成员。
devt
32位主从设备号,高12位为主设备号,低20位为从设备号。
knode_class
设备所属类的设备集节点,用于链入具有相同类的设备链表中。该链的表头为class->p -> class_devices。
*class
标识设备所属类。
**groups
设备属性组,会体现到sysfs系统中,用户可操作。
*release
当指向设备的最后一个引用被删除时,内核调用该方法;它将从内嵌的kobject的release方法中调用。所有指向核心注册的device结构都必须有一release方法,否则内核将打印出错误信息。
图2-2给出了设备与总线、驱动等连接关系。文件系统子框表明设备是通过kobj将自己与sysfs中的文件夹连接到一起的。子设备、总线、驱动、类等子系统给设备进行相应分类,并使用klist和klist_node将它们链到一起。每个子系统同时提供 ***_for_each_*** 和 ***_find_***两个遍历函数来查找并操作指定设备。
图2-2:设备结构图
方法
设备架构初始化:
int __init devices_init(void);
1 在sysfs目录下创建设备子集(kset)和目录/sys/devices;并将设备通用事件处理函数device_uevent_ops注册给该设备子集。之后所有设备都会添加到devices目录下。
2另外,kset在初始化时,会将公共的kobj_type一组函数注册到kset中内嵌的kobj中,其中包括释放kset函数kset_release和一组属性show/store函数kobj_attr_show/kobj_attr_store。
3 kset注册成功后,向用户空间发送添加对象的uevent事件。
4 创建目录/sys/dev,并在该目录下创建两个目录/sys/dev/block和/sys/dev/char。这两个目录下维护一个按字符设备/块设备的主次号码(major:minor)链接到真实的设备(/sys/devices下)的符号链接文件。
设备注册/去注册函数:
int device_register(struct device *dev);
void device_unregister(struct device *dev);
注册函数先后执行的操作有:获取设备所在子集,初始化设备数据,将设备加入到sysfs系统中,增加设备默认属性及操作方法,加设备到类设备中,加设备到总线,向用户空间发送添加设备uevent事件,尝试为设备获取驱动。
采用此方法注册的设备都会获取释放对象(device_release)及处理所属属性(dev_attr_show/dev_attr_store)的方法(也就是kobj_type结构)。
如果用户空间程序用sysfs来读取设备属性的值,sysfs的read函数先调用的是对象属性处理函数dev_attr_show,因为sysfs能处理或者说能看得见的只有对象级的东西,我们从图3的图中也可表明这一点。
去注册是注册的逆向操作。去注册后,用户不可再操作设备。
遍历子设备:
int device_for_each_child(struct device *parent, void *data,
int (*fn)(struct device *dev, void *data));
struct device *device_find_child(struct device *parent, void *data,
int (*match)(struct device *dev, void *data));
函数说明请参考前一节数据结构分析中klist_children项。
设备通用事件(uevent)处理函数:
static int dev_uevent_filter(struct kset *kset, struct kobject *kobj);
static const char *dev_uevent_name(struct kset *kset, struct kobject *kobj);
static int dev_uevent(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env);
dev_uevent_filter设备级事件过滤函数,发送uevent事件前会先调用此函数检查是否发送该事件,返回0则不发送,为1则调用总线或类级过滤函数再次检查。
dev_uevent_name获取设备所属总线或类的名称。
dev_uevent设备级事件处理函数,向事件中添加指定设备的环境变量,如:设备主次编号,设备类型,设备使用的驱动。如果设备有所属总线或类,本函数还会调用总线或类级事件处理函数。
详细事件处理流程将在uevent章节中描述。
设备属性文件添加删除:
int device_create_file(struct device *dev, struct device_attribute *attr);
void device_remove_file(struct device *dev, struct device_attribute *attr);
创建/删除设备属性文件。
设备的对象(kobj)属性文件操作函数:
static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr, char *buf);
static ssize_t dev_attr_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count);
从sysfs层面看,只能识别对象级的属性文件处理函数(如上面这两个函数的格式)。设备使用这两个函数实现对象到设备属性文件处理的转换。它们使用container_of方法实现内核对象(kobj)到对应设备的转换。通过设备找到属性处理函数,并调用找到的函数(格式如下)来读取/存储指定设备的属性值。
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};
驱动
让设备运行起来的一组函数。
数据结构
struct device_driver {
const char *name;
struct bus_type *bus;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
struct attribute_group **groups;
struct driver_private *p;
/* 省略了部分成员 */
};
*name
驱动程序的名称。与kobj->name相同。
*bus
该驱动使用的总线。
*probe
用来查询特定设备是否存在,以及该驱动程序能否操作它。
*remove
删除设备时使用remove函数告知驱动不可再操作此设备。
*shutdown
关机时调用shutdown函数关闭设备。
**groups
驱动属性组,会体现到sysfs系统中,用户可操作。
*p
该成员用于处理与设备、总线以及sysfs之间的连接关系,如下:
struct driver_private {
struct kobject kobj;
struct klist klist_devices;
struct klist_node knode_bus;
struct module_kobject *mkobj;
struct device_driver *driver;
};
方法
操作device_driver结构的函数有注册/去注册函数,增删属性及属性组文件函数,遍历设备函数。
注册函数:
int driver_register(struct device_driver *drv);
该函数实现向相应总线的驱动子集(kset)中注册驱动对象,并将总线上可以使用该驱动的设备加入到该驱动的设备链表中。
注意,每个驱动对象均赋予driver_ktype类型,用来释放对象和处理属性文件。
可以用下面函数去注册一个驱动:
void driver_unregister(struct device_driver *drv);
驱动对象释放函数:
static void driver_release(struct kobject *kobj);
该函数同过driver_ktype注册给驱动对象,用来释放对象。
驱动对象属性文件处理函数:
static ssize_t drv_attr_show(struct kobject *kobj, struct attribute *attr, char *buf);
static ssize_t drv_attr_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count);
这两个函数也是通过driver_ktype注册给驱动对象的,用于操作对象的属性。如同驱动一节对应函数描述的那样。这两个函数也只是个适配函数,每个驱动有自己的属相处理函数。sysfs只是通过这两个适配函数去访问驱动属性处理函数。实现方法仍然是通过container_of行为由对象获取外层包含该对象的驱动结构,从而获取驱动属性处理函数。注意,这两个函数定义在bus.c中。
增删属性文件函数:
int driver_create_file(struct device_driver *drv, struct driver_attribute *attr);
void driver_remove_file(struct device_driver *drv, struct driver_attribute *attr);
用户可以通过操作增加到sysfs中驱动属性文件来修改驱动属性值。
增删属性组文件函数:
static int driver_add_groups(struct device_driver *drv, struct attribute_group **groups);
static void driver_remove_groups(struct device_driver *drv, struct attribute_group **groups);
这两个函数可以一次向系统增删一组驱动属性。
遍历设备函数:
int driver_for_each_device(struct device_driver *drv, struct device *start,
void *data, int (*fn)(struct device *, void *));
struct device *driver_find_device(struct device_driver *drv, struct device *start, void *data,
int (*match)(struct device *dev, void *data));
这两函数均用于遍历驱动上的设备,并对设备执行fn或match操作。具体差异请读代码。
总线
总线是处理器与一个或者多个设备之间的通道。在设备模型中,所有的设备都通过总线相连。
数据结构
设备模型用bus_type结构表示总线,如下:
struct bus_type {
const char *name;
struct bus_attribute *bus_attrs;
struct device_attribute *dev_attrs;
struct driver_attribute *drv_attrs;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
struct bus_type_private *p;
/* 省略了部分成员 */
};
*name
总线名称。
*bus_attrs
总线属性集。
*dev_attrs
总线设备属性集。
*drv_attrs
总线驱动属性集。
*match
当一个总线上的新设备或者新驱动程序被添加时,会调用此函数。如果指定的驱动程序可以处理指定的设备,该函数返回1。
*uevent
总线级别的uevent处理。向用户空间发送uevent时间前,使用该方法给总线增加环境变量。
*probe
探测指定设备是否在总线上。
*p
struct bus_type_private {
struct kset subsys;
struct kset *drivers_kset;
struct kset *devices_kset;
struct klist klist_devices;
struct klist klist_drivers;
};
subsys 表示总线集;*drivers_kset和*devices_kset是从sysfs角度描述总线支持的驱动集和设备集,而klist_devices和klist_drivers是总线支持的设备及驱动链表,用于遍历总线上的驱动和设备。
方法
总线初始化函数:
int __init buses_init(void);
系统初始化时调用。在sysfs根目录下增加bus目录,并创建bus子集(kset),注册子集uevent事件处理函数。完成后向用户空间发送增加对象uevent事件。
另外,kset在初始化时,会将公共的kobj_type一组函数注册到kset中内嵌的kobj中,其中包括释放kset函数kset_release和一组属性show/store函数kobj_attr_show/kobj_attr_store。注意,所有系统子集都会这么做,且使用一组同样的函数,包括设备子集和类子集。
总线注册函数:
int bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);
向系统注册/去注册总线时,使用此两函数。这里要注意的是注册的每一个总线在sysfs中不仅仅是一个目录(kobject),它还是一个子集。即总线是以子集(kset)方式注册的,而非对象(kobject)。更重要的是每个总线子集下同时注册了设备(devices)和驱动(drivers)两个子集(kset)。前面讲到设备初始化时也会注册devices子集。事实上,这儿的设备子集只是保存设备的链接。而驱动没有初始化函数,因此驱动对象都保存在这儿各总线的驱动子集里。
uevent属性文件操作函数:
static ssize_t bus_uevent_store(struct bus_type *bus, const char *buf, size_t count);
用户可以通过修改总线文件夹下的uevent文件内容,触发内核向用户空间发送uevent事件,事件类型由用户写入的内容决定。
总线属性文件操作函数:
static ssize_t bus_attr_show(struct kobject *kobj, struct attribute *attr, char *buf);
static ssize_t bus_attr_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count);
与设备和驱动对应的对象属性操作函数功能近似,这里不再赘述。记住,这只是个适配函数。为sysfs服务。
增删属性函数:
int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);
void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);
增加删除总线属性文件,一次一个。
static int bus_add_attrs(struct bus_type *bus);
static void bus_remove_attrs(struct bus_type *bus);
增加删除总线属性文件,一次多个,其实就是将总线提供的属性列表一次性增加到系统中。我们从数据结构中可知bus_type不仅为自己提供了属性列表。也为设备和驱动各提供了一份。因此,总线也提供了相应的添加/删除方法,类似于上面两个函数,不再列出。
总线遍历设备函数:
int bus_for_each_dev(struct bus_type *bus, struct device *start,
void *data, int (*fn)(struct device *, void *));
struct device *bus_find_device(struct bus_type *bus, struct device *start, void *data,
int (*match)(struct device *dev, void *data));
前者对指定总线上所有设备进行fn()操作。若fn()返回非零值,则视为出错,退出遍历。而后者只是用match()对总线上每个设备进行匹配,找到则返回对应的设备。
总线遍历驱动函数:
int bus_for_each_drv(struct bus_type *bus, struct device_driver *start,
void *data, int (*fn)(struct device_driver *, void *));
对指定总线上的所有驱动进行fn()操作。若fn()返回非零值,则视为出错,退出遍历。
向总线增加驱动:
int bus_add_driver(struct device_driver *drv);
该函数为驱动注册函数的一部分,将驱动对象注册到对应总线的驱动(drivers)子集下。
向总线增加设备:
int bus_add_device(struct device *dev);
该函数为设备注册函数的一部分,但并不像驱动那样,它只将设备子集的指定设备链接到总线的设备(devices)子集下。
设备绑定驱动函数:
void bus_attach_device(struct device *dev);
为设备获取合适的驱动。
int device_reprobe(struct device *dev);
将设备与其驱动解绑,然后为设备获取新的驱动。这通常为热插拔服务。
int bus_rescan_devices(struct bus_type *bus);
为指定的bus上的每个设备重新绑定驱动,如果设备还没有驱动,则为其重新探测驱动并绑定。
类
类是一个设备的高层视图,它抽象出了底层的实现细节。比如驱动程序看到的是SCSI磁盘和ATA磁盘,但是在类的层次上,它们都是磁盘而已。类允许用户空间使用设备所能提供的功能,而不关心设备是如何连接的,以及它们是如何工作的。
数据结构
struct class {
const char *name;
struct module *owner;
struct class_attribute *class_attrs;
struct device_attribute *dev_attrs;
struct kobject *dev_kobj;
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
void (*class_release)(struct class *class);
void (*dev_release)(struct device *dev);
struct class_private *p;
};
*name
类名称。
*owner
类所属模块。
*class_attrs
类属性集。
*dev_attrs
类给其所有设备定义的公共属性集。
*dev_kobj
在devices_init函数中创建了/sys/dev/char和/sys/dev/block两个对象,dev_kobj指向二者之一,添加对象时(device_add),会将对象链接到这个目录下。按照我的理解,内核设计者是想在/sys/dev/char或/sys/dev/block目录下将各设备按类分目录存放,此处的dev_kobj即是为了创建这个分目录。遗憾的是该功能并未使用,我们看到char和block下没有分目录,全是以major:minor格式命名的链接文件。这样我们就可以通过主次设备号快速找到对应的设备了。
*dev_uevent
类级uevent事件构造函数。
*class_release
用于释放指定的类。
*dev_release
用于释放指定的设备。
*p
struct class_private {
struct kset class_subsys;
struct klist class_devices;
struct list_head class_interfaces;
struct class *class;
};
class_subsys是该类在sysfs中的代表,它表明每个类都是一个子集。class_devices将所有归属于该类的设备串成一个链表,便于遍历查询或对设备进行操作。class_interfaces用于链接所有类接口。class指向所属的class对象。
方法
类初始化函数:
int __init classes_init(void);
系统初始化时调用。在sysfs根目录下增加class(/sys/class)目录,并创建class子集(kset),注册子集uevent事件处理函数。完成后向用户空间发送增加对象uevent事件。
另外,kset在初始化时,会将公共的kobj_type一组函数注册到kset中内嵌的kobj中,其中包括释放kset函数kset_release和一组属性show/store函数kobj_attr_show/kobj_attr_store。注意,所有系统子集都会这么做,且使用一组同样的函数,包括设备子集和总线子集。
类注册/去注册函数:
int __class_register(struct class *cls, struct lock_class_key *key);
void class_unregister(struct class *cls);
向系统增加/去除类子集和相应路径。
类创建/删除函数:
struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key);
void class_destroy(struct class *cls);
与类注册/去注册函数功能一致,只是对以上两个函数的简单封装。
遍历类设备函数:
int class_for_each_device(struct class *class, struct device *start,
void *data, int (*fn)(struct device *, void *));
struct device *class_find_device(struct class *class, struct device *start, void *data,
int (*match)(struct device *, void *));
与总线或设备中的遍历功能类似,不再赘述。