PC6下载站

分类分类

Linux设备驱动

关注+2010-02-03作者:

一. Linux设备概述
在概念上一般把设备分为字符设备、块设备。字符设备是指设备发送和接收数据以字符形式的进行;而块设备则以整个数据缓冲区的形式进行。但是,由于网络设备等有其非凡性,实际上系统对它们单独处理。

系统用主设备号(MAJOR)加次设备(MINOR)号来唯一标识一个设备。相同主设备号表示同一类设备,例如都是硬盘;次设备号标识同类设备的个数。所有设备在适当的目录(通常在/dev目录下)下必须有相应的文件,这样字符设备和块设备都可以通过文件操作的系统调用了完成。不同的是,块设备操作经常要和缓冲区打交道,更加复杂一点。



[目录]

--------------------------------------------------------------------------------


数据结构

二. 主要数据结构
与设备治理有关的主要数据结构如下:
1、登记设备治理
系统对已登记设备的治理是由chrdevs和blkdevs这两张列表来完成的:
/*srcfsdevices.c*/
strUCt device_struct {
const char * name; //指向设备名称
struct file_operations * fops; //指向设备的访问操作函数集,file_operati
ons定义在
include/linux/fs.h中
};
static struct device_struct chrdevs[MAX_CHRDEV] = {
{ NULL, NULL },
}; //所有系统登记的字符设备列表
static struct device_struct blkdevs[MAX_BLKDEV] = {
{ NULL, NULL },
} //所有系统登记的块设备列表

实际上这两张列表的结构是一样的,但在登记时每个结构元素的值会不同(见初始化部分)。linux对设备的进行访问时,访问文件系统中相应的文件,通过文件系统和文件的属性描述块,系统可以找到该文件系统或文件对应设备的设备号。在实际访问列表时,以chrdevs[MAJOR][MINOR]或blkdevs[MAJOR][MINOR]形式访问,相同主设备号(MAJOR)的元素中fops的内容相同。

文件系统中相关的的数据结构如下:
struct super_block {
kdev_t s_dev; //该文件系统所在设备的设备标志符

} //每个文件系统对应一个super_block
struct inode {
kdev_t i_dev; //该文件所在设备的设备标志符通过它可以找到在设备列表中
… 相应设备
} //每个文件对应一个inode

2、I/O请求治理

系统会把一部分系统内存作为块设备驱动程序与文件系统接口之间的一层缓冲区,每个缓冲区与某台块设备中的特定区域相联系,文件系统首先试图存在相应的缓冲区,如未找到就向该设备发出I/O读写请求,由设备驱动程序对这些请求进行处理。因此,需要有相应的数据结构进行治理。

/*srcincludelinuxlkdev.h*/
struct blk_dev_struct {
void (*request_fn)(void); //指向请求处理函数的指针,请求处理函数是写设备驱动程序的重要一环,设备驱动程序在此函数中通过outb向位于I/O空间中的设备命令寄存器发出命令

struct request * current_request; //指向当前正在处理的请求,它和plug共同维护了该设备的请求队列
struct request plug; //这是LINUX2.0版本与以前版本的一个不同之处,plug主要被用于异步提前读写操作,在这种情况下,由于没有非凡的请求,为了提高系统性能,需要等发送完所有的提前读写请求才开始进行请求处理,即unplug_device。
struct tq_struct plug_tq; //设备对应的任务队列
};
/*srcdriverslockll_rw_blk.c*/
struct blk_dev_struct blk_dev[MAX_BLKDEV];
其中每个请求以request的类型的结构进行传递,定义如下:
/*srcincludelinuxlk_dev.h*/
struct request {
volatile int rq_status; //表示请求的状态
kdev_t rq_dev; //是该请求对应的设备号,kdev_t是unsigned s
hort类型,高8位是主设备号,低8位是从设备号,每一请求都针对一个设备发出的;
int cmd; //表示该请求对应的命令,取READ或WRITE;
int errors;
unsigned long sector; //每一扇区的字节数
unsigned long nr_sectors; //每一扇区的扇区数
unsigned long current_nr_sectors; //当前的扇区数;

char * buffer; //存放buffer_head.b_data值,表示发出请求的
数据存取地址;
struct semaphore * sem; //一个信号量,用来保证设备读写的原语操作,

sem=0时才能处理该请求;
struct buffer_head * bh; //读写缓冲区的头指针
struct buffer_head * bhtail; //读写缓冲区的尾指针
struct request * next; //指向下一个请求
};

