博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[Contiki系列论文之1]Contiki——为微传感器网络而生的轻量级的、灵活的操作系统...
阅读量:5890 次
发布时间:2019-06-19

本文共 9903 字,大约阅读时间需要 33 分钟。

说明:本系列文章翻译自Contiki之父Adam Dunkels经典论文,版权归原作者全部。

Contiki是由Adam Dunkels及其团队开发的系统,研读其论文是对深入理解Contiki系统的最佳资料。

-----------------------------------------------------------------------------------------------------------------------------------------------------------
摘要:

     无线传感器网络由大量微小的联网可通信设备组成。对于大规模网络而言,动态地下载代码到网络中是极其重要的。在本论文中。我们描写叙述了一个支持动态地载入、替换个人储蓄和服务的轻量级操作系统——Contiki。

Contiki基于事件驱动(event-driven)内核,并提供可选的抢占式多线程。在一个资源受限的环境中,Contiki能做到保持基础系统轻量、紧凑的同一时候,还能够灵活地动态载入和卸载程序和服务。

1.介绍

      无线传感器网络由大量具有无线通信能力的微传感器设备组成。这些传感器设备能够自治地构成网络并数据传输。通常情况下。这些传感器设备是资源受限的。板载电池或者太阳能面板仅仅能提供有限的电源供应。此外。微小的尺寸和低廉的价格也限制着系统的复杂性。

典型的传感器的配置包含8位微控制器、大约100kb的代码存储空间和小雨20kb的RAM。依据摩尔定律,这些设备在将来会越来越小、越来越便宜。虽然这意味着传感器网络能够被扩展到更大的范围,但却不一定说明相关资源会受到更小的限制。

      作为一个为传感器节点设计操作系统的设计者连说。其面临的挑战在于找到一个轻量级的机制和抽象,以在资源受限的设备上提供足够丰富的运行环境。我们已经开发了Contiki——针对资源受限环境而开发的操作系统。Contiki提供了个人程序和服务动态载入、卸载的功能。其内核基于时间驱动(event-driven),并支持抢占式多线程机制。哪些明白须要多线程的程序将被链接为一个库。从而实现可抢占式多线程。
      Contiki由C语言实现。已被移植到很多微处理器架构,包含TI MSP430、Ateml AVR。

我们眼下将Contiki执行在ESB平台[5]。ESB使用的微处理器是MSP430,带有2kbRAM,60kbROM。执行在1MHz。这个微处理器能够可选地重烧部分片上flash存储。

      本篇论文的贡献主要在两个方面。第一是灵活性。一个资源受限传感器设备之上可载入程序和服务。第二个通用性。抢占式多线程不必再内核最底层实现,而是构建于事件驱动内核顶层作为应用程序库。这同意基于线程的程序执行在基于事件的内核之上,降低系统各部分重入带来的开支、栈空间。

1.1执行时下载代码

      我们期望无线传感器网络是大规模的,在每一个网络里由成百上千,甚至成千上万的节点。当为这种大规模传感器网络开发软件时,动态地下载程序代码到网络里就显得尤为重要。此外,在网络执行期间,可能会对其打补丁[9]。

普通情况下,单独用人去收集和又一次烧写全部的传感器设备是不可行的。其实。眼下已有非常多种方法能够将代码分发到无线传感器网络中。对于这些方法。因为通信将占用大部分有效节点能源。所以降低发送到网络中的字节数是关键。

      大多数用于嵌入式的操作系统都须要编译、下载一个包括完整系统的二进制镜像到设备中。

这个二进制镜像包括操作系统、系统库和执行在系统之上的实际应用程序。

而与此不同的是,Contiki同意在执行期间载入、卸载个人应用程序和服务。

大多数情况下,应用程序远远小于整个系统,因此在通过网络传输时Contiki须要更少的能量。

1.2可移植性

      随着不同的传感器设备平台的添加。我们须要一个能够在不用硬件平台移植的通用软件基础架构。

当前有效的传感器平台上都载有全然不同的配置。

因为应用程序相关的特性,我们期望着部分在将来不会改变。

在今天,平台间的统一不变的特性仅仅有一个——不使用段机制或者内存保护机制的CPU架构。

