您的位置:首页 > 运维架构 > Linux

Linux设备模型分析之kobject(基于3.10.1内核)

2017-03-26 13:48 507 查看
一、kobject结构定义

kobject是Linux设备模型的最底层数据结构,它代表一个内核对象。

kobject结构体定义在include/linux/kobject.h文件中:



[cpp]view
plaincopy

60structkobject{

61constchar*name;

62structlist_headentry;

63structkobject*parent;

64structkset*kset;

65structkobj_type*ktype;

66structsysfs_dirent*sd;

67structkrefkref;

68unsignedintstate_initialized:1;

69unsignedintstate_in_sysfs:1;

70unsignedintstate_add_uevent_sent:1;

71unsignedintstate_remove_uevent_sent:1;

72unsignedintuevent_suppress:1;

73};


name是这个内核对象的名字,在sysfs文件系统中,name将以一个目录的形式出现。

entry用于将该内核对象链接进其所属的kset的内核对象链表。

parent代表该内核对象的父对象,用于构建内核对象的层次结构。

kset是该内核对象所属的“内核对象集合”。

ktype是该内核对象的sysfs文件系统相关的操作函数和属性。

sd表示该内核对象对应的sysfs目录项。

kref的核心数据是一个原子型变量,用于表示该内核对象的引用计数。

state_initialized表示该内核对象是否已经进行过了初始化,1表示已经初始化过了。

state_in_sysfs表示该内核对象是否已经在sysfs文件系统中建立一个入口点。

state_add_uevent_sent表示加入内核对象时是否发送uevent事件。

state_remove_uevent_sent表示删除内核对象时是否发送uevent事件。

uevent_suppress表示内核对象状态发生变化时,是否发送uevent事件。


二、kobject初始化分析

分析kobject,我们从kobject_init_and_add函数开始看,该函数完成对kobject的初始化,建立kobject的层次结构,并将kobject添加到sysfs文件系统中,该函数定义在lib/kobject.c文件中,其内容如下:

[cpp]viewplaincopy360/**
361*kobject_init_and_add-initializeakobjectstructureandaddittothekobjecthierarchy
362*@kobj:pointertothekobjecttoinitialize
363*@ktype:pointertothektypeforthiskobject.
364*@parent:pointertotheparentofthiskobject.
365*@fmt:thenameofthekobject.
366*
367*Thisfunctioncombinesthecalltokobject_init()and
368*kobject_add().Thesametypeoferrorhandlingafteracallto
369*kobject_add()andkobjectlifetimerulesarethesamehere.
370*/
371intkobject_init_and_add(structkobject*kobj,structkobj_type*ktype,
372structkobject*parent,constchar*fmt,...)
373{
374va_listargs;
375intretval;
376
377kobject_init(kobj,ktype);
378
379va_start(args,fmt);
380retval=kobject_add_varg(kobj,parent,fmt,args);
381va_end(args);
382
383returnretval;
384}


从注释中可以看出,kobject_init_and_add函数可以分为两部分,一个是kobject_init函数,对kobject进行初始化,另一个是kobject_add_varg函数,将kobject添加到kobject层次结构中。

先来看kobject_init函数,其定义如下:

[cpp]viewplaincopy256/**
257*kobject_init-initializeakobjectstructure
258*@kobj:pointertothekobjecttoinitialize
259*@ktype:pointertothektypeforthiskobject.
260*
261*Thisfunctionwillproperlyinitializeakobjectsuchthatitcanthen
262*bepassedtothekobject_add()call.
263*
264*Afterthisfunctioniscalled,thekobjectMUSTbecleanedupbyacall
265*tokobject_put(),notbyacalltokfreedirectlytoensurethatallof
266*thememoryiscleanedupproperly.
267*/
268voidkobject_init(structkobject*kobj,structkobj_type*ktype)
269{
270char*err_str;
271
272if(!kobj){
273err_str="invalidkobjectpointer!";
274gotoerror;
275}
276if(!ktype){
277err_str="musthaveaktypetobeinitializedproperly!\n";
278gotoerror;
279}
280if(kobj->state_initialized){
281/*donoterroroutassometimeswecanrecover*/
282printk(KERN_ERR"kobject(%p):triedtoinitaninitialized"
283"object,somethingisseriouslywrong.\n",kobj);
284dump_stack();
285}
286
287kobject_init_internal(kobj);
288kobj->ktype=ktype;
289return;
290
291error:
292printk(KERN_ERR"kobject(%p):%s\n",kobj,err_str);
293dump_stack();
294}