对不同块设备的所有请求都放在请求数组all_requests中,该数组实际上是一个请求缓冲池,请求的释放与获取都是针对这个缓冲池进行;同时各个设备的请求用next指针联结起来,形成各自的请求队列。定义如下:

/*srcdriverslokcll_rw_blk.c*/
static struct request all_requests[NR_REQUEST];

3、中断请求

设备进行实际的输入/输出操作时,假如时间过长而始终被占用CPU,就会影响系统的效率,必须有一种机制来克服这个问题而又不引起其他问题。中断是最理想的方法。和中断有关的数据结构是;

struct irqaction {
void (*handler)(int, void *, struct pt_regs *); //指向设备的中断响应函数,它在系统初始化时被置入。当中断发生时,系统自动调用该函数
unsigned long flags; //指示了中断类型,如正常中断、快速中断等
unsigned long mask; //中断的屏蔽字
const char *name; //设备名
void *dev_id; //与设备相关的数据类型,中断响应函数可以根

据需要将它转化所需的数据指针,从而达到访问系统数据的功能
struct irqaction *next; //指向下一个irqaction
};

由于中断数目有限,且很少更新,所以系统在初始化时,从系统堆中分配内存给每一个irq_action指针,通过next指针将它们连成一个队列。

4、高速缓冲区

为了加速对物理设备的访问速度,linux将块缓冲区放在Cache内,块缓冲区是由buffer_head连成的链表结构。buffer_head的数据结构如下:
/*includelinuxfs.h*/
struct buffer_head {
unsigned long b_blocknr; /* block number */
kdev_t b_dev; /* device (B_FREE = free) */
kdev_t b_rdev; /* Real device */
unsigned long b_rsector; /* Real buffer location on disk */
struct buffer_head * b_next; /* Hash queue list */
struct buffer_head * b_this_page; /* circular list of buffers in one page *
/
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head * b_next_free;
unsigned int b_count; /* users using this block */
unsigned long b_size; /* block size */
char * b_data; /* pointer to data block (1024 bytes) */
unsigned int b_list; /* List that this buffer appears */
unsigned long b_flushtime; /* Time when this (dirty) buffer should be
written */
unsigned long b_lru_time; /* Time when this buffer was last used. */
struct wait_queue * b_wait;
struct buffer_head * b_prev; /* doubly linked list of hash-queue */
struct buffer_head * b_prev_free; /* doubly linked list of buffers */
struct buffer_head * b_reqnext; /* request queue */
};

块缓冲区主要由链表组成。空闲的buffer_head组成的链表是按块大小的不同分类组成,linux目前支持块大小为512、1024、2048、4096和8192字节;第二部分是正在用的块,块以Hash_table的形式组织,具有相同hash索引的缓冲块连在一起,hash索引根据设备标志符和该数据块的块号得到;同时将同一状态的缓冲区块用LRU算法连在一起。对缓冲区的各个链表定义如下:
/* fsuffer.c*/
static struct buffer_head ** hash_table;
static struct buffer_head * lru_list[NR_LIST] = {NULL, };
static struct buffer_head * free_list[NR_SIZES] = {NULL, };
static struct buffer_head * unused_list = NULL;
static struct buffer_head * reuse_list = NULL;





[目录]

--------------------------------------------------------------------------------


初始化