在这样的架构下。程序代码被存放于可又一次编程的ROM,程序数据被存放于RAM。我们设计了Contiki操作系统,其提供CPU多线程抽象(且是唯一的抽象)。并支持程序和服务的可载入性。考虑到传感器网络中应用的相关特性,我们觉得其它抽象被设计为库或者服务、提供动态机制更合适

1.3事件驱动系统

      在内存严重受限的环境中,多线程模型的相关操作占领了大部分的系统内存资源。

由于每个线程都有自己的栈,而且由于通常非常难提前知道一个线程须要多大的栈空间。所以系统给线程预分配栈是总是足够多的。当线程被创建时。须要为每个栈分配内存,栈中的内存仅仅能被相应线程使用。不能被当前执行的多个线程共享。此外,线程并发模型须要锁机制来保护能够修修改态资源的线程。

    

      为了在不为每一个线程分配栈、提供锁机制的同一时候依旧能够提供并发服务。我们使用了事件驱动机制。

在事件驱动系统中,进程以事件操作的方式实现。因为一个事件操作不能被堵塞。全部的进程能够使用同样的栈,从而能够在进程间有效地实现稀缺的内存资源的共享。与此同一时候。因为两个事件操作从不并发地执行,普通情况下锁机制也不须要了。

      虽然事件驱动系统在非常多种传感器网络应用中都能非常好的工作,可是依旧存在问题——程序猿非常难去管理状态驱动编程模型,换句话说。不是全部的程序都可以非常方便地使用状态机来描写叙述,比方password学中的评估算法时间长度。通常,在标准的CPU平台下,这须要几秒钟的时间。

在一个纯粹的事件驱动操作系统中,时间长度计算全然独占CPU。使系统不可以响应外部时间。假设操作系统基于可抢占式的多线程模型,这将不是问题,由于时间长度计算能被抢占。

      为了结合时间驱动和可抢占式线程,Contiki使用了一个混合模型:系统基于时间驱动内核。而将可抢占式多线程以可选应用库的形式实现,在程序须要它时才被链接。

      本论文剩下的部分以例如以下的形式组织:第2章回想相关的工作。第3章描写叙述Contiki系统的框架。第4章描写叙述Contiki内核的设计,第5章描写叙述Contiki服务的概念,第6、7章描写叙述Contiki怎样处理库、怎样处理通信支持,第8章描写叙述可抢占式多线程的实现,第9章描写叙述我们使用该系统的经验。最后,第10章作总结。

2.相关工作

      TinyOS[15]应该是针对指定应用和受限传感器设备而开发的最早的操作系统。TinyOS也是基于轻量级的事件机制。全部的程序执行都是在一个从执行到结束任务里执行。TinyOS使用了一个指定的类型语言来完毕小组件[12]系统。而这些组件被静态地链接到内核中。链接后。就非常难改动系统了[17]。相反地,Contiki提供一个动态结构,同意程序和驱动在执行时无需又一次链接就被替换。

为了给TinyOS提供执行时重编程功能,Levis和Culler为TinyOS设备开发了一个虚拟机——Mate[17]。

这个虚拟机为了典型传感器网络应用而单独设计的。非常相似地,MagneOS操作系统[7]使用了java虚拟机来将应用分发到传感器网络。使用虚拟机而不适用本地机器代码的优点是虚拟机代码能够写得非常小,在传输代码到网络中时能够减小能量消耗。

可是其缺点是添加了解释代码的能量消耗,由于对于长期执行的程序而言,在传输二进制代码时节省的能量将在执行代码时被消耗。

Contiki程序使用本地代码,因此能够被适用于各种类型的程序,包含不丢失执行效率的底层设备驱动。

      SensorWare[8]提供了一个针对可编程传感器的抽象脚本语言,可是他们的目标平台不是像我们这样的资源受限的平台。相似的。EmStar[3]环境被设计用于资源更少的受限系统。Reijers和Langendoen[21]使用一个补丁语言去小改处于执行的系统的二进制镜像。这在全部节点执行同样二进制代码的网络中是有效的,可是一旦有一点点不同的程序或者程序的版本号不同一时候就变得特别复杂。
      Mantio操作系统[3]使用传统的可抢占式多线程模型操作。

通过下载程序镜像到EEPROM,再从EEPROM烧写到flash ROM, Mantio既支持又一次编程完整的操作系统,也支持又一次烧写部分存储空间。因为多线程的理论,每一个Mantis程序必须从系统堆分配栈空间,必须使用锁机制以实现共享变量相互排斥。

