注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

民主与科学

独立之人格,自由之思想

 
 
 

日志

 
 

Binder的表述形式  

2012-05-10 21:27:53|  分类: 深入研究 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
本文摘自: http://blog.csdn.net/universus/article/details/6211589
一、前言
考察一次Binder通信的全过程(如图1所示)会发现,Binder存在于系统以下几个部分中:
· 应用程序进程:分别位于Server进程和Client进程中.
· Binder驱动:分别管理为Server端的Binder实体和Client端的引用
· 传输数据:由于Binder可以跨进程传递,需要在传输数据中予以表述
在系统不同部分,Binder实现的功能不同,表现形式也不一样。接下来逐一探讨Binder在各部分所扮演的角色和使用的数据结构。
图1
Binder的表述形式 - hubingforever - 民主与科学
 
二、Binder 在应用程序中的表述
  虽然Binder用到了面向对象的思想,但并不限制应用程序一定要使用面向对象的语言,无论是C语言还是C++语言都可以很容易的使用Binder来通信。例如尽管Android主要使用java/C++,象ServiceManager这么重要的进程就是用C语言实现的。不过面向对象的方式表述起来更方便,所以本文假设应用程序是用面向对象语言实现的。
  Binder本质上只是一种底层通信方式,和具体服务没有关系。为了提供具体服务,Server必须提供一套接口函数以便Client通过远程访问使用各种服务。这时通常采用Proxy设计模式:将接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数,所不同的是Server端是真正的功能实现,而Client端是对这些函数远程调用请求的包装。如何将Binder和Proxy设计模式结合起来是应用程序实现面向对象Binder通信的根本问题。
   BinderServer端的表述(Binder实体)在代码中是由BnInterface来完成的.Binder Client端的表述(Binder引用)在代码中是由BpInterfaceBpBinder来完成的,如图1。
图1
Binder的表述形式 - hubingforever - 民主与科学
 

2.1 Binder在Server端的表述 – Binder实体
   Binder在Server端的表述(Binder实体)在代码中是由BnInterface来完成的.做为Proxy设计模式的基础,首先定义一个抽象接口类封装Server所有功能,其中包含一系列纯虚函数留待Server和Proxy各自实现。由于这些函数需要跨进程调用,须为其一一编号,从而Server可以根据收到的编号决定调用哪个函数。其次就要引入Binder了。Server端定义另一个Binder抽象类处理来自Client的Binder请求数据包,其中最重要的成员是虚函数onTransact()。该函数分析收到的数据包,调用相应的接口函数处理请求。
  接下来采用继承方式以接口类和Binder抽象类为基类构建Binder在Server中的实体,实现基类里所有的虚函数,包括公共接口函数以及数据包处理函数:onTransact()。这个函数的输入是来自Client的binder_transaction_data结构的数据包。该结构里有个成员code,包含这次请求的接口函数编号。onTransact()将case-by-case地解析code值,从数据包里取出函数参数,调用接口类中相应的,已经实现的公共接口函数。函数执行完毕,如果需要返回数据就再构建一个binder_transaction_data包将返回数据包填入其中。关于binder_transaction_data的详细结构请参考《Binder通信协议
   那么各个Binder实体的onTransact()又是什么时候调用呢?这就需要驱动参与了。Binder实体须要以Binde传输结构flat_binder_object形式发送给其它进程才能建立Binder通信,而Binder实体指针就存放在该结构的handle域中。驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节点,将Binder实体指针记录在该节点中。如果接下来有其它进程向该Binder发送数据,驱动会根据节点中记录的信息将Binder实体指针填入binder_transaction_datatarget.ptr中返回给接收线程。接收线程从数据包中取出该指针,reinterpret_cast成Binder抽象类并调用onTransact()函数。由于这是个虚函数,不同的Binder实体中有各自的实现,从而可以调用到不同Binder实体提供的onTransact()。
2.2 Binder 在Client端的表述 – Binder远程引用
   Binder 在Client端的表述(Binder远程引用)在代码中是由BpInterfaceBpBinder来完成的。
