Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 17 May 2012 14:07:28 -0600
From:      Ian Lepore <freebsd@damnhippie.dyndns.org>
To:        Svatopluk Kraus <onwahe@gmail.com>
Cc:        freebsd-arm@freebsd.org, hackers@freebsd.org
Subject:   Re: ARM + CACHE_LINE_SIZE + DMA
Message-ID:  <1337285248.1503.308.camel@revolution.hippie.lan>
In-Reply-To: <CAFHCsPUdZXGKFvmVGgaEUsfhwd28mNVGaY84ExcJp=ogQxzPJQ@mail.gmail.com>
References:  <CAFHCsPUdZXGKFvmVGgaEUsfhwd28mNVGaY84ExcJp=ogQxzPJQ@mail.gmail.com>

next in thread | previous in thread | raw e-mail | index | archive | help
On Thu, 2012-05-17 at 15:20 +0200, Svatopluk Kraus wrote:
> Hi,
> 
> I'm working on DMA bus implementation for ARM11mpcore platform. I've
> looked at implementation in ARM tree, but IMHO it only works with some
> assumptions. There is a problem with DMA on memory block which is not
> aligned on CACHE_LINE_SIZE (start and end) if memory is not coherent.
> 
> Let's have a buffer for DMA which is no aligned on CACHE_LINE_SIZE.
> Then first cache line associated with the buffer can be divided into
> two parts, A and B, where A is a memory we know nothing about it and B
> is buffer memory. The same stands for last cache line associatted with
> the buffer. We have no problem if a memory is coherent. Otherwise it
> depends on memory attributes.
> 
> 1. [no cache] attribute
> No problem as memory is coherent.
> 
> 2. [write throught] attribute
> The part A can be invalidated without loss of any data. It's not problem too.
> 
> 3. [write back] attribute
> In general, there is no way how to keep both parts consistent. At the
> start of DMA transaction, the cache line is written back and
> invalidated. However, as we know nothing about memory associated with
> part A of the cache line, the cache line can be filled again at any
> time and messing up DMA transaction if flushed. Even if the cache line
> is only filled but not flushed during DMA transaction, we must make it
> coherent with memory after that. There is a trick with saving part A
> of the line into temporary buffer, invalidating the line, and
> restoring part A in current ARM (MIPS) implementation. However, if
> somebody is writting to memory associated with part A of the line
> during this trick, the part A will be messed up. Moreover, the part A
> can be part of another DMA transaction.
> 
> To safely use DMA with no coherent memory, a memory with [no cache] or
> [write throught] attributes can be used without problem. A memory with
> [write back] attribute must be aligned on CACHE_LINE_SIZE.
> 
> However, for example mbuf, a buffer for DMA can be part of a structure
> which can be aligned on CACHE_LINE_SIZE, but not the buffer itself. We
> can know that nobody will write to the structure during DMA
> transaction, so it's safe to use the buffer event if it's not aligned
> on CACHE_LINE_SIZE.
> 
> So, in practice, if DMA buffer is not aligned on CACHE_LINE_SIZE and
> we want to avoid bounce pages overhead, we must support additional
> information to DMA transaction. It should be easy to support the
> information about drivers data buffers. However, what about OS data
> buffers like mentioned mbufs?
> 
> The question is following. Is or can be guaranteed for all or at least
> well-known OS data buffers which can be part of DMA access that the
> not CACHE_LINE_SIZE aligned buffers are surrounded by data which
> belongs to the same object as the buffer and the data is not written
> by OS when given to a driver?
> 
> Any answer is appreciated. However, 'bounce pages' is not an answer.
> 
> Thanks, Svata

I'm adding freebsd-arm@ to the CC list; that's where this has been
discussed before.

Your analysis is correct... to the degree that it works at all right
now, it's working by accident.  At work we've been making the good
accident a bit more likely by setting the minimum allocation size to
arm_dcache_align in kern_malloc.c.  This makes it somewhat less likely
that unrelated objects in the kernel are sharing a cache line, but it
also reduces the effectiveness of the cache somewhat.