相反地。Contiki因为使用基于事件的、没有抢占的调度器,依次避免了多栈分配和锁机制。可抢占式多线程以库的方式提供,当须要的时候才被链接到程序。

      Contiki中的可抢占式多线程与fibers[4]类似。

轻量级的fibers有Welsh和Mainland[23]实现。余轻量级的fibers不同的是,Contiki不会限制当前线程数量的最大值——2。此外。不像fibers那样。Contiki中的线程支持可抢占式。

      与Exokernel[11]和Nemesis[16]类似,Contiki尽量降低抽象的数量,以保持内核尺寸的最小化。抽象以库的形式实现,而且差点儿实用下层硬件的全然訪问权限。

Exokernel追求性能、Nemesis追求服务质量,Contiki追求减小尺寸、维持灵活。

与Exokernel不同的是,Contiki不支持保护机制,由于Contiki的硬件在设计时就不支持内存保护。

3.系统概述

      一个执行的Contiki系统由内核、库、程序载入器和一系列进程组成。进程可能是应用程序或者服务,服务为多个进程提供功能。

全部的进程。包含应用程序和服务。能够在执行时动态载入。进程间通信总是通过内核进行的,可是内核不提供邮件抽象层,而让设备驱动、应用直接与硬件通信。

      一个进程(process)有一个事件处理函数(event hander)或者可选的轮询处理函数(poll hander function)定义。进程的状态保存在进程的私有内存中,内核中仅仅有一个指向该状态的指针。在ESB平台[5],进程状态由23字节组成。全部的进程共享同样的地址空间。而不是执行在不同的保护域。进程间通过邮差事件(posting events)通信。
      

    
图1. 分区——内核、可载入程序
      Contiki系统被分为两部分:即图1中的核心和被载入程序。分区在编译时指定,且在Contiki的部署中是特定的。典型地。核心由Contiki内核、程序载入器、执行时的通用部分、支持的库、设备驱动中用来与硬件通信的通信栈组成。

核心被编译成单一的二进制镜像文件,并被优先部署到设备上。在部署之后。即使使用程序载入器去覆盖核心或者给核心打补丁,核心一般不会被改动。

      程序通过程序载入器载入到系统中。

程序载入器通过使用通信栈或者直接使用像EEPROM之类的附加存储设备来获得程序二级制文件。典型地,在程序被烧写内存之前是存放在EEPROM里,然后被载入系统。

4.内核架构

      Contiki内核由一个轻量级调度器组成。调度器派遣事件去执行进程,并周期性地调用进程的轮询处理器。

全部的程序都通过内核低筒的事件派遣或者通过轮询机制执行。内核一旦被调度后,将不会抢占时间处理区。因此时间处理器必须执行到结束。只是,将在第8章中看到。时间处理器必须使用内部机制来实现可抢占式。

      内核支持两种事件:异步和同步时间。异步事件是一种延迟处理调用的一种形式:异步事件是内核里的一个队列。在一段时间后会被派遣到目标进程。同步事件与异步事件类似。可是会被马上派遣到须要被调度的目标进程中去。

当目标进程处理完事件后,会返回结果给邮差进程。这与Spring操作系统中的门抽象类似,能够被看做进程间的程序调用。

      除事件之外。内核提供轮询机制。轮询能够看做在异步事件中调度的高优先级的事件。进程的轮询一般用于与硬件相关的操作,比方检车硬件设备的状态。

当一个轮询被调度时,全部的实现了轮询处理器的进程将会依照他们优先级被调度。

      Contiki内核为全部进程运行提供唯一的共享的栈。因为事件处理器的调用时栈是倒回的(rewound)。异步事件的使用降低了栈空间。

4.1两级调度层级

      Contiki中的全部事件调度都在一个级别上完毕,因此事件不能抢占其它事件运行。事件仅仅能被中断抢占。正常情况下。中断事件由硬件中断实施,可是也可能由根本的实时运行者(an underlying real-time executive)实施。Linux内核里已经使用后来的技术来提供实时保证。

      为了可以支持一个根本的实时运行体,Contiki从不关闭中断。由于这个原因。Contiki不同意中断处理器派遣事件,由于那会导致在事件处理器中产生竞争条件。相反地。内核提供了一个轮询标记。用以请求轮询事件。这个标记给中断处理器提供了一个请求马上轮询的方法。