做为Proxy设计模式的一部分,Client端的Binder同样要继承Server提供的公共接口类并实现公共函数。但这不是真正的实现,而是对远程函数调用的包装:将函数参数打包,通过Binder向Server发送申请并等待返回值。为此Client端的Binder还要知道Binder实体的相关信息,即Binder实体的句柄引用。该句柄引用或是由ServiceManager转发过来的实名Binder的句柄引用;或是由另一个进程直接发送过来的对匿名Binder的句柄引用。
   由于继承了同样的公共接口类,Client Binder提供了与Server Binder一样的函数原型,使用户感觉不出Server是运行在本地还是远端。Client Binder中,公共接口函数的包装方式是:创建一个binder_transaction_data数据包,将其对应的编码填入code域,将调用该函数所需的参数填入data.buffer指向的缓存中,并指明数据包的目的地,那就是已经获得的Binder实体的句柄引用,填入数据包的target.handle。注意这里和Server的区别:实际上target域是个联合体,包括ptrhandle两个成员,前者用于接收数据包的Server,指向 Binder实体对应的内存空间;后者用于作为请求方的Client,存放Binder实体的句柄,告知驱动数据包将路由给哪个实体。数据包准备好后,通过驱动接口发送出去。经过BC_TRANSACTION/BC_REPLY回合完成函数的远程调用并得到返回值。
三、 Binder 在传输数据中的表述
3.1、  普遍传输数据中的Binder表述
 Binder可以塞在数据包的有效数据中越进程边界从一个进程传递给另一个进程,这些传输中的Binder用结构flat_binder_object表示,如下表所示:
表 1 Binder传输结构:flat_binder_object

成员

含义

unsigned long type

表明该Binder的类型,包括以下几种:

BINDER_TYPE_BINDER:表示传递的是Binder实体(对应于binder_node),并且指向该实体的引用都是强类型;

BINDER_TYPE_WEAK_BINDER:表示传递的是Binder实体(对应于binder_node),并且指向该实体的引用都是弱类型;

BINDER_TYPE_HANDLE:表示传递的是强类型的Binder节点引用(对应于binder_ref)

BINDER_TYPE_WEAK_HANDLE:表示传递的是弱类型的Binder节点引用(对应于binder_ref)

BINDER_TYPE_FD:表示传递的是文件形式的Binder,详见下节

unsigned long flags

该域只对第一次传递Binder实体时有效,因为此刻驱动需要在内核中创建相应的实体节点,有些参数需要从该域取出:

第0-7位:代码中用FLAT_BINDER_FLAG_PRIORITY_MASK取得,表示处理本实体请求数据包的线程的最低优先级。当一个应用程序提供多个实体时,可以通过该参数调整分配给各个实体的处理能力。

第8位:代码中用FLAT_BINDER_FLAG_ACCEPTS_FDS取得,置1表示该实体可以接收其它进程发过来的文件形式的Binder。由于接收文件形式的Binder会在本进程中自动打开文件,有些Server可以用该标志禁止该功能,以防打开过多文件。

union {

void *binder;

signed long handle;

};

联合体binder/handle就对应于binder实体节点binder_node的ptr或者binder引用节点binder_desc的desc,取决于他的type是BINDER还是HANDLE。

void *cookie;

该域只对Binder实体有效,存放与该Binder有关的附加信息。

在BINDER类型时就是binder实体节点binder_node的cookie,在HANDLE时是null,没啥用。

  无论是Binder实体还是对实体的引用都从属与某个进程,所以该结构不能透明地在进程之间传输,必须经过驱动翻译。例如当Server把Binder实体传递给Client时,在发送数据流中,flat_binder_object中的typeBINDER_TYPE_BINDER,binder指向Server进程用户空间地址。如果透传给接收端将毫无用处,驱动必须对数据流中的这个Binder做修改:将type该成BINDER_TYPE_HANDLE;为这个Binder在接收进程中创建位于内核中的引用并将引用号(引用句柄)填入handle中。对于发生数据流中引用类型的Binder也要做同样转换。经过处理后接收进程从数据流中取得的Binder引用才是有效的,才可以将其填入数据包binder_transaction_datatarget.handle域,向Binder实体发送请求。