三. 设备的初始化
LINUX启动时,完成了实模式下的系统初始化(arch/i386/boot/setup.S)与保护模式下的核心初始化包括初始化寄存器和数据区(arch/i386/boot/compressed/head.S)、核心代码解压缩、页表初始化(arch/i386/kernel/head.S)、初始化idt、gdt和ldt等工作
后,系统转入了核心。调用函数start_kernel启动核心(init/main.c)后,将继续各方面的初始化工作,其中start_kernel最后将调用kernel_thread (init, NULL, 0),创建init进程进行系统配置(其中包括所有设备的初始化工作)。
static int init(void * unused)
{
…………
/* 创建后台进程bdflush,以不断循环写出文件系统缓冲区中"脏"的内容 */
kernel_thread(bdflush, NULL, 0);
/* 创建后台进程kswapd,专门处理页面换出工作 */
kswapd_setup();
kernel_thread(kswapd, NULL, 0);
…………
setup();
…………
在setup函数中,调用系统调用sys_setup()。sys_setup()的定义如下:
//fs/filesystems.c
asmlinkage int sys_setup(void)
{
static int callable = 1;
… …
if (!callable)
return -1;
callable = 0;
… …
device_setup();
… …
在该系统调用中,静态变量callable保证只被调用实际只一次,再次调用时后面的初始化程序不执行。在该调用开始就先进行设备的初始化:device_setup()。
//dirvers/block/genhd.c
void device_setup(void)
{
extern void console_map_init(void);
… …
chr_dev_init();
blk_dev_init();
… …
可以看到device_setup()将依次执行chr_dev_init()、blk_dev_init()等各类设备的初始化程序。每个具体的init函数的内容和具体设备就有关了,但是它们都有一些必须完成的任务:
1、 告诉内核这一驱动程序使用的主设备号,同时提供指向file_operation的指针,以完成对chrdevs和blkdevs的初始化。
2、 对块设备,需要将输入/输出处理程序的入口地址告诉内核。
3、 对块设备,需要告诉缓冲区设备存取的数据块的大小。





[目录]

--------------------------------------------------------------------------------


治理流程

四. 设备治理的流程
下面我们介绍一下整个设备治理的流程。我们以块设备为例,字符设备的流程也和块设备类似,只是没有请求队列治理。
首先,文件系统通过调用ll_rw_block发出块读写命令,读写请求治理层接到命令后,向系统申请一块读写请求缓冲区,在填写完请求信息后,请求进入设备的读写请求队列等候处理。假如队列是空的,则请求立即得到处理,否则由系统负责任务调度,唤醒请求处理。在请求处理过程中,系统向I/O空间发出读写指令返回。当读写完毕后,通过中断通知系统,同时调用与设备相应的读写中断响应函数。





[目录]

--------------------------------------------------------------------------------


添加字符设备

五. 添加一个字符设备
作为对linux设备治理的分析的总结,我们介绍一下如何添加一个设备,首先介绍如何添加一个字符设备。在后面的文章中,我们将新添加的设备称为新设备,说明以我们实现的虚拟的字符设备为例,步骤基本如下:
1. 确定设备的设备名称和主设备号:
我们必须找一个还没有被使用的主设备号,分配给自己的字符设备。假设主设备号为30(在2.0.34的内核中还没有以30作为主设备号的字符设备)。

2. 确定编写需要的file_operations中的操作函数,包括:
static int my_open(struct inode * inode,struct file * file)
//通过宏指令MINOR()提取inode参数的I_rdev字段,确定辅助设备号,然后检查相应的读写忙标志,看新设备是否已经打开。假如是,返回错误信息;
否则置读写忙标志为true,阻止再次打开新设备。
static void my_release(struct inode * inode,struct file * file)
//同my_open类似,只是置读写忙标志为false,答应再次打开新设备。
static int my _write(struct inode * inode,struct file * file,const char * bu
ffer,int count)

//用于对该设备的写
static int my _read(struct inode * inode , struct file * file,char * buffer,
int count)
//用于对该设备的读
static int my_ioctl(struct inode * inode, struct file * file,
unsigned int cmd, unsigned long arg)
//用于传送非凡的控制信息给设备驱动程序,或者长设备驱动程序取得状态信息,在我们实现的虚拟字符设备中,这个函数的功能是用来打开和关闭跟踪功能。

3. 确定编写需要的初始化函数:
void my_init(void)
//首先需要将上述的file_operations中的操作函数的地址赋给某个file_operations的结构变量my_fops中的相应域;然后调用标准内核函数登记该设备:register_chrdev(30,"mychd",&my_fops);最后对必要的变量(例如读写忙标志、跟踪标志等)赋初值。

4. 在drivers/char/mem.c中添加相应语句;
在chr_dev_init函数之前添加drgn_init的原型说明:
void my_init (void);
在chr_dev_init函数的return语句之前添加以下语句:
my_init (); //用于在字符设备初始化时初始化新设备

5. 修改drivers/char/Makefile;
假设我们把所以必要的函数写mychd.c中,则找到"L_OBJS := tty_io.o n_tty.o con
sole.o "行,将"mychd.o"加到其中。
6. 将该设备私有的*.c,*.h复制到目录drivers/char下。
7. 用命令:make clean;make dep;make zImage重新编译内核。
8. 用mknod命令在目录/dev下建立相应主设备号的用于读写的非凡文件。
完成了上述步骤,你在linux环境下编程时就可以使用新设备了。





[目录]

--------------------------------------------------------------------------------


添加块设备

六. 添加一个块设备
接下来我们将介绍如何添加一个块设备。在后面的文章中,我们将新添加的块设备称为新设备,块设备的添加过程和字符设备有相似之处,我们将主要介绍其不同点,步骤基本如下:
1. 确定设备的设备名称和主设备号
我们必须找一个还没有被使用的主设备号,分配给自己的新设备。假设主设备号为30(在2.0.34的内核中还没有以30作为主设备号的块设备),则需要在include/linux/major.h中加入如下句:
#define MY_MAJOR 30
这样我们可以通过MY_MAJOR来确定设备为新设备,保证通用性。

2. 确定编写需要的file_operations中的操作函数:
static int my_open(struct inode * inode,struct file * file)
static void my_release(struct inode * inode,struct file * file)
static int my_ioctl(struct inode * inode, struct file * file,
unsigned int cmd, unsigned long arg)
由于使用了高速缓存,块设备驱动程序就不需要包含自己的read()、write()和fsync()函数,但必须使用自己的open()、 release()和 ioctl()函数,这些函数的作用和字符设备的相应函数类似。

3. 确定编写需要的输入/输出函数:
static int my _read(void) //正确处理时返回值为1,错误时返回值为0
static int my _write(void) //正确处理时返回值为1,错误时返回值为0
值得注重的是这两个函数和字符设备中的my read()、mywrite()函数不同:

参数不同:字符设备中的函数是带参数的,用于对其工作空间(也可以看成是简单的缓冲区)中的一定长度(长度也是参数传递的)的字符进行读写;而块设备的函数是没有参数的,它通过当前请求中的信息访问高速缓存中的相应的块,因此不需要参数输入。
调用形式不同:字符设备中的函数地址是存放在file_operations中的,在对字符设备进行读写时就调用了;而块设备的读写函数是在需要进行实际I/O时在request中调用。这在后面可以看到。

4. 确定编写需要的请求处理函数:
static void my_request(void)
在块设备驱动程序中,不带中断服务子程序的请求处理函数是简单的,典型的格式如下

static void my_request(void)
{
loop:
INIT_REQUEST;

if (MINOR(CURRENT->dev)>MY_MINOR_MAX)
{
end_request(0);
goto loop;
}

if (CURRENT ->cmd==READ)
//CUREENT是指向请求队列头的request结构指针
{
end_request(my_read()); //my_read()在前面已经定义
goto loop;
}


if (CURRENT ->cmd==WRITE)
{
end_request(my_write()); //my_write()在前面已经定义
goto loop;
}

end_request(0);
goto loop;
}
实际上,一个真正的块设备一般不可能没有中断服务子程序,另外设备驱动程序是在系统调用中被调用的,这时由内核程序控制CPU,因此不能抢占,只能自愿放弃;因此驱动程序必须调用sleep_on()函数,释放对CPU的占用;在中断服务子程序将所需的数据复制到内核内存后,再由它来发出wake_up()调用,

5. 假如需要,编写中断服务子程序
实际上,一个真正的块设备一般不可能没有中断服务子程序,另外设备驱动程序是在系统调用中被调用的,这时由内核程序控制CPU,因此不能抢占,只能自愿放弃;因此驱动程序必须调用sleep_on()函数,释放对CPU的占用;在中断服务子程序将所需的数据复制到内核内存后,再由它来发出wake_up()调用。
另外两段中断服务子程序都要访问和修改特定的内核数据结构时,必须要仔细协调,以防止出现灾难性的后果。

首先,在必要时可以禁止中断,这可以通过sti()和cli()来答应和禁止中断请求。
其次,修改特定的内核数据结构的程序段要尽可能的短,使中断不至于延时过长。含有中断服务子程序的块设备驱动程序的编写相对比较复杂,我们还没有完全实现,主要问题是在中断处理之间的协调。因为这些程序是要加入内核的,系统默认为你是完全正确的,假如引起循环或者中断长时间不响应,结果非常严重。我们正在努力实现这一程序。

6. 确定编写需要的初始化函数:
void my_init(void)
需要将的file_operations中的操作函数的地址赋给某个file_operations的结构变量my_fops中的相应域;一个典型的形式是:
struct file_operations my_fops=
{
0,
block_read,
block_write,
0,
0,
my_ioctl,
0,
my_open,
my_release,
block_fsync,
0,
0,
0,
}
my_init中需要作的工作有:

首先调用标准内核函数登记该设备:
register_chrdev(MY_MOJOR,"my--bdev",&my_fops);
将request()函数的地址告诉内核:
blk_dev[MY_MAJOR].request_fn=DEVICE_REQUEST;
DEVICE_REQUEST是请求处理函数的地址,它的定义将在稍后可以看到。
告诉新设备的高速缓存的数据块的块大小:
my_block_size=512; //也可以是1024等等
blksize_size[MY_MAJOR]=& my_block_size;
为了系统在初始化时能够对新设备进行初始化,需要在blk_dev_init()中添加一行代码,可以插在blk_dev_init()中return 0的前面,格式为:
my_init();

7. 在include/linux/blk.h中添加相应语句;
到目前为止,除了DEVICE_REQUEST符合外,还没有告诉内核到那里去找你的request()函数,为此需要将一些宏定义加到blk.h中。在blk.h中找到类似的一行:
#endif /*MAJOR_NR==whatever */
在这行前面加入如下宏定义:
#elif (MAJOR_NR==whatever)

static void my_request(void);
#define DEVICE_NAME "MY_BLK_DEV" //驱动程序名称
#define DEVICE_REQUEST my_request //request()函数指针
#define DEVIEC_NR(device) (MINOR(device)) //计算实际设备号
#define DEVIEC_ON(device) //用于需要打开的设备
#define DEVIEC_OFF(device) //用于需要关闭的设备
8. 修改drivers/block/Makefile;
假设我们把所以必要的函数写mybd.c中,则找到"L_OBJS := tty_io.o n_tty.o cons
ole.o "行,将"mybd.o"加到其中。
9. 将该设备私有的*.c,*.h复制到目录drivers/block下。
10. 用命令:make clean;make dep;make zImage重新编译内核。
11. 用mknod命令在目录/dev下建立相应主设备号的用于读写的非凡文件。
完成了上述步骤,你在linux环境下编程时就可以使用新设备了。





[目录]

--------------------------------------------------------------------------------


一个虚拟的字符设备驱动程序

七. 一个虚拟的字符设备驱动程序
以下是一个虚拟的字符设备驱动程序,该程序是我和潘刚同学的试验结果,本来我们还打算写一个虚拟的块设备驱动程序,由于时间关系,没有能够完全明白中断中断服务子程序的编写方法,因此没有没有能够实现一个可以所有的虚拟的块设备,非常遗憾。不过主要步骤已经在上文中进行了介绍,我想再有一段时间应该能够完成,到时候一定交给李老师看一下。
虚拟的字符设备驱动程序如下,在潘刚同学的试验报告中也有介绍:
/* drgn.h */
#ifdef KERNEL
#define TRACE_TXT(text) {if(drgn_trace) {console_print(text);console_print("


");}}
#define TRACE_CHR(chr) {if(drgn_trace) console_print(chr);}
#define DRGN_READ 1
#define DRGN_WRITE 0
#endif
#define FALSE 0
#define TRUE 1
#define MAX_BUF 120
#define DRGN_TRON (('M' << icon_cool.gif0x01)
#define DRGN_TROFF (('M' << icon_cool.gif0x02)
struct drgn_buf
{
int buf_size;
char buffer[MAX_BUF];
struct drgn_buf *link;
};
/* drgn.c */
#define KERNEL
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/tty.h>
#include <linux/signal.h>
#include <linux/errno.h>
#include <linux/malloc.h>
#include <linux/mm.h>
#include <asm/io.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <asm/irq.h>
#include "drgn.h"
static int drgn_trace;
static int write_busy;
static int read_busy;
static struct drgn_buf * qhead;
static struct drgn_buf * qtail;
static int drgn_read(struct inode * , struct file * , char * , int );
static int drgn_write(struct inode * , struct file * , const char *, int );
static int drgn_ioctl(struct inode * , struct file * , unsigned int , unsig
ned long );
static int drgn_open(struct inode *,struct file *);
static void drgn_release(struct inode *,struct file *);
/* extern void console_print(char *);*/
struct file_operations drgn_fops=
{
NULL,
drgn_read,
drgn_write,
NULL,
NULL,
drgn_ioctl,
NULL,
drgn_open,
drgn_release,
NULL,
NULL,
NULL,
NULL
};
void drgn_init(void)
{
drgn_trace=TRUE;
if(register_chrdev(30,"drgn",&drgn_fops))
TRACE_TXT("Cannot register drgn driver as major device 30.")
else
TRACE_TXT("Tiny devie driver registered successfully.")
qhead=0;
write_busy=FALSE;
read_busy=FALSE;
/* drgn_trace=FALSE;*/
return;
}
static int drgn_open(struct inode * inode,struct file * file)
{
TRACE_TXT("drgn_open")
switch (MINOR(inode->i_rdev))
{
case DRGN_WRITE:
if(write_busy)
return -EBUSY;
else{
write_busy=TRUE;
return 0;
}
case DRGN_READ:
if(read_busy)
return -EBUSY;
else{
read_busy=TRUE;
return 0;
}
default:
return -ENXIO;
}
}
static void drgn_release(struct inode * inode,struct file * file)
{
TRACE_TXT("drgn_release")
switch (MINOR(inode->i_rdev))
{
case DRGN_WRITE:
write_busy=FALSE;
return;
case DRGN_READ:
read_busy=FALSE;
return;
}
}
static int drgn_write(struct inode * inode,struct file * file,

const char * buffer,int count)
{
int i,len;
struct drgn_buf * ptr;
TRACE_TXT("drgn_write")
if (MINOR(inode->i_rdev)!=DRGN_WRITE)
return -EINVAL;
if ((ptr=kmalloc(sizeof(struct drgn_buf),GFP_KERNEL))==0)
return -ENOMEM;
len=count < MAX_BUF?count:MAX_BUF;
if (verify_area(VERIFY_READ,buffer,len))
return -EFAULT;
for(i=0;i < count && i<MAX_BUF;++i)
{
ptr->buffer[i]=(char) get_user((char*)(buffer+i));
TRACE_CHR("w")
}
ptr->link=0;
if(qhead==0)
qhead=ptr;
else
qtail->link=ptr;
qtail=ptr;
TRACE_CHR("
")
ptr->buf_size=i;
return i;
}
static int drgn_read(struct inode * inode , struct file * file,
char * buffer, int count)
{
int i,len;
struct drgn_buf * ptr;
TRACE_TXT("drgn_read")
if(MINOR(inode->i_rdev)!=DRGN_READ)
return -EINVAL;
if (qhead==0)
return -ENODATA;
ptr=qhead;
qhead=qhead->link;
len=count < ptr->buf_size?count:ptr->buf_size;
if (verify_area(VERIFY_WRITE,buffer,len))
return -EFAULT;
for (i=0; i<count && i<ptr->buf_size; ++i)
{
put_user((char) ptr->buffer[i],(char *)(buffer+i));
TRACE_CHR("r")
}
TRACE_CHR("
")
kfree_s(ptr,sizeof(struct drgn_buf));
return i;
}
static int drgn_ioctl(struct inode * inode, struct file * file,
unsigned int cmd, unsigned long arg)
{
TRACE_TXT("drgn_ioctl")
/* if (cmd==DRGN_TRON){
drgn_trace=TRUE;
return 0;
}
else
if (cmd==DRGN_TROFF){
drgn_trace=FALSE;
return 0;
}
else
return -EINVAL;*/
switch(cmd)
{
case DRGN_TRON:
drgn_trace=TRUE;
return 0;
case DRGN_TROFF:
drgn_trace=FALSE;
return 0;
default:
return -EINVAL;
}
}
 

展开全部

相关文章

更多+相同厂商

热门推荐

  • 最新排行
  • 最热排行
  • 评分最高
排行榜

    点击查看更多

      点击查看更多

        点击查看更多

        说两句网友评论

          我要评论...
          取消