Another factor, not mentioned in your analysis, is the size of the IO
operation.  Even if the beginning of the DMA buffer is cache-aligned, if
the size isn't exactly a multiple of the cache line size you still have
the partial flush situation and all of its problems.

It's not guaranteed that data surrounding a DMA buffer will be untouched
during the DMA, even when that surrounding data is part of the same
conceptual object as the IO buffer.  It's most often true, but certainly
not guaranteed.  In addition, as Mark pointed out in a prior reply,
sometimes the DMA buffer is on the stack, and even returning from the
function that starts the IO operation affects the cacheline associated
with the DMA buffer.  Consider something like this:

    void do_io()
    {
        int buffer;
        start_read(&buffer);
        // maybe do other stuff here
        wait_for_read_done();
    }

start_read() gets some IO going, so before it returns a call has been
made to bus_dmamap_sync(..., BUS_DMASYNC_PREREAD) and an invalidate gets
done on the cacheline containing the variable 'buffer'.  The act of
returning from the start_read() function causes that cacheline to get
reloaded, so now the stale pre-DMA value of the variable 'buffer' is in
cache again.  Right after that, the DMA completes so that ram has a
newer value that belongs in the buffer variable and the copy in the
cacheline is stale.  

Before control gets into the wait_for_read_done() routine that will
attempt to handle the POSTREAD partial cacheline flush, another thread
gets control and begins setting up a new DMA for another device,
different buffer.  This time it's a read using a 64K buffer for disk IO.
The busdma sync code calls cpu_dcache_inv_range() for that buffer but
because the range is so large, it gets turned into a
cpu_dcache_wbinv_all() because that's cheaper than looping through an
arbitrarily large range invalidating a line at a time.  

Except, ooops, that means we write back to ram the cacheline holding the
stale value of the 'buffer' variable, wiping out the data brought in by
DMA before the partial cachline flush code could do its dance to
preserve it.  

There are several variations of the above scenario; it doesn't require a
stack-allocated buffer to trigger a writeback of a stale value.  Any
cacheline that gets dirtied after a PREREAD invalidate can end up
overwriting fresh DMA data in ram with stale data from the cacheline at
any time, because any call to cpu_dcache_inv_range() or
cpu_dcache_wbinv_range() can get turned into a cpu_dcache_wbinv_all().
That means any DMA operation and also a context switch which calls
cpu_dcache_wbinv_all().

If you rule out bounce buffers as a solution, then I think that may
leave us with just one option:  make the pages uncacheable for the
duration of the DMA.  Essentially force a DMA_COHERENT buffer if the
driver didn't already do so.  I think doing so blindly may have
performance implications every bit as bad as using bounce buffers.  (For
example, turning off cache on a stack page could really hurt.)  The code
to do the remapping already exists in pmap.c as part of handling
multiple mappings for VIVT caches.  From the busdma code you could
accomplish the remapping by making a temporary writable kernel mapping
for the buffer pages in the PRE handling and undo that mapping in the
POST handling.

It may be that a hybrid approach would work.  For an unaligned buffer,
if it isn't already DMA_COHERENT, then if it is below a certain size
bounce it, otherwise remap the buffer's pages.  When I was knee-deep in
this problem last summer one of the things I noticed in our systems was
that large DMA operations (1 KByte or larger buffers) tend to be
DMA_COHERENT buffers, and when not they're already cache aligned and a
power of two in size.  For us the partial cacheline flush situations are
almost always caused by tiny IO of 1 to 128 bytes length, usually
serial-comms or usb related.

I think pre-allocating a few pages for bouncing small unaligned IO would
be a big win compared to remapping the pages as uncacheable.  The
remapping has to take locks and search lists of pages and so on; it
should be way faster to do a small memcpy() instead.  Buffers bigger
than some (perhaps tunable) limit would get remapped instead of
bounced.  

It also might be nice to have a knob to enable logging when bouncing or
remapping is used to avoid partial cacheline operations, to make it easy
to find drivers that could be tweaked for better performance.  If you're
bouncing 2 or 3 operations per second with a 4-byte buffer that's no big
deal.  If every network packet is resulting in bouncing/remapping you'd
want to know about that.

-- Ian





Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?1337285248.1503.308.camel>