4.2可载入程序

      通过执行时重定位功能,以及二进制格式中包含的重定位信息,能够实现程序的可载入特性。当载入器载入程序到系统中时,它会先基于二级制提供的信息来尽可能分配足够的内存空间。假设内存分配失败,就会终止程序载入。

      将程序被载入到内存后,载入器会调用程序的初始化函数。程序的初始化函数可能会開始或者替换一个或多个进程。

4.3节电模式

      在传感器网络中,总是希望当网络不活动时可以关闭节点电源以达到减小能量消耗的目的。电源保护机制既依赖于应用[18],也依赖于网络协议[20]。

Contiki内核包括了非显式节电抽象。可是须要应用去指定去实现这个机制。为了可以帮助应用决定什么时候关闭系统电源,时间调度器公开了时间队列的大小。当没有事件被调度的时候,可以依据这个信息来关闭处理器。当处理器响应中断被唤醒的时候,通过轮询处理器处理外部事件。

5.服务

图2.应用函数调用服务的过程

      在Contiki中。服务是为其它进程提供特定功能的进程,能够看成是共享库的一种形式。服务在执行时能够被动态地替换,因此也必须被动态地链接。

典型的服务包含通信协议栈、传感器设备驱动程序、高级别功能(比方传感器数据处理算法)。

      服务层紧紧靠近内核,负责管理服务。详细地说,服务层负责跟踪正在执行的服务。并提供一种方法以找到被安装的服务。一个服务由一个文本字符串所标识,用于描写叙述该服务。服务层使用通用的字符串匹配方法来查询已经安装的服务。
      服务由服务接口和实现该接口的进程组成。服务接口由一个版本和一个函数表组成。

函数表由指向该函数接口的函数指针组成。

      应用程序通过根库(stub library)与服务通信。根库被链接到应用程序,然后使用服务层找到服务多相应的进程。一旦一个服务被定位,根将服务进程的进程ID缓存起来,用于将来全部的服务请求。

      程序通过服务接口根调用服务。在调用服务的过程中。程序不必知道他所调用的接口是用于服务的特殊接口。服务第一次被调用时,服务接口根会在服务层查找服务。

假设指定的服务存在。查找时将返回一个指向该服务接口的指针。而后存在服务接口中的版本将与根中的版本进行检查。除了版本之外,服务接口中还包括了指向服务函数的指针。假设服务根中的版本与服务接口中的版本成功匹配。服务接口根将调用被请求的函数。

5.1服务替换

      与全部的进程一样,服务也能够在正在执行的Contiki系统中被动态地载入和替换。

因为服务进程的进程ID用于标识进程,假设服务进程被替换,保留其进程ID是至关重要的。基于这个原因。内核提供了特殊的机制用于替换进程和保留进程ID。

      当一个服务被替换的时候。内核通过给服务进程邮寄一个特殊的事件。作为对该事件的响应,服务必须从系统中移除自己。
      很多服务内部有一个状态,用于传递到新进程。内核提供了一种方法。将一个指针传递到新的服务进程,新服务进程能够依据传入的状态产生一个状态描写叙述符,用于描写叙述进程的状态。

保存有状态的内存必须从共享源里分配,由于当老进程被移除的时候会又一次分配进程内存。

      服务的状态描写叙述符中包括服务的版本。因此内核不会尝试去载入具有不兼容版本的同样服务。

6.库

      Contiki内核仅仅提供最主要的CPU复用和事件处理特性,系统其他部分以系统库的形式实现。并被可选择地链接到程序中。

程序与库的链接有三种方式。第一种。作为核心的一部分的库。被静态地链接到程序中。另外一种,作为可载入程序的一部分的库,被静态地链接到程序中。

第三种,程序能够调用服务来实现一个特定的库。以服务形式存在的库,能够在执行时被动态地替换。

      典型地,执行时的库,比方常常被使用的库。最好放到Contiki核心。非常少被使用,或者应用相关的库,更适合与可载入程序链接。作为核心一部分的库。总是存在系统中,因此不必被包括到可载入程序库中。
      举个样例。如果程序使用函数memcpy()拷贝内存。使用atoi()将字符串转变为整数。函数memcpy()是常常被使用的C库函数,而atoi()被用得不是那么多。

