Linux设备模型(2)_Kobject

作者:wowo 发布于:2014-3-7 0:25 分类:统一设备模型

1. 前言

Kobject是Linux设备模型的基础,也是设备模型中最难理解的一部分(可参考Documentation/kobject.txt的表述)。因此有必要先把它分析清楚。

2. 基本概念

由“Linux设备模型(1)_基本概念”可知,Linux设备模型的核心是使用Bus、Class、Device、Driver四个核心数据结构,将大量的、不同功能的硬件设备(以及驱动该硬件设备的方法),以树状结构的形式,进行归纳、抽象,从而方便Kernel的统一管理。

而硬件设备的数量、种类是非常多的,这就决定了Kernel中将会有大量的有关设备模型的数据结构。这些数据结构一定有一些共同的功能,需要抽象出来统一实现,否则就会不可避免的产生冗余代码。这就是Kobject诞生的背景。

目前为止,Kobject主要提供如下功能:

  1. 通过parent指针,可以将所有Kobject以层次结构的形式组合起来。
  2. 使用一个引用计数(reference count),来记录Kobject被引用的次数,并在引用次数变为0时把它释放(这是Kobject诞生时的唯一功能)。
  3. 和sysfs虚拟文件系统配合,将每一个Kobject及其特性,以文件的形式,开放到用户空间(有关sysfs,会在其它文章中专门描述,本文不会涉及太多内容)。

注1:在Linux中,Kobject几乎不会单独存在。它的主要功能,就是内嵌在一个大型的数据结构中,为这个数据结构提供一些底层的功能实现。
注2:Linux driver开发者,很少会直接使用Kobject以及它提供的接口,而是使用构建在Kobject之上的设备模型接口。

3. 代码解析

3.1 在Linux Kernel source code中的位置

在Kernel源代码中,Kobject由如下两个文件实现:

  • include/linux/kobject.h
  • lib/kobject.c

其中kobject.h为Kobject的头文件,包含所有的数据结构定义和接口声明。kobject.c为核心功能的实现。

3.2 主要的数据结构

在描述数据结构之前,有必要说明一下Kobject, Kset和Ktype这三个概念。

Kobject是基本数据类型,每个Kobject都会在"/sys/“文件系统中以目录的形式出现。

Ktype代表Kobject(严格地讲,是包含了Kobject的数据结构)的属性操作集合(由于通用性,多个Kobject可能共用同一个属性操作集,因此把Ktype独立出来了)。
注3:在设备模型中,ktype的命名和解释,都非常抽象,理解起来非常困难,后面会详细说明。

