T9的空间

You will never walk alone!

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  69 随笔 :: 0 文章 :: 28 评论 :: 0 Trackbacks

置顶随笔 #

     摘要: 这是在一个百度贴吧里转载的OJ及有用的网址收集!  阅读全文
posted @ 2008-09-09 21:20 Torres 阅读(2436) | 评论 (32)编辑 收藏

一直想有一个blog,做做笔记,整理整理思路,基于这个思想,这个blog诞生了!以后就靠这个来祭奠生活了。
posted @ 2008-08-11 12:30 Torres 阅读(114) | 评论 (1)编辑 收藏

2014年7月8日 #

许下一个美好愿望吧,期望能在空余的时间把多年前买的一本书看完。
Pushing yourself when no one else is around.
posted @ 2014-07-08 20:11 Torres 阅读(82) | 评论 (0)编辑 收藏

2013年12月9日 #

From Unix某论坛,忘记名字了...但这段文字从宏观上讲清楚了Linux Memory的构架。
1.
内核初始化:

    *
内核建立好内核页目录页表数据库,假设物理内存大小为len,则建立了[3G--3G+len]::[0--len]这样的虚地址vaddr和物理地址paddr的线性对应关系;
    *
内核建立一个page数组,page数组和物理页面系列完全是线性对应,page用来管理该物理页面状态,每个物理页面的虚地址保存在page->virtual中;
    *
内核建立好一个free_list,将没有使用的物理页面对应的page放入其中,已经使用的就不用放入了;

2.
内核模块申请内存vaddr = get_free_pages(mask,order)

    *
内存管理模块从free_list找到一个page,将page->virtual作为返回值,该返回值就是对应物理页面的虚地址;
    *
pagefree_list中脱离;
    *
模块使用该虚拟地址操作对应的物理内存;

3.
内核模块使用vaddr,例如执行指令mov(eax, vaddr)

    * CPU
获得vaddr这个虚地址,利用建立好的页目录页表数据库,找到其对应的物理内存地址;
    *
eax的内容写入vaddr对应的物理内存地址内;

4.
内核模块释放内存free_pages(vaddr,order)

    *
依据vaddr找到对应的page
    *
将该page加入到free_list中;

5.
用户进程申请内存vaddr = malloc(size)

    *
内存管理模块从用户进程内存空间(0--3G)中找到一块还没使用的空间vm_area_struct(start--end)
    *
随后将其插入到task->mm->mmap链表中;

6.
用户进程写入vaddr(0-3G),例如执行指令mov(eax, vaddr)

    * CPU
获得vaddr这个虚地址,该虚地址应该已经由glibc库设置好了,一定在3G一下的某个区域,根据CR3寄存器指向的current->pgd查当前进程的页目录页表数据库,发现该vaddr对应的页目录表项为0,故产生异常;
    *
在异常处理中,发现该vaddr对应的vm_area_struct已经存在,为vaddr对应的页目录表项分配一个页表;
    *
随后从free_list找到一个page,将该page对应的物理页面物理首地址赋给vaddr对应的页表表项,很明显,此时的vaddrpaddr不是线性对应关系了;
    *
pagefree_list中脱离;
    *
异常处理返回;
    * CPU
重新执行刚刚发生异常的指令mov(eax, vaddr)
    * CPU
获得vaddr这个虚地址,根据CR3寄存器指向的current->pgd,利用建立好的页目录页表数据库,找到其对应的物理内存地址;
    *
eax的内容写入vaddr对应的物理内存地址内;  

7.
用户进程释放内存vaddrfree(vaddr)

    *
找到该vaddr所在的vm_area_struct
    *
找到vm_area_struct:start--end对应的所有页目录页表项,清空对应的所有页表项;
    *
释放这些页表项指向物理页面所对应的page,并将这些page加入到free_list队列中;
    *
有必要还会清空一些页目录表项,并释放这些页目录表项指向的页表;
    *
task->mm->mmap链中删除该vm_area_struct并释放掉;

综合说明:

    *
可用物理内存就是free_list中各page对应的物理内存;
    *
页目录页表数据库的主要目的是为CPU访问物理内存时转换vaddr-->paddr使用,分配以及释放内存时不会用到,但是需要内核内存管理系统在合适时机为CPU建立好该库;
    *