因此,在这个特殊的样例中,memcpy()将被包括到系统核心,而atoi()不会被包括。当程序被链接成二进制时,函数memcpy()的静态地址将会被再次链接到核心。然而,C库中实现auoi()的那部分目标代码必须被包括到程序二进制中。

7.通信支持

图3.通信栈

      通信是传感器网络中的基础概念。在Contiki中。为了使执行时替换成为可能,通信是以服务的形式实现的。

多个通信栈被同一时候载入。

通过实验研究发现。这能够被用于评估和比較不同的通信协议。并且。正如图3所看到的。一个通信栈可能会被分成不同的服务,这就是的执行时替换部分通信栈成为可能。

      通信服务使用服务机制去调用其它服务,并使用同步事件去与应用程序通信。

因为同步事件处理器被要求一旦执行起来就必须执行到结束,这就使得所欲的通信处理使用一个buffer成为可能。

设备驱动程序读取接收到的包到通信buffer中。然后就调用上层的通信服务。通信栈负责处理包头。并邮寄一个同步事件给应用程序,告知已经接收到此包。应用程序对包的内容作出响应。然后可选地在buffer中保持一个响应。最后将控制权返回通信栈。

通信栈预先准备须要发送到外部的新的包头。然后将控制权限交给设备驱动程序,这样就完毕了一个包的传输。

8.可抢占式多线程

      在Contiki中,可抢占式多线程是在基于事件的内核之上以库的形式实现的。库是以可选的形式被链接到应用程序中的,即应用程序明白指出须要进行多线程模型操作的时候。才将库链接到应用程序中。库被分为两类:与平台独立的部分——用于与事件内核相互作用。以及与平台相关的部分——用于实现栈的切换和抢占机制。通常,抢占通过一个定时器中断实现,保持处理器的寄存器到栈中,然后切换回内核栈。在实际中,当移植平台相关部分库时,仅仅须要重写非常少的代码。举个样例,对于MSP430平台,仅仅用了25行C代码。

      与常规Contiki进程不同。每一个线程都须要一个独立的栈。

库中提供了必要的栈管理函数。在线程明白地推出、或者被抢占之前。每一个线程在各自的栈上运行。

图4.多线程库的API

      图4展示了多线程库中的API。它由六个函数(mt_yield(), mt_post(), mt_wait(), mt_exit(),mt_start() and mt_exec())组成。mt_exec()函数被事件处理器调用,用于运行实际的线程调度。

9.讨论

      我们已经用Contiki操作系统实现了非常多传感器网络应用程序。比方多跳路由、运动检測。

9.1空中编程(over-the-air programming)

      我们已经实现了一个简单的空中编程协议。该协议使用点对点通信。传输一个单一的二进制文件到选定的集线器。该二进制文件存储在EEPROM,并在本节点接收到完整的程序时。被广播到邻居节点。

假设发生包丢失,能够由邻居节点发出低电平信号作为示意。然后由集线器节点修复。

我们打算在将来实现更好的协议。比方Trickle算法。

      我们做了一个实验,动态地分布40个节点到报警系统中。我们使用了空中重编程方法和人工有线重编程方法。最初。程序载入机制不是很完好,在我们的实验中不能使用程序载入。我们的目标程序大约为6KB。加上Contiki核心和C库,完整的系统镜像接近30KB。使用人工有线重编程的方法,为一个独立的传感器节点编程花费了超过30秒。而一共40个点检,对整个网络冲编程至少须要30分钟。相反地。通过空中重编程的方法。仅仅使用了2分钟。这减小了一个数量级。

9.2代码尺寸

表1.编译后的代码尺寸(字节)

      对于一个为受限设备开发的操作系统。代码的尺寸和RAM的利用率必须足够小,这样才干给执行在系统之上的应用留下足够的空间。

表1展示了Contiki在两个架构上的代码尺寸和RAM使用率:TI MSP430和Atmel AVR。表中的数字翻译了核心组件和应用案例——传感器数据复制服务 的尺寸。

这个数据复制服务由服务接口根和和服务自身实现两部分组成。眼下。程序载入器仅仅在MSP430平台上实现了。

      Contiki的代码尺寸比TinyOS大,比Mantis系统小。因为提供了不同的服务。contiki的事件内核比TinyOS大是理所当然的。