276-279行,如果ktype为空,则返回一个错误,所以从kobject_init_and_add函数中必须传递一个非空的ktype过来。

280-285行,如果kobject已经被初始化过,则打印错误信息。

288行,将传递过来的ktype赋值给kobj->ktype。

287行,调用kobject_init_internal函数,该函数定义在lib/kobject.c文件中,其定义如下:

[cpp]viewplaincopy143staticvoidkobject_init_internal(structkobject*kobj)
144{
145if(!kobj)
146return;
147kref_init(&kobj->kref);
148INIT_LIST_HEAD(&kobj->entry);
149kobj->state_in_sysfs=0;
150kobj->state_add_uevent_sent=0;
151kobj->state_remove_uevent_sent=0;
152kobj->state_initialized=1;
153}


该函数完成对kobject成员变量的初始化,唯一值得一看的是对kobj->kref即引用计数的初始化,这是通过kref_init函数完成的,该函数定义在include/linux/kref.h文件中,其定义如下:

[cpp]viewplaincopy24structkref{
25atomic_trefcount;
26};
27
28/**
29*kref_init-initializeobject.
30*@kref:objectinquestion.
31*/
32staticinlinevoidkref_init(structkref*kref)
33{
34atomic_set(&kref->refcount,1);
35}


引用计数的核心变量是一个原子变量,初始化为1。

至此,kobject_init函数我们就分析完了。

回到kobject_init_and_add函数,

379行,va_start用于处理可变参数,这里不再详细解释。

380行,调用kobject_add_varg函数,该函数定义在lib/kobject.c文件中,其内容如下:

[cpp]viewplaincopy297staticintkobject_add_varg(structkobject*kobj,structkobject*parent,
298constchar*fmt,va_listvargs)
299{
300intretval;
301
302retval=kobject_set_name_vargs(kobj,fmt,vargs);
303if(retval){
304printk(KERN_ERR"kobject:cannotsetnameproperly!\n");
305returnretval;
306}
307kobj->parent=parent;
308returnkobject_add_internal(kobj);
309}


302行,调用kobject_set_name_vargs解析可变参数,设置kobject.name。

307行,设置kobject.parent为parent。

308行,调用kobject_add_internal函数,该函数定义在lib/kobject.c文件中,其内容如下:

[cpp]viewplaincopy156staticintkobject_add_internal(structkobject*kobj)
157{
158interror=0;
159structkobject*parent;
160
161if(!kobj)
162return-ENOENT;
163
164if(!kobj->name||!kobj->name[0]){
165WARN(1,"kobject:(%p):attemptedtoberegisteredwithempty"
166"name!\n",kobj);
167return-EINVAL;
168}
169
170parent=kobject_get(kobj->parent);
171
172/*joinksetifset,useitasparentifwedonotalreadyhaveone*/
173if(kobj->kset){
174if(!parent)
175parent=kobject_get(&kobj->kset->kobj);
176kobj_kset_join(kobj);
177kobj->parent=parent;
178}
179
180pr_debug("kobject:'%s'(%p):%s:parent:'%s',set:'%s'\n",
181kobject_name(kobj),kobj,__func__,
182parent?kobject_name(parent):"<NULL>",
183kobj->kset?kobject_name(&kobj->kset->kobj):"<NULL>");
184
185error=create_dir(kobj);
186if(error){
187kobj_kset_leave(kobj);
188kobject_put(parent);
189kobj->parent=NULL;
190
191/*benoisyonerrorissues*/
192if(error==-EEXIST)
193WARN(1,"%sfailedfor%swith"
194"-EEXIST,don'ttrytoregisterthingswith"
195"thesamenameinthesamedirectory.\n",
196__func__,kobject_name(kobj));
197else
198WARN(1,"%sfailedfor%s(error:%dparent:%s)\n",
199__func__,kobject_name(kobj),error,
200parent?kobject_name(parent):"'none'");
201}else
202kobj->state_in_sysfs=1;
203
204returnerror;
205}


164-168行,检查是否已经设置kobject.name,如果没有设置,则返回错误。所以前面必须已经设置好kobject.name。