对于用户进程在6中获得的物理页面,有两个页表项对应,一个就是内核页目录页表数据库的某个pte[i ],一个就是当前进程内核页目录页表数据库的某个 pte[j],但是只有一个page和其对应。如果此时调度到其他进程,其他进程申请并访问某个内存,则不会涉及到该物理页面,因为其分配时首先要从 free_list中找一个page,而该物理页面对应的page已经从free_list中脱离出来了,因此不存在该物理页面被其他进程改写操作的情况。内核中通过get_free_pages等方式获取内存时,也不会涉及到该物理页面,原理同前所述。
posted @ 2013-12-09 17:42 Torres 阅读(230) | 评论 (0)编辑 收藏

from: http://www.cnblogs.com/whjiang/articles/1387364.html

1. 尽可能不要创建global reference和global weak reference. 创建这两类引用的JNI接口NewGlobalReference和NewGlobalWeakReference内部实现

有一个锁。这个锁使得在多处理器上的可扩展性非常差,因为各个线程都在等待这个锁。所以尽量不要在native保存java 对象的引用,情愿在每次
JNI call时都带点参数。当然,在native保持java对象的local reference是非常危险的,绝对不能那样干。

2. 尽量不要使用GetPrimitiveArrayCritical/ReleasePrimitiveArrayCritical来pin住Java内存。JVM本身没有提供任何只pin住一块Java内存而不影
响GC的操作,所以这个操作是会阻止GC进行的。作为补偿,ReleasePrimitiveArrayCritical会产生一次隐式的GC调用。这样就可能出现在需要GC的时
候无法GC,而在不需要GC时进行无意义GC的情况。另外,这两个操作的实现中在某些情况下也可能触发锁。解决方法:如果是小块内存的话,情愿使
用Get<Type>ArrayRegion和Set<Type>ArrayRegion来在native和Java之间复制内存。

3. 在Java appliation中尽量不要创建phantom reference或者soft reference。这些reference会极大的影响GC。
我们先来谈谈JVM的GC。GC分为minor GC和full GC。Java内存分为young和old两代。在young memory中,每个线程都有自己的内存分配块(不和其它
线程共享),而old memory是由所有线程共享的。minor GC只对young memory作GC,而full GC对所有内存都做GC。minor GC是可以多线程并行进行的
,而full GC默认只能单线程执行。所以,一次full GC需要的时间可以是minor GC是10倍以上(可以用-verbose:gc观察)。所以在一般应用中,
minor GC的次数应该是full GC的10倍左右是比较理想的。minor GC会将无法收集的对象移动到old memory中去。

minor GC不会对phantom reference和soft reference进行收集,只有full GC才会。这样的问题就是大量的这类对象积聚起来,产生许多的内存复制。

这样每次minor GC可能就基本上没有释放多少内存,使得full GC就会被频繁触发。可能出现minor GC和full GC次数1:1的情况,甚至全是full GC。

这样,无论是性能还是可扩展性都是非常差的。

weak reference的影响好像小一些,但也应该尽量避免。

 

在JNI开发中,使用这3种reference的主要目的是保证native资源的释放。因为java对象的finalize方法是不保证被调用的,所以必须用这些

reference来帮助实现native资源释放。为了避免上述的问题,一种可行的方法是将native资源serialize成一块内存,然后放到java对象中保存,从
而避免使用这些reference。

4. NIO是不错的Java和native之间share memory的方法。但要注意它的性能。首先一定要设置对big endian还是little endian,防止JVM做额外的
endian转换动作。其次是在启动JVM时一定要加上-server选项,否则nio性能会非常差。在-server mode下,nio性能大概比java数组慢30%~50%.在-
client mode下,性能差1倍以上。

5. 尽量不要在JNI去new Java String对象。这个比在java层new慢很多。

6. 对java大对象(比方说数组),一定要仔细的tune他的大小。一般来说,小对象对GC比较友好。因为对象分配时先看每个线程自己的young memory
,如果找的到足够大的内存的话,就分配。否则在old memory中分配。因为old memory是shared,所以可能有锁开销。而且old memory中的对象只有
full GC才能释放它。所以对象比较小是比较好的。JVM的内存分配算法是为小对象优化过的,大量小对象的分配是很高效的,所以不用怕把大对象拆小。
在某些情况下,如果知道对象会活的很久的话,就让它大一点,增加它直接分配在old memory中的概率。这样可以节约young GC拷贝这个对象到old 

