This document is intended to guide a new ODP application developer. Further details about ODP may be found at the ODP home page.
ODP is an API specification that allows many implementations to provide platform independence, automatic hardware acceleration and CPU scaling to high performance networking applications. This document describes how to write an application that can successfully take advantage of the API.
ODP consists of three separate but related component parts. First, ODP is an
abstract API specification that describes a functional model for
data plane applications. This specification covers many common data plane
application programming needs, such as the ability to receive, manipulate, and
transmit packet data, without specifying how these functions are performed. This
is quite intentional. It is precisely because ODP APIs do not have a preferred
embodiment that they permit innovation in how these functions can
be realized on various platforms that offer implementations of ODP. To achieve
this goal, ODP APIs are described using abstract data types whose definition
is left up to the ODP implementer. For example, in ODP packets are referenced
by abstract handles of type
odp_packet_t, and packet-related APIs take
arguments of this type. What an
odp_packet_t actually is is not part of the
ODP API specification—that is the responsibility of each ODP implementation.
The ODP API specification is designed to permit wide latitude on the part of implementations while at the same time supporting highly efficient processing, especially for APIs that are executed frequently.
Both applications and implementations must comply with the API specification. If not otherwise documented, results are undefined if an application acts against the specification. For example, if an application passes bad parameters to an ODP API one implementation may report an error, while another may not check them (to maximize performance) but would just crash while using the bad values.
Note that many ODP component areas provide an
odp_xxx_capability() API that
returns platform-specific information regarding valid input to other APIs in
that component. For best portability applications should always use these
capability APIs to determine valid parameter input.
Open Source, open contribution, BSD-3 licensed.
Vendor and platform neutral.
Application-centric. Covers functional needs of data plane applications.
Ensures portability by specifying the functional behavior of ODP.
Both applications and implementations must conform to the API specification.
Defined jointly and openly by application writers and platform implementers.
Architected to be implementable on a wide range of platforms efficiently
Sponsored, governed, and maintained by the Linaro Networking Group (LNG)
Second, ODP consists of multiple implementations of this API specification, each tailored to a specific target platform. ODP implementations determine how each ODP abstract type is represented on that platform and how each ODP API is realized. On some platforms, ODP APIs will be realized using specialized instructions that accelerate the functional behavior specified by the API. On others, hardware co-processing engines may completely offload the API so that again it can be performed with little or no involvement by a CPU. In all cases, the application sees the same functional behavior independent of how a given platform has chosen to realize it. By allowing each platform the freedom to determine how best to realize each API’s specified functional behavior in an optimal manner, ODP permits applications written to its APIs to take full advantage of the unique capabilities of each platform without the application programmer needing to have specialist knowledge of that platform or to be concerned with how best to tune the application to a particular platform. This latter consideration is particularly important in Network Function Virtualization (NFV) environments where the application will run on a target platform chosen by someone else.
One size does not fit all—supporting multiple implementations allows ODP to adapt to widely differing internals among platforms.
Anyone can create an ODP implementation tailored to their platform
Distribution and maintenance of each implementation is as owner wishes
Open source or closed source as business needs determine
Have independent release cycles and service streams
Allows HW and SW innovation in how ODP APIs are implemented on each platform.
To make it easy to get started with implementing ODP on a new platform, ODP supplies a number of reference implementations that can serve as a starting point. The two primary references implementations supplied by ODP are odp-linux and odp-dpdk
The odp-linux reference implementation is a pure SW implementation of the ODP API that relies only on the Linux programming API. As a functional model for ODP, it enables ODP to be bootstrapped easily to any platform that supports a Linux kernel.
The odp-dpdk reference implementation is a pure SW implementation of the ODP API that uses DPDK as a SW accelerator. In particular, odp-dpdk offers superior I/O performance for systems that use NICs, allowing ODP applications to take immediate full advantage of the various NIC device drivers supported by DPDK.
Open source, open contribution, BSD-3 licensed.
Provide easy bootstrapping of ODP onto new platforms
Implementers free to borrow or tailor code as needed for their platform
Implementers retain full control over their implementations whether or not they are derived from a reference implementation.
Third, to ensure consistency between different ODP implementations, ODP consists of a validation suite that verifies that any given implementation of ODP faithfully provides the specified functional behavior of each ODP API. As a separate open source component, the validation suite may be used by application writers, system integrators, and platform providers alike to confirm that any purported implementation of ODP does indeed conform to the ODP API specification.
Synchronized with ODP API specification
Maintained and distributed by LNG
Open source, open contribution, BSD-3 licensed.
Key to ensuring application portability across all ODP implementations
Tests that ODP implementations conform to the specified functional behavior of ODP APIs.
Can be run at any time by users and vendors to validate implementations of ODP.
1.1. ODP API Specification Versioning
As an evolving standard, the ODP API specification is released under an incrementing version number, and corresponding implementations of ODP, as well as the validation suite that verifies API conformance, are linked to this version number. ODP versions are specified using a standard three-level number (major.minor.fixlevel) that are incremented according to the degree of change the level represents. Increments to the fix level represent clarification of the specification or other minor changes that do not affect either the syntax or semantics of the specification. Such changes in the API specification are expected to be rare. Increments to the minor level represent the introduction of new APIs or functional capabilities, or changes to the specified syntax or functional behavior of APIs and thus may require application source code changes. Such changes are well documented in the release notes for each revision of the specification. Finally, increments to the major level represent significant structural changes that most likely require some level of application source code change, again as documented in the release notes for that version.
1.2. ODP Implementation Versioning
ODP implementations are free to use whatever release naming/numbering conventions they wish, as long as it is clear what level of the ODP API a given release implements. A recommended convention is to use the same three level numbering scheme where the major and minor numbers correspond to the ODP API level and the fix level represents an implementation-defined service level associated with that API level implementation. The LNG-supplied ODP reference implementations follow this convention.
1.3. ODP Validation Test Suite Versioning
The ODP validation test suite follows these same naming conventions. The major and minor release numbers correspond to the ODP API level that the suite validates and the fix level represents the service level of the validation suite itself for that API level.
1.4. ODP Design Goals
ODP has three primary goals that follow from its component structure. The first is application portability across a wide range of platforms. These platforms differ in terms of processor instruction set architecture, number and types of application processing cores, memory organization, as well as the number and type of platform specific hardware acceleration and offload features that are available. ODP applications can move from one conforming implementation to another with at most a recompile.
Second, ODP is designed to permit data plane applications to avail themselves of platform-specific features, including specialized hardware accelerators, without specialized programming. This is achieved by separating the API specification from their implementation on individual platforms. Since each platform implements each ODP API in a manner optimal to that platform, applications automatically gain the benefit of such optimizations without the need for explicit programming.
Third, ODP is designed to allow applications to scale out automatically to support many core architectures. This is done using an event based programming model that permits applications to be written to be independent of the number of processing cores that are available to realize application function. The result is that an application written to this model does not require redesign as it scales from 4, to 40, to 400 cores.
2. Organization of this Document
This document is organized into several sections. The first presents a high level overview of ODP applications, the ODP API component areas, and their associated abstract data types. This section introduces ODP APIs at a conceptual level. The second provides a tutorial on the programming model(s) supported by ODP, paying particular attention to the event model as this represents the preferred structure for most ODP applications. This section builds on the concepts introduced in the first section and shows how ODP applications are structured to best realize the three ODP design goals mentioned earlier. The third section provides a more detailed overview of the major ODP API components and is designed to serve as a companion to the full reference specification for each API. The latter is intended to be used by ODP application programmers, as well as implementers, to understand the precise syntax and semantics of each API.
3. ODP Applications and Packet Flow
Data plane applications are fundamentally concerned with receiving, examining, manipulating, and transmitting packets. The distinguishing feature of the data plane is that these applications are mostly concerned with the lowest layers of the ISO stack (Layers 2 and 3) and they have very high to extreme performance requirements. ODP is designed to provide a portable framework for such applications.
At the highest level, an ODP Application is a program that uses one or more ODP APIs. Because ODP is a framework rather than a programming environment, applications are free to also use other APIs that may or may not provide the same portability characteristics as ODP APIs.
ODP applications vary in terms of what they do and how they operate, but in general all share the following characteristics:
They are organized into one or more threads that execute in parallel.
These threads communicate and coordinate their activities using various synchronization mechanisms.
They receive packets from one or more packet I/O interfaces.
They examine, transform, or otherwise process packets.
They transmit packets to one or more packet I/O interfaces.
At the highest level, an ODP application looks as follows:
Packets arrive and are received (RX) from a network interface represented by a PktIO abstraction. From here they go either directly to Queues that are polled by ODP Threads, or can pass through the Classifier and sorted into Queues that represent individual flows. These queues can then be dispatched to application threads via the Scheduler.
Threads, in turn can invoke various ODP APIs to manipulate packet contents prior to disposing of them. For output processing, packets make by directly queued to a PktIO output queue or else they may be handed to the Traffic Manager for programmatic Quality of Service (QoS) processing before winding up being transmitted (TX). Note that output interfaces may operate in loopback mode, in which case packets sent to them are re-routed back to the input lines for "second pass" processing. For example, an incoming IPsec packet cannot be properly classified (beyond being IPsec traffic) until it is decrypted. Once decrypted and its actual contents made visible, it can then be classified into its real flow.
What is important to note is that the only part of the above diagram that need be written are the boxes in yellow that contain the application logic. Everything else shown here is provided by the ODP framework and available for use by any ODP application. This represents the "machinery" of a data plane application and is structured to allow applications written to the ODP APIs to be both portable and optimized for each platform that offers an ODP implementation without additional programming effort.
4. ODP API Concepts
ODP programs are built around several conceptual structures that every application programmer needs to be familiar with to use ODP effectively. The main ODP concepts are: Thread, Event, Queue, Pool, Shared Memory, Buffer, Packet, PktIO, Time, Timer, and Synchronizer.
The thread is the fundamental programming unit in ODP. ODP applications are
organized into a collection of threads that perform the work that the
application is designed to do. ODP threads may or may not share memory with
other threads—that is up to the implementation. Threads come in two "flavors":
control and worker, that are represented by the abstract type
A control thread is a supervisory thread that organizes the operation of worker threads. Worker threads, by contrast, exist to perform the main processing logic of the application and employ a run to completion model. Worker threads, in particular, are intended to operate on dedicated processing cores, especially in many core processing environments, however a given implementation may multitask multiple threads on a single core if desired (typically on smaller and lower performance target environments).
In addition to thread types, threads have associated attributes such as thread mask and scheduler group that determine where they can run and the type of work that they can handle. These will be discussed in greater detail later.
Events are what threads process to perform their work. Events can represent
new work, such as the arrival of a packet that needs to be processed, or they
can represent the completion of requests that have executed asynchronously.
Events can also represent notifications of the passage of time, or of status
changes in various components of interest to the application. Events have an
event type that describes what it represents. Threads can create new events
or consume events processed by them, or they can perform some processing on
an event and then pass it along to another component for further processing.
References to events are via handles of abstract type
functions are provided to convert these into specific handles of the
appropriate type represented by the event.
A queue is a message passing channel that holds events. Events can be added to a queue via enqueue operations or removed from a queue via dequeue operations. The endpoints of a queue will vary depending on how it is used. Queues come in two major types: plain and scheduled, which will be discussed in more detail when the event model is introduced. Queues may also have an associated context, which represents a persistent state for all events that make use of it. These states are what permit threads to perform stateful processing on events as well as stateless processing.
Queues are represented by handles of abstract type
A pool is a shared memory area from which elements may be drawn. Pools represent the backing store for events, among other things. Pools are typically created and destroyed by the application during initialization and termination, respectively, and then used during processing. Pools may be used by ODP components exclusively, by applications exclusively, or their use may be shared between the two. Pools have an associated type that characterizes the elements that they contain. The two most important pool types are Buffer and Packet.
Pools are represented by handles of abstract type
4.5. Shared Memory
Shared memory represents raw blocks of storage that are sharable between threads. They are the building blocks of pools but can be used directly by ODP applications if desired.
Shared memory is represented by handles of abstract type
A buffer is a fixed sized block of shared storage that is used by ODP components and/or applications to realize their function. Buffers contain zero or more bytes of application data as well as system maintained metadata that provide information about the buffer, such as its size or the pool it was allocated from. Metadata is an important ODP concept because it allows for arbitrary amounts of side information to be associated with an ODP object. Most ODP objects have associated metadata and this metadata is manipulated via accessor functions that act as getters and setters for this information. Getter access functions permit an application to read a metadata item, while setter access functions permit an application to write a metadata item. Note that some metadata is inherently read only and thus no setter is provided to manipulate it. When object have multiple metadata items, each has its own associated getter and/or setter access function to inspect or manipulate it.
Buffers are represented by handles of abstract type
Packets are received and transmitted via I/O interfaces and represent
the basic data that data plane applications manipulate.
Packets are drawn from pools of type
Unlike buffers, which are simple objects,
ODP packets have a rich set of semantics that permit their inspection
and manipulation in complex ways to be described later. Packets also support
a rich set of metadata as well as user metadata. User metadata permits
applications to associate an application-determined amount of side information
with each packet for its own use.
Packets are represented by handles of abstract type
4.8. Packet I/O (PktIO)
PktIO is how ODP represents I/O interfaces. A pktio object is a logical port capable of receiving (RX) and/or transmitting (TX) packets. This may be directly supported by the underlying platform as an integrated feature, or may represent a device attached via a PCIE or other bus.
PktIOs are represented by handles of abstract type
The time API is used to measure time intervals and track time flow of an application and presents a convenient way to get access to an implementation-defined time source. The time API consists of two main parts: local time API and global time API.
4.9.1. Local time
The local time API is designed to be used within one thread and obtaining
local time may be more efficient in some implementations than global
time. Local time stamps are local to the calling thread and should not be
shared with other threads, as local time is not guaranteed to be consistent
between threads. Current local time can be read with
4.9.2. Global time
The global time API is designed to be used for tracking time between threads.
So, global time stamps may safely be shared between threads. Current global
time can be read with
Both local and global time is not wrapped during the application life cycle.
The time API includes functions to operate with time, such as
odp_time_cmp(), conversion functions like
To get rate of time source
are used. To wait,
odp_time_wait_until() are used,
during which a thread potentially busy loops the entire wait time.
odp_time_t opaque type represents local, global and PktIO timestamps.
4.9.3. Portability Considerations
The ODP Time APIs are designed to permit high-precision relative time
measurement within an ODP application. No attempt is made to correlate an
odp_time_t object with "wall time" or any other external time reference.
As defined by the ODP specification,
odp_time_t values are required to
be unique over a span of at least 10 years. Most implementations will choose
to implement time values using 64-bit values, whose wrap times exceed 500
years, making wrapping concerns not relevant to ODP applications.
Timers are how ODP applications measure and respond to the passage of time.
Timers are drawn from specialized pools called timer pools that have their
own abstract type (
odp_timer_pool_t). Applications may have many timers
active at the same time and can set them to use either relative or absolute
time. When timers expire they create events of type
serve as notifications of timer expiration.
Multiple threads operating in parallel typically require various synchronization services to permit them to operate in a reliable and coordinated manner. ODP provides a rich set of locks, barriers, and similar synchronization primitives, as well as abstract types for representing various types of atomic variables. The ODP event model also makes use of queues to avoid the need for explicit locking in many cases. This will be discussed in the next section.
5. ODP Components
Building on ODP concepts, ODP offers several components that relate to the flow of work through an ODP application. These include the Classifier, Scheduler, and Traffic Manager. These components relate to the three main stages of packet processing: Receive, Process, and Transmit.
The Classifier provides a suite of APIs that control packet receive (RX) processing.
The classifier provides two logically related services:
Verifying and extracting structural information from a received packet.
Applying Pattern Matching Rules (PMRs) to the parsed results to assign an incoming packet to a Class of Service (CoS).
Combined, these permit incoming packets to be sorted into flows, which are logically related sequences of packets that share common processing requirements. While many data plane applications perform stateless packet processing (e.g., for simple forwarding) others perform stateful packet processing. Flows anchor state information relating to these groups of packets.
A CoS determines two variables for packets belonging to a flow:
The pool that they will be stored in on receipt
The queue that they will be added to for processing
The PMRs supported by ODP permit flow determination based on combinations of packet field values (tuples). The main advantage of classification is that on many platforms these functions are performed in hardware, meaning that classification occurs at line rate as packets are being received without any explicit processing by the ODP application.
Note that the use of the classifier is optional. Applications may directly receive packets from a corresponding PktIO input queue via direct polling if they choose.
The Scheduler provides a suite of APIs that control scalable event processing.
The Scheduler is responsible for selecting and dispatching one or more events
to a requesting thread. Event selection is based on several factors involving
both the queues containing schedulable events and the thread making an
ODP queues have a scheduling priority that determines how urgently events on them should be processed relative to events contained in other queues. Queues also have a scheduler group id associated with them that must match the associated scheduler group thread mask of the thread calling the scheduler. This permits events to be grouped for processing into classes and have threads that are dedicated to processing events from specified classes. Threads can join and leave scheduler groups dynamically, permitting easy application response to increases in demand.
When a thread receives an event from the scheduler, it in turn can invoke other processing engines via ODP APIs (e.g., crypto processing) that can operate asynchronously. When such processing is complete, the result is that a completion event is added to a schedulable queue where it can be scheduled back to a thread to continue processing with the results of the requested asynchronous operation.
Threads themselves can enqueue events to queues for downstream processing by other threads, permitting flexibility in how applications structure themselves to maximize concurrency.
5.3. Traffic Manager
The Traffic Manager provides a suite of APIs that control traffic shaping and Quality of Service (QoS) processing for packet output.