Kset是一个特殊的Kobject(因此它也会在"/sys/“文件系统中以目录的形式出现),它用来集合相似的Kobject(这些Kobject可以是相同属性的,也可以不同属性的)。

  • 首先看一下Kobject的原型
 1: /* Kobject: include/linux/kobject.h line 60 */
 2: struct kobject {
 3:     const char *name;
 4:     struct list_head    entry;
 5:     struct kobject      *parent;
 6:     struct kset     *kset;
 7:     struct kobj_type    *ktype;
 8:     struct sysfs_dirent *sd;
 9:     struct kref     kref;
 10:    unsigned int state_initialized:1;
 11:    unsigned int state_in_sysfs:1;
 12:    unsigned int state_add_uevent_sent:1;
 13:    unsigned int state_remove_uevent_sent:1;
 14:    unsigned int uevent_suppress:1;
 15: };

name,该Kobject的名称,同时也是sysfs中的目录名称。由于Kobject添加到Kernel时,需要根据名字注册到sysfs中,之后就不能再直接修改该字段。如果需要修改Kobject的名字,需要调用kobject_rename接口,该接口会主动处理sysfs的相关事宜。

entry,用于将Kobject加入到Kset中的list_head。

parent,指向parent kobject,以此形成层次结构(在sysfs就表现为目录结构)。

kset,该kobject属于的Kset。可以为NULL。如果存在,且没有指定parent,则会把Kset作为parent(别忘了Kset是一个特殊的Kobject)。

ktype,该Kobject属于的kobj_type。每个Kobject必须有一个ktype,或者Kernel会提示错误。

sd,该Kobject在sysfs中的表示。

kref,"struct kref”类型(在include/linux/kref.h中定义)的变量,为一个可用于原子操作的引用计数。

state_initialized,指示该Kobject是否已经初始化,以在Kobject的Init,Put,Add等操作时进行异常校验。

state_in_sysfs,指示该Kobject是否已在sysfs中呈现,以便在自动注销时从sysfs中移除。

state_add_uevent_sent/state_remove_uevent_sent,记录是否已经向用户空间发送ADD uevent,如果有,且没有发送remove uevent,则在自动注销时,补发REMOVE uevent,以便让用户空间正确处理。

uevent_suppress,如果该字段为1,则表示忽略所有上报的uevent事件。

注4:Uevent提供了“用户空间通知”的功能实现,通过该功能,当内核中有Kobject的增加、删除、修改等动作时,会通知用户空间。有关该功能的具体内容,会在其它文章详细描述。

  • Kset的原型为
 1: /* include/linux/kobject.h, line 159 */
 2: struct kset {
 3:     struct list_head list;
 4:     spinlock_t list_lock;
 5:     struct kobject kobj;
 6:     const struct kset_uevent_ops *uevent_ops;
 7: };

list/list_lock,用于保存该kset下所有的kobject的链表。

kobj,该kset自己的kobject(kset是一个特殊的kobject,也会在sysfs中以目录的形式体现)。

uevent_ops,该kset的uevent操作函数集。当任何Kobject需要上报uevent时,都要调用它所从属的kset的uevent_ops,添加环境变量,或者过滤event(kset可以决定哪些event可以上报)。因此,如果一个kobject不属于任何kset时,是不允许发送uevent的。

  • Ktype的原型为
 1: /* include/linux/kobject.h, line 108 */
 2: struct kobj_type {
 3:     void (*release)(struct kobject *kobj);
 4:     const struct sysfs_ops *sysfs_ops;
 5:     struct attribute **default_attrs;
 6:     const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
 7:     const void *(*namespace)(struct kobject *kobj);
 8: };

release,通过该回调函数,可以将包含该种类型kobject的数据结构的内存空间释放掉。

sysfs_ops,该种类型的Kobject的sysfs文件系统接口。

default_attrs,该种类型的Kobject的atrribute列表(所谓attribute,就是sysfs文件系统中的一个文件)。将会在Kobject添加到内核时,一并注册到sysfs中。

child_ns_type/namespace,和文件系统(sysfs)的命名空间有关,这里不再详细说明。

 

总结,Ktype以及整个Kobject机制的理解。
Kobject的核心功能是:保持一个引用计数,当该计数减为0时,自动释放(由本文所讲的kobject模块负责) Kobject所占用的meomry空间。这就决定了Kobject必须是动态分配的(只有这样才能动态释放)。

而Kobject大多数的使用场景,是内嵌在大型的数据结构中(如Kset、device_driver等),因此这些大型的数据结构,也必须是动态分配、动态释放的。那么释放的时机是什么呢?是内嵌的Kobject释放时。但是Kobject的释放是由Kobject模块自动完成的(在引用计数为0时),那么怎么一并释放包含自己的大型数据结构呢?

这时Ktype就派上用场了。我们知道,Ktype中的release回调函数负责释放Kobject(甚至是包含Kobject的数据结构)的内存空间,那么Ktype及其内部函数,是由谁实现呢?是由上层数据结构所在的模块!因为只有它,才清楚Kobject嵌在哪个数据结构中,并通过Kobject指针以及自身的数据结构类型,找到需要释放的上层数据结构的指针,然后释放它。

讲到这里,就清晰多了。所以,每一个内嵌Kobject的数据结构,例如kset、device、device_driver等等,都要实现一个Ktype,并定义其中的回调函数。同理,sysfs相关的操作也一样,必须经过ktype的中转,因为sysfs看到的是Kobject,而真正的文件操作的主体,是内嵌Kobject的上层数据结构!


顺便提一下,Kobject是面向对象的思想在Linux kernel中的极致体现,但C语言的优势却不在这里,所以Linux kernel需要用比较巧妙(也很啰嗦)的手段去实现,


3.3 功能分析

3.3.1 Kobject使用流程

Kobject大多数情况下(有一种例外,下面会讲)会嵌在其它数据结构中使用,其使用流程如下:

  1. 定义一个struct kset类型的指针,并在初始化时为它分配空间,添加到内核中
  2. 根据实际情况,定义自己所需的数据结构原型,该数据结构中包含有Kobject
  3. 定义一个适合自己的ktype,并实现其中回调函数
  4. 在需要使用到包含Kobject的数据结构时,动态分配该数据结构,并分配Kobject空间,添加到内核中
  5. 每一次引用数据结构时,调用kobject_get接口增加引用计数;引用结束时,调用kobject_put接口,减少引用计数
  6. 当引用计数减少为0时,Kobject模块调用ktype所提供的release接口,释放上层数据结构以及Kobject的内存空间

 

上面有提过,有一种例外,Kobject不再嵌在其它数据结构中,可以单独使用,这个例外就是:开发者只需要在sysfs中创建一个目录,而不需要其它的kset、ktype的操作。这时可以直接调用kobject_create_and_add接口,分配一个kobject结构并把它添加到kernel中。

3.3.2 Kobject的分配和释放

前面讲过,Kobject必须动态分配,而不能静态定义或者位于堆栈之上,它的分配方法有两种。

1. 通过kmalloc自行分配(一般是跟随上层数据结构分配),并在初始化后添加到kernel。这种方法涉及如下接口:

 1: /* include/linux/kobject.h, line 85 */
 2: extern void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
 3: extern __printf(3, 4) __must_check
 4: int kobject_add(struct kobject *kobj, struct kobject *parent,
 5:                 const char *fmt, ...);
 6: extern __printf(4, 5) __must_check
 7: int kobject_init_and_add(struct kobject *kobj,
 8:             struct kobj_type *ktype, struct kobject *parent,
 9:             const char *fmt, ...);

kobject_init,初始化通过kmalloc等内存分配函数获得的struct kobject指针。主要执行逻辑为:

  • 确认kobj和ktype不为空
  • 如果该指针已经初始化过(判断kobj->state_initialized),打印错误提示及堆栈信息(但不是致命错误,所以还可以继续)
  • 初始化kobj内部的参数,包括引用计数、list、各种标志等
  • 根据输入参数,将ktype指针赋予kobj->ktype

kobject_add,将初始化完成的kobject添加到kernel中,参数包括需要添加的kobject、该kobject的parent(用于形成层次结构,可以为空)、用于提供kobject name的格式化字符串。主要执行逻辑为:

  • 确认kobj不为空,确认kobj已经初始化,否则错误退出
  • 调用内部接口kobject_add_varg,完成添加操作

kobject_init_and_add,是上面两个接口的组合,不再说明。

==========================内部接口======================================

kobject_add_varg,解析格式化字符串,将结果赋予kobj->name,之后调用kobject_add_internal接口,完成真正的添加操作。

kobject_add_internal,将kobject添加到kernel。主要执行逻辑为:

  • 校验kobj以及kobj->name的合法性,若不合法打印错误信息并退出
  • 调用kobject_get增加该kobject的parent的引用计数(如果存在parent的话)
  • 如果存在kset(即kobj->kset不为空),则调用kobj_kset_join接口加入kset。同时,如果该kobject没有parent,却存在kset,则将它的parent设为kset(kset是一个特殊的kobject),并增加kset的引用计数
  • 通过create_dir接口,调用sysfs的相关接口,在sysfs下创建该kobject对应的目录
  • 如果创建失败,执行后续的回滚操作,否则将kobj->state_in_sysfs置为1

kobj_kset_join,负责将kobj加入到对应kset的链表中。

这种方式分配的kobject,会在引用计数变为0时,由kobject_put调用其ktype的release接口,释放内存空间,具体可参考后面有关kobject_put的讲解。

2. 使用kobject_create创建

Kobject模块可以使用kobject_create自行分配空间,并内置了一个ktype(dynamic_kobj_ktype),用于在计数为0是释放空间。代码如下:

 1: /* include/linux/kobject.h, line 96 */
 2: extern struct kobject * __must_check kobject_create(void);
 3: extern struct kobject * __must_check kobject_create_and_add(const char *name,
 4:             struct kobject *parent);
 1: /* lib/kobject.c, line 605 */
 2: static void dynamic_kobj_release(struct kobject *kobj)
 3: {
 4:     pr_debug("kobject: (%p): %s\n", kobj, __func__);
 5:     kfree(kobj);
 6: }
 7:  
 8: static struct kobj_type dynamic_kobj_ktype = {
 9:     .release    = dynamic_kobj_release,
 10:    .sysfs_ops  = &kobj_sysfs_ops,
 11: };

kobject_create,该接口为kobj分配内存空间,并以dynamic_kobj_ktype为参数,调用kobject_init接口,完成后续的初始化操作。

kobject_create_and_add,是kobject_create和kobject_add的组合,不再说明。

dynamic_kobj_release,直接调用kfree释放kobj的空间。

3.3.3 Kobject引用计数的修改

通过kobject_get和kobject_put可以修改kobject的引用计数,并在计数为0时,调用ktype的release接口,释放占用空间。

 1: /* include/linux/kobject.h, line 103 */
 2: extern struct kobject *kobject_get(struct kobject *kobj);
 3: extern void kobject_put(struct kobject *kobj);

kobject_get,调用kref_get,增加引用计数。

kobject_put,以内部接口kobject_release为参数,调用kref_put。kref模块会在引用计数为零时,调用kobject_release。

==========================内部接口======================================

kobject_release,通过kref结构,获取kobject指针,并调用kobject_cleanup接口继续。

kobject_cleanup,负责释放kobject占用的空间,主要执行逻辑如下:

  • 检查该kobject是否有ktype,如果没有,打印警告信息
  • 如果该kobject向用户空间发送了ADD uevent但没有发送REMOVE uevent,补发REMOVE uevent
  • 如果该kobject有在sysfs文件系统注册,调用kobject_del接口,删除它在sysfs中的注册
  • 调用该kobject的ktype的release接口,释放内存空间
  • 释放该kobject的name所占用的内存空间

3.3.4 Kset的初始化、注册

Kset是一个特殊的kobject,因此其初始化、注册等操作也会调用kobject的相关接口,除此之外,会有它特有的部分。另外,和Kobject一样,kset的内存分配,可以由上层软件通过kmalloc自行分配,也可以由Kobject模块负责分配,具体如下。

 1: /* include/linux/kobject.h, line 166 */
 2: extern void kset_init(struct kset *kset);
 3: extern int __must_check kset_register(struct kset *kset);
 4: extern void kset_unregister(struct kset *kset);
 5: extern struct kset * __must_check kset_create_and_add(const char *name,
 6:             const struct kset_uevent_ops *u,
 7:             struct kobject *parent_kobj);

kset_init,该接口用于初始化已分配的kset,主要包括调用kobject_init_internal初始化其kobject,然后初始化kset的链表。需要注意的时,如果使用此接口,上层软件必须提供该kset中的kobject的ktype。

kset_register,先调用kset_init,然后调用kobject_add_internal将其kobject添加到kernel。

kset_unregister,直接调用kobject_put释放其kobject。当其kobject的引用计数为0时,即调用ktype的release接口释放kset占用的空间。

kset_create_and_add,会调用内部接口kset_create动态创建一个kset,并调用kset_register将其注册到kernel。

==========================内部接口======================================

kset_create,该接口使用kzalloc分配一个kset空间,并定义一个kset_ktype类型的ktype,用于释放所有由它分配的kset空间。

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

标签: Linux 内核 设备模型 kobject ktype kset

评论:

loren
2017-05-09 20:34
楼主,在3.3.2节的第一种方法中,如果我定义了一个ktype,但是其中的release回调函数没有定义(或者设置为NULL),这种case下使用这种方法来创建kobject是否会有问题呢?因为我看kobject_init()和kobject_add()两个函数都没有去检查ktype.release是否为空,而只是检查了ktype是否为空,不知道是不是我哪里理解有问题。
BTW:我看的是3.16.0的内核
wowo
2017-05-10 08:27
@loren:release的检查,只有在kobject_cleanup时,做了一次,如果为空,会给出一个debug信息,要求fix:
        if (t && !t->release)                                                  
                pr_debug("kobject: '%s' (%p): does not have a release() "      
                         "function, it is broken and must be fixed.\n",        
                         kobject_name(kobj), kobj);    
至于其它的,要有开发者自己把握了。
tinylaker
2016-05-03 20:07
好文点赞,如果可以添加设备模型的结构图进行说明就更完美了
wowo
2016-05-03 21:41
@tinylaker:多谢建议,下次整理的时候,一定填上!
maze
2016-07-27 09:30
@wowo:http://pic002.cnblogs.com/images/2012/314660/2012012909402335.gif
http://pic002.cnblogs.com/images/2012/314660/2012012909435638.gif
不知道这两张图对您是否有帮助
wowo
2016-07-27 22:03
@maze:多谢分享,图画的很好:-)
kevin
2016-05-03 18:27
谢谢!常看常新,刚入行的时候读这篇文章完全不知所云,现在快一年了,再回过头来看真的帮助我理解了很多东西!