memory中的开销。

posted @ 2013-12-09 17:40 Torres 阅读(267) | 评论 (0)编辑 收藏

2013年10月21日 #

第二章
编译和链接
这章比较笼统,都是概念,熟悉基本流程的人,建议不看。
 
记录一些基本流程
 
拿GCC来说
用GCC编译链接生成可执行文件的过程分为下面4个步骤
Prepressing,Compilation,Assembly,Linking
 
Prepressing 预处理
从.c -> .i (gcc -E)
主要是处理,前缀为‘#’的语句
 
define的直接替换
对define这种,从我观察应该是这样
对源文件类似.c直接扫描 看到define的符号直接加到表中
然后在替换的时候会做递归检查,直到符号是最终定义。
 
所以这个与define的顺序没有太大关系了,只要不循环嵌套
 
类似这种
#define M (N + 1)
#define N 2
 
这种在用到M的时候,会先替换成 (N + 1)但是发现替换的表达式中还有未决symbol,那么
就再进行替换 (2 + 1)
 
表中并不会直接写成 M (2 + 1),至少我看到的GCC行为是这样。
 
include的也是直接导入
 
另外预编译选项 #ifdef 之类的会处理掉,删掉所有注释
 
#pragma是要被保留给编译器的,这是编译器选项,例如 pragma pack是用来指定字节对齐标准的
 
Compilation 编译
从 .i -> .s (gcc -S)
现在的GCC版本都把预编译和编译做到了一个可执行程序中 -> ccl
 
编译的主要过程
扫描,语法分析,语义分析,源代码优化,代码生成,目标代码优化
Source code --Scanner--> Tokens --Parser--> Syntax Tree
--Semantic Analyzer--> Commented Syntax Tree --Source code Optimizer--> Intermediate Representation
--Code Generator--> Target Code --Code Optimizer-->Final Target Code
 
Assembly 汇编
从 .s -> .o (gcc -c) -->汇编器as
 
Linking 链接
ld做的事情
link的时候由于每个源文件都是单独编译,那么必须处理一些外来的symbol
包括函数和全局变量
posted @ 2013-10-21 17:01 Torres 阅读(100) | 评论 (0)编辑 收藏

2013年10月18日 #

第一章
印象:
硬件PCI/ISA的架构
North Bridge相当于人的心脏,连接所有高速设备,CPU ->大脑
南桥芯片则负责低速设备连接
  
SMP
 
中间层 是解决很多问题的大方向
Any problem in computer science can be resolved by another layer of indirection
 
CPU密集型 IO密集型
这两种类型的Process,理论上优先级高的,也就是说最应该先得到CPU的是IO密集型
通俗的理解应该是IO密集型做完事情花的CPU时间最少,然后就会等待IO设备的反应,这样可以让设备性能最大化
 
Memory
分段分页 MMU
 

线程安全和线程模型
 

其中线程安全有两件事情要注意
Semaphore
Mutex
上面这两个可以做成全局的,并不一定是By Process的,例如POSIX pthread在
对Mutex做attr设定的时候就可以指定为 shared process
也就是说一个Process可以加锁,另外一个可以释放他。
另外这种Mutex必须处在共享内存中,否则没办法访问。有亲缘关系的Process可以通过mmap一个匿名映射做到
anyway有很多方式了。
 
Critical Section
这个是Inter Process的东西。
 
关于线程互斥的lock的问题
RW lock就是对普通lock记录两个状态来供read or write操作选择
 
属于线程本身的东西 TLS/Stack/Register
 
有时候编译器会为了做优化
内存和寄存器的数据会出现不sync的状态。
即使你用lock来做保护,也不一定能OK。然后volatile就出现了。 
 
volatile最主要的作用就是thread内保证编译器不要做优化,防止这种不sync带来的问题。
一般是这样例如x变量,thread_1读到x变量放到了寄存器中,因为可能马上会再访问它,那么对x进行操作后就不会写回内存
这样即使你加了lock,这个时候lock也被释放掉了(操作完成),但是结果未能Sync,那么thread 2来访问x的时候,在内存
中拿到的值就变成dirty状态了。
 