170行,对过kobject_get函数取得kobject.parent,该函数定义在lib/kobject.c文件中,其内容如下:

[cpp]viewplaincopy521/**
522*kobject_get-incrementrefcountforobject.
523*@kobj:object.
524*/
525structkobject*kobject_get(structkobject*kobj)
526{
527if(kobj)
528kref_get(&kobj->kref);
529returnkobj;
530}


注意,该函数除了返回对应的kobject,还通过调用kref_get函数,增加该kobject的引用计数。

173-178行,如果kobject.kset不为空,则调用kobj_kset_join函数,将kobject.entry链接进kobject.kset.list中。另外,如果parent为空,则将kset.kobject设置为kobject.parent。

185行,调用create_dir函数创建kobject在sysfs文件系统中对应的目录结构,该函数定义在lib/kobject.c文件中,其内容如下:

[cpp]viewplaincopy47staticintcreate_dir(structkobject*kobj)
48{
49interror=0;
50error=sysfs_create_dir(kobj);
51if(!error){
52error=populate_dir(kobj);
53if(error)
54sysfs_remove_dir(kobj);
55}
56returnerror;
57}


通过sysfs_create_dir函数创建sysfs文件系统目录结构,这里就不再继续追踪了,后面在分析sysfs文件系统时我们会分析这个函数。

202行,如果在sysfs文件系统中创建目录结构成功,则将kobj->state_in_sysfs设置为1。

至此,kobject_add_internal函数我们就分析完了;相应的kobject_add_varg函数也就分析完了;相应的kobject_init_and_add函数也就分析完了。可以看到,kobject_init_and_add函数完成了对kobject初始化,建立了kobject的层次结构,并将kobject添加到sysfs文件系统中。


三、kobject的属性

kobject.ktype代表这个kobject的类型属性,它是structkobj_type类型,该结构体定义在include/linux/kobject.h文件中,如下:

[cpp]viewplaincopy108structkobj_type{
109void(*release)(structkobject*kobj);
110conststructsysfs_ops*sysfs_ops;
111structattribute**default_attrs;
112conststructkobj_ns_type_operations*(*child_ns_type)(structkobject*kobj);
113constvoid*(*namespace)(structkobject*kobj);
114};


109行,release函数用于在释放kobject对象时进行一些清理工作。

110行,sysfs_ops是structsysfs_ops类型的指针,该结构体定义在include/linux/sysfs.h文件中,其定义如下:

[cpp]viewplaincopy124structsysfs_ops{
125ssize_t(*show)(structkobject*,structattribute*,char*);
126ssize_t(*store)(structkobject*,structattribute*,constchar*,size_t);
127constvoid*(*namespace)(structkobject*,conststructattribute*);
128};


sysfs_ops定义了对structattribute进行操作的函数,其中,show用于将要显示的内容填充到第三个参数指定的内存空间中,并最终显示给用户空间。store用于设置attribute内容。

111行,default_attrs是一个structattribute数组,其中保存了kobject的默认attribute。structattribute定义在include/linux/sysfs.h文件中,其内容如下:

[cpp]viewplaincopy26structattribute{
27constchar*name;
28umode_tmode;
29#ifdefCONFIG_DEBUG_LOCK_ALLOC
30boolignore_lockdep:1;
31structlock_class_key*key;
32structlock_class_keyskey;
33#endif
34};


在将kobject加入到sysfs文件系统后,在该kobject对应的目录下,会创建kobject的属性文件,default_attrs数组中指定了几个属性,就会创建几个对应的属性文件,那么sysfs_ops->show在读属性文件时被调用,sysfs_ops->store在写属性文件时被调用,下面我们就来分析一下这两个函数是怎样被调用的。

当用户空间的程序要对属性文件进行读写操作时,首先要通过open函数打开该属性文件,通过一系列的函数调用,会调用到sysfs_open_file函数,该函数定义在fs/sysfs/file.c文件中,其内容如下:

