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

在Ubuntu上为Android系统编写Linux内核驱动程序(老罗学习笔记1)

2015-04-07 19:54 387 查看
这里,我们不会为真实的硬件设备编写内核驱动程序。为了方便描述为Android系统编写内核驱动程序的过程,我们使用一个虚拟的硬件设备,这个设备只有一个4字节的寄存器,它可读可写。想起我们第一次学习程序语言时,都喜欢用“Hello,World”作为例子,这里,我们就把这个虚拟的设备命名为“hello”,而这个内核驱动程序也命名为hello驱动程序。其实,Android内核驱动程序和一般Linux内核驱动程序的编写方法是一样的,都是以Linux模块的形式实现的,具体可参考前面Android学习启动篇一文中提到的LinuxDeviceDrivers一书。不过,这里我们还是从Android系统的角度来描述Android内核驱动程序的编写和编译过程。

一.参照前面两篇文章在Ubuntu上下载、编译和安装Android最新源代码和在Ubuntu上下载、编译和安装Android最新内核源代码(LinuxKernel)准备好Android内核驱动程序开发环境。

二.进入到kernel/common/drivers目录,新建hello目录:

USER-NAME@MACHINE-NAME:~/Android$cdkernel/common/drivers

USER-NAME@MACHINE-NAME:~/Android/kernel/common/drivers$mkdirhello

三.在hello目录中增加hello.h文件:

#ifndef_HELLO_ANDROID_H_
#define_HELLO_ANDROID_H_

#include<linux/cdev.h>            //对字符设备结构cdev以及一系列的操作函数定义
#include<linux/semaphore.h>         //信号量使能的头文件

#defineHELLO_DEVICE_NODE_NAME"hello"  //字节名字
#defineHELLO_DEVICE_FILE_NAME"hello"  //文件名
#defineHELLO_DEVICE_PROC_NAME"hello"  //任务名
#defineHELLO_DEVICE_CLASS_NAME"hello"  //类名

structhello_android_dev{          //驱动结构体定义(虚拟的硬件设备)
intval;                   //设备里面的寄存器地址
structsemaphoresem;           //信号量,用于同步访问寄存器val
structcdevdev;              //dev成员变量是一个内嵌的字符设备,这个Linux驱动程序自定义字符设备结构体的标准方法   
};

#endif


这个头文件定义了一些字符串常量宏,在后面我们要用到。此外,还定义了一个字符设备结构体hello_android_dev,这个就是我们虚拟的硬件设备了,val成员变量就代表设备里面的寄存器,它的类型为int,sem成员变量是一个信号量,是用同步访问寄存器val的,dev成员变量是一个内嵌的字符设备,这个Linux驱动程序自定义字符设备结构体的标准方法。

四.在hello目录中增加hello.c文件,这是驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备的寄存器的值,包括读和写。这里,提供了三种访问设备寄存器的方法,一是通过proc文件系统来访问,二是通过传统的设备文件的方法来访问,三是通过devfs文件系统来访问。下面分段描述该驱动程序的实现。

size_t:在32位系统上定义为unsignedint也就是说在32位系统上是32位无符号整形。在64位系统上定义为unsignedlong也就是说在64位系统上是64位无符号整形。size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。所以,它在数组下标和内存管理函数之类的地方广泛使用。而ssize_t这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必需是signed.意即:它表示的是signedsize_t类型的。)

首先是包含必要的头文件和定义三种访问设备的方法:

[b]/*---------------------------------------[/b]

[b](三)该文件包[/b]

[b]---------------------------------------*/[/b]

#include<linux/init.h>  //包含了模块的初始化的宏定义以及一些其他函数的初始化函数

/*MODULE.H:写内核驱动的时候必须加载这个头文件,作用是动态的将模块加载到内核中去,常用的宏定义如MODULE_LICESENCE(),MODULE_AUTHOR(),等在此文件中
而且kobject,kset结构体题及其操作函数也在这个结构体中
*/

#include<linux/module.h>    