另外一种过度优化就是CPU做的优化,有些上下语义无关的指令,CPU有可能会调整运行顺序。
书中有个经典样例
一段 Singleton pattern的double-check的代码
volatile T* pInst = NULL;
T* getInstance()
{
if (pInst == NULL)
{
lock();
if (pInst == NULL)
pInst = new T();
unlock();
}
return pInst;
}

 
这里有两点
第一,double-check 也就是双if能避免过多的无用的get lock,降低消耗
对临界区需要做保护的资源,可以提前去取状态,如果符合自己的预期,而且短时间不会有变化,那么就不用去拿锁了
不知道为啥我想到了unlikely,但仔细想一下,功能完全不同。
 
第二点也就是要说的CPU的过度优化
这里已经是声明volatile了,所以没有寄存器和内存不sync的问题
但是由于这里new需要先 malloc出空间,然后call T的constructor。
所以有可能会发生这种情况,malloc出空间后,把地址付给pInst,然后去做初始化;
这样就有可能另外一个线程取得的object是没有被完全初始化好的,是否会出问题depend on T的具体实现了。
许多CPU提供了barrier指令用来解决上面提到的问题。
 
线程模型
这个东西,我看了下,开始没看明白,这边书这个东西没讲清楚,后来去网上找了些资料。用户线程和内核线程的对应关系取决于调度单位。
也就是说内核把什么东西当做一个调度单位
 
拿Linux来说吧,Process是线程集和资源集
 
调度的时候,那些共享资源的Task(thread)之间的调度肯定比那些跨Process不共享资源的thread做context switch消耗的资源
多得多。
 
基于调度消耗之类的考量
模型分为下面几种
 
一对一,也就是说 user space create出来的线程就是和kernel的调度单位相同,称一一对应
 
一对多,应该是这样一种情况,kernel看到的是Process,userspace自己实现出来自己的thread,这个thread,kernel是不知道的
调度的时候kernel负责分批CPU给他能看到的Process,上层userspace自己来调度分配这个Process获得的CPU time给这个process中的
各个线程。
这样的分配就可以保证在一定的时间内只需要做一些register和stack的切换,不会有memory等等的switch。
坏处是上面的thread只要一个被suspend,那么这个Process里面的其他thread也就被suspend住了,一般上层调度程序
不会假定其他的thread能run,所以一般会是kernel把CPU time给其他process
 
多对多,就是一种混合的情况了,我想到了Android,但是Android是一对一模型,dalvik会保证Java thread对应下面一个
native thread,想说的是,这种虚拟机架构可以做成多对多的样子,一个native thread run一个JVM,JVM开出来很多Java Thread,
JVM负责调度这些Java Thread,Native负责调度JVM所在的Thread。
不知道我有没有讲错。

posted @ 2013-10-18 19:42 Torres 阅读(92) | 评论 (0)编辑 收藏

2013年10月14日 #

     之前就翻过一次,这次再看看,期望这次能读到我想知道的东西...
     这个书的名字我自己觉得有点屌丝风格了,Orz,期望编者勿怪
posted @ 2013-10-14 17:07 Torres 阅读(113) | 评论 (0)编辑 收藏

2013年6月7日 #

伪终端.

这个是Muxd一直用的东西
相当于一个双向PIPE
Process A open ptm得到fdm,然后fork出process B,process B open pts得到fds,然后将0,1,2都dup到fds上
那么fds就变成了process B的控制终端
后面再process B中做标准IO操作的时候就会像PIPE直接影响到fdm
终端行规程在pts之上

没什么特别要记录的,在网上search了下,贴个链接,里面会介绍一些基本概念.
http://www.cnblogs.com/Anker/archive/2012/12/25/2832568.html

posted @ 2013-06-07 18:52 Torres 阅读(111) | 评论 (0)编辑 收藏

终端IO
每个终端都有输入输出队列
队列都有长度,如果输入超过输入队列MAX_INPUT长度,大多数unix系统会回显响铃来处理。
但是对于输出队列来讲,虽然输出队列也有长度,但是一旦写不进去的时候,写进程会suspend掉
直至有空闲空间

终端行规程 terminal line discipline
会帮忙做规范处理

终端设备属性 ->termios

struct termios {
 tcflag_t c_iflag;
 tcflag_t c_oflag;
 tcflag_t c_cflag;
 tcflag_t c_lflag;
 cc_t c_line;
 cc_t c_cc[NCCS];
};