[cpp]viewplaincopy326staticintsysfs_open_file(structinode*inode,structfile*file)
327{
328structsysfs_dirent*attr_sd=file->f_path.dentry->d_fsdata;
329structkobject*kobj=attr_sd->s_parent->s_dir.kobj;
330structsysfs_buffer*buffer;
331conststructsysfs_ops*ops;
332interror=-EACCES;
333
334/*needattr_sdforattrandops,itsparentforkobj*/
335if(!sysfs_get_active(attr_sd))
336return-ENODEV;
337
338/*everykobjectwithanattributeneedsaktypeassigned*/
339if(kobj->ktype&&kobj->ktype->sysfs_ops)
340ops=kobj->ktype->sysfs_ops;
341else{
342WARN(1,KERN_ERR"missingsysfsattributeoperationsfor"
343"kobject:%s\n",kobject_name(kobj));
344gotoerr_out;
345}
346
347/*Fileneedswritesupport.
348*Theinode'spermsmustsayit'sok,
349*andwemusthaveastoremethod.
350*/
351if(file->f_mode&FMODE_WRITE){
352if(!(inode->i_mode&S_IWUGO)||!ops->store)
353gotoerr_out;
354}
355
356/*Fileneedsreadsupport.
357*Theinode'spermsmustsayit'sok,andwethere
358*mustbeashowmethodforit.
359*/
360if(file->f_mode&FMODE_READ){
361if(!(inode->i_mode&S_IRUGO)||!ops->show)
362gotoerr_out;
363}
364
365/*Noerror?Great,allocateabufferforthefile,andstoreit
366*itinfile->private_dataforeasyaccess.
367*/
368error=-ENOMEM;
369buffer=kzalloc(sizeof(structsysfs_buffer),GFP_KERNEL);
370if(!buffer)
371gotoerr_out;
372
373mutex_init(&buffer->mutex);
374buffer->needs_read_fill=1;
375buffer->ops=ops;
376file->private_data=buffer;
377
378/*makesurewehaveopendirentstruct*/
379error=sysfs_get_open_dirent(attr_sd,buffer);
380if(error)
381gotoerr_free;
382
383/*opensucceeded,putactivereferences*/
384sysfs_put_active(attr_sd);
385return0;
386
387err_free:
388kfree(buffer);
389err_out:
390sysfs_put_active(attr_sd);
391returnerror;
392}


340行,将kobj->ktype->sysfs_ops赋值给ops,这样就取得了sysfs_ops结构体,也就取得了sysfs_ops.show和sysfs_ops.store函数。

374行,将buffer->needs_read_fill设置为1,后面在执行读操作时会用到这个值。

375行,将buffer->ops设置为ops。

376行,将buffer保存在file->private_data中,方便其它函数使用。

如果用户空间程序要对属性文件进行读取操作,最终会调用到sysfs_read_file函数,该函数定义在fs/sysfs/file.c文件中,其内容如下:

[cpp]viewplaincopy108/**
109*sysfs_read_file-readanattribute.
110*@file:filepointer.
111*@buf:buffertofill.
112*@count:numberofbytestoread.
113*@ppos:startingoffsetinfile.
114*
115*Userspacewantstoreadanattributefile.Theattributedescriptor
116*isinthefile's->d_fsdata.Thetargetobjectisinthedirectory's
117*->d_fsdata.
118*
119*Wecallfill_read_buffer()toallocateandfillthebufferfromthe
120*object'sshow()methodexactlyonce(ifthereadishappeningfrom
121*thebeginningofthefile).Thatshouldfilltheentirebufferwith
122*allthedatatheobjecthastoofferforthatattribute.
123*Wethencallflush_read_buffer()tocopythebuffertouserspace
124*intheincrementsspecified.
125*/
126
127staticssize_t
128sysfs_read_file(structfile*file,char__user*buf,size_tcount,loff_t*ppos)
129{
130structsysfs_buffer*buffer=file->private_data;
131ssize_tretval=0;
132
133mutex_lock(&buffer->mutex);
134if(buffer->needs_read_fill||*ppos==0){
135retval=fill_read_buffer(file->f_path.dentry,buffer);
136if(retval)
137gotoout;
138}
139pr_debug("%s:count=%zd,ppos=%lld,buf=%s\n",
140__func__,count,*ppos,buffer->page);
141retval=simple_read_from_buffer(buf,count,ppos,buffer->page,
142buffer->count);
143out:
144mutex_unlock(&buffer->mutex);
145returnretval;
146}