TinyOS的事件内核仅提供了一个FIFO事件队列调度器,而Contiki内核既支持FIFO事件,也支持优先级的轮训处理器。并且,对于TinyOS这类系统而言,Contiki的灵活性也须要很多其它的执行时代码。

      RAM的需求依赖于系统配置的最大进程个数(p)。异步事件队列的最大尺寸(e)以及多线程操作中的线程栈大小(s)。

9.3可抢占式

图5.在可抢占计算期间响应时间有非常少的增量

      抢占式的目的是使长时间执行更有意义。可以响应到来的事件,比方传感器输入或者到来的通信数据包。

图5描写叙述了当Contiki正在执行一个须要8秒才干完毕的可抢占式线程是怎样响应到来的通信数据包的。曲线由200个往返ping包(每一个包40直接)測试而来的。ping包的过程大约从第5秒開始到第13秒结束。

在这期间。往返的时间略微添加了可是依旧可以响应ping包。

      我们的測试方法是每隔200ms,从1.4GHz的PC通过57600kb/s的速度向执行Contiki的ESB节点发送包。

数据包之所以通过串行线而不是无线连接,是为了避免无线电影响,比方位错位或者MAC冲突。

9.4可移植性

      我们已经将Contiki移植到了非常多架构上。包含TI MSP430和Atmel AVR。其它人已经将系统一直到Hitachi SH3 和Zilog Z80。

移植过程包含写启动代码、设备驱动、架构相关的程序载入器以及多线程库中的栈切换代码。内核和服务层不须要做不论什么修改。

      因为内核和服务层不须要不论什么修改,第一个I/O驱动程序写完后就能够进行实际的port測试了。Atem AVR的port移植由我们自己完毕。包含有效的设备驱动程序,一共仅仅用了2小时。Zilog Z80的port移植由第三方完毕,也仅仅用了一天。

10.总结

      我们已经描写叙述了为内存受限系统而设计的Contiki操作系统。为了减小系统的大小。Contiki基于事件驱动内核。因为对于长时间执行的程序来说,非常难使用事件驱动系统中的状态机编程。Contiki提供了执行时在事件驱动内核之上的抢占式多线程应用库,这个库是可选地被链接到应用中,即当应用明白说明须要多线程模型时才干链接。

      一个正在执行的Contiki分为两部分:核心和可载入程序。核心由内核、一些类基本服务、语言执行时部分和支持的库组成。载入程序能够在执行时被独立地载入、卸载。

共享功能以共享库的形式实现。服务能够被独立更新或者替换,也添加了系统灵活性。

      我们能够看出,在保持基本系统轻量级和紧凑的同一时候。做到了在资源受限系统上灵活地载入、卸载程序和服务。虽然我们的内核是基于事件驱动的。也为应用程序提供了可抢占式的多线程支持。
      因为Contiki的动态特性,它能够被用于传感器网络的多硬件、多应用甚至多用户。我们将在支持安全机制方面继续我们的工作。

     

你可能感兴趣的文章
nc 局域网聊天+文件传输(netcat)
查看>>
C++它 typedef void *HANDLE
查看>>
Git常用命令
查看>>
Linux下查看MySQL的安装路径
查看>>
C#获取磁盘列表与信息
查看>>
mysql学习笔记4---mysql 复制---源代码
查看>>
Linux设备驱动之semaphore机制【转】
查看>>
每天一个linux命令(25):linux文件属性详解
查看>>
【android】getDimension()、getDimensionPixelOffset()和getDimensionPixelSize()区别详解
查看>>
HDU 3280 Equal Sum Partitions(二分查找)
查看>>
第一个之出现一次的字符
查看>>
go微服务框架go-micro深度学习(三) Registry服务的注册和发现
查看>>
expectFAQ(附一个python批量任务脚本)--持续更新
查看>>
HDU 2492 Ping pong
查看>>
JPA的Embeddable注解
查看>>
Maven在Eclipse中的实用小技巧
查看>>
步步为营Hibernate全攻略(一)构建Hibernate框架环境
查看>>
【开放源代码】【谐波数据生成器】【上位机软件】(版本:0.00)
查看>>
Hibernate基础-HelloWord
查看>>
Android Studio系列教程四--Gradle基础
查看>>