[译] Contiki——为微传感器网络而生的轻量级的、灵活的操作系统

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

摘要

  无线传感器网络由大量微小的联网可通信设备组成。对于大规模网络而言,动态地下载代码到网络中是极其重要的。在本论文中,我们描述了一个支持动态地加载、替换个人储蓄和服务的轻量级操作系统——Contiki。Contiki基于事件驱动(event-driven)内核,并提供可选的抢占式多线程。在一个资源受限的环境中,Contiki能做到保持基础系统轻量、紧凑的同时,还可以灵活地动态加载和卸载程序和服务。

介绍

  无线传感器网络由大量具有无线通信能力的微传感器设备组成。这些传感器设备可以自治地构成网络并传输数据。通常情况下,这些传感器设备是资源受限的。板载电池或者太阳能面板只能提供有限的电源供应。此外,微小的尺寸和低廉的价格也限制着系统的复杂性。典型的传感器的配置包括8位微控制器、大约100kb的代码存储空间和小雨20kb的RAM。根据摩尔定律,这些设备在将来会越来越小、越来越廉价。尽管这意味着传感器网络可以被扩展到更大的范围,但却不一定说明相关资源会受到更小的限制。

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

  Contiki由C语言实现,已被移植到许多微处理器架构,包括TI MSP430、Ateml AVR。我们目前将Contiki运行在ESB平台[5]。ESB使用的微处理器是MSP430,带有2kbRAM,60kbROM,运行在1MHz。这个微处理器可以可选地重烧部分片上flash存储。

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

运行时下载代码

  我们期望无线传感器网络是大规模的,在每个网络里由成百上千,甚至成千上万的节点。当为这样的大规模传感器网络开发软件时,动态地下载程序代码到网络里就显得尤为重要。此外,在网络运行期间,可能会对其打补丁[9]。一般情况下,单独用人去收集和重新烧写所有的传感器设备是不可行的。事实上,目前已有很多种方法可以将代码分发到无线传感器网络中。对于这些方法,由于通信将占用大部分有效节点能源,所以减少发送到网络中的字节数是关键。

  大多数用于嵌入式的操作系统都需要编译、下载一个包含完整系统的二进制镜像到设备中。这个二进制镜像包括操作系统、系统库和运行在系统之上的实际应用程序。而与此不同的是,Contiki允许在运行期间加载、卸载个人应用程序和服务。大多数情况下,应用程序远远小于整个系统,因此在通过网络传输时Contiki需要更少的能量。

可移植性

  随着不同的传感器设备平台的增加,我们需要一个可以在不用硬件平台移植的通用软件基础架构。当前有效的传感器平台上都载有完全不同的配置。由于应用程序相关的特性,我们期望着部分在将来不会改变。在今天,平台间的统一不变的特性只有一个——不使用段机制或者内存保护机制的CPU架构。在这种架构下,程序代码被存放于可重新编程的ROM,程序数据被存放于RAM。我们设计了Contiki操作系统,其提供CPU多线程抽象(且是唯一的抽象),并支持程序和服务的可加载性。考虑到传感器网络中应用的相关特性,我们认为其他抽象被设计为库或者服务、提供动态机制更合适

事件驱动系统

  在内存严重受限的环境中,多线程模型的相关操作占据了大部分的系统内存资源。因为每一个线程都有自己的栈,并且由于通常很难提前知道一个线程需要多大的栈空间,所以系统给线程预分配栈是总是足够多的。当线程被创建时,需要为每个栈分配内存,栈中的内存只能被对应线程使用,不能被当前运行的多个线程共享。此外,线程并发模型需要锁机制来保护可以修改动态资源的线程。

  为了在不为每个线程分配栈、提供锁机制的同时依然可以提供并发服务,我们使用了事件驱动机制。在事件驱动系统中,进程以事件操作的方式实现。由于一个事件操作不能被阻塞,所有的进程可以使用相同的栈,从而可以在进程间有效地实现稀缺的内存资源的共享。与此同时,由于两个事件操作从不并发地运行,一般情况下锁机制也不需要了。

  尽管事件驱动系统在很多种传感器网络应用中都能很好的工作,但是依然存在问题——程序员很难去管理状态驱动编程模型,换句话说,不是所有的程序都可以很方便地使用状态机来描述,比如密码学中的评估算法时间长度。通常,在标准的CPU平台下,这需要几秒钟的时间。在一个纯粹的事件驱动操作系统中,时间长度计算完全独占CPU,使系统不能够响应外部时间。如果操作系统基于可抢占式的多线程模型,这将不是问题,因为时间长度计算能被抢占。

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

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