134行,因为我们在sysfs_open_file函数中将buffer->needs_read_fill赋值为1,所以135行fill_read_buffer函数会被调用,它会调用sysfs_ops.show函数填充要显示给用户的数据。fill_read_buffer函数同样定义在fs/sysfs/file.c文件中,其内容如下:

[cpp]viewplaincopy56/**
57*fill_read_buffer-allocateandfillbufferfromobject.
58*@dentry:dentrypointer.
59*@buffer:databufferforfile.
60*
61*Allocate@buffer->page,ifithasn'tbeenalready,thencallthe
62*kobject'sshow()methodtofillthebufferwiththisattribute's
63*data.
64*Thisiscalledonlyonce,onthefile'sfirstreadunlessanerror
65*isreturned.
66*/
67staticintfill_read_buffer(structdentry*dentry,structsysfs_buffer*buffer)
68{
69structsysfs_dirent*attr_sd=dentry->d_fsdata;
70structkobject*kobj=attr_sd->s_parent->s_dir.kobj;
71conststructsysfs_ops*ops=buffer->ops;
72intret=0;
73ssize_tcount;
74
75if(!buffer->page)
76buffer->page=(char*)get_zeroed_page(GFP_KERNEL);
77if(!buffer->page)
78return-ENOMEM;
79
80/*needattr_sdforattrandops,itsparentforkobj*/
81if(!sysfs_get_active(attr_sd))
82return-ENODEV;
83
84buffer->event=atomic_read(&attr_sd->s_attr.open->event);
85count=ops->show(kobj,attr_sd->s_attr.attr,buffer->page);
86
87sysfs_put_active(attr_sd);
88
89/*
90*ThecodeworksfinewithPAGE_SIZEreturnbutit'slikelyto
91*indicatetruncatedresultoroverflowinnormalusecases.
92*/
93if(count>=(ssize_t)PAGE_SIZE){
94print_symbol("fill_read_buffer:%sreturnedbadcount\n",
95(unsignedlong)ops->show);
96/*Trytostrugglealong*/
97count=PAGE_SIZE-1;
98}
99if(count>=0){
100buffer->needs_read_fill=0;
101buffer->count=count;
102}else{
103ret=count;
104}
105returnret;
106}


75-78行,如果buffer->page为空,即以前没有为buffer->page分配过内存,就调用get_zeroed_page为它分配一页内存。

85行,调用ops->show(kobj,attr_sd->s_attr.attr,buffer->page)函数,注意ops->show即sysfs_ops.show函数的参数,第三个参数是刚分配的buffer->page,而buffer->page最终是要拷贝并显示给用户空间的内容,到这里,我们就可以明白sysfs_ops.show函数的作用是将要显示给用户空间的内容填充到第三个参数指定的内存空间中(默认是一页的内存空间)。

回到sysfs_read_file函数,执行完135行fill_read_buffer函数后,下面来到141行,调用simple_read_from_buffer(buf,count,ppos,buffer->page,buffer->count),该函数用于将要显示的数据拷贝到用户空间,注意第一个参数buf是用户空间传递进来的缓冲区地址,第三个参数buffer->page是由sysfs_ops.show填充的那一页内存空间。该函数定义在fs/libfs.c文件中,其内容如下:

[cpp]viewplaincopy568/**
569*simple_read_from_buffer-copydatafromthebuffertouserspace
570*@to:theuserspacebuffertoreadto
571*@count:themaximumnumberofbytestoread
572*@ppos:thecurrentpositioninthebuffer
573*@from:thebuffertoreadfrom
574*@available:thesizeofthebuffer
575*
576*Thesimple_read_from_buffer()functionreadsupto@countbytesfromthe
577*buffer@fromatoffset@pposintotheuserspaceaddressstartingat@to.
578*
579*Onsuccess,thenumberofbytesreadisreturnedandtheoffset@pposis
580*advancedbythisnumber,ornegativevalueisreturnedonerror.
581**/
582ssize_tsimple_read_from_buffer(void__user*to,size_tcount,loff_t*ppos,
583constvoid*from,size_tavailable)
584{
585loff_tpos=*ppos;
586size_tret;
587
588if(pos<0)
589return-EINVAL;
590if(pos>=available||!count)
591return0;
592if(count>available-pos)
593count=available-pos;
594ret=copy_to_user(to,from+pos,count);
595if(ret==count)
596return-EFAULT;
597count-=ret;
598*ppos=pos+count;
599returncount;
600}