这样做也是出于安全性考虑:应用程序不能随便猜测一个引用号填入target.handle中就可以向Server请求服务了,因为驱动并没有为你在内核中创建该引用,必定会被驱动拒绝。唯有经过身份认证确认合法后,由‘权威机构’(Binder驱动)亲手授予你的Binder才能使用,因为这时驱动已经在内核中为你使用该Binder做了注册,交给你的引用号是合法的
下表总结了当flat_binder_object结构穿过驱动时驱动所做的操作:
表 2 驱动对flat_binder_object的操作

Binder 类型( type 域)

在发送方的操作

在接收方的操作

BINDER_TYPE_BINDER

BINDER_TYPE_WEAK_BINDER

只有实体所在的进程能发送该类型的Binder。如果是第一次发送驱动将创建实体在内核中的节点,并保存binder,cookie,flag域。

如果是第一次接收该Binder则创建实体在内核中的引用;将handle域替换为新建的引用号;将type域替换为BINDER_TYPE_(WEAK_)HANDLE

BINDER_TYPE_HANDLE

BINDER_TYPE_WEAK_HANDLE

获得Binder引用的进程都能发送该类型Binder。驱动根据handle域提供的引用号查找建立在内核的引用。如果找到说明引用号合法,否则拒绝该发送请求。

如果收到的Binder实体位于接收进程中:将ptr域替换为保存在节点中的binder值;cookie替换为保存在节点中的cookie值;type替换为BINDER_TYPE_(WEAK_)BINDER。

如果收到的Binder实体不在接收进程中:如果是第一次接收则创建实体在内核中的引用;将handle域替换为新建的引用号

BINDER_TYPE_FD

验证handle域中提供的打开文件号是否有效,无效则拒绝该发送请求。

在接收方创建新的打开文件号并将其与提供的打开文件描述结构绑定。

3.2、以文件形式传输的 Binder的表述
   除了通常意义上用来通信的Binder,还有一种特殊的Binder:文件Binder。这种Binder的基本思想是:将文件看成Binder实体,进程打开的文件号看成Binder的引用。一个进程可以将它打开文件的文件号传递给另一个进程,从而另一个进程也打开了同一个文件,就象Binder的引用在进程之间传递一样。
   一个进程打开一个文件,就获得与该文件绑定的打开文件号。从Binder的角度,linux在内核创建的打开文件描述结构struct file是Binder的实体,打开文件号是该进程对该实体的引用。既然是Binder那么就可以在进程之间传递,故也可以用flat_binder_object结构将文件Binder通过数据包发送至其它进程,只是结构中type域的值为BINDER_TYPE_FD,表明该Binder是文件Binder。而结构中的handle域则存放文件在发送方进程中的打开文件号。我们知道打开文件号是个局限于某个进程的值,一旦跨进程就没有意义了。这一点和Binder实体用户指针或Binder引用号是一样的,若要跨进程同样需要驱动做转换。驱动在接收Binder的进程空间创建一个新的打开文件号,将它与已有的打开文件描述结构struct file勾连上,从此该Binder实体又多了一个引用。新建的打开文件号覆盖flat_binder_object中原来的文件号交给接收进程。接收进程利用它可以执行read(),write()等文件操作。
   传个文件为啥要这么麻烦,直接将文件名用Binder传过去,接收方用open()打开不就行了吗?其实这还是有区别的。首先对同一个打开文件共享的层次不同:使用文件Binder打开的文件共享linux VFS中的struct file,struct dentry,struct inode结构,这意味着一个进程使用read()/write()/seek()改变了文件指针,另一个进程的文件指针也会改变;而如果两个进程分别使用同一文件名打开文件则有各自的struct file结构,从而各自独立维护文件指针,互不干扰。其次是一些特殊设备文件要求在struct file一级共享才能使用,例如android的另一个驱动ashmem,它和Binder一样也是misc设备,用以实现进程间的共享内存。一个进程打开的ashmem文件只有通过文件Binder发送到另一个进程才能实现内存共享,这大大提高了内存共享的安全性,道理和Binder增强了IPC的安全性是一样的