相关工作

  TinyOS[15]应该是针对指定应用和受限传感器设备而开发的最早的操作系统。TinyOS也是基于轻量级的事件机制,所有的程序执行都是在一个从运行到结束任务里执行。TinyOS使用了一个指定的类型语言来完成小组件[12]系统。而这些组件被静态地链接到内核中。链接后,就很难修改系统了[17]。相反地,Contiki提供一个动态结构,允许程序和驱动在运行时无需重新链接就被替换。为了给TinyOS提供运行时重编程功能,Levis和Culler为TinyOS设备开发了一个虚拟机——Mate[17]。这个虚拟机为了典型传感器网络应用而单独设计的。很相似地,MagneOS操作系统[7]使用了java虚拟机来将应用分发到传感器网络。使用虚拟机而不适用本地机器代码的好处是虚拟机代码可以写得很小,在传输代码到网络中时可以减小能量消耗。但是其缺点是增加了解释代码的能量消耗,因为对于长期运行的程序而言,在传输二进制代码时节省的能量将在执行代码时被消耗。Contiki程序使用本地代码,因此可以被适用于各种类型的程序,包括不丢失执行效率的底层设备驱动。

  ensorWare[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的硬件在设计时就不支持内存保护。

系统概述

  一个运行的Contiki系统由内核、库、程序加载器和一系列进程组成。进程可能是应用程序或者服务,服务为多个进程提供功能。所有的进程,包括应用程序和服务,可以在运行时动态加载。进程间通信总是通过内核进行的,但是内核不提供邮件抽象层,而让设备驱动、应用直接与硬件通信。

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

![](1.png) 图1. 分区——内核、可加载程序

  Contiki系统被分为两部分:即图1中的核心和被加载程序。分区在编译时指定,且在Contiki的部署中是特定的。典型地,核心由Contiki内核、程序加载器、运行时的通用部分、支持的库、设备驱动中用来与硬件通信的通信栈组成。核心被编译成单一的二进制镜像文件,并被优先部署到设备上。在部署之后,即使使用程序加载器去覆盖核心或者给核心打补丁,核心一般不会被修改。

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

内核架构

  Contiki内核由一个轻量级调度器组成。调度器派遣事件去运行进程,并周期性地调用进程的轮询处理器。所有的程序都通过内核低筒的事件派遣或者通过轮询机制执行。内核一旦被调度后,将不会抢占时间处理区,因此时间处理器必须运行到结束。不过,将在第8章中看到,时间处理器必须使用内部机制来实现可抢占式。

  内核支持两种事件:异步和同步时间。异步事件是一种延迟处理调用的一种形式:异步事件是内核里的一个队列,在一段时间后会被派遣到目标进程。同步事件与异步事件类似,但是会被立即派遣到需要被调度的目标进程中去。当目标进程处理完事件后,会返回结果给邮差进程。这与Spring操作系统中的门抽象类似,可以被看做进程间的程序调用。

  除事件之外,内核提供轮询机制。轮询可以看做在异步事件中调度的高优先级的事件。进程的轮询一般用于与硬件相关的操作,比如检车硬件设备的状态。当一个轮询被调度时,所有的实现了轮询处理器的进程将会按照他们优先级被调度。

  Contiki内核为所有进程执行提供唯一的共享的栈。由于事件处理器的调用时栈是倒回的(rewound),异步事件的使用减少了栈空间。

两级调度层级

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

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

可加载程序

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

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

节电模式

  在传感器网络中,总是希望当网络不活动时能够关闭节点电源以达到减小能量消耗的目的。电源保护机制既依赖于应用[18],也依赖于网络协议[20]。Contiki内核包含了非显式节电抽象,但是需要应用去指定去实现这个机制。为了能够帮助应用决定什么时候关闭系统电源,时间调度器公开了时间队列的大小。当没有事件被调度的时候,可以根据这个信息来关闭处理器。当处理器响应中断被唤醒的时候,通过轮询处理器处理外部事件。

服务

![](2.png) 图2.应用函数调用服务的过程

  在Contiki中,服务是为其他进程提供特定功能的进程,可以看成是共享库的一种形式。服务在运行时可以被动态地替换,因此也必须被动态地链接。典型的服务包括通信协议栈、传感器设备驱动程序、高级别功能(比如传感器数据处理算法)。

  服务层紧紧靠近内核,负责管理服务。具体地说,服务层负责跟踪正在运行的服务,并提供一种方法以找到被安装的服务。一个服务由一个文本字符串所标识,用于描述该服务。服务层使用通用的字符串匹配方法来查询已经安装的服务。

  服务由服务接口和实现该接口的进程组成。服务接口由一个版本号和一个函数表组成。函数表由指向该函数接口的函数指针组成。

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

  程序通过服务接口根调用服务。在调用服务的过程中,程序不必知道他所调用的接口是用于服务的特殊接口。服务第一次被调用时,服务接口根会在服务层查找服务。如果指定的服务存在,查找时将返回一个指向该服务接口的指针,而后存在服务接口中的版本号将与根中的版本号进行检查。除了版本号之外,服务接口中还包含了指向服务函数的指针。如果服务根中的版本号与服务接口中的版本号成功匹配,服务接口根将调用被请求的函数。

服务替换

  与所有的进程一样,服务也可以在正在运行的Contiki系统中被动态地加载和替换。由于服务进程的进程ID用于标识进程,如果服务进程被替换,保留其进程ID是至关重要的。基于这个原因,内核提供了特殊的机制用于替换进程和保留进程ID。

  当一个服务被替换的时候,内核通过给服务进程邮寄一个特殊的事件。作为对该事件的响应,服务必须从系统中移除自己。

  许多服务内部有一个状态,用于传递到新进程。内核提供了一种方法,将一个指针传递到新的服务进程,新服务进程可以根据传入的状态产生一个状态描述符,用于描述进程的状态。保存有状态的内存必须从共享源里分配,因为当老进程被移除的时候会重新分配进程内存。

  服务的状态描述符中包含服务的版本号,因此内核不会尝试去加载具有不兼容版本号的相同服务。

  Contiki内核只提供最基本的CPU复用和事件处理特性,系统其它部分以系统库的形式实现,并被可选择地链接到程序中。程序与库的链接有三种方式。第一种,作为核心的一部分的库,被静态地链接到程序中。第二种,作为可加载程序的一部分的库,被静态地链接到程序中。第三种,程序可以调用服务来实现一个特定的库。以服务形式存在的库,可以在运行时被动态地替换。

  典型地,运行时的库,比如经常被使用的库,最好放到Contiki核心。很少被使用,或者应用相关的库,更适合与可加载程序链接。作为核心一部分的库,总是存在系统中,因此不必被包含到可加载程序库中。

  举个例子,假设程序使用函数memcpy()拷贝内存,使用atoi()将字符串转变为整数。函数memcpy()是经常被使用的C库函数,而atoi()被用得不是那么多。因此,在这个特殊的例子中,memcpy()将被包含到系统核心,而atoi()不会被包含。当程序被链接成二进制时,函数memcpy()的静态地址将会被再次链接到核心。然而,C库中实现auoi()的那部分目标代码必须被包含到程序二进制中。

通信支持

![](3.png) 图3.通信栈

  通信是传感器网络中的基础概念。在Contiki中,为了使运行时替换成为可能,通信是以服务的形式实现的。多个通信栈被同时加载。通过实验研究发现,这可以被用于评估和比较不同的通信协议。而且,正如图3所示,一个通信栈可能会被分成不同的服务,这就是的运行时替换部分通信栈成为可能。

  通信服务使用服务机制去调用其他服务,并使用同步事件去与应用程序通信。由于同步事件处理器被要求一旦运行起来就必须运行到结束,这就使得所欲的通信处理使用一个buffer成为可能。设备驱动程序读取接收到的包到通信buffer中,然后就调用上层的通信服务。通信栈负责处理包头,并邮寄一个同步事件给应用程序,告知已经接收到此包。应用程序对包的内容作出响应,然后可选地在buffer中保持一个响应,最后将控制权返回通信栈。通信栈预先准备需要发送到外部的新的包头,然后将控制权限交给设备驱动程序,这样就完成了一个包的传输。

可抢占式多线程

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

  与常规Contiki进程不同,每个线程都需要一个独立的栈。库中提供了必要的栈管理函数。在线程明确地推出、或者被抢占之前,每个线程在各自的栈上执行。

![](4.png) 图4.可抢占式多线程

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

讨论

  我们已经用Contiki操作系统实现了很多传感器网络应用程序,比如多跳路由、运动检测。

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

  我们已经实现了一个简单的空中编程协议。该协议使用点对点通信,传输一个单一的二进制文件到选定的集线器。该二进制文件存储在EEPROM,并在本节点接收到完整的程序时,被广播到邻居节点。如果发生包丢失,可以由邻居节点发出低电平信号作为示意,然后由集线器节点修复。我们打算在将来实现更好的协议,比如Trickle算法。

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

代码尺寸

![](5.png) 表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)。