结合 bus.c,在 bus_register 中通过调用 kset_create_and_add 初始化了一个 driver_kset,在函数bus_add_driver 中,priv->kobj.kset = bus->p->driver_kset; 再调用 kobject_init_and_add();为其添加了 ktype,在 driver_ktype 中实现了 release 函数,跟文中总结的 kobj 使用流程一模一样。

感谢窝窝们,谢谢你们的无私分享。
wowo
2016-05-03 21:41
@kevin:能够帮助到大家,是蜗窝的荣幸,不用客气:-)
Musiclover
2016-03-26 23:11
作为新手,wowo的文章应该时常拿出来读一读。第一次读的时候根本不懂wowo在说什么,时隔多日再次拜读,感觉豁然开朗,感谢wowo的无私奉献!
bat0301
2016-03-18 11:11
小弟入行linux驱动两个月,最近发现wowo的网址,帮助颇大,感谢~
wowo
2016-03-18 13:15
@bat0301::-)欢迎常来~~
dsf90
2016-03-21 15:38
@wowo:Hi wowo:
    今天刚发现这块宝地,我是linux新手,正在学习imx6ul linux的电源管理、省电优化。先了解了芯片硬件手册、PMIC使用,这几天在看linux电源管理,了解了一周还是云里雾里的不知道从何处下手。看了博主电源管理的博文,感觉基础还有差距,看不太明白,能否指点一下,新手该从何入手呀?
