aos/doc/014-bulk-transfer/bulk-transfer.tex
Daniel Schwyn 6d444bf552 Main handout
Signed-off-by: Daniel Schwyn <daniel.schwyn@inf.ethz.ch>
2022-03-03 14:57:51 +01:00

1128 lines
53 KiB
TeX

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Copyright (c) 2011, ETH Zurich.
% All rights reserved.
%
% This file is distributed under the terms in the attached LICENSE file.
% If you do not find this file, copies can be found by writing to:
% ETH Zurich D-INFK, Universitaetstrasse 6, CH-8092 Zurich. Attn: Systems Group.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\documentclass[a4paper,twoside]{report} % for a report (default)
\usepackage{bftn} % You need this
\usepackage{hyperref} % You need this
\title{Bulk Transfer} % title of report
\author{Pravin Shinde} % author
\tnnumber{014} % give the number of the tech report
\tnkey{Bulk Transport: Cross-domain transfer facility for large data}
% Short title, will appear in footer
% \date{Month Year} % Not needed - will be taken from version history
\begin{document}
\maketitle
%
% Include version history first
%
\begin{versionhistory}
\vhEntry{1.0}{11.08.2011}{TR}{Initial version}
\end{versionhistory}
% \intro{Abstract} % Insert abstract here
% \intro{Acknowledgements} % Uncomment (if needed) for acknowledgements
\tableofcontents % Uncomment (if needed) for final draft
% \listoffigures % Uncomment (if needed) for final draft
% \listoftables % Uncomment (if needed) for final draft
\chapter{Introduction}
\label{chap:intro}
This technical note is aimed to describe the design of suitable cross
domain bulk transport for Barrelfish. This chapter gives a motivation
for this design by providing the details about existing
implementations of bulk-transport in Barrelfish and reasoning
about why those are not good enough. The \autoref{chap:design}
contains the actual details about the design. So, if you are
already convinced of the need for better bulk-transport in
Barrelfish, then you can directly jump to the \autoref{chap:design}
\section{Existing implementation in Barrelfish}
There are two bulk transport mechanisms that exist in the current(as of August
2011) implementation of Barrelfish.
\texttt{$lib/barrelfish/bulk\_transfer.c$}
and \textit{pbufs} used in the network stack. We will discuss them briefly in
following two sub-sections.
\subsection{$bulk\_transfer.c/h$}
This is the official bulk transfer method supported by Barrelfish. In brief, this
facility works by sharing a continuous piece of memory (in form of capability)
between two domains. These domains then map this physical memory into their
virtual address-space. The virtual address where this shared memory is loaded
can be different. So, participating processes can't exchange the virtual
addresses directly. Bulk transfer mechanism works by dividing the entire
memory area into the blocks of fixed size. The domain which initiated the
bulk transfer is responsible for managing these blocks. This responsibility
includes following.
\begin{enumerate}
\item Allocation and deallocation of the blocks.
\item Maintaining the information about free and used blocks.
\end{enumerate}
So, this bulk transfer facility works in master-slave setup. The master
allocates the blocks and passes the index of the block to slave.
Once the block is passed to the slave, master should not touch it.
The slave can locate the block by adding the \texttt{(index*block-size)} to the
virtual address pointing to the beginning of the shared memory area.
Now slave can read/modify the contents in the memory area of this block. Once
the slave is done with accessing the block, it can pass the block-index
back to the master. Master can either access the block or release it
for future use.
\subsubsection{Limitations}
\paragraph{Security}
This transfer mechanism can only be used between domains which are
co-operative and willing to follow the protocol set for using the bulk
transfer mechanism. Malicious domain can corrupt the data in shared
memory by writing at random locations in it
or can refuse to return the blocks once passed to it.
\paragraph{More than two domains}
The design of this bulk transport does not stop you from using it with
more than two domains as long as the protocol is followed. But current
implementation does not track which domain is holding the buffer. So,
one can use the current implementation with multiple domains as long
as the domains are co-operative and applications are willing to deal
with added complexity of tracking which domains hold which buffers.
\paragraph{Current usage}
Due to the relatively simple nature of the implementation, its use is
limited only to few places. And hence it is not throughly tested in
various scenarios.
\subsection{pbufs}
The network stack uses its own custom bulk transport mechanisms. Lets
call them pbufs. These pbufs work in similar way to above mechanism,
but are more flexible. The application shares some piece of physical memory
with network driver which is used as shared memory. Then application
creates a list of pbuf structures, each one of them holding an offset
within shared memory, length, \textit{pbuf-id} and the shared memory id to
which they belong. The key difference here is that the these pbufs
may not hold consecutive memory locations even when \textit{pbuf-id}'s are
consecutive. In contrast with Barrelfish bulk-transfer where buffer-id
value is enough to find the location of buffer within shared memory,
pbuf needs to store the offset separately in another list. So,
pbufs provide another layer of indirection to allow more
flexible use of memory.
The way this mechanism currently works is that, application creates
pool of initial pbufs and registers them with network driver. Both,
application and network driver maintain the list of pbufs in their
private memory. This list is kept in synchronization by sending
explicit messages. Now each pbuf can in principle point to any
buffer of any size, located anywhere in the shared memory.
This flexibility is used by application when it receives a data from
the network driver. Application creates a new pbuf structure with
same pbuf-id but pointing at new buffer location and send it back to
the driver as new free pbuf to use. And the location of previous
buffer is used for processing the data. This way, application can
return the pbufs back to driver ASAP without getting affected by how
long does the data processing takes. When application is done with
processing the data in that buffer, it releases that buffer. This
released buffer is then used to create new pbuf which will be
registered again with driver in future.
\subsubsection{Limitations}
\paragraph{Needs more memory}
The shared memory needs to be bigger than the memory shared with
network driver in form of registered pbufs. This is because, at any
given point in time, some pbufs will be in application data processing
phase and hence can-not be used by driver to receive the new data.
\paragraph{Data corruption}
This bulk-transfer mechanism assumes co-operative domains. Both
domains have read/write access to the shared memory at all the time.
This means that if they do not follow the protocol correctly, they may
end up writing at same physical location, leading to data corruption.
\paragraph{Complicated memory reclamation}
The current implementation of pbufs assumes that application can have
multiple threads and all of them can access the data in pbufs which
are delivered to the application. This complicates the problem of
detecting when exactly all threads are done with accessing the
particular pbuf. The current implementation uses a reference count
mechanism for this detection. Even though this memory reclamation is
functional, it is complicated and one can easily get it wrong, leading
to memory leaks.
\paragraph{Supporting more than two domains}
In theory, this design can be used with more than two domains which
are co-operating with each other, but implementation is not designed
with such a case in mind. The problematic issues will be tracking
which memory buffer is with which domain, and when the memory buffer
can be reused.
\subsubsection{More issues with current network stack implementation}
Following issues are not exactly due to the bulk transfer mechanisms,
but are mainly the implementation issues of network stack. I am
documenting them here for sake of completeness. Also, knowing the
issues in current stack will help in understanding the design
decisions made.
\paragraph{Favors the setup with driver on different core}
It is optimized for the case where application and network driver are
running on separate core. It is reasonable to prefer such cases
for multicore architectures where presence of large number of cores is
assumed. This preference has resulted on overly dependence on
efficiency of UMP messages. Current implementation sends around 2
messages (send-packet, tx-done) for transmitting single packet
assuming that packet fits into one pbuf. Similarly on packet
receiving side, each received packet involves 2 messages
(register-pbuf, packet-received). The performance impact of these
messages is not strongly visible when application and driver are on
different core. But there are cases like SCC where you may end up
running the driver on same core as an application. In such scenarios
LMP's are used for communication which involves doing context-switch in
sending every message. This leads to lot of performance
in-efficiencies when current network stack implementation is used in
LMP contexts. Specially on those hardware where cost of
context-switch is relatively high.
%I have tried to measure the performance difference on \texttt{nos4-6}
%boxes with webserver cache loading test, but the performance
%difference was not significant. I have not investigated the reasons
%behind them yet, but my preliminary guess was that context switches on
%these machines are quite efficient or webserver cache loading test
%does not stress the network stack enough to see any significant
%difference.
%\section{Questions to be asked about bulk transfer mechanisms}
%\begin{enumerate}
% \item Does it uses memory efficiently?
% \item Can it be used with mutually non-trusting domains?
% \item Can it be used across more than two domains?
% \item Can consumer/producer put a back-pressure on other side?
% \item Can it support multiple producer and multiple consumers?
% \item Can it allow implementation of \textit{True Zero Copy}
% sending and receiving?
% \item Can it lead to deadlock if one domain is only sending without
% checking for arriving messages?
%\end{enumerate}
\section{Related work}
The issues of bulk-transport has been investigated quite a few times
in the past by work like \textit{F-bufs}, \textit{R-bufs} and
\textit{Beltway buffers} so the solution of this problem is known to
the large extent. What this document is aiming is to choose
right approach and right solution which will suit the
requirements of the Barrelfish.
%The work which has particularly influenced this design are
%\begin{enumerate}
% \item F-bufs: Helped in clearly understanding the problems in
% designing cross-domain transfer facility.
% \item R-bufs: Helped in understanding how to implement
% scatter-gather packet sending and receiving.
% \item Beltway buffers: Gave clear idea of how to design a
% system with multiple ring-buffer to get lot of flexibility.
% The problem with beltway buffers is that they are
% designed to be POSIX compatible and due to that, the whole
% functionality of these buffers is pushed into trusted OS code-base.
% The applications are unaware of these buffers. Also, these buffers
% can be used mostly to short-circuit the data path between different
% trusted kernel sub-systems. If applications are going to modify the
% data then beltway buffers don't help much.
%\end{enumerate}
\section{Requirements of Barrelfish}
Following are the ideal requirements from the bulk-transport
mechanism.
\begin{enumerate}
\item Avoid data copy as much as possible, if can't avoid,
then try to push it into user-space/user-core.
\item Ability to batch the notifications.
\item Should work with more than two domains.
\item Should work with multiple producers and multiple consumers.
\item True zero copy capability(scatter-gather packet
sending/receiving).
\end{enumerate}
The solution that we are trying to design tried to satisfy as many
of these requirements as possible.
\chapter{Design}
\label{chap:design}
This chapter discusses the design of bulk-transport mechanism
that will be implemented in future for Barrelfish network
stack.
\section{Bird's eye view of design}
We are aiming to design a cross domain bulk transport mechanism,
which supports following features.
\begin{enumerate}
\item Should reduce the data copy as much as possible.
\item Should exploit the fact that complete data-isolation
is not always needed.
\item More than two separate domains should be able to share
the data without copying it.
\item Number of explicit notifications needed should be low.
\item It should work in \textit{single producer, single consumer}
and \textit{single producer, multiple consumers}
\end{enumerate}
\section{Terminology used}
This section briefly explains the term used in the design
description. This should help in the understanding by reducing
the ambiguity of the terms.
\begin{enumerate}
\item \textbf{Generator:} An entity which is generating the data. It
can be software or hardware entity. For example, Network
Interface Card(NIC) is a generator, or the application code
which is generating the packets to send over the NIC.
\item \textbf{data-element:} The data-element is what generator
is generating. In case of NIC device, a packet will be a
data-element.
\item \textbf{Producer:} An entity which is managing the generated data.
This management contains providing access mitigation, notifying
interested consumers, reclaiming the memory when data-element
is consumed. The example of producer will be \textit{NIC device
driver}.
The distinction between generator and producer is
rather fine. They are intentionally kept separate because
generators can have hardware constraints which does not allow
it to provide all functionalities.
\item \textbf{Consumer:} An entity which is consuming the data-elements.
\item \textbf{Slot:} The contiguous piece of memory where data-element is
entirely or partially stored. A data-element can span over
more than one slots.
\item \textbf{Shared-pool:} A contiguous piece of memory accessible
in read/write mode to producer and accessible in read only mode to
consumers. Shared pools are divided into above slots.
\item \textbf{Production-pool:} It is a collection of all
shared-pools used by the particular producer. The initial
shared-pool is added by the producer, and later on every consumer
which joins the system contributes a new shared-pool to the
production-pool.
\item \textbf{Free slot:} A slot which is available, and can be given
to generator.
\item \textbf{In-generation slot:} A slot is given to generator. Only
the generator should read/write this slot now. This is the only
mode when data can be written in the slot.
\item \textbf{In-consumption slot:} One or more consumers are currently
consuming this slot. In this mode, slot is strictly read-only
no one should modify the data in slot in this mode.
\item \textbf{Slot-pointer:} A data-structure which holds enough
information to locate particular slot present in any shared-pool
with producer. These are typically used in cross domain
communication to identify the exact slot. A typical slot-pointer
should have a shared-pool-id and offset within shared-pool to
find the particular slot.
\item \textbf{Classification:} This term refers to the
classification of data-elements between consumers. In other words,
it is a process of deciding which consumers should receive this
particular data-element. In network stack parlance, this
classification will map to deciding which process should get the
received packet.
\end{enumerate}
\section{The Producer}
This section describes the producer and generator together with
their responsibilities. Even though producer and generator can
be a different entities internally, the consumers will only see
the producer interface.
\subsection{Generator}
Lets make a rough sketch of typical generator. Here we are aiming
for the NIC devices. Typically, these devices will have an
internal queue (RX-queue) of slots where the packet received
from wire will be copied. These generators are also capable
of generating a notification in form of interrupt. Now the
way RX-queues work on different devices differ in some way.
Few devices expect contiguous memory in the slots whereas
other devices are capable of DMAing the packet in non-contiguous
memory as well.
The software generators like application
logic which creates new packet to send out is much simpler than
NIC devices but can have quirky behavior like above described
NIC devices.
From bulk-transport point-of-view we will be treating the generator as
a part of producer.
\subsection{The Producer abstraction}
The producer abstraction is nothing but the combination of generator
and remaining producer functionalities.
The producer abstraction is what consumer sees and interacts with.
It is responsible for managing the data produced by generator and
using shared-pools to get that data till consumers. It is also
responsible for managing the shared-pool memory and co-ordinating
the access to this shared resource.
\subsection{Shared-pool}
Shared-pool is the area where producer will generate the data
and consumers will read it from. The producer breaks this
shared-pool into \textit{slots}. The size of slot
is based on the capability of generator and should be multiple of
cache-line size. The details of how to choose
the slot size and the side-effects of small or large size are
discussed in \autoref{sec:slotsize}.
The producer is responsible for managing these shared-pools, and it
does by maintaining the list of shared-pools. At the time of producer
initialization there will be only one shared-pool which is added by
the producer itself. As new consumers join the system, each one of
them will contribute an additional shared-pool.
\subsubsection{Releasing the shared-pool}
If any consumer decides to leave the system, then producer
stops using the shared-pool provided by him. The producer will
wait till all the slots in that shared-pool are free and then it will
ask all consumers who have mapped this shared-pool to release the
pool. Once all consumers release the pool then producer can also
release the pool and inform the consumer who contributed the
shared-pool about completion of the process.
This is rather long process and depends on all the consumers for it's
completion. But this process is not part of the critical path, it is
only part of the tear-down process so some inefficiency can be
tolerated here.
%The virtual address where the shared-pool is mapped need not be same for
%the producer and consumers.
\subsubsection{Ideal size of production-pool}
The production-pool is just a collection of all valid shared-pools
available at given time. Ideally, production-pool should be big enough
to accommodate the queue of the generator (ie. All the descriptors
in RX-queue of NIC device) and all the data-elements which are with
consumers and are not released back (in-consumption state).
So, the total memory size should be proportional to the capacity of
the generator(ie. NIC device) and number of consumers.
So, one way to look at this is that, producer will provide shared-pool
which can satisfy the needs of generator and each consumer will
contribute a shared-pool to allow in-consumption data-elements.
Each consumer is allowed to keep only fixed amount of
in-consumption data-elements based on the size of shared-pool it
contributed. This way, fixing the size of allowed pending
in-consumption data-elements limits the ability to consumers to
over-consume the slots from production-pool.
\subsection{A meta-slot structure}
This structure hold the additional information about each slot in
a shared-pool. It can be seen as index table on shared-pool.
This structure is private to producer and is used to manage the
slots within shared-pool. Following are the key elements of
this structure.
\begin{enumerate}
\item \textbf{slot-id:} The slot identifier.
\item \textbf{offset:} The location of slot within the shared pool.
\item \textbf{data-len:} Size of valid data in the slot.
\item \textbf{state:} The state of slot (Free, in-Generation,
in-Consumption)
\item \textbf{consumer-list:} Which consumers are accessing it?
\item \textbf{Next, prev:} Used to maintain the list (eg. Free slots)
\end{enumerate}
The producer maintains a separate meta-slot list for very shared-pool
available with the producer. This setup enables producer to track
every slot and its state.
Other than this, the producer also maintains the list of consumers who
have registered.
\subsection{shared and private data-structure}
This section briefly describes which of above data-structures are
private and which of them are shared with other entities.
\subsubsection{Private data-structures}
The producer maintains lot of state in private data-structures. It
includes following.
\begin{enumerate}
\item List of consumers connected. This list also maintains the
state of each consumer with respect to the communication with
producer.
\item List of shared-pool-meta-data. This meta-data includes
following information.
\begin{enumerate}
\item Consumers having access to this shared-pool.
\item meta-slot structure containing private information about
state of each slot in this shared-pool.
\item List of free slots in this shared-pool.
\item The generator-queue which is used by generator to create
data-elements is shared with generator. But as we treat generator as
internal entity, we mark this as private data instead of shared
data.
\end{enumerate}
\end{enumerate}
\subsubsection{Shared data-structures}
The producer shares following data-structures with different entities.
\begin{enumerate}
\item Shared-pools can be shared with any consumer.
\item For every consumer, a consumer-queue (see
\ref{sec:consumer-queue}) is pairwise shared with that consumer.
\end{enumerate}
\section{Consumer}
This section describes the internals of the consumer. Consumer mainly
consist of a consumer-queue data-structure which allows sharing of
slots between producer and consumer. Consumer also has read-only
access to the shared-pools.
\subsection{Consumer-queue}
\label{sec:consumer-queue}
This section describes the consumer-queue in details.
The consumer-queue is a shared data-structure between consumer and
producer and it is exclusive between each pair of consumer-producer.
This data-structure is also maintained on contiguous shared memory
which is read/writable by both consumer and producer. This
data-structure is shared ring-buffer with few virtual-registers
which are used to manage the access the ring-buffer.
\begin{enumerate}
\item \textbf{write-register:} This virtual register is the first
element in the consumer-queue. This element should be of size
cache-line to avoid any cache-conflicts. This element can be
modified only by the producer and consumer can only read it.
The value in this virtual-register contains the write-index for
consumer-queue. \textit{The write-index points to the slot-pointer within
consumer-queue that producer will provide next.}
\item \textbf{read-register:} This virtual register is the second
element in the consumer-queue. This element should be of size
cache-line to avoid any cache-conflicts. This element can be
modified only by the consumer and producer can only read it.
The value in this virtual-register contains the read-index for
consumer-queue. \textit{The read-index points to the slot-pointer within
consumer-queue that consumer will consume next.}
\item \textbf{queue-size-register:} This virtual register is the third
element in the consumer-queue. This element can be modified by
producer and consumer can only read it. The value in this
virtual-register indicate the number of slot-pointers which are
valid in the consumer-queue. This register allows dynamic adjustment queue-size
based on the current load. Similar feature is implemented in the
\textit{beltway buffers} with the claim that keeping the size of
queue as small as possible helps in cache friendliness. This
feature is actually optional and will be added in later stages.
To implement this feature, one also need to made decision like when
the queue size should be reduced and when it should be increased
again. Producer can make these decisions based on how much
preference it wants to give to this particular consumer over others.
\item \textbf{Slot-pointers:} After above three registers, the rest
of the space in consumer-queue is used for storing slot-pointers.
The slot pointers are used to point a particular slot in one of the
shared-pools within production-pool of the producer.
Slot-pointers have following information.
\begin{enumerate}
\item \texttt{shared-pool-id:} Id of the shared-pool which is
holding this particular slot. These id's are given by the
producer and will be unique within that particular producer for given
shared-pool. As the shared-pools within particular
producer may increase/decrease over time, the consumer may receive
a slot-pointer with shared-pool-id which it has not mapped yet.
In such a case, it should send a message to producer asking for
read-only access to this new shared-pool and then map it into
the virtual address-space. Once the shared memory frame
associated with shared-pool memory is mapped, consumer can
continue to access the slot using slot-pointer.
\item \texttt{slot-index:} The index of slot within that
shared-buffer. This value is only useful for producer as this
slot-index maps into the meta-slot structure which is private to
the producer. Consumer should not alter this value. In case
malicious consumer alters this value, the producer will be able
to detect it as meta-slot structure maintains the information
about which all consumers are currently consuming the slot and
whenever the producer reclaims the slots freed by consumer, it
validates if this consumer was consuming the slot being released.
The reason for maintaining this information in slot-index even
when it is not useful to consumer is because it speeds up the
producer in relocating the slot within shared-pool when it is
freed by the consumer.
\item \texttt{Offset:} The start of the slot within shared-buffer.
In case of fixed size slot, offset can be calculated by
slot-index.
\item \texttt{More:} As the data-element can span more than one
slots, this flag tells us if there are more slot-pointers
following which belongs to same data-element. This flag is
equivalent to the \textbf{More Fragments} bit in the \textbf{IP
protocol}. In contrast to IP protocol there is no fragment
identification number, but the slot-pointers belonging to same
data-elements are assumed to follow each other. So, the order in
which the slot-pointers are added to the consumer-queue is
important. As we have only one producer which is adding the
slot-pointers in \textit{available to consume} section and also
this producer is dealing with one data-element at one time,
maintaining this order is fairly simple.
\end{enumerate}
\end{enumerate}
\subsubsection{Conditions on consumer-queue registers}
\begin{enumerate}
\item Queue empty condition: read-index == write-index
\item Queue full condition: ((write-index + 1) \% size) == read-index
\item Elements available to consume \newline
(assuming queue not empty):\newline
if (write-index $>$ read-index) \newline
then \{[read-index, (write-index - 1)]\} \newline
else \{[read-index, (size - 1)], [0, (write-index - 1)]\} \newline
\item Elements which are already consumed and are now free. \newline
(assuming queue not empty): \newline
if (write-index $>$ read-index) \newline
then \{[write-index, (size - 1)], [0, (read-index - 1)]\} \newline
else \{[write-index, (read-index - 1)]\} \newline
\end{enumerate}
\subsection{shared and private data-structure}
This section briefly describes which of above data-structures are
private and which of them are shared with other entities.
\subsubsection{Private data-structures}
The consumer does not have to maintain lot of private state based on
which model of slot-consumption is used (refer
\ref{sec:memory-management}). When doing out-of-order consumption
it will have to maintain some state about which slots are in
consumption and which slots are free and ready to go back to producer.
Every consumer will also need some private state to remember the state
of the producer and the consumer-queue status. If this consumer is
directly communicating with other consumers then it will also need
to maintain some private state about that communication (refer
\ref{slot-forward}).
\subsubsection{Shared data-structures}
The consumer shares following data-structures with producer and other
consumers.
\begin{enumerate}
\item The shared-pool that it has contributed to producer and
potentially shared with all consumers.
\item The consumer-queue is exclusively shared with producer.
\end{enumerate}
\section{Events/notification/communication between consumer and
producer}
This section describes the communication between the consumer and the
producer.
\subsection{From Producer to Consumer}
This paragraph describes the events sent by producer to
consumers.
\begin{enumerate}
\item \textbf{More data arrived:} This notification is sent to
consumer whenever the consumer-queue is empty and new data-element
arrived. This callback is not triggered for every arrival of data,
but only when consumer is not explicitly polling for data. Whenever
new data arrives on the empty queue, producer can assume that
consumer is not polling the channel as queue is empty, and hence
producer should send this notification to consumer to wake it up.
In other case where queue is non-empty, consumer is already aware of
the presence of data-elements there. Consumers are expected to
deal with dynamically growing of queue-size and hence adding
more elements to non-empty queue without sending explicit
notifications should not break the consumer-logic.
\item \textbf{Consumer-queue full:} This notification is sent to
consumer whenever consumer-queue is full and producer is not able
to add new data-elements to the consumer-queue. Triggering of this
notification means that consumer is slow in consuming the data from
the consumer-queue. And result of this producer is going to drop
the data-elements which were aimed for this consumer till there
is more free space with this consumer. This message also means that
producer is not going to actively check if consumer-queue has a free
space or not. It is a responsibility of consumer to send
a notification to the producer whenever it is ready to receive more
data.
\item \textbf{Consumer-queue almost full:} This is an optional
notification and is designed as refinement of the basic approach.
This additional event can be
called when the number of free slot-pointers in the queue drops bellow
certain threshold. This notification can be used as a warning sign
to the consumer that either it takes some action to free up more
slot-pointers in consumer-queue, or producer will soon start dropping the
data-elements addressed to this consumer.
\item \textbf{Error event:} This notification is sent when something
unexpectedly goes wrong. The information and severity of the error
will inform the consumer that if the problem is transient or
permanent. And based on the problem type, consumer can take
corrective actions. Few samples of these errors are corruption of
consumer-queue, fetal error in producer, etc.
\item \textbf{Add shared-pool:} This is an optional notification which
allow producer to push the notifications about newly added
shared-pools. This message is optional because consumers
can lazily ask for these new shared-pools whenever they
encounter them as part of slot-pointer while consuming
data-elements. This notification is not considered as part of
critical path or part of the data-flow. This notification will be
generated only on the arrival of new consumer or if producer decides
to increase the available shared-pools. This can be classified as
setup or maintenance path.
\item \textbf{Remove shared-pool:} This notification is opposite of above
\textit{add shared-pool} call. It tells consumer is that producer
will not be using a particular shared-pool from now onwards
for whatever reason (eg. the consumer which gave that shared-pool
frame has terminated the connection with producer). So, every
consumer which receives this message should remove the mapping for
shared-pool from it's virtual memory. This notification is
also relatively rare and will be generated only when one of the
consumer decides to quit or if producer decides to reduce the number
of available shared-pool. So, this notification should not
be considered as part of critical-path or data-flow. It can be
classified into setup or maintenance path.
\end{enumerate}
\subsection{From Consumer to Producer}
This paragraph describes the notifications sent by consumer to the
producer.
\begin{enumerate}
\item \textbf{Consumer-queue space available:} This notification is
sent to the producer when consumer-queue is full and new free slot-pointer is
added creating new space. Once the queue is full, producer is not expected to check
the queue status until it receives this message saying that now
application has more space to receive packet. As an optimization,
consumer might wait till some more free-space is accumulated before
informing the producer to re-start the flow. This way the
inefficiency similar to \textit{silly window behavior} in TCP
flow control can be avoided.
\item \textbf{Get frame for shared-pool-id:} This notification is
sent to the producer when consumer receives a slot-pointer with
shared-pool-id which is not mapped by this consumer yet. By
sending this notification, consumer is asking for the read-only
access to this shared-pool. The producer should respond to
this message with either with valid capability or with error.
\item \textbf{Forwarding slot-pointer to other consumer:}
\label{slot-forward}
This message is related to the ability of consumers to forward
slot-pointers within each other. There are two cases here. In
first case, the consumer forwards the slot-pointer to other
consumer and then also collects it back before declaring it as free.
In this case, producer does not need to be informed at-all about
this sharing as consumer is taking responsibility of properly
freeing the slot. But in case where consumer just wants to forward the
slot-pointer and continue on it's own working without bothering
about when and how other consumer is going to stop accessing
that slot, then it should inform the producer that it has forwarded
that slot to some other consumer. It can be done with this
message. The reason we need this separate message is that,
because of sharing there will be two consumers which will be
releasing the slots and producer should be aware of them.
The information from this message is used to update the
meta-slot structure inside producer for given slot with one more consumer.
This message contains the slot identification information
and consumer identification information. The other consumer can
now release the slot whenever it is done using it's consumer-queue,
and producer can validate and account that operation correctly.
This forwarding has some more implications which are not fully
explored in this document. Here are few leads on that. Firstly,
this path allows consumer to receive the slots without being
initiated by producer and this slot will not be a part of
consumer-queue. So, the producer need to somehow account for these
slots so that it can maintain the fairness in the number of
outstanding slots per consumer. This mechanism also needs some way
by which two consumers can talk with each other and exchange the
information like consumer-id's, producer-id and slot-pointers.
Also, consumers should be careful in dealing this information as
consumer-id and slot-pointers are only valid in context of particular
producer.
\end{enumerate}
\subsection{The communication between producer and generator}
The communication between producer and generator is very
hardware dependent, and I am not sure if it should be part of this
document. Following are the generic notifications which are exchanged
between the producer and generator. These notifications are aimed to
maintain the generator-queue.
\begin{enumerate}
\item Add empty slot (from producer to generator): This message will
add a new free slot into generator-queue.
\item Data-element generated (from generator to producer): This
message informs the producer that there is new data-element in the
generator queue that should be handled. It is equivalent of
\textit{packet received interrupt} from NIC hardware to the device
driver.
\item Generator-queue full (from generator to producer): This
message tells the producer that there is no space left for generator
to create more data-elements.
\end{enumerate}
\subsection{The communication between consumers}
TBD: The communication between consumers is needed to share the
slot-pointers between them.
\section{Initialization}
\label{sec:initialization}
This section briefly provides the steps involved in initialization of
producer and consumers.
\subsection{Producer initialization}
At producer initialization, following steps will happen.
\begin{enumerate}
\item Create and initialize the empty list of shared-pools and meta-slots.
\item Create and add the initial shared-pool big enough to satisfy
generator needs.
\item Divide the shared-pool into slots, create a meta-slots list
for this shared-pool and add it to meta-slots list.
\item Create an empty list of consumers.
\item Initialize the generator (if needed). This might involve
populating the generator-queue with free slots so that generator can
start producing data.
\item Loop: wait for next event/message and process it accordingly.
\end{enumerate}
\subsection{Consumer registration}
As part of initialization process, consumer should register itself
with the producer. This initialization process includes following
steps.
\begin{enumerate}
\item Create a new contiguous memory frame and provide it to
producer as new shared-pool frame.
\item Get the list of registered shared-pools from producer and
map it as read-only in the virtual address-space of consumer.
\item Create a new contiguous memory block to maintain a
consumer-queue.
\item Initialize this memory block with consumer-queue
data-structure.
\item Share this memory block with producer.
\item Send a notification \textit{consumer-queue space available} to
the producer so that it will start sending data-elements.
\item Loop: wait for next event/message and process it accordingly.
\end{enumerate}
\section{Memory management (slot management)}
\label{sec:memory-management}
The slots are most important aspect of this bulk transport mechanism.
They hold the data which will be sent across the domains. Now lets
see how exactly these slots work.
\subsection{Slot state machine}
Slots are created by the producer in the memory provided by
shared-pools. Once created, slot remains valid until and unless the
shared-pool holding the slot needs to be freed. In the lifetime of
the slot, it goes through following states.
\begin{enumerate}
\item \textbf{Free:} This is the initial state, in this state slot
is free and ready to be used. The producer maintains a list of free
slots so that it can quickly find them whenever they are needed.
\item \textbf{In-generation:} This is the second stage of slot. In
this state, the slot is owned by the generator and no one should
access it(not even in read state!). This is the only state in which
the data will be written into the slot.
\item \textbf{In-classification:} This is the third and transient
state. This is the state when generator returns the slot after it
has generated the data. The producer will use the classification to
figure out which consumers this data-element should go. The
producer is free to use any type of classification that is needed.
The description of how that classification should happen is not in
the scope of this document. Once classified, the producer will
update the consumer-queues of each selected consumer with
slot-pointer and if needed, send them the notifications. Once the
slot-pointers are added into consumer-queue, the state of slot is
updated to the \textbf{in-consumption}.
If no consumer is interested, then the slot is marked as free and
added back to the list of free slots.
This is the stage where \textbf{zero copy data access} is achieved.
As instead of actually copying the data from the slot, slot-pointers
are provided to the consumers. These slot-pointers provide
zero-copy access and data-sharing capabilities.
\item \textbf{In-consumption:} This is the state in which data is
available to the consumers. Here, consumer has multiple options
about what to do with this slot.
\begin{itemize}
\item \textbf{In-order consumption:}
Consume the slot as quickly as possible, and once consumed
add it to freed slot by moving the read-index.
As consumer is dealing with one data-element at a time,
there is no need for complex state management and
free-slot management inside consumer-queue is sufficient enough
without needing any additional state machinery.
\item \textbf{Out-of-order consumption:}
Saves the newly arrived slot-pointer and replace it with one of
the other slot-pointers which has been previously consumed and then add this
replaced slot to freed slot by moving the read index. This method
allows you to consume the slots at leisure and you can have more
than one data-elements in consumption at any given moment of time.
It also allows you to
free up the slots which don't hold useful data quickly and release
the slots with important data later on. This method gives much
more flexibility but at cost of added free-slot management within
consumer. As consumer is releasing the slot in out-of-order of
their consumption, it has to maintain some state about which slots
are released and which are still in consumption.
\item \textbf{Private-mutable consumption:}
If the application needs a private access to the slots where it
can modify it then it should make copy of this slot in it's
private memory and release the actual slot. This way, application
is free to do anything it wants with the private slot. This
solution scales well with large number of private-mutable
consumers because the data-copy operation is performed by
consumers and not by producer. As data-copy operation is
typically most expensive, we want to push it out from producer
into consumers. If consumers are running on separate cores then
the data-copy operation of one consumer will not affect other
consumers.
\end{itemize}
Once the consumer is done with consumption, the slot will be
returned to the producer. And when all consumers who are sharing
the slot report that they are done with consumption then producer
can mark the slot as free and add it to the list of free slots for
future reuse.
\end{enumerate}
\subsection{Slot management inside producer}
The producer maintains a private meta-slot data-structure for each
slot to track these slots. It also maintains the list of free,
in-generation and in consumption slots.
\subsection{Slot management inside consumer}
The slot management is not needed in consumer if they are following
in-order or private-mutable consumption. In these two cases, the
consumer-queue implicitly does the slot management on behalf of consumers.
In case of out-of-order consumption, the consumer will have to
maintain the list of all slot-pointers which are currently accessible
and also the list of slot-pointers which are done with the use and can
be released as free slots.
\subsection{Freeing up the slots}
Whenever consumer is done with accessing/consuming the data-element,
it should be added back to the free-slot-pointers area of the consumer
queue. It is important to note that the order in which slots will be
freed by consumer need not match the order in which it consumed the
slots. This flexibility allows consumer to take longer time on
certain slots while quickly returning the slots which have arrived
afterwards. Consumer can hold back certain number of slots without
releasing them, but this number depends on the length of the consumer
queue. As the producer controls the consumer-queue length it can give
some consumers more slots than other based on the configurable
policies. The policy that initial implementation of this buffer will
be using is that, the size of consumer-queue will be directly
proportional to the size of shared-pool that particular consumer has
contributed.
\subsection{Slot size}
\label{sec:slotsize}
The size of the slot is greatly dictated by the capabilities of the
generator. If generator needs
continuous memory to produce data-element then the size of
slot will be the size of biggest data-element. If the
generator can DMA the data-element into multiple non-contiguous
memory locations then, the slot size should be based on
the average data-element size. In any case, the slot size
must alway be a multiple of cache size and every slot should
be cache aligned. This is mostly due to the fact that each
data element might get consumed by different consumer running
on different cores, and we do not want cache conflicts when
they are accessing different data-elements.
The dis-advantage of having slots of size of largest data-element
is that it leads to internal fragmentation and waste of memory
when packets are small. On other hand, when slot size is small,
we reduce the capacity of NIC hardware as typically RX-queue
will have limited number of entries, and by using small sized
slots, we will fill up the entries quickly.
\section{Security and trust model}
\label{sec:security-trust}
This section discusses the trust model assumed by this bulk transport
design and it's security implications.
\subsection{Slot access security}
\label{sec:slot-security}
This bulk transport mechanism works by sharing the memory and it
needs security against invalid memory accesses. This security is
provided by the slot-pointers. Slot-pointers are designed to be valid
across the virtual address-spaces of all consumers. Slot-pointers
contain information like shared-pool-id which can be used to find the
starting address and length of the pool. When consumer is calculating
actual pointer, it can always validate that the pointer lies within
shared-pool by making sure that offset is always smaller than
shared-pool size.
\subsection{Security against memory invalidation}
\label{sec:memory-security}
The more problematic case is when one of the consumer dies or
invalidate the memory given to producer without informing first.
This case can lead to invalid memory access in producer,
generator and other consumers as one of the shared-pool is not
accessible any longer. The protection against such a case
should be provided by operating system by not removing the memory
frame as long as it is mapped by at-least one domain/process. If
operating system does not provide such an assurance and if the
consumers are non-trustworthy then producer should somehow take the
complete ownership of the memory provided by the consumer, or provide
all the memory itself.
From above discussion, it can be seen that the trust mainly lies with
producer and not the consumer. Consumers can't do much damage to
producer as all the actions of consumers can be validated by the
producer. For example, when any consumer tries to free up the slot,
the producer can verify if the consumer really holds that slot or not.
\subsection{Security against data sniffing}
\label{sec:security-sniffing}
The only damage that consumer can do is to try and read the
data-elements which are not destined to it. Consumers can do this
as they have read-only access to entire production-pool. Performing
this type of snooping is not trivial as all the meta-data about the
slots is maintained in the private memory by the producer, so consumer
can only guess about location and type of data inside the other
visible slots. But nevertheless unauthorized data-sniffing between
consumers is theoretically possible in this design.
This shortcoming can be overcome by sharing the shared-pool pairwise
between consumer and producer and then by either adding the overhead
of performing data-copy, or by using generator capability to perform
early classification of data elements. The \ref{sec:privacy} will
give more details about these stricter privacy models.
\section{Privacy model}
\label{sec:privacy}
This bulk transport is designed to work in relaxed privacy model with
possibility of tightening the privacy model by either using early
classification capabilities of generator or at added cost of
data-copy. This section describes all of these modes.
\subsection{Relaxed privacy model}
\label{sec:relaxed-privacy}
The bulk-transport design described most of the above document is
based the relaxed privacy model. The reason for describing this model
in detail is that other models which gives better privacy (described
in \ref{sec:data-copy-privacy},
\ref{sec:generator-classification-privacy}) are sub-set of this model
hence they are easy to understand once this model is clear.
In this model, all consumers have read-only access to entire
production-pool. And hence in theory malicious consumers can sniff the
data-elements addressed to other consumers. The
\ref{sec:security-sniffing} gives some description of this issue from
the security perspective.
\subsection{Strict privacy model with additional data-copy}
\label{sec:data-copy-privacy}
In this model, shared-pools are not shared with all consumers, but
every shared-pool is pairwise shared between producer and with only
one consumer (one who has contributed that shared-pool). The initial
shared-pool contributed by the producer itself is shared between
producer and generator. This shared-pool will not be directly
accessible to any consumer. All the data generated in this pool which
is private to producer and generator and then based on the
classification of the data-element, it is explicitly copied into the
shared-pool of the selected consumers. This way, other consumers can
never see the data-elements which are not destined to them. This
stricter privacy policy comes with the cost of additional data-copy.
Also, as producer is performing this data-copy, this can adversely
affect the throughput of the whole system.
\subsection{Strict privacy model with early classification support
from generator}
\label{sec:generator-classification-privacy}
In this model as well, shared-pools are not shared with all
consumers, but every shared-pool is shared between
producer and with only one consumer.
This model depends on the capabilities of generator to provide
stricter privacy model. If the generator is smart enough and can
classify the data-elements before they are created, then these
data-elements can be directly created into the shared-pool of selected
consumer. This avoids additional copy from the producers pool to the
shared-pool of the consumer.
This is an ideal model which provides strict privacy without
compromising on the performance. But it needs smarter generators.
The recent NIC hardwares like \textbf{Solarflair} and \textbf{e10000}
are capable of performing such a classification.
\section{Adaptability with different hardware}
This section discusses the ability of this design to
adapt with different designs and features of NIC hardwares.
We believe that the interface
provided by the producer is generic enough to cover most of the
functionalities provided by the smarter generators. And as we can
control the integration between producer and generator without affecting
the consumers, we can easily adapt to exploit the different hardware
features provided by the generators. One of the example of such an
adaptation can be seen in \ref{sec:generator-classification-privacy},
the design discussed here easily adapted to exploit the early
classification capabilities of the generator to give stricter privacy
based zero copy bulk transport.
\section{Limitations}
This design still misses some good features in favor of keeping the
complexity reasonable. These features include support for
multiple-producer, multiple-consumer setup and ability for consumer to
specify where data-element should be generated (true zero copy
receive).
\section{Conclusion}
This chapter provides the details about the new cross-domain transfer
facility design. This facility allows zero-copy network
implementation which can support sharing between more than two domains
as well as multi-level sharing (sharing by consumers between
consumers). This solution also reduce the number of messages
exchanged by sending them only when it is absolutely necessary.
\end{document}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%