local flag影响驱动程序和用户之间的接口
Android上tcflag_t->
typedef unsigned int tcflag_t;

cc_t
typedef unsigned char cc_t;

control flag中很多选项标志都是用几位标识然后用或来做选择

isatty的实现,借助tcgetattr的出错机制,成功返回0;否则返回-1,带上ENOTTY

int
isatty (int  fd)
{
  struct termios term;

  return tcgetattr (fd, &term) == 0;
}

Anyway,终端IO很复杂...

posted @ 2013-06-07 14:40 Torres 阅读(98) | 评论 (0)编辑 收藏

2013年6月6日 #

IPC

首先讲到的是PIPE,这个并不陌生,相互通信的Process必须具有关系,一般是同父亲的
然后讲到了协同进程
基本是说有一个进程专门用来接收输入,然后处理,然后返回结果
这个就可以用PIPE来实现

创建两个PIPE,一个用于输入给协同进程,另外一个用于接收协同进程的输出
fork之后在子进程中将标准输入输出都dup到管道上

而协同进程的写法可以比较common,只用关心标准输入输出。
PIPE在写的时候如果有多个写进程,那么写的数据小于 PIPE_BUF 则不会乱序,否则自己应该就需要做同步了。

然后就是FIFO,这个就是用mkfifo创建一个file,大家都去用。
PIPE和FIFO都是半双工的

XSI IPC ->即之前System V IPC
消息队列 信号量 共享存储器

在无关进程之间共享存储段,一个是使用上面V系统shm;另外一个是使用mmap将同一文件map到他们自己的进程空间。

另外就是网络IPC了
算是复习下吧,之前这块儿看的比较多
int socket(int domain, int type, int protocol)
domain标识address family -> AF_INET, AF_INET6, AF_UNIX(AF_LOCAL), AF_UNSPEC
type标识socket类型 -> SOCK_DGRAM(UDP), SOCK_RAW(IP), SOCK_SEQPACKET, SOCK_STREAM(TCP)

一般protocol都设置为0,一般address family和type就能确认要使用的protocol

SOCK_SEQPACKET和SOCK)STREAM很像,前一个提供面向数据报文的服务,而后面这种则是面对流。
SOCK_SEQPACKET使用场景SCTP,贴一个SCTP的简要介绍。
http://www.cnblogs.com/qlee/archive/2011/07/13/2105717.html

涉及网络就必须要清楚字节序的问题,字节序与字符编码是两件不同的事情,都要想清楚的,但是要提一下UTF-8这是一种专门设计用来做网络传输的字符编码
无需关心字节序的问题,所以传输的上层数据可以用UTF-8,就不用担心local host和remote host的主机字节序不一样而导致乱序了。

但是很多协议头必须做符合TCP/IP协议的字节序规范,TCP/IP是big endian
关于Big endian和Litter endian我比较容易忘记具体的样子,但是他的出发点是低地址,低地址装的是最高有效字节那么就是big endian,否则就是litter endian。

关于socket有几个可能比较模糊的地方
connect时,也就是client端,如果fd没有绑定到某个地址,那么kernel会帮忙将其绑定到默认地址; 所以bind这件事情是Client和Server都需要做的事情,调用listen之前也一样
然后accept的时候,如果你关心对方的地址,那么提供addr(足够)和len,返回的时候就能知道。
另外UDP(SOCK_DGRAM)也可以Call connect,之后就可以直接call send/write,而不必每次都call sendto指定对端地址。

sendto中flag一般给0,如果是紧急数据给MSG_OOB

文件描述符的传输,目的是想让不同的process在不同的文件描述符中共享文件表。
所以做法上系统会这样,传输文件表指针然后放到接收端的第一个可用的文件描述符上。
这个我在linux上没找到实现,因为这个功能还是有蛮多替代方案的。

posted @ 2013-06-06 17:22 Torres 阅读(85) | 评论 (0)编辑 收藏

2013年6月5日 #

13章在讲Daemon Process,没什么特别好写的。
14 ->高级IO

低速系统调用,也就是有信号发生会返回 errno 为 EINTR的

磁盘文件IO虽然有延时,但是这个不能算是低速系统调用