#include<linux/types.h>   //原系统数据类型
#include<linux/fs.h>      //file_operations、inode_operations、super_operations结构体(文件系统函数定义头文件)

#include<linux/proc_fs.h>   /*proc文件系统是由软件创建,被内核用来向外界报告信息的一个文件系统,,http://www.cnblogs.com/Ph-one/p/4399326.html*/
#include<linux/device.h>  //在这个头文件中包含了bus的一些函数和drifver的一些函数,以及class_create()等函数
#include<asm/uaccess.h>    /*copy_to_user和copy_from_user*/
#include"hello.h"        //自定义头文件

/*主设备和从设备号变量;设备号有什么意义,作用是什么,大小有无关系*/
staticinthello_major=0;  //主设备
staticinthello_minor=0/*设备类别和设备变量*/
staticstructclass*hello_class=NULL;
staticstructhello_android_dev*hello_dev=NULL;

/*---------------------------------------

(二)

/*------------------------------------------------------------------------------------
-*/
/*传统的设备文件操作方法*/
staticinthello_open(structinode*inode,structfile*filp);    //打开
staticinthello_release(structinode*inode,structfile*filp);    //释放
staticssize_thello_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos);
staticssize_thello_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos);

/*设备文件操作方法表*/
/*第一个file_operations成员根本不是一个操作;它是一个指向拥有这个结构的模块的指针.
这个成员用来在它的操作还在被使用时阻止模块被卸载.几乎所有时间中,
它被简单初始化为THIS_MODULE,一个在<linux/module.h>中定义的宏.*/
staticstructfile_operationshello_fops={/*对应用层出口*/
.owner=THIS_MODULE,
.open=hello_open,//尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法.如果这个项是NULL,设备打开一直成功,但是你的驱动不会得到通知.
.release=hello_release,//在文件结构被释放时引用这个操作.如同open,release可以为NULL
.read=hello_read,
.write=hello_write,
};
---------------------------------------*/

/*------------------------------------------------------------------------------------
sysfs访问设置设备属性-*/

/*访问设置属性方法*/
staticssize_thello_val_show(structdevice*dev,structdevice_attribute*attr,char*buf);
staticssize_thello_val_store(structdevice*dev,structdevice_attribute*attr,constchar*buf,size_tcount);

/*定义设备属性*/
staticDEVICE_ATTR(val,S_IRUGO|S_IWUSR,hello_val_show,hello_val_store);

/*----------------------------------------------------------------------------------*/



/*---------------------------------------

(一)

定义传统的设备文件访问方法,主要是定义hello_open、hello_release、hello_read和hello_write这四个打开、释放、读和写设备文件的方法:

/*打开设备方法*/
staticinthello_open(structinode*inode,structfile*filp){
structhello_android_dev*dev;

/*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/
dev=container_of(inode->i_cdev,structhello_android_dev,dev);//container_of()的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址
                         //structcdev*

filp->private_data=dev;        //void*private_data:该成员是系统调用时保存状态信息非常有用的资源

return0;
}

/*设备文件释放时调用,空实现*/
staticinthello_release(structinode*inode,structfile*filp){
return0;//函数返回零,就是释放,整个退出,好笑
}

/*读取设备的寄存器val的值*/
staticssize_thello_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos){
ssize_terr=0;
structhello_android_dev*dev=filp->private_data;//将系统保存状态信息(地址)传给Hello结构体首地址,,(hello_android_dev*dev:这样定义是不是有问题??没问题)

/*同步访问*/
if(down_interruptible(&(dev->sem))){        
  /*这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步*/
    return-ERESTARTSYS;
}

if(count<sizeof(dev->val)){        //这一步的作用是做什么的呢?
gotoout;
}

/*将寄存器val的值拷贝到用户提供的缓冲区*/
if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){//拷贝成功返回‘1’;
    /*copy_to_user:从内核区中读取数据到用户区

     #include<linux/uaccess.h>
    unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);
    如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。
    *to是用户空间的指针,
    *from是内核空间指针,
     n表示从内核空间向用户空间拷贝数据的字节数
   */

err=-EFAULT;
gotoout;
}