四、Binder 在驱动中的表述
  驱动是Binder通信的核心,系统中所有的Binder实体以及每个实体在各个进程中的引用都登记在驱动中;驱动需要记录Binder引用->实体之间多对一的关系;为引用找到对应的实体;在某个进程中为实体创建或查找到对应的引用;记录Binder的归属地(位于哪个进程中);通过管理Binder的强/弱引用创建/销毁Binder实体等等。
  对于Binder实体,在本地进程里来看,很简单,用他自己的一个指针就能表示,但是从驱动角度来看,光一个进程内指针是不够的,驱动需要区分所有进程的指针,必须再加上一个参数,由于进程本身是操作系统独一无二的,所以驱动里用进程和进程内指针这两个参数就能唯一代表一个Binder实体。加上一些附带的信息,驱动用struct binder_node结构来指向Binder实体,我们暂称为‘Binder实体节点’或Binder节点对于Binder实体在远程进程中的引用,在驱动里也是用二元组来表示的,第一维依旧是进程,驱动必须知道这个表示是为哪个远程进程维护的,第二维是一个数,一个32位标识uint32_t desc它是一个从0开始编排的数字,它只跟这个binder在这个进程中的出场顺序有关,进程中出现的第一个非本地(远程)的binder被记住0,第二个被记住1,以此类推,跟这个binder实际所在的进程,实际的指针都没关系。另外,一个进程里的所有Binder节点引用uint32_t desc都是统一排序的。加上一些附带信息,对于binder在远程进程中的表示,驱动struct binder_ref结构体来表示,我们暂称它为驱动中的Binder引用Binder节点引用Binder节点引用中有一个指向Binder实体节点指针node。通过该指针Binder节点引用就和Binder实体节联系起来了。因为对于一个Binder实体节,在一个进程中最多只有一个Binder节点引用指向它,所以对于一个进程,确定了指向Binder实体节点指针node就能确定,也能确定一个Binder节点引用
  驱动里的Binder是什么时候创建的呢?前面提到过,为了实现实名Binder的注册,系统必须创建第一只鸡(ServiceManager )创建的,用于注册实名Binder的Binder实体,负责实名Binder注册过程中的进程间通信。既然创建了实体就要有对应的引用:驱动将所有进程中的0号句柄引用都预留给该Binder实体,即所有进程的0号句柄引用天然地都指向注册实名Binder专用的Binder(ServiceManager对应的Binder),无须特殊操作即可以使用0号引用来注册实名Binder。接下来随着应用程序不断地注册实名Binder,不断向 ServiceManager 索要Binder的引用,不断将Binder从一个进程传递给另一个进程,越来越多的Binder以传输结构 - flat_binder_object的形式穿越驱动做跨进程的迁徙。
    驱动不仅仅只是完成binder的转换和传递,必要时还得创建binder_node和binder_ref。由于binder_transaction_datadata.offset数组的存在,所有流经驱动的Binder都逃不过驱动的眼睛。Binder将对这些穿越进程边界的Binder做如下操作:检查传输结构的type域,如果是BINDER_TYPE_BINDERBINDER_TYPE_WEAK_BINDER则创建Binder的实体节点;如果是BINDER_TYPE_HANDLEBINDER_TYPE_WEAK_HANDLE则创建Binder的引用;如果是BINDER_TYPE_FD则为进程打开文件,无须创建任何数据结构。详细过程可参考表2。随着越来越多的Binder实体或引用在进程间传递,驱动会在内核里创建越来越多的节点或引用,当然这个过程对用户来说是透明的。
  binder_node的创建只会发生在两种情况中:一种情况是通过ioctl(BINDER_SET_CONTEXT_MGR),这个是某个进程(注:即service manager进程,比较特殊,这个binder_node不是对象,ptr和cookie都为0)把自己注册为binder的服务程序。另外一种情况在进程间通过transaction传递binder时。当transaction传递一个本地binder时,如果该binder在驱动中并没有记录,驱动便调用binder_new_node为其创建一个binder_node。
  而binder_ref的创建也只会发生在两种情况中:一种情形是跟service manager相关的,进程与service manager通信时,如果还没有service manager的远程binder时,便会创建一个。另外一种情形是在进程间通过transaction传递binder时。不论传递远程还是本地binder,如果该binder在目标进程中没有记录,驱动调binder_get_ref_for_node为目标进程创建一个binder_ref。
