• No results found

Generics versus Singletons

MainC.SoftwareInit -> AppC; }

Listing 4.21: Fan–out on SoftwareInit the nesC compiler will generate something like the following code3:

error_t MainC__SoftwareInit__init() { error_t result;

result = AppA__SoftwareInit__init();

result = ecombine(result, AppB__SoftwareInit__init()); result = ecombine(result, AppC__SoftwareInit__init());

return result;

}

Listing 4.22: Resulting code from fan–out on SoftwareInit

Combine functions should be associative and commutative, to ensure that the result of a fan-out call does not depend on the order in which the commands (or event) are executed (in the case of fan-out, this order is picked by the compiler).

Some return values don’t have combine functions, either due to programmer oversight or the semantics of the data type. Examples of the latter include things like data pointers: if both calls return a pointer, say, to a packet, there isn’t a clear way to combine them into a single pointer. If your program has fan-out on a call whose return value can’t be combined, the nesC compiler will issue a warning along the lines of

“calls to Receive.receive in CC2420ActiveMessageP are uncombined” or

“calls to Receive.receive in CC2420ActiveMessageP fan out, but there is no combine function specified for the return value.”

Programming Hint 14: NEVER IGNORE COMBINE WARNINGS.

4.5

Generics versus Singletons

Configurations can be generics, just as modules can. For example, the standard TinyOS abstraction for sending packets is the component AMSenderC, a generic that takes a single parameter, the AM (active message) type of packet to be sent:

generic configuration AMSenderC(am_id_t AMId) {

provides {

interface AMSend;

interface Packet;

interface AMPacket;

interface PacketAcknowledgements as Acks;

} }

Listing 4.23: AMSenderC signature

3The nesC compiler actually compiles to C, which it then passes to a native C compiler. Generally, it uses as the delimiter

between component, interface, and function names. By not allowing in the middle of names, the nesC compiler enforces component encapsulation (there’s no way to call a function with a from within nesC and break the component boundaries).

59 4.5. Generics versus Singletons

AM types allow TinyOS applications to send multiple types of packets: they are somewhat like ports in UDP/TCP sockets. AM types are 8 bits. There is also a generic receiving component, AMReceiverC, which takes an AM type as a parameter. AM types allow multiple components and subsystems to receive and send packets without having to figure out whose packets are whose, unless two try to use the same AM type.

This raises the question: why aren’t all components generics? There are some low-level components that inherently can’t be generics, such as those that provide direct access to hardware. Because these components actually represent physical resources (registers, buses, pins, etc.), there can only be one. There are very few of these low-level components though. More commonly, singletons are used to provide a global name which many components can use. To provide better insight on why this is important, let’s step through what generics are and how they work.

4.5.1 Generic components, revisited

Generic components allow many systems to use independent copies of a single implementation. For example, a large application might include multiple network services, each of which uses one or more instances of AMSenderC to send packets. Because all share a common implementation, bugs can be fixed and optimizations can be applied in a single place, and over time they will all benefit from improvements.

While a generic component has a global name, such as AMSenderC, each instance has a local name. Two separate references to new AMSenderC are two separate components. Take, for example, the application configuration RadioCountToLedsAppC:

configuration RadioCountToLedsAppC {}

implementation {

components MainC, RadioCountToLedsC as App, LedsC;

components new AMSenderC(AM_RADIO_COUNT_MSG);

components new AMReceiverC(AM_RADIO_COUNT_MSG);

components new TimerMilliC();

components ActiveMessageC; App.Boot -> MainC.Boot; App.Receive -> AMReceiverC; App.AMSend -> AMSenderC; App.AMControl -> ActiveMessageC; App.Leds -> LedsC; App.MilliTimer -> TimerMilliC; App.Packet -> AMSenderC; } Listing 4.24: RadioCountToLedsAppC

Because RadioCountToLedsAppC has to instantiate a new AMSenderC, that instance is private to RadioCountToLedsAppC. No other component can name that AMSenderC’s interfaces or access its functionality. For example, RadioCountToLedsAppC does not wire AMSenderC’s PacketAcknowledgements interface, either through direct wiring via -> or through an export via =. Since this AMSenderC is private to RadioCountToLedsAppC, that means no component can wire to its PacketAcknowledgements, because they cannot name it.

While generics enable components to reuse implementations, sharing a generic among multiple components requires a little bit of work. For example, let’s say you want to share a pool of packet buffers between two components. TinyOS has a component, PoolC, which encapsulates a fixed-size pool of objects which components can dynamically allocate and free. PoolC is a generic configuration that takes two parameters,

4.6. Exercises 60

the type of memory object in the pool and how many there are:

generic configuration PoolC(typedef pool_t, uint8_t POOL_SIZE) {

provides interface Pool<pool_t>; }

Listing 4.25: PoolC

How do we share this pool between two different components? One way would be to write a configuration that wires both of them to a new instance:

components A, B, new PoolC(message_t, 8);

A.Pool -> PoolC; B.Pool -> PoolC;

But what if we don’t even know which two want to share it? For example, it might be that we just want to have a shared packet pool, which any number of components can use. Making a generic’s interfaces accessible across a program requires giving it a global name.

4.5.2 Singleton components, revisited

Unlike generics, singleton components introduce a global name for a component instance that any other component can reference. So, to follow the previous example, one easy way to have a pool that any component can use would be to write it as a singleton component, say PacketPoolC. But we’d like to be able to do this without copying all of the pool code. It turns out that doing so is very easy: you just give a generic instance a global name by wrapping it up in a singleton. For example, here’s the implementation of PacketPoolC, assuming you want an 8-packet pool:

configuration PacketPoolC {

provides interface Pool<message_t> }

implementation {

components new PoolC(message_t, 8);

Pool = PoolC; }

Listing 4.26: Exposing a generic component instance as a singleton

All PacketPoolC does is instantiate an instance of PoolC that has 8 message t structures, then exports the Pool interface as its own. Now, any component that wants to access this shared packet pool can just wire to PacketPoolC.

While you can make singleton instances of generic components in this way, you can’t make generic versions of singleton components. Singletons inherently have only a single copy of their code. Every component that wires to PacketPoolC wires to the same PacketPoolC: there is no way to create multiple copies of it. If you needed two packet pools, you could just make another singleton with a different name.

4.6

Exercises

1. Take the TinyOS demo application RadioCountToLeds and trace through its components to figure out which components are auto-wired to SoftwareInit. There might be more than you expect: the Telos platform, for example, has 10. Hint: you can use the nesdoc tool to simplify your task.

61 4.6. Exercises

2. If you want multiple components to handle a received packet, you must either not allow buffer-swapping or must make a copy for all but one of the handlers. Write a component library that lets an application create a component supporting a non-swapping reception interface. Hint: you’ll need to write a singleton wrapper.

Execution model

This chapter presents TinyOS’s execution model, which is based on split-phase operations, run-to-completion tasks and interrupt handlers. Chapter 3 introduced components and modules, Chapter 4 introduced how to connect components together through wiring. This chapter goes into how these components execute, and how you can manage the concurrency between them in order to keep a system responsive. This chapter focuses on tasks, the basic concurrency mechanism in nesC and TinyOS. We defer discussion of concurrency issues relating to interrupt handlers and resource sharing to Chapter 11, as these typically only arise in very high-performance applications and low-level drivers.

5.1

Overview

As we saw in Chapter 3.4, all TinyOS I/O (and long-running) operations are split-phase, avoiding the need for threads and allowing TinyOS programs to execute on a single stack. In place of threads, all code in a TinyOS program is executed either by a task or an interrupt handler. A task is in effect a lightweight deferred procedure call: a task can be posted at anytime and posted tasks are executed later, one-at-a-time, by the TinyOS scheduler. Interrupts, in contrast can occur at any time, interrupting tasks or other interrupt handlers (except when interrupts are disabled).

While a task or interrupt handler is declared within a particular module, its execution may cross component boundaries when it calls a command or signals an event (Figure 5.1). As a result, it isn’t always immediately clear whether a piece of code is only executed by tasks or if it can also be executed from an interrupt handler. Because code that can be excuted by an interrupt handler has to be much more aware of concurrency issues (as it may be called at any time), nesC distinguishes between synchronous (sync) code that may only be executed by tasks and asynchronous (async) code that may be executed by both tasks and interrupt handlers, and requires that asynchronous commands or events be declared with the async keyword (both in interfaces and in the actual module implementation).

Writing components that implement async functions requires a few advanced nesC features, and tasks and synchronous code are sufficient for many applications so we defer further discussion of asynchronous code and interrupt handlers to Chapter 11.