err=sizeof(dev->val);

out:

up(&(dev->sem));

returnerr;
}

/*写设备的寄存器值val*/
staticssize_thello_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos){
structhello_android_dev*dev=filp->private_data;
ssize_terr=0;

/*同步访问*/
if(down_interruptible(&(dev->sem))){
return-ERESTARTSYS;
}

if(count!=sizeof(dev->val)){
gotoout;
}

/*将用户提供的缓冲区的值写到设备寄存器去,读成功返回‘0’,失败返回数据字节数*/
if(copy_from_user(&(dev->val),buf,count)){//这个buf是个地址
err=-EFAULT;
gotoout;
}

err=sizeof(dev->val);

out:
up(&(dev->sem));
returnerr;
}


[b]---------------------------------------*/[/b]

定义通过sysfs文件系统访问方法,这里把设备的寄存器val看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现hello_val_show和hello_val_store两个方法,同时定义了两个内部使用的访问val值的方法__hello_get_val和__hello_set_val:

/*读取寄存器val的值到缓冲区buf中,内部使用*/
staticssize_t__hello_get_val(structhello_android_dev*dev,char*buf){
intval=0;

/*同步访问*/
if(down_interruptible(&(dev->sem))){
return-ERESTARTSYS;//函数直接跳出,没有执行读取任务
}

val=dev->val;
up(&(dev->sem));

returnsnprintf(buf,PAGE_SIZE,"%d\n",val);//snprintf:将可变个参数(...)按照format格式化成字符串,然后将其复制到str中,不是太懂?答:snprintf有数据格式转换的功能


}

/*把缓冲区buf的值写到设备寄存器val中去,内部使用*/
staticssize_t__hello_set_val(structhello_android_dev*dev,constchar*buf,size_tcount){
intval=0;

/*将字符串转换成数字*/
  /*

    就是解析字符串cp中8,10,16进制数字,返回值是解析的数字,endp指向第一个不是数值的字符串起始处,base:进制

    unsignedlonglongsimple_strtoull(constchar*cp,char**endp,unsignedintbase)

*/
val
=simple_strtol(buf,NULL,10);//将一个字符串转换成unsigendlonglong型数据,十进制

/*同步访问*/
if(down_interruptible(&(dev->sem))){
return-ERESTARTSYS;
}

dev->val=val;
up(&(dev->sem));

returncount;
}

/*读取设备属性val*/
staticssize_thello_val_show(structdevice*dev,structdevice_attribute*attr,char*buf){
structhello_android_dev*hdev=(structhello_android_dev*)dev_get_drvdata(dev);//返回驱动数据的指针,参数设备指针

return__hello_get_val(hdev,buf);
}