可抢占式

![](6.png) 图5.在可抢占计算期间响应时间有很少的增量

  抢占式的目的是使长时间运行更有意义,能够响应到来的事件,比如传感器输入或者到来的通信数据包。图5描述了当Contiki正在运行一个需要8秒才能完成的可抢占式线程是如何响应到来的通信数据包的。曲线由200个往返ping包(每个包40直接)测试而来的。ping包的过程大约从第5秒开始到第13秒结束。在这期间,往返的时间稍微增加了但是依然能够响应ping包。

  我们的测试方法是每隔200ms,从1.4GHz的PC通过57600kb/s的速度向运行Contiki的ESB节点发送包。数据包之所以通过串行线而不是无线连接,是为了避免无线电影响,比如位错位或者MAC冲突。

可移植性

  我们已经将Contiki移植到了很多架构上,包括TI MSP430和Atmel AVR。其他人已经将系统一直到Hitachi SH3 和Zilog Z80。移植过程包括写启动代码、设备驱动、架构相关的程序加载器以及多线程库中的栈切换代码。内核和服务层不需要做任何改动。

  由于内核和服务层不需要任何改动,第一个I/O驱动程序写完后就可以进行实际的端口测试了。Atem AVR的端口移植由我们自己完成,包括有效的设备驱动程序,一共只用了2小时。Zilog Z80的端口移植由第三方完成,也只用了一天。

总结

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

  一个正在运行的Contiki分为两部分:核心和可加载程序。核心由内核、一些类基本服务、语言运行时部分和支持的库组成。加载程序可以在运行时被独立地加载、卸载。共享功能以共享库的形式实现。服务可以被独立更新或者替换,也增加了系统灵活性。

  我们可以看出,在保持基本系统轻量级和紧凑的同时,做到了在资源受限系统上灵活地加载、卸载程序和服务。尽管我们的内核是基于事件驱动的,也为应用程序提供了可抢占式的多线程支持。

  由于Contiki的动态特性,它可以被用于传感器网络的多硬件、多应用甚至多用户。我们将在支持安全机制方面继续我们的工作。