wowo
2016-03-21 21:24
@dsf90:这个问题还真不好回答,因为电源管理是系统级的事情,涉及到的东西太多,对一个新手来说,不太容易掌握。建议你从一个点入手,例如系统是怎么供电的、怎么启动的,不要试图一下子去理解太多东西
wangbo
2016-03-09 16:44
Kobject所占用的meomry空间。这就决定了Kobject必须是动态分配的(只有这样才能动态释放)。

在字符设备cdev机构体中,cdev的分配分为两种:静态和动态,那如果静态分配,cdev.kobj也是静态分配?是不是哪里我理解错了?
wowo
2016-03-09 18:45
@wangbo:Sorry,当初的表述有误,一直没有改过来。具体您可以看本页下面和linuxer的讨论。多谢~~
wangbo
2016-03-11 10:24
@wowo:好的,谢谢
electrlife
2016-02-23 11:24
struct device_driver {
        const char              *name;
        struct bus_type         *bus;

        struct module           *owner;
        const char              *mod_name;      /* used for built-in modules */

        bool suppress_bind_attrs;       /* disables bind/unbind via sysfs */

        const struct of_device_id       *of_match_table;
        const struct acpi_device_id     *acpi_match_table;

        int (*probe) (struct device *dev);
        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;
};
electrlife
2016-02-23 11:23
device_driver 中没有发现kobject啊?
wowo
2016-02-23 11:42
@electrlife:在‘p’里面,注册driver的时候,会在bus_add_driver中分配空间并赋值。
electrlife
2016-02-23 12:16
@wowo:谢谢wowo,看到了,这里有些不解,既然kobject是一个基类,正常来说应该放到对应的数据结构中,如struct device, 这里为何device_driver,确是间接的方式?这里有什么玄机吗?
wowo
2016-02-23 15:18
@electrlife:struct driver_private和bus/class中的类似,就是一些数据结构的组合而已,没有特殊含义,可参考http://www.wowotech.net/device_model/bus.html中描述。
linux_emb
2015-12-14 10:21
“Kobject是基本数据类型,每个Kobject都会在"/sys/“文件系统中以目录的形式出现。”
对这一句有点疑问?我看LDD3 说每个Kobject不一定出现在sys中,但Kset中的每个Kobject会出现在sys中。
wowo
2015-12-14 11:13
@linux_emb:抱歉,这句表述是有误的。很多时候,为了好理解,我倾向于把一个东西简单化,但却没有照顾到正确性,以后坚决努力改正。
这一句,真正想表达是:在sysfs中,kobject对应的是目录。至于其它的,我相信95%的读者是不关心的。
另外,kobject太抽象了,而sysfs中目录却比较具象,所以绑在一起会加深理解。
感谢指针,等等我修正一下表述。
linux_emb
2015-12-14 13:12
@wowo:其实我很能理解WOWO,我们很难将一件事情的所有细节都说明,如果那样的话源码是最能体现细节的。不过对于说明原理,这样的取舍是很正确的,Kobject本来就特别抽象,这样便于理解。
再次表示对wowo的敬佩。。。
wowo
2015-12-14 15:52
@linux_emb:您过奖了…
欢迎常来,多提宝贵意见。
谢谢
R.G.
2015-11-16 11:21
博主您好!看了您的文章,我开始对Linux设备驱动分层思想的实现有了一点理解,特来分享于斯。如有错误,望指出。