APUE介绍的低速系统调用
PIPE,终端设备,网络设备 读写
读无数据/写无空间(例如TCP卡Congestion window)

打开某些特殊文件
加记录锁的文件读写
ioctl,IPC


文件锁又叫做 byte-range locking,针对特定的文件区域,适合数据库文件
Posix标准
int fcntl(int fd, int cmd, .../* struct flock* flockptr */)
cmd -> F_GETLK,F_SETLK,F_SETLKW
F_SETLKW是F_SETLK的Blocking版本 W means wait

重要数据结构是struct flock ->
struct flock {
 short l_type;
 short l_whence;
 off_t l_start;
 off_t l_len;
 pid_t l_pid;
 __ARCH_FLOCK_PAD
};

锁定整个file的方式: l_whence = SEEK_SET, l_start = 0, l_len = 0

l_type的两类lock
F_RDLCK,F_WRLCK这两种锁的特性很像rw lock

不过与读写锁不一样的是或者这样讲
Posix.1没有规定下面这种情况: process A在某文件区间上设置了一把读锁;process B尝试在这个文件区间加上写锁的时候suspend;process C再尝试获取读锁,如果允许

process C拿到读锁,那么process B将会可能永远拿不到写锁,活活饿死

pthread里面的rw lock的实现会在这种情况下suspend掉process C的读锁请求;但是目前文件区域锁的实现我不太确定

这里看文件区域锁还是比较容易带来deadlock的
例如process A锁住F1的某个区域,然后去锁F2的一个区域,这时候F2的这个区域被process B锁住,那么process A就会suspend,如果这个时候process B过来要锁F1的这个区域
就会发生deadlock


关于文件区域锁的继承和释放
1.fork后,文件区域锁并不继承,继承了就完了,不同的process就有可能同时干同一件事情,把数据搞坏
2.close(fd)后 fd对应的文件锁就被释放了,文件锁挂在inode上,close的时候kernel会去扫描对应的inode上与这个PID相关的lock,释放掉,而并不去关心是哪个文件描述符或

者是哪个文件表,这很重要,因为lockf中并不记录fd,他们只是弱关联关系,这个很重要。
3.exec后,文件锁会继承原来执行程序的锁(fork之后拿到的lock),如果fd带有close-on-exec那么根据第二条,这个fd对应的file上的锁都会被释放。


后面讲了STREAMS,感觉linux上用到的不多,需要在编译kernel时动态加载

IO多路转接,主要是为了实现polling既所谓的轮询
主要函数有select,pselect,poll,epoll
select也会算是低速系统调用,那么就有可能被信号打断
pselect有参数可以设定信号屏蔽集,也提供更高精度的timer

poll的方式与select有不太一样的地方,但是功能相同,epoll更适合大数据量。

readv和writev
记住下面两条就够了
一个称为scatter read(散步读);另外一个称为gather write(聚集写)
这两个函数会面对一个buffer链表。


readn和writen
这个比较像现在Android里面socket的read和write方式,保证能read/write n byte数据,在内部做循环
我比较好奇这两个是否会处理signal,想来应该是会处理的,遇到EINTR帮忙重启就好了

我没有找到Bionic库的实现


存储映射IO
这个很重要,mmap用的很多,映射到process空间的位置在 stack以下,heap以上的部分,map完后返回低地址。

#include<sys/mman.h>
void* mmap(void* addr, size_t len, int prot, int flag, int filedes, off_t off)

prot -> PROT_READ,PROT_WRITE,PROT_EXEC,PROT_NONE
prot指定的对映射存储区的保护不能超过文件的open权限

在 flag为 MAP_FIXED的时候OS会保证分配的memory起始地址为addr,否则只是给OS一个建议。
一般建议addr给0,让OS来决定。

MAP_SHARED是说对映射区域的存储(write)会导致修改该文件。
MAP_PRIVATE则是对映射区域的操作会常见一个映射文件的副本。


后面有个例子用了lseek
使用lseek增加文件长度的方式,先lseek一个值,如果这个值大于文件本身的长度,那么下一次写就会加长该文件,并且在文件
中形成一个空洞,未写过的内容全部读为0。
mmap只能map文件的最大长度,超过的地方没办法同步到文件。

posted @ 2013-06-05 16:59 Torres 阅读(104) | 评论 (0)编辑 收藏

仅列出标题  下一页