4.1 Binder 实体在驱动中的表述
Binder驱动中有个struct binder_node结构,用来指向Binder实体,对该结构我们暂称它为‘Binder实体节点’或“Binder节点”,它隶属于提供实体的进程,
struct binder_node结构如表3所示:
表 3 Binder节点描述结构:binder_node

成员

含义

int debug_id;

用于调试,它是binder_node在驱动里的全局id。/proc/binder/下的信息里就会大量使用这个debug_id,有了它就知道指的是哪个binder_node了,除了binder_node,还有几类信息也有debug_id,这几类的debug_id在驱动里从1开始统一排的。其实也可以用debug_id这个一维的索引来表示binder,不过效率会比红黑树低,所以它只能当做debug信息用

struct binder_work work;

当本节点引用计数发生改变,需要通知所属进程时,通过该成员挂入所属进程的to-do队列里,唤醒所属进程执行Binder实体引用计数的修改。

union {

struct rb_node rb_node;

struct hlist_node dead_node;

};

每个进程都维护一棵红黑树(binder_proc的nodes域),以Binder实体在用户空间的指针,即本结构的ptr成员为索引存放该进程所有的Binder实体。这样驱动可以根据Binder实体在用户空间的指针很快找到其位于内核的节点。rb_node用于将本节点链入该红黑树中。

dead_node用于处理死亡的binder的,当一个进程结束时,他的本地binder自然也就挂了,binder_node也就没必要存在了,但是由于别的进程可能正在使用这个binder,所以一时半会,驱动还没法直接移除这个binder_node,不过由于进程挂了,驱动没法继续把这个binder_node挂靠在对应的binder_proc下,只好把它挂靠在binder_dead_nodes下面,直到通知所有进程切断与该节点的引用后,该节点才可能被销毁。。不过,这个binder_dead_nodes其实也没啥意义,也就是打印/proc/binder/state时用了一下,看看当前有多少已经死亡但没移除的binder_node。驱动里真正移除一个死亡的binder_node是靠引用计数的机制来完成的。

struct binder_proc *proc;

本成员指向节点所属的进程,即提供该节点的进程。

struct hlist_head refs;

本成员是队列头,所有指向本节点的引用都链接在该队列里。这些引用可能隶属于不同的进程。通过该队列可以遍历指向该节点的所有引用。

由于一个本地binder可以被多个进程使用,于是一个binder_node可能有多个binder_ref来对应,所以用refs这个链表用来记录它的引用,而binder_ref的node_entry域就是用于挂载到这个链表的,这个没用红黑树,因为这个表一般比较小,而且用不着搜索。rb_node_desc用于binder_proc的refs_by_desc,rb_node_node用于binder_proc的refs_by_node,这两都是二叉搜索树,前一个key是desc,后一个key是node。如果已知binder的远程表示proc+desc,就可以用refs_by_desc查找,如果知道本地表示则用refs_by_node查找,看看某个远程proc是否已经具备该binder_node的远程表示。strong和weak两个变量是用于引用的,death则是用于死亡通知的,以后再写。

int internal_strong_refs;

用以实现强指针的计数器:产生一个指向本节点的强引用该计数就会加1。

int local_weak_refs;

驱动为传输中的Binder设置的弱引用计数。如果一个Binder打包在数据包中从一个进程发送到另一个进程,驱动会为该Binder增加引用计数,直到接收进程通过BC_FREE_BUFFER通知驱动释放该数据包的数据区为止。

int local_strong_refs;

驱动为传输中的Binder设置的强引用计数。同上。

void __user *ptr;

指向用户空间Binder实体的指针,来自于flat_binder_objectbinder成员

void __user *cookie;

指向用户空间的附加指针,来自于flat_binder_object的cookie成员

unsigned has_strong_ref;

unsigned pending_strong_ref;

unsigned has_weak_ref;

unsigned pending_weak_ref

这一组标志用于控制驱动与Binder实体所在进程交互式修改引用计数

unsigned has_async_transaction;

该成员表明该节点在to-do队列中有异步交互尚未完成。驱动将所有发送往接收端的数据包暂存在接收进程或线程开辟的to-do队列里。对于异步交互,驱动做了适当流控:如果to-do队列里有异步交互尚待处理则该成员置1,这将导致新到的异步交互存放在本结构成员 – asynch_todo队列中,而不直接送到to-do队列里。目的是为同步交互让路,避免长时间阻塞发送端。

unsigned accept_fds

表明节点是否同意接受文件方式的Binder,来自flat_binder_object中flags成员的FLAT_BINDER_FLAG_ACCEPTS_FDS位。由于接收文件Binder会为进程自动打开一个文件,占用有限的文件描述符,节点可以设置该位拒绝这种行为。

int min_priority

设置处理Binder请求的线程的最低优先级。发送线程将数据提交给接收线程处理时,驱动会将发送线程的优先级也赋予接收线程,使得数据即使跨了进程也能以同样优先级得到处理。不过如果发送线程优先级过低,接收线程将以预设的最小值运行。

该域的值来自于flat_binder_object中flags成员。

struct list_head async_todo

异步交互等待队列;用于分流发往本节点的异步交互包

每个进程都有一棵红黑树用于存放创建好的节点,以Binder在用户空间的指针作为索引。每当在传输数据中侦测到一个代表Binder实体的flat_binder_object,先以该结构的binder指针为索引搜索红黑树;如果没找到就创建一个新节点添加到树中。由于对于同一个进程来说内存地址是唯一的,所以不会重复建设造成混乱。
4.2 Binder 引用在驱动中的表述
和实体一样,Binder的引用也是驱动根据传输数据中的flat_binder_object创建的,隶属于获得该引用的进程,用struct binder_ref结构体表示,我们暂称它为驱动中的Binder引用Binder节点引用
表 4 Binder引用描述结构:binder_ref

成员

含义

int debug_id;

调试用

struct rb_node rb_node_desc;

每个进程有一棵红黑树,进程所有引用以引用号(即本结构的desc域)为索引添入该树中。本成员用做链接到该树的一个节点。

即用于把该binder_ref挂载到binder_procrefs_by_desc域

struct rb_node rb_node_node;

每个进程又有一棵红黑树,进程所有引用以节点实体在驱动中的内存地址(即本结构的node域)为索引添入该树中。本成员用做链接到该树的一个节点。

即用于把该binder_ref挂载到binder_procrefs_by_node域

struct hlist_node node_entry;

该域将本引用做为节点链入所指向的Binder实体节点binder_node中的refs队列

struct binder_proc *proc;

本引用所属的进程

struct binder_node *node;

本引用所指向的节点(Binder实体节点)

uint32_t desc;

本结构的引用号

int strong;

强引用计数

int weak;

弱引用计数

struct binder_ref_death *death;

应用程序向驱动发送BC_REQUEST_DEATH_NOTIFICATION或BC_CLEAR_DEATH_NOTIFICATION命令从而当Binder实体销毁时能够收到来自驱动的提醒。该域不为空表明用户订阅了对应实体销毁的‘噩耗’。

  就象一个对象有很多指针一样,同一个Binder实体可能有很多引用,不同的是这些引用可能分布在不同的进程中。和实体一样,每个进程使用红黑树存放所有正在使用的引用。
不同的是Binder的引用可以通过两个键值索引:
· 对应实体在内核中的地址。注意这里指的是驱动创建于内核中的binder_node结构的地址,而不是Binder实体在用户进程中的地址。实体在内核中的地址(即节点地址)是唯一的,用做索引不会产生二义性;但实体本身可能来自不同用户进程,而实体本身在不同用户进程中的地址可能重合,不能用来做索引。驱动利用该红黑树在一个进程中快速查找某个Binder实体所对应的引用(一个实体在一个进程中只建立一个引用)。
· 引用号引用号是驱动为引用分配的一个32位标识,在一个进程内是唯一的,而在不同进程中可能会有同样的值,这和进程的打开文件号很类似。引用号将返回给应用程序,可以看作Binder引用在用户进程中的句柄。除了0号引用在所有进程里都固定保留给 ServiceManager ,其它值由驱动动态分配。向Binder发送数据包时,应用程序将引用号填入binder_transaction_data结构的target.handle域中表明该数据包的目的Binder。驱动根据该引用号在红黑树中找到引用的binder_ref结构,进而通过其node域知道目标Binder实体所在的进程及其它相关信息,实现数据包的路由。
结束!
  评论这张
 
阅读(940)| 评论(1)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017