从面向对象的角度来看:
    - struct kobj(及其相关结构如kset, ktype等)属于最抽象的基类,代码最简洁,最不具体;
    - struct device(及其相关结构如device_driver,device_type等)是对kobj的封装,是第一层派生类;
    - 再上层的结构(如platform_device等),是在struct device的基础上再封装一次,是第二层派生类。

因此,例如我们创建了一个struct platform_device的实例,使用完毕后要释放它。那么这个过程按道理应该是:
    - 系统内部先调用platform_device的remove函数,它只处理自己层特有的变量;
    - 完毕后,系统调用第一层派生类struct device的release函数,处理了自己这一层的特有变量;
    - 最后,调用基类kobject的release函数,将整个空间释放掉。

整个过程应该会跟C++析构过程比较类似,上述的“系统内部”也应该类似于C++编译器自动生成的代码,因为C++中析构函数的逆向调用是自动进行的,并没有在派生类的析构函数中显示调用。类似地,在此处上层的release中也不会显式调用下层的release,都是由系统内部完成的。

以上乃个人愚见,还请不吝赐教。
wowo
2015-11-16 12:26
@R.G.:您的总结很到位啊,设备模型就是基于这种思路展开的。
Vedic
2015-11-23 14:59
@R.G.:好文章,好评论!!!!
electrlife
2016-08-19 15:14
@R.G.:总结的非常好,学习了!不懂C++,这里有几点不解希望不吝赐教:
在设备模型中,基类的空间是在派生类里的,即是由派生类分配的,因此正常的逻辑应该是直接由派生类释放即可,我想Linux也应该是通过这种形式,只不过过程曲折了点,简单表述如下:ObjectRemove->DevicePut-->KobjPut->KrefPut->KobjectRelease->DeviceRelease->ObjectRelease,最终还是回到由派生类释放。对于C++,不知其基类的空间是由派生类提供的还是说是由基类本身自己创建的,如果是自己创建的,那它的析构过程和Linux这里的弄型应该是不同的。比如Linux中,也可在在派生类里通过指针的方式来引用基类,这样释放时就由每一层来处理,可能变成下面的形式ObjectRemove->ObjectRelease->DeviceRelease->KobjectRelease。

不知这样理解是否正确!
wowo
2016-08-20 09:24
@electrlife:说实话,我仅有的那一点C++知识,也早还给学校了,呵呵~~
“在设备模型中,基类的空间是在派生类里的,即是由派生类分配的,因此正常的逻辑应该是直接由派生类释放即可,我想Linux也应该是通过这种形式”,我觉得这样的理解是对的:
我自己申请的空间,肯定是我自己释放才比较合理。
当然,释放的时机,可以让上层的core帮忙决定。

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog