Tinkertoy: Build your own operating systems for IoT devices
Wang, Seltzer (2022)
What kind of paper is this?
- It's a 'build' a thing, but
- The thing is really a different way of building operating systems
The Story
- Internet of things applications are evolving: First generation systems
were simple sensor-backed data collection; Second generation (current)
systems construct an elaborate interconnected network of end devices backed
by cloud services. Third generation applications are going to become even
more complicated.
- But IoT devices are resource constrained and do not need the full
functionality of a general purpose OS. At the same time, different applications
require different functionality from the OS.
- Tinkertoy provides a collection of small, highly configurable OS
components that can combined and configured (in a few tens of lines of
code) to produce a custom IoT Operating System.
- An IoT world enabled by Tinkertoy consumes less memory and power,
enables a broad class of applications, and opens the way to tomorrow's more
sophisticated IoT applications.
Goal
- Provide developers a set of flexible, interacting components
from which they can assemble tiny, custom operating systems for IoT
applications.
- Provide this flexibility without a performance penalty.
- Be more resource efficient than competitors.
HW Target
- Single-core (ARM)
- No MMU
- Limited memory
- Sensors and/or acutators
- Communication capability
Architectural Overview
- OS is composed of modules.
- Modules are composed of components.
- You can replace components with a custom built one.
- Components are implemented by building blocks.
- Building blocks can be swapped for blocks of the same type (e.g.,
replacing a FIFO queue with a Priority queue). A developer could
also provide a custom building block.
- Constraints ensure (at compile time) that the set of components
and building blocks being assembled are compatible.
Design Principles
- Constrained Flexibility
- Templates make kernel modules generic. E.g., a scheduler schedules
objects of an abstract type T.
- C++ Concepts implement constraints. E.g., if i want a priority
scheduler, then I use a concept that says that the object scheduled
by the scheduler has a priority and that that priority can be compared.
- Code Reusability: Use functors (instead of virtual methods) so that
building blocks can be customized without paying the overhead of
indirect function calls.
- Code Composability
- Use C++ fold expressions to create new building blocks at compile time.
- folds allow you to execute a sequence of one or more functors to
produce a block.
Modules
- Scheduler: Components
- Policy: Defines the scheduling policy through two methods: ready and next.
- Event Handlers: A scheduler can choose which of ten events
it wants to respond to.
- Task Control Block Constraints: Specify what the Task Control Block
has to contain to work with the scheduler (e.g., if we have a priority
scheduler, then the TCB must be prioritizable (and you get to decide
from where those priorities come).
- Memory Allocator: provides fixed-size, fast pool, and binary buddy
allocators, using four components:
- Memory Block: A region of memory available for allocation.
- Static Aligner: A functor to provide proper alignment of allocations.
- Primitive Steps:
- Allocate = Get Free Block + Mark Block Used + Block to Pointer
- De-Allocate = Pointer to Block + Mark Block Free + Put Free Block
- Account Book: Metadata to track free/allocated space.
- Context Switcher:
- to-kernel: handles interrupts and saves state upon kernel entrance
- from-kernel: Sets up for next task to run (restores its state)
- Implementations for both x86 and ARM
- Context switcher is independent from execution model (thread based
or event-based)
- Single system call, syscall, uses whatever calling convention is
imposed by constraints (e.g., stack or register).
- Kernel dispatches from system calls to kernel service routines.
- Dispatcher: All mechanism, no policy
- specific half: invokes specifically requested functionality
- common half: invokes routines shared by all tasks, such as
checking for pending signals
- dispatcher relies on functors to dispatch to appropriate service
routine.
- No subcomponents, just a builder to assemble the dispatcher from the
collection of functors.
- Kernel Service Routines (System calls): Properties
- Non-blocking: all KSR are single-threaded and one-shot (must run to
completion). Sometimes this means decomposing a single application task
into a sequence of KSRs.
- TCB-independent: Kernel service routines are independent of the
task control block, however, if a KSR requires something of the TCB
(e.g., a unique identifier), constraints enforce that requirement.
- Component-independent: Remain independent, but compatible using
constraints to establish at compile time that the scheduler can properly
schedule KSRs.
- Execution models: Requires LOTS of components!
- Task Control Block Components: select some subset of the 10 existing
components or buid your own, for example, one of
Shared stack dedicated non-recyclable stack, or dedicated
recyclable stack.
Optionally, you can request system call support, etc.
(See Table iii).
- TCB Initializers and Finalizers: both setup and teardown
are implemented via Kernel Service Routines.
- In a thread-based execution model, applications can write
KSRs to create and destroy threads.
- In an event-driven model, the application defines the
events to be handled as KSRs as well.
Comparison Systems
- FreeRTOS
- Written in C
- Thread-based
- Modularity through C macros (works at API level, but not
code level).
- Customization via macros in header files
- Zephyr
- Written in C
- Thread based
- Linux-like
- Lots of scheduling algorithms
- Kernel config similar to Linux (this is how you customize
a system)
- Customizations are closer to Tinkertoy and finer-grain
than in FreeRTOS
- Both systems allow disabling functionality that is not
present in Tinkertoy, enabling a fairer comparison.
- Under active development (i.e., more modern than some of
the others).
- Can run on the evaluation platform.
- Both take a 'remove what you do not need' approach while
Tinkertoy takes a 'build what you do need approach'
Eval
- Strategy: Identify three different kernel types and
construct an app that requires one of each.
- The three kernels are:
a monitor kernel, an actuator kernel, and a gateway kernel.
- These three kernels correspond to the three generations
of IoT applications (respectively).
- Evaluation Metrics: Implementation effort,
memory footprint and runtime performance.
- Automatic Watering System Application
- A monitor kernel runs on one device that has a moisture
sensor that you place in your plant. The application on this
device polls the sensor and when the soil is too dry, it sends
a "water me" message to the actuator application; when the
soil is sufficiently damp, it sends a "stop watering me"
message.
- The actuator kernel runs on a device that controls a
water bottle. It opens the bottle on "water me" messages,
stops on "stop watering me" messages, and when it runs out
of water, it sends a message to a Linux server via the gateway
application.
- The gateway kernel runs on a gateway device that
connects the IoT network to the internet, translating
CoAP messages to HTTP meassage.
- HW: Stellaris LM3S811 board, emulated by ARM Fast
Models. The processor is a 50 MHz ARM Cortex-M3 with
64 KB Flash and 8 KB SRAM.
- Results
- Implementation effort: tens of lines of code and a significant
amount of reuse.
- Flash footprint: Other systems are 1.5x to almost 5x more
flash hungry.
- Stack footprint: Other systems range from 10% more
stack consumption to a bit more than 2.5x.
- Performance looks like a wash.