/*写设备属性val*/
staticssize_thello_val_store(structdevice*dev,structdevice_attribute*attr,constchar*buf,size_tcount){
structhello_android_dev*hdev=(structhello_android_dev*return__hello_set_val(hdev,buf,count);
}


/*----------------------------------------------

device_attribute:

structdriver_attribute{
structattributeattr;
ssize_t(*show)(structdevice_driver*driver,char*buf);
ssize_t(*store)(structdevice_driver*,constchar*buf,size_tcount);
};


Devicedriverscanexportattributesviatheirsysfsdirectories.DriverscandeclareattributesusingaDRIVER_ATTRmacrothatworksidenticallytotheDEVICE_ATTRmacro.实例:在GSENSOR8452驱动中申明的属性,可以在ADB中查看到属性。

[b]属性部分出口确实不太清楚[/b]

---------------------------------------------------*/

定义通过proc文件系统访问方法,主要实现了hello_proc_read和hello_proc_write两个方法,同时定义了在proc文件系统创建和删除文件的方法hello_create_proc和hello_remove_proc:

proc:在用户态检查内核状态的机制

/*读取设备寄存器val的值,保存在page缓冲区中*/
staticssize_thello_proc_read(char*page,char**start,off_toff,intcount,int*eof,void*data){
//off_t类型默认是32位的longint
if(off>0){
*eof=1;
return0;
}

return__hello_get_val(hello_dev,page);
}

/*把缓冲区的值buff保存到设备寄存器val中去*/
staticssize_thello_proc_write(structfile*filp,constchar__user*buff,unsignedlonglen,void*data){
interr=0;
char*page=NULL;

if(len>PAGE_SIZE){//PAGE_SIZE这个如何定义,量多少?
printk(KERN_ALERT"Thebuffistoolarge:%lu.\n",len);//保存量超标
return-EFAULT;
}

page=(char*)__get_free_page(GFP_KERNEL);
if(!page){
      /*没有足够的内存,你必须处理这种错误!*/
printk(KERN_ALERT"Failedtoallocpage.\n");
return-ENOMEM;
}

/*先把用户提供的缓冲区值拷贝到内核缓冲区中去*/
if(copy_from_user(page,buff,len)){
printk(KERN_ALERT"Failedtocopybufffromuser.\n");
err=-EFAULT;
gotoout;
}

err=__hello_set_val(hello_dev,page,len);

out:
free_page((unsignedlong)page);
returnerr;//为‘0’拷贝失败(与上面相反)
}

/*创建/proc/hello文件*/
staticvoidhello_create_proc(void){
//proc_dir_entry:http://www.cnblogs.com/Ph-one/p/4411557.html
structproc_dir_entry*entry;
entry=create_proc_entry(HELLO_DEVICE_PROC_NAME,0,NULL);
if(entry){
entry->owner=THIS_MODULE;
entry->read_proc=hello_proc_read;
entry->write_proc=hello_proc_write;
}
}

/*删除/proc/hello文件*/
staticvoidhello_remove_proc(void){
remove_proc_entry(HELLO_DEVICE_PROC_NAME,NULL);
}


/*--

structfile──字符设备驱动相关重要结构

文件结构代表一个打开的文件描述符,它不是专门给驱动程序使用的,系统中每一个打开的文件在内核中都有一个关联的structfile。它由内核在open时创建,并传递给在文件上操作的任何函数,知道最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。

--*/

/*--

printk是在内核中运行的向控制台输出显示的函数,printk相当于printf的孪生姐妹.

--*/

最后,定义模块加载和卸载方法,这里只要是执行设备注册和初始化操作:

/*初始化设备*/
staticint__hello_setup_dev(structhello_android_dev*dev){
interr;
dev_tdevno=MKDEV(hello_major,hello_minor);//dev_t:unsignedint类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号
          //MKDEV:获取设备在设备表中的位置
memset(dev,0,sizeof(structhello_android_dev));//清零

cdev_init(&(dev->dev),&hello_fops);//空间的申请,并且清零
dev->dev.owner=THIS_MODULE;
dev->dev.ops=&hello_fops;//动态内存定义初始化

/*注册字符设备,初始化后将它添加到系统中*/
err=cdev_add(&(dev->dev),devno,1);
if(err){
returnerr;
}

/*初始化信号量和寄存器val的值*/
init_MUTEX(&(dev->sem));
dev->val=0;

return0;
}

/*模块加载方法*/
staticint__inithello_init(void){
interr=-1;
dev_tdev=0;
structdevice*temp=NULL;

printk(KERN_ALERT"Initializinghellodevice.\n");

/*动态分配主设备和从设备号*/
err=alloc_chrdev_region(&dev,0,1,HELLO_DEVICE_NODE_NAME);
if(err<0){
printk(KERN_ALERT"Failedtoallocchardevregion.\n");
gotofail;
}

hello_major=MAJOR(dev);
hello_minor=MINOR(dev);

/*分配hello设备结构体变量*/
hello_dev=kmalloc(sizeof(structhello_android_dev),GFP_KERNEL);
if(!hello_dev){
err=-ENOMEM;
printk(KERN_ALERT"Failedtoallochello_dev.\n");
gotounregister;
}

/*初始化设备*/
err=__hello_setup_dev(hello_dev);
if(err){
printk(KERN_ALERT"Failedtosetupdev:%d.\n",err);
gotocleanup;
}

/*在/sys/class/目录下创建设备类别目录hello*/
hello_class=class_create(THIS_MODULE,HELLO_DEVICE_CLASS_NAME);
if(IS_ERR(hello_class)){  //IS_ERR检验函数,1:错,0:对
err=PTR_ERR(hello_class);
printk(KERN_ALERT"Failedtocreatehelloclass.\n");
gotodestroy_cdev;
}

/*在/dev/目录和/sys/class/hello目录下分别创建设备文件hello*/****
temp=device_create(hello_class,NULL,dev,"%s",HELLO_DEVICE_FILE_NAME);//创建一个设备节点,节点名为:HELLO_DEVICE_FILE_NAME

if(IS_ERR(temp)){//http://www.cnblogs.com/Ph-one/p/4414540.htmlerr=PTR_ERR(temp);//将指针转换成错误号
printk(KERN_ALERT"Failedtocreatehellodevice.");
gotodestroy_class;
}

/*在/sys/class/hello/hello目录下创建属性文件val*/
err=device_create_file(temp,&dev_attr_val);
if(err<0){
printk(KERN_ALERT"Failedtocreateattributeval.");
gotodestroy_device;
}

dev_set_drvdata(temp,hello_dev);/*创建/proc/hello文件*/
hello_create_proc();

printk(KERN_ALERT"Succeddedtoinitializehellodevice.\n");
return0;

destroy_device:
device_destroy(hello_class,dev);

destroy_class:
class_destroy(hello_class);//删class

destroy_cdev:
cdev_del(&(hello_dev->dev));//删

cleanup:
kfree(hello_dev);//kfree函数很简单,将Slab对象释放到阵列缓存中,如果缓存满了,则批量释放到Slab缓存中

unregister:
unregister_chrdev_region(MKDEV(hello_major,hello_minor),1);//释放hello_major在设备号,1个设备

fail:
returnerr;
}

/*模块卸载方法*/
staticvoid__exithello_exit(void){//__exit:标记退出代码,对于非模块无效
dev_tdevno=MKDEV(hello_major,hello_minor);
   //dev_t:unsignedint类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号

printk(KERN_ALERT"Destroyhellodevice.\n");//驱动破坏

/*删除/proc/hello文件*/
hello_remove_proc();

/*销毁设备类别和设备*/
if(hello_class){
device_destroy(hello_class,MKDEV(hello_major,hello_minor));
class_destroy(hello_class);
}

/*删除字符设备和释放设备内存*/
if(hello_dev){
cdev_del(&(hello_dev->dev));//释放cdev占用的内存
kfree(hello_dev);
}

/*释放设备号*/
unregister_chrdev_region(devno,1);//当用到这个函数的时候,就不能用杂项设备misc,中的misc_deregister(&tiny4412_led_dev);杂项设备相当于设备号为10的字符设备
}

MODULE_LICENSE("GPL");//模块许可证
MODULE_DESCRIPTION("FirstAndroidDriver");//模块描述

module_init(hello_init);//module_init除了初始化加载之外,还有后期释放内存的作用
module_exit(hello_exit);
//module_exit的参数卸载时同__init类似,如果驱动被编译进内核,则__exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作,显然__init和__exit对动态加载的模块是无效的,只支持完全编译进内核


/*--

/*判断返回的指针是错误信息还是实际地址,即指针是否落在最后一页

是实际地址:落在最后一页,返回‘0’
不是实际地址:没有落在最后一页,返回‘1’

*/
staticinlinelongIS_ERR(constvoid*ptr)//☆☆
{
returnIS_ERR_VALUE((unsignedlong)ptr);
}


--*/

/*--

unregister_chrdev_region|释放设备号
在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,其函数原型为:
引用voidunregister_chrdev_region(dev_tfirst,unsignedintcount);
first为第一个设备号,count为申请的设备数量
--*/

以上是通过devfs和proc两种方式访问设备属性;

五.在hello目录中新增Kconfig和Makefile两个文件,其中Kconfig是在编译前执行配置命令makemenuconfig时用到的,而Makefile是执行编译命令make是用到的:

Kconfig文件的内容

configHELLO
tristate"FirstAndroidDriver"
defaultn
help
Thisisthefirstandroiddriver.

Makefile文件的内容
obj-$(CONFIG_HELLO)+=hello.o
在Kconfig文件中,tristate表示编译选项HELLO支持在编译内核时,hello模块支持以模块、内建和不编译三种编译方法,默认是不编译,因此,在编译内核前,我们还需要执行makemenuconfig命令来配置编译选项,使得hello可以以模块或者内建的方法进行编译。
在Makefile文件中,根据选项HELLO的值,执行不同的编译方法。
六.修改arch/arm/Kconfig和drivers/kconfig两个文件,在menu"DeviceDrivers"和endmenu之间添加一行:
source"drivers/hello/Kconfig"
这样,执行makemenuconfig时,就可以配置hello模块的编译选项了。.
七.修改drivers/Makefile文件,添加一行:
obj-$(CONFIG_HELLO)+=hello/
八.配置编译选项:
USER-NAME@MACHINE-NAME:~/Android/kernel/common$makemenuconfig
找到"DeviceDrivers"=>"FirstAndroidDrivers"选项,设置为y。
注意,如果内核不支持动态加载模块,这里不能选择m,虽然我们在Kconfig文件中配置了HELLO选项为tristate。要支持动态加载模块选项,必须要在配置菜单中选择Enableloadablemodulesupport选项;在支持动态卸载模块选项,必须要在Enableloadablemodulesupport菜单项中,选择Moduleunloading选项。
九.编译:
USER-NAME@MACHINE-NAME:~/Android/kernel/common$make
编译成功后,就可以在hello目录下看到hello.o文件了,这时候编译出来的zImage已经包含了hello驱动。
十.参照在Ubuntu上下载、编译和安装Android最新内核源代码(LinuxKernel)一文所示,运行新编译的内核文件,验证hello驱动程序是否已经正常安装:
USER-NAME@MACHINE-NAME:~/Android$emulator-kernel./kernel/common/arch/arm/boot/zImage&
USER-NAME@MACHINE-NAME:~/Android$adbshell

进入到dev目录,可以看到hello设备文件:
root@android:/#cddev
root@android:/dev#ls

进入到proc目录,可以看到hello文件:
root@android:/#cdproc
root@android:/proc#ls
访问hello文件的值:
root@android:/proc#cathello
0

root@android:/proc#echo'5'>hello

root@android:/proc#cathello

5
进入到sys/class目录,可以看到hello目录:

root@android:/#cdsys/class
root@android:/sys/class#ls
进入到hello目录,可以看到hello目录:
root@android:/sys/class#cdhello
root@android:/sys/class/hello#ls

进入到下一层hello目录,可以看到val文件:

root@android:/sys/class/hello#cdhello

root@android:/sys/class/hello/hello#ls
访问属性文件val的值:

root@android:/sys/class/hello/hello#catval

5

root@android:/sys/class/hello/hello#echo'0'>val

root@android:/sys/class/hello/hello#catval

0
至此,我们的hello内核驱动程序就完成了,并且验证一切正常。这里我们采用的是系统提供的方法和驱动程序进行交互,也就是通过proc文件系统和devfs文件系统的方法,下一篇文章中,我们将通过自己编译的C语言程序来访问/dev/hello文件来和hello驱动程序交互,敬请期待。

原创:http://blog.csdn.net/luoshengyang/article/details/6580267

出自:http://www.cnblogs.com/Ph-one/p/4399233.html

瘋耔注解添加修改
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: