The Need for Asynchronous, Zero-Copy Network I/O

The Need for Asynchronous, Zero-Copy Network I/O
Problems and Possible Solutions
Ulrich Drepper
Red Hat, Inc.
[email protected]
Abstract
of data. It also would help to avoid congestion since network buffers can earlier be freed
which in turn ensures that retransmits due to
full network buffers are minimized.
The network interfaces provided by today’s
OSes severely limit the efficiency of network
programs. The kernel copies the data coming in
from network interface at least once internally
before making the data available in the userlevel buffer. This article explains the problems
and introduces some possible solutions. These
necessarily cover more than just the network interfaces themselves, there is a bit more support
needed.
1
Existing interfaces like the POSIX AIO functions fall short of providing the necessary functionality. This is not only due to the fact that
pre-posting of buffers is only possible at a limited scale. A perhaps bigger problem is the
expensive event handling. The event handling
itself has requirements and challenges which
currently cannot be worked around (like waking up too many waiters).
In the remainder of the paper we will see the
different set of interfaces which are needed:
Introduction
Writing scalable network applications is today
more challenging than ever. The problem is
the antiquated (standardized) network API. The
Unix socket API is flexible and remains usable
but with ever higher network speeds, new technologies like interconnects, and the resulting
expected scalability we reach the limits. CPUs
and especially their interface to the memory
subsystem are not capable of dealing with the
high volume of data in the available short timeframes.
• event handling
• physical memory handling
• asynchronous network interfaces
The event handling must work with the existing
select()/poll() interfaces. It also should
be generic enough to be usable for other events
which currently do not map to file descriptors in
the moment (like message queues, futexes, etc).
This way we might finally have one unified inner loop in the event handling of a program.
What is needed is an asynchronous interface for
networking. The asynchronicity would primarily be a means to avoid unnecessary copying
1
Physical memory suddenly becomes important
because network devices address only physical
memory. But physical memory is invisible in
the Unix ABI and this is very much welcome.
If this is not the case the Copy-On-Write concept used on CPUs with MMU-support would
not work. The implementation of functions like
fork() becomes harder. The proposal will
center around making physical memory regions
objects the kernel knows to handle.
sending
read()
recv()
recvfrom()
recvmsg()
write()
send()
sendto()
sendmsg()
Table 1: Network APIs
receiving
sending
read()1
write()1
aio_read() aio_write()
lio_listio()
Finally, using event and physical memory handling, it is possible to define sane interfaces for
asynchronous network handling. In the following sections we will see some ideas on how this
can happen.
Table 2: AIO APIs
Linux provides an asynchronous mode for terminals, sockets, pipes, and FIFOs. If a file
descriptor has the O_ASYNC flag set calls to
read() and write() immediately return and
the kernel notifies the program about completion by sending a signal. This is a slightly
more complicated and more restrictive version
of the AIO interfaces than when using SIGEV_
SIGNAL (see below) and therefore suffers in addition to its own limitation of those of SIGEV_
SIGNAL.
It is not at all guaranteed that these interfaces
will stand the test of time or will even be implemented. The intention of this paper is to get the
ball rolling because the problems are pressing
and we definitely need something along these
lines. Starting only from the bottom (i.e., from
the kernel implementation) has the danger of
ignoring the needs of programmers and might
miss the bigger picture (e.g., integration into a
bigger event handling scheme, for instance).
2
receiving
For truly asynchronous operations on files the
POSIX AIO functions from Table 2 are available. With these interfaces it is possible to submit a number of input and output requests on
one or more file descriptors. Requests are filled
as the data becomes available. No particular
order is guaranteed but requests can have priorities associated with them and the implementation is supposed to order the requests by priority. The interfaces also have a synchronous
mode which comes in handy from time to time.
Interesting here is the asynchronous mode. The
big problem to solve is that somehow the program has to be able to find out when the submitted requests are handled. There are three modes
The Existing Implementation
Network stacks in Unix-like OSes have more
or less the same architecture today as they had
10–20 years ago. The interface the OS provides
for reading and writing from and to network interfaces consists of the interfaces in Table 1.
These interfaces all work synchronously. The
interfaces for reading return only when data
is available or in error conditions. In nonblocking mode they can return immediately if
no data is available, but this is no real asynchronous handling. The data is not transferred
to userlevel in an asynchronous fashion.
1 With
2
the O_ASYNC flag set for the descriptor.
• for the read() and recv() interfaces it
would be necessary that the program is
making such a call just before when the
data arrives. If there is no such call outstanding the kernel has to use its own
buffers or (for unreliable protocols) it can
discard the data.
defined by POSIX:
SIGEV_SIGNAL The completion is signaled
by sending a specified signal to the process. Which thread receives the signal is
determined by the kernel by looking at the
signal masks. This makes it next to impossible to use this mechanism (and O_
ASYNC) in a library which might be linked
into arbitrary code.
• with aio_read() and the equivalent
lio_listio() operation it is possible to
pre-post a number of buffers. When the
number goes down more buffers can be
pre-posted. The main problem with these
interfaces is what happens next. Somehow the program needs to be notified
about arrival of data. The three mechanisms described above are either based
on polling (SIGEV_NONE) or are far too
heavy-weight. Imagine sending 1000s
of signals a second corresponding to the
number of incoming packages. Creating
threads is even more expensive.
SIGEV_THREAD The completion is signaled
by creating a thread which executes a
specified function. This is quite expensive
in spite of NPTL.
SIGEV_NONE No notification is sent. The pro-
gram can query the state of the request
using the aio_error() interface which
returns EINPROGRESS in case the request
has not yet been finished. This is also possible for the other two modes but it is crucial for SIGEV_NONE.
Another problem is that for unreliable protocols
it might be more important to always receive
the last arriving data. It might contain more
relevant information. In this case data which
arrived before should be sacrificed.
The POSIX AIO interfaces are designed for file
operations. Descriptors for sockets might be
used with the Linux implementation but this
is not what the functions are designed for and
there might be problems.
A second problem all implementations have in
common is that the caller can provide arbitrary
memory regions for input and output buffer to
the kernel. This is in general wanted. But if
the network hardware is supposed to transfer
directly into the memory regions specified it is
necessary for the program to use memory that
is special. The network hardware uses Direct
Memory Access (DMA) to write into RAM instead of passing data through the CPU. This
happens at a level below the virtual address
space management, DMA only uses physical
addresses.
All I/O interfaces have some problems in common: the caller provides the buffer into which
the received data is stored. This is a problem
in most situation for even the best theoretical
implementation. Network traffic arrives asynchronously, mostly beyond the control of the
program. The incoming data has to be stored
somewhere or it gets lost.
To avoid copying the data more than once it
would therefore be necessary to have buffers
usable by the user available right the moment
when the data arrives. This means:
Besides possible limitations on where the RAM
for the buffers is located in the physical address
3
space, the biggest problem is that the buffers
must remain in RAM until used. Ordinarily
userlevel programs do not see physical RAM;
the virtual address is an abstraction and the OS
might decide to remove memory pages from
RAM to make room for other processes. If this
would appear while an network I/O request is
pending the DMA access of the network hardware would touch RAM which is now used for
something else.
their website at http://www.opengroup.
org/icsc/. They arrived at the same set of
three problems and proposed solutions. Their
solutions are not implemented, though, and
they have some problems. Most importantly,
the event handling does not integrate with the
file-descriptor-based event handling.
3
This means while buffers are used for DMA
they must not be evicted from RAM. They must
be locked. This is possible with the mlock()
interface but this is a privileged operation. If
a process would be able to lock down arbitrary amounts of memory it would impact all
the other processes on the system which would
be starved of resources. Recent Linux kernels
allow unprivileged processes to lock down a
modest amount of memory (by default eight
pages or so) but this would not be enough for
heavily network oriented applications.
Memory Handling
The main requirement on the memory handling
is to provide memory regions which are available at userlevel and which can be directly accessed by hardware other than the processor.
Network cards and disk controllers can transfer data without the help of the CPU through
DMA. DMA addresses memory based on the
physical addresses. It does not matter how the
physical memory is currently used. If the virtual memory system of the OS decides that a
page of RAM should be used for some other
purpose the devices would overwrite the new
user’s memory unless this is actively prevented.
There is no demand-paging as for the userlevel
code.
The POSIX AIO interfaces certainly show the
way for the interfaces which can solve the networking problems. But we have to solve several problems:
To be sure the DMA access will use the correct buffer, it is necessary to prevent swapping
the destination pages out. This is achieved by
using mlock(). Memory locking depletes the
amount of RAM the system can use to keep as
much of the combined virtual memory of all
processes in RAM. This can severely limit the
performance of the system or eventually prevent it from making any progress. Memory
locking is therefore a privileged operation. This
is the first problem to be solved.
• make DMA-ready memory available to
unprivileged applications;
• create an efficient event handling mechanism which can handle high volumes of
events;
• create I/O interfaces which can use the
new memory and event handling. As a
bonus they should be usable for disk I/O
as well.
The situation is made worse by the fact that
locking can only be implemented on a perpage-basis. Locking one small object on a page
ties down the entire page.
At this point it should be mentioned that a
working group of the OpenGroup, the Interconnect Software Consortium, tried to tackle this
problem. The specification is available from
4
One possibility would be avoid locking pages
in the program and have the kernel instead do
the work all by itself and on-demand. That
means if a network I/O request specifies a
buffer the kernel could automatically make sure
that the memory page(s) containing the buffer
is locked. This would be the most elegant solution from the userlevel point-of-view. But
it would mean significant overhead: for every
operation the memory page status would have
to be checked and if necessary modified. Network operations can be frequent and multiple
buffers can be located on the same page. If
this is known the checks performed by the kernel would be unnecessary and if they are performed the kernel must keep track how many
DMA buffers are located on the page. This solution is likely to be unattractive.
system of the OS can move the page around in
physical memory or even swap it out.
One relatively minor change to the kernel could
allow for such optimizations. If the mmap()
call could be passed a new flag MAP_DMA the
kernel would know what the buffer is used for.
It could keep track of the users of the page and
avoid locking it unless it is necessary. In an
initial implementation the flag could be treated
as an implicit mlock() call. If the flag is correctly implemented it would also be possible to
specify different limits on the amount of memory which can be locked and for DMA respectively. This is no full solution to the problem of
requiring privileges to lock memory, though (an
application could simply have a read() call
pending all the time).
It is possible to defer solving this problem,
fully or in part, to the user. In the least accommodating solution, the kernel could simply
require the userlevel code to use mmap() and
mprotect() with a new flag to create DMAable memory regions. Inside these memory
regions the program can carve out individual
buffers, thereby mitigating the problem of locking down many pages which are only partially
used as buffers. This solution puts all the burden on the userlevel runtime.
The MAP_DMA flag could also help dealing with
the effects of fork(). The POSIX specification requires that no memory locking is inherited by the child. File descriptors are inherited
on the other hand. If parts of the solution for
the new network interfaces uses file descriptors
(as it is proposed later) we would run into a
problem: the interface is usable but before the
first use it would be necessary to re-lock the
memory. With the MAP_DMA flag this could be
avoided. The memory would simply automatically re-locked when it is used in the child for
the first time. To help in situations where the
memory is not used at all after fork(), for example, if an exec call immediately follows, all
MAP_DMA memory is unlocked in the child.
It also has a major disadvantage. Pages locked
using mlock() are locked until they are unlocked or unmapped. But for the purpose
of DMA the pages need not be permanently
locked. The locking is really only needed while
I/O requests using DMA are being executed.
For the network I/O interfaces we are talking
about here the kernel always knows when such
a request is pending. Therefore it is theoretically possible for the kernel to lock the pages
on request. For this the pages would have to be
specially marked. While no request is pending
or if a network interface is used which does not
provide DMA access the virtual memory sub-
Using this one flag alone could limit the performance of the system, though. The kernel will
always have to make sure that the memory is
locked when an I/O request is pending. This
is overhead which could potentially be a limiting factor. The programmer oftentimes has better knowledge of the program semantics. She
would know which memory regions are used
for longer periods of time so that one explicit
5
lock might be more appropriate than implicit
locking performed by the kernel.
epoll_wait()
epoll_pwait()
poll()
ppoll()
select()
pselect()
Table 3: Notification APIs
A second problem is fragmentation. A program is usually not one homogeneous body of
code. Many separate libraries are used which
all could perform network I/O. With the MAP_
DMA method proposed so far each of the libraries would have to allocate its own memory
region. This use of memory might be inefficient
because of the granularity of memory locking
and because not all parts of the program might
need the memory concurrently.
4
Event Handling
The existing event handling mechanisms of
POSIX AIO uses polling, signals, or the creation of threads. Polling is not a general solution. Signals are not only costly, they are also
unreliable. Only a limited, small number of
signals can be outstanding at any time. Once
the limit is reached a program has to fall back
on alternative mechanisms (like polling) until
the situation is rectified. Also, due to the limitations imposed on code usable in signal handlers, writing programs using signal notification is awkward and error-prone. The creation
of threads is even more expensive and despite
the speed of NPTL has absolute no chance to
scale with high numbers of events.
To solve the issue, the problem has to be tackled
at a higher level. We need to abstract the memory handling. Providing interfaces to allocate
and deallocate memory would give the implementation sufficient flexibility to solve these issues and more. The allocation interfaces could
still be implemented using the MAP_DMA flag
and the allocation functions could “simply” be
userlevel interfaces and no system calls. One
possible set of interfaces could look like this:
What is needed is a completely new mechanism for event notification. We cannot use the
same mechanisms as used for synchronous operations on a descriptor for a socket or a file.
If data is available and can be sent, this does
not mean that an asynchronously posted request
has been fulfilled.
int dma_alloc(dma_mem_t *handlep,
size_t size, unsigned int flags);
int dma_free(dma_mem_t handle,
size_t size);
The structure of a program designed to run on
a Unix-y system requires that the event mechanism can be used with the same interfaces
used today for synchronous notification (see
Table 3). It would be possible to invent a completely new notification handling mechanism
and map the synchronous file descriptor operation to it. But why? The existing mechanism
work nicely, they scale well, and programmers
are familiar with them. It also means existing
code does not have to be completely rewritten.
The interfaces which require DMA-able memory would be passed a value of type dma_mem_
t. How this handle is implemented would
be implementation defined and could in fact
change over time. An initial, trivial implementation could even do without support for something like MAP_DMA and use explicit mlock()
calls.
Creating a separate channel (e.g., file descriptor) for each asynchronous I/O request is not
6
scalable. The number of I/O requests can be
high enough to forbid the use of the poll
and select interfaces. The epoll interfaces
would also be problematic because for each request the file descriptor would have to be registered and later unregistered. This overhead
is too big. Furthermore, is file descriptor has a
certain cost in the kernel and therefore the number is limited.
is sufficient, it allows the programmer to refer to additional data allocated somewhere else.
There is no need to allow adding an arbitrary
amount of data. The event data structure can
therefore be of fixed length. This simplifies the
event implementation and possibly allows it to
perform better. If the transmission of the event
structure would be implemented using socket
the SOCK_SEQPACKET type can be used. The
structure could look like this:
What is therefore needed is a kind of bus used
to carry the notifications for many requests.
A mechanism like netlink would be usable.
The netlink sockets receive broadcast traffic
for all the listeners and each process has to filter
out the data which it is interested in. Broadcasting makes netlink sockets unattractive (at
best) for event handling. The possible volume
of notifications might be overwhelming. The
overhead for the unnecessary wake-ups could
be tremendous.
typedef struct event_data {
enum { event_type_aio,
event_type_msq,
event_type_sig } ev_type;
union {
aio_ctx_t *ev_aio;
mqd_t *ev_msq;
sigevent_t ev_sig;
} ev_un;
ssize_t ev_result;
int ev_errno;
void *ev_data;
} event_data_t;
If filtering is accepted as not being a viable
implementation requirement we have as a requirement for the solution that each process
can create multiple, independent event channel,
each capable of carrying arbitrarily many notification events from multiple sources. If we
would not be able to create multiple independent channels a program could not concurrently
and uncoordinatedly create such channels.
This structure can be used to signal events
other than AIO completion. It could be a general mechanism. For instance, there currently
is no mechanism to integrate POSIX message
queues into poll() loops. With an extension to the sigevent structure it could be possible to register the event channel using the
mq_notify() interface. The kernel can be extended to send events in all kinds of situations.
Each channel could be identified by a descriptor. This would then allow the use of the notification APIs in as many places as necessary
independently. At each site only the relevant
events are reported which allows the event handling to be as efficient as possible.
One possible implementation consists of introducing a new protocol family PF_EVENT. An
event channel could then be created with:
An event is not just an impulse, it has to
transmit some information. The request which
caused the event has to be identified. It is usually2 regarded best to allow the programmer
add additional information. A single pointer
2 See
int efd = socket(PF_EVENT,
SOCK_SEQPACKET, 0);
the sigevent structure.
7
int ev_send(int s, const void *buf, size_t len, int flags, ev_t ec,
void *data);
int ev_sendto(int s, const void *buf, size_t len, int flags,
const struct sockaddr *to, socklen_t tolen, ev_t ec, void *data);
int ev_sendmsg(int s, const struct msghdr *msg, int flags, ev_t ec,
void *data);
int ev_recv(int s, void *buf, size_t len, int flags, ev_t ec,
void *data);
int ev_recvfrom(int s, void *buf, size_t len, int flags,
struct sockaddr *to, socklen_t tolen, ev_t ec, void *data);
int ev_recvmsg(int s, struct msghdr *msg, int flags, ev_t ec,
void *data);
Figure 1: Network Interfaces with Event Channel Parameters
The returned handle could be used in poll()
calls and be used as the handle for the event
channel. There are two potential problems
which need some thought:
ec_t ec_create(unsigned flags);
int ec_destroy(ec_t ec);
int ec_to_fd(ec_t ec);
int ec_next_event(ec_t ec,
event_data_t *d);
• The kernel cannot allow the event queue
to take up arbitrary amounts of memory.
There has to be an upper limit on the number of events which can be queued at the
same time. When this happens a special
event should be generated. It might be
possible to use out-of-band notification for
this so that the error is recognized right
away.
The ec_to_fd() function returns a file descriptor which can be used in poll() or
select() calls. An implementation might
choose to make this interface basically a no-op
by implementing the event channel descriptor
as a file descriptor. The ec_next_event()
function returns the next event. A call might
result in a normal read() or recv call but
it might also use a user-level-visible buffer to
avoid the system call overhead. The events signaled by poll() etc can be limited to the arrival of new data. I.e., the userlevel code is responsible for clearing the buffers before waiting for the next event using poll(). The kernel is involved in the delivery of new data and
therefore this type of event can be quite easily
be generated.
• The number of events on a channel can potentially be high. In this case the overhead
of all the read()/recv() calls could be
a limiting factor. It might be beneficial to
apply some of the techniques for the network I/O discussed in the next section to
this problem as well. Then it might be
possible to poll for new events without the
system call overhead.
To enable optimizations like possible userlevelvisible event buffers the actual interface for the
event handling should be something like this:
Handles of type ec_t can be passed to the
asynchronous interfaces. The kernel can then
8
int aio_send(struct aiocb *aiocbp, int flags);
int aio_sendto(struct aiocb *aiocbp, int flags,
const struct sockaddr *to, socklen_t tolen);
int aio_sendmsg(struct aiocb *aiocbp, int flags);
int aio_recv(struct aiocb *aiocbp, int flags);
int aio_recvfrom(struct aiocb *aiocbp, int flags, struct sockaddr *to,
socklen_t tolen);
int aio_recvmsg(struct aiocb *aiocbp, int flags);
Figure 2: Network Interfaces matching POSIX AIO
create appropriate events on the channel. There
will be no fixed relationship between the file
descriptor or socket used in the asynchronous
operation and the event channel. This gives the
most flexibility to the programmer.
5
is the additional value passed back as part of
the event_data_t object read from the event
channel. The event notification would signal
the type of operation by setting ev_type appropriately. Success and the amount of data
received or transmitted are stored in the ev_
errno and ev_result elements.
I/O Interfaces
There are two objections to this approach. First,
the other frequently used interfaces for sockets
(read() and write()) are not handled. Although their functionality is a strict subset of
recv() and send() respectively it might be a
deterrent. The second argument is more severe:
there is no justification to limit the event handling to network transfer. The same functionality would be “nice to have”TM for file, pipe, and
FIFO I/O. Extending the read() and write()
interfaces in the same way as the network I/O
interfaces makes no sense, though. We already
have interfaces which could be extended.
There are several possible levels of innovation
and complexity which can go into the design of
the asynchronous I/O interfaces. It makes sense
to go through them in sequence of increasing
complexity. The more complicated interfaces
will likely take advantage of the same functionality the less complicated need, too. Mentioning the new interfaces here is not meant to imply that all interfaces should be provided by the
implementation.
The simplest of the interfaces can extend the
network interfaces with asynchronous variants
which use the event handling introduced in the
previous section. One possibility is to extend
interfaces like recv() and send() to take additional parameters to use event channels. The
result is seen in Figure 1.
With a simple extension of the sigevent
structure we can reuse the POSIX AIO interfaces. All that would be left to do is to define
appropriate versions of the network I/O interfaces to match the existing POSIX AIO interfaces and change the aiocb structure slightly.
The new interfaces can be seen in Figure 2. The
aiocb structure needs to have one additional
element:
Calls to these functions immediately return.
Valid requests are simply queued and the notifications about the completion are sent via
the event channel ec. The data parameter
9
• programs could get more efficient notification of events (at least more efficient than
signals and thread creation), even for file
I/O;
struct aiocb {
...
struct msghdr *aio_msg;
...
};
• network operations which require the extended functionality of the recv and
send interfaces can be performed asynchronously;
It is used in the aio_sendmsg() and aio_
recvmsg() calls. The implementation can
chose to reuse the memory used for the aio_
buf element because it never gets used at the
same time as aio_msg. The other four interfaces use aio_buf and aio_nbytes to specify the source and destination buffer respectively.
• by pre-posting buffers with aio_read()
or the aio_recv() and now the aio_
recv and aio_send interfaces network
I/O might be able to avoid intermediate
buffers.
Especially the first two points are good arguments to implement these interfaces or at the
very least allow the existing POSIX AIO in interfaces use the event channel notification. As
explained in section section 2 the memory handling of the POSIX AIO functions makes direct use by the network hardware cumbersome
and slower than necessary. Additionally the
system call overhead is high when many interfaces use the event channel notification. As explained network requests have to be submitted.
This can potentially be solved by extending the
lio_listio() interface to allow submit multiple requests at once. But this will not solve
the problem of the resulting event notification
storm. For this we need more radical changes.
The <signal.h> header has to be extended to
define SIGEV_EC. If the sigev_notify element of the sigevent structure is set to this
value the completion is signal by an appropriate
event available on an event channel. The channel is identified by a new element which must
be added to the sigevent structure:
struct sigevent {
...
ec_t sigev_ec;
...
};
6
The additional pointer value which is passed
back to the application is also stored in the
sigevent structure. The application has to
store it in sigev_value.sival_ptr which
is in line with all the other uses of this part of
the sigevent structure.
Advanced I/O Interfaces
For the more advanced interfaces we need to integrate the DMA memory handling into the I/O
interfaces. We need to consider synchronous
and asynchronous interfaces. We could ignore
the synchronous interfaces and require the use
of lio_listio or an equivalent interface but
this is a bit cumbersome to use.
Introducing these additional AIO interfaces and
the SIGEV_EC notification mechanism would
help to solve some problems.
10
int dma_assoc(int sock, dma_mem_t mem, size_t size, unsigned flags);
int dma_disassoc(int sock, dma_mem_t, size_t size);
Figure 3: Association of DMA-able memory to Sockets
int sio_reserve(dma_mem_t dma, void **memp off, size_t size);
int sio_release(dma_mem_t dma, void *mem, size_t size);
Figure 4: Network Buffer Memory Management
For network interfaces it is ideally the interface
which controls the memory into which incoming data is written. Today this happens with
buffers allocated by and under full control of
the kernel. It is conceivable to allow applications to allocate buffers and assign them to a
given interface. This is where dma_alloc()
comes in. The latter possibility has some distinct advantages; mainly, it gives the program
the opportunity to influence the address space
layout. This can be necessary for some programs.3
volume by adding additional buffers.
Once the memory is associated with the socket
the application cannot use it anymore as it
pleases until dma_disassoc() is called. The
kernel has to be notified if the memory is written to and the kernel needs to tell the application when data is available to be read. Otherwise the kernel might start using a DMA
memory region which the program is also using, thus overwriting the data. We therefore
need at least interfaces as shown in Figure 4.
The sio_reserve() interface allows to reserve (parts of) the DMA-able buffer for writing by the application. This will usually be
done in preparation of a subsequent send operation. The dma parameter is the value returned by a previous call to dma_alloc(). We
use a size parameter because this allows the
DMA-able buffer to be split into several smaller
pieces. As explained in section 3 it is more efficient to allocate larger blocks of DMA-able
memory instead of many smaller ones because
memory locking only works with page granularity. The implementation is responsible for
not using the same part of the buffer more than
once at the same time. A pointer to the available memory is returned in the variable pointed
to by memp.
It is usually not possible to associate each network interface with a userlevel process. The
network interface is in most cases a shared resource. The usual Unix network interface rules
therefore need to be followed. A userlevel process opens a socket, binds the socket to a port,
and it can send and receive data. For the incoming data the header decides which port the remote party wants to target. Based on the number, the socket is selected. Therefore the association of the DMA-able buffer should be with a
socket. What is needed are interfaces as can be
seen in Figure 3. It probably should be possible
to associate more than one DMA-able memory
region with a socket. This way it is possible to
dynamically react to unexpected network traffic
When reading from the network the situation
is reversed: the kernel will allocate the mem-
3 For
instance, when address space is scarce or when
fixed addresses are needed.
11
int sio_send(int sock, const void *buf, size_t size, int flags);
int sio_sendto(int sock, const void *buf, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
int sio_sendmsg(int sock, const void *buf, size_t size, int flags);
int sio_recv(int sock, void **buf, size_t size, int flags);
int sio_recvfrom(int sock, const void **buf, size_t size, int flags,
struct sockaddr *to, socklen_t tolen);
int sio_recvmsg(int sock, const void **buf, size_t size, int flags);
Figure 5: Advanced Synchronous Network Interfaces
ory region into which it stores the incoming
data. This happens using the kernel-equivalent
of the sio_reserve() interface. Then the
program is notified about the location and size
of the incoming data. Until the program is done
handling the data the buffer cannot be reused.
To signal that the data has been handled, the
sio_release() interface is used. It is also
possible to use the interface to abort the preparation of a write operation by undoing the effects of a previous sio_reserve() call.
tion must be able to decide the location of the
buffer containing the returned data. The resulting interfaces can be seen in Figure 5.
The programmer has to make sure the buffer
pointers passed to the sio_send functions
have been returned by a sio_reserve() call
or as part of the notification of a previous sio_
recv call. The implementation can potentially
detect invalid pointers.
When the sio_recv functions return, the
pointer pointed to by the second parameter contains the address of the returned data. This address is in the DMA-able memory area associated with the socket. After the data is handled
and the buffer is not used anymore the application has to mark the region as unused by calling
sio_release(). Otherwise the kernel would
run out of memory to store the incoming data
in.
The sio_reserve() and sio_release()
interfaces basically implement dynamic memory allocation and deallocation. It adds an undue burden on the implementation to require
a full-fledged malloc-like implementation. It
is therefore suggested to require a significant
minimum allocation size. If reservations are
also rounded according to the minimum size
this will in turn limit the number of reservations
which can be given out at any given time. It is
possible to use a simple bitmap allocator.
For the asynchronous interfaces one could
imagine simply adding a sigevent structure
parameter to the sio_recv and sio_send interfaces. This is unfortunately not sufficient.
The program must be able to retrieve the error status and the actual number of bytes which
have been received or sent. There is no way
to transmit this information in the sigevent
structure. We could extend it but would duplicate functionality which is already available.
The asynchronous file I/O interfaces have the
What remains to be designed are the actual network interfaces. For the synchronous interfaces we need the equivalent of the send and
recv interfaces. The send interfaces can basically work like the existing Unix interfaces
with the one exception that the memory block
containing the data must be part of a DMA-able
memory region. The recv interfaces need to
have one crucial difference: the implementa12
same problem and the solution is the AIO control block structure aiocb. It only makes sense
to extend the POSIX AIO interfaces. We already defined the additional interfaces needed
in Figure 2. What is missing is the tie-in with
the DMA handling.
I/O. They can help solving a long-standing
problem of the interfaces Unix systems provide. Programs, be it server or interactive programs, are often designed with a central loop
from which the various activities requested are
initiated. There can be one thread working the
inner loop or many. The requested actions can
be performed by the thread which received the
request or a new thread can be created which
performs the action. The threads in the program
are then either waiting in the main loop or busy
working on an action. If the action could potentially be delayed significantly the thread would
add the wait event to the list the main loop handles and then enters the main loop again. This
achieves maximum resource usage.
For this the most simplistic approach is to extend aiocb structure by adding an element
aio_dma_buf of type dma_mem_t replacing
the aio_buf pointer for DMA-ready operations. To use aio_dma_buf instead of aio_
buf the caller passes the new AIO_DMA_BUF
flag to the aio_recv and aio_send interfaces. For the lio_listio() interface it is
possible to define new operations LIO_DMA_
READ and LIO_DMA_WRITE. This leaves the
existing aio_read() and aio_write() interfaces. It would be possible to define alternative interfaces which take a flag parameter or
one could simply ignore the problem and tell
people to use lio_listio() instead.
In reality this is not so easy. Not all events can
be waited on with the same mechanism. POSIX
does not provide mechanisms to use poll() to
wait for messages to arrive in message queues,
for mutexes to be unlocked, etc. This is where
the event channels can help. If we can associate an event channel with these objects the
kernel could generate events whenever the state
changes.
The implementation of the AIO functions to
receive data when operating on DMA-able
buffers could do more than just pass the request to the kernel. The implementation can
keep track of the buffers involved and check for
available data in them before calling the kernel. If data is available the call can be avoided
and the appropriate buffer can be made known
through an appropriate event. When writing the
data could be written into the DMA-able buffer
(if necessary). Depending on the implementation of the user-level/kernel interaction of the
DMA-able buffers it might or might not be necessary to make a system call to notify the kernel
about the new pending data.
7
For POSIX message queues there is fortunately
not much which needs to be done. The mq_
notify() interface takes a sigevent structure parameter. Once the implementation is extended to handle SIGEV_EC for I/O it should
work here, too. One question to be answered
is what to pass as the data parameter which can
be used to identify the request.
For POSIX semaphore we need a new interface to initiate asynchronous waiting. Figure 6 shows the prototype for sem_await().
The first two parameters are the same as for
sem_wait(). The latter two parameters specify the event channel and the parameter to pass
back. When the event reports a successful operation the semaphore has been posted. It is not
necessary to call sem_wait() again.
Related Interfaces
The event channel mechanism is general
enough to be used in other situations than just
13
int sem_await(sem_t semdes, const struct timespec *abstime,
ec_t ec, void *data);
int pthread_mutex_alock(pthread_mutex_t *mutex, ec_t ec, void *data);
Figure 6: Additional Event Channel Users
The actual implementation of this interface will
be more interesting. Semaphores and also mutexes are implemented using futexes. Only part
of the actual implementation is in the kernel.
The kernel does not know the actual protocol
used for the synchronization primitive, this is
left to the implementation. In case the event
channel notification is requested the kernel will
have to learn about the protocol.
Along the way we sketched out a event handling implementation which is not only efficient enough to keep up with the demands
of the network interfaces. It is also versatile
enough to finally allow implementing a unified inner loop of all event driven programs.
With poll or select interfaces being able
to receive event notifications for currently unobservable objects like POSIX/SysV message
queues and futexes many programs have the opportunity to become much easier because special handling for these cases can be removed.
Once the POSIX semaphore problem is solved
it is easy enough to add support for the POSIX
mutexes, read-write mutexes, barriers, etc. The
pthread_mutex_alock() interface is Figure 6 is a possible solution. The other synchronization primitives can be similarly handled. This extends also to the System V message queues and semaphores. The difference
for the latter two is that the implementation is
already completely in the kernel and therefore
the implementation should be significantly simpler.
8
Summary
The proposed interfaces for network I/O have
the potential of great performance improvements. They avoid using the most limiting
resources in a modern computer: memory-toCPU cache and CPU cache-to-memory bandwidth. By minimizing the number of copies
which have to be performed the CPUs have the
chance of keeping up with the faster increasing
network speeds.
14