可以看到,该函数就是将buffer->page拷贝到用户指定的缓冲区中,这样用户空间就得到了想要的数据。

至此,我们就可以明白sysfs_ops.show的作用及其被调用的流程了。

同样的道理,我们再来看sysfs_ops.store的调用流程:

对属性文件的写操作,最终会调用到sysfs_write_file函数,该函数定义在fs/sysfs/file.c文件中,其内容如下:

[cpp]viewplaincopy210/**
211*sysfs_write_file-writeanattribute.
212*@file:filepointer
213*@buf:datatowrite
214*@count:numberofbytes
215*@ppos:startingoffset
216*
217*Similartosysfs_read_file(),thoughworkingintheoppositedirection.
218*Weallocateandfillthedatafromtheuserinfill_write_buffer(),
219*thenpushittothekobjectinflush_write_buffer().
220*Thereisnoeasywayforustoknowifuserspaceisonlydoingapartial
221*write,sowedon'tsupportthem.Weexpecttheentirebuffertocome
222*onthefirstwrite.
223*Hint:ifyou'rewritingavalue,firstreadthefile,modifyonlythe
224*thevalueyou'rechanging,thenwriteentirebufferback.
225*/
226
227staticssize_t
228sysfs_write_file(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos)
229{
230structsysfs_buffer*buffer=file->private_data;
231ssize_tlen;
232
233mutex_lock(&buffer->mutex);
234len=fill_write_buffer(buffer,buf,count);
235if(len>0)
236len=flush_write_buffer(file->f_path.dentry,buffer,len);
237if(len>0)
238*ppos+=len;
239mutex_unlock(&buffer->mutex);
240returnlen;
241}


234行,调用fill_write_buffer(buffer,buf,count)函数将用户空间传递进来的数据保存到buffer->page中,该函数定义在fs/sysfs/file.c文件中,其内容如下:

[cpp]viewplaincopy148/**
149*fill_write_buffer-copybufferfromuserspace.
150*@buffer:databufferforfile.
151*@buf:datafromuser.
152*@count:numberofbytesin@userbuf.
153*
154*Allocate@buffer->pageifithasn'tbeenalready,then
155*copytheuser-suppliedbufferintoit.
156*/
157
158staticint
159fill_write_buffer(structsysfs_buffer*buffer,constchar__user*buf,size_tcount)
160{
161interror;
162
163if(!buffer->page)
164buffer->page=(char*)get_zeroed_page(GFP_KERNEL);
165if(!buffer->page)
166return-ENOMEM;
167
168if(count>=PAGE_SIZE)
169count=PAGE_SIZE-1;
170error=copy_from_user(buffer->page,buf,count);
171buffer->needs_read_fill=1;
172/*ifbufisassumedtocontainastring,terminateitby\0,
173soe.g.sscanf()canscanthestringeasily*/
174buffer->page[count]=0;
175returnerror?-EFAULT:count;
176}


236行,调用flush_write_buffer函数,该函数就会调用sysfs_ops.store函数将用户空间传递进来的数据写到kobject中。flush_write_buffer函数定义在fs/sysfs/file.c文件中,其内容如下:

[cpp]viewplaincopy179/**
180*flush_write_buffer-pushbuffertokobject.
181*@dentry:dentrytotheattribute
182*@buffer:databufferforfile.
183*@count:numberofbytes
184*
185*Getthecorrectpointersforthekobjectandtheattributewe're
186*dealingwith,thencallthestore()methodfortheattribute,
187*passingthebufferthatweacquiredinfill_write_buffer().
188*/
189
190staticint
191flush_write_buffer(structdentry*dentry,structsysfs_buffer*buffer,size_tcount)
192{
193structsysfs_dirent*attr_sd=dentry->d_fsdata;
194structkobject*kobj=attr_sd->s_parent->s_dir.kobj;
195conststructsysfs_ops*ops=buffer->ops;
196intrc;
197
198/*needattr_sdforattrandops,itsparentforkobj*/
199if(!sysfs_get_active(attr_sd))
200return-ENODEV;
201
202rc=ops->store(kobj,attr_sd->s_attr.attr,buffer->page,count);
203
204sysfs_put_active(attr_sd);
205
206returnrc;
207}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: