7

I recently came across this article which provided a nice intro to memory mapped files and how it can be shared between two processes. Here is the code for a process that reads in the file:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMapReader {

 /**
  * @param args
  * @throws IOException 
  * @throws FileNotFoundException 
  * @throws InterruptedException 
  */
 public static void main(String[] args) throws FileNotFoundException, IOException, InterruptedException {

  FileChannel fc = new RandomAccessFile(new File("c:/tmp/mapped.txt"), "rw").getChannel();

  long bufferSize=8*1000;
  MappedByteBuffer mem = fc.map(FileChannel.MapMode.READ_ONLY, 0, bufferSize);
  long oldSize=fc.size();

  long currentPos = 0;
  long xx=currentPos;

  long startTime = System.currentTimeMillis();
  long lastValue=-1;
  for(;;)
  {

   while(mem.hasRemaining())
   {
    lastValue=mem.getLong();
    currentPos +=8;
   }
   if(currentPos < oldSize)
   {

    xx = xx + mem.position();
    mem = fc.map(FileChannel.MapMode.READ_ONLY,xx, bufferSize);
    continue;   
   }
   else
   {
     long end = System.currentTimeMillis();
     long tot = end-startTime;
     System.out.println(String.format("Last Value Read %s , Time(ms) %s ",lastValue, tot));
     System.out.println("Waiting for message");
     while(true)
     {
      long newSize=fc.size();
      if(newSize>oldSize)
      {
       oldSize = newSize;
       xx = xx + mem.position();
       mem = fc.map(FileChannel.MapMode.READ_ONLY,xx , oldSize-xx);
       System.out.println("Got some data");
       break;
      }
     }   
   }

  }

 }

}

I have, however, a few comments/questions regarding that approach:

If we execute the reader only on an empty file, i.e run

  long bufferSize=8*1000;
  MappedByteBuffer mem = fc.map(FileChannel.MapMode.READ_ONLY, 0, bufferSize);
  long oldSize=fc.size();

This will allocate 8000 bytes which will now extend the file. The buffer that this returns has a limit of 8000 and a position of 0, therefore, the reader can proceed and read empty data. After this happens, the reader will stop, as currentPos == oldSize.

Supposedly now the writer comes in (code is omitted as most of it is straightforward and can be referenced from the website) - it uses the same buffer size, so it will write first 8000 bytes, then allocate another 8000, extending the file. Now, if we suppose this process pauses at this point, and we go back to the reader, then the reader sees the new size of the file and allocates the remainder (so from position 8000 until 1600) and starts reading again, reading in another garbage...

I am a bit confused whether there is a why to synchronize those two operations. As far as I see it, any call to map might extend the file with really an empty buffer (filled with zeros) or the writer might have just extended the file, but has not written anything into it yet...

Bober02
  • 15,034
  • 31
  • 92
  • 178
  • 1
    Anytime I see "write" and "shared data", I think synchronization will be needed. – duffymo Mar 03 '14 at 17:39
  • I don't know what you mean by 'whether there is a why to synchronize', but opening lots of memory mapped files, or the same one multiple times, is a very bad idea anyway, for garbage collection reasons, as there is no well-defined time that the memory concerned can be released. And there's no particular advantage to mapping in tiny quantities like 8k: you may as well just use buffered streams, which have that much buffering by default, and none of this malarkey about what to do when the file is extended. Mapped files are best when used on a very small number, such as one, of very large files. – user207421 Mar 03 '14 at 22:02
  • OK, got it - open one large file. Still, this is the mean for IPC, so I want to know how that can be achieved i..e one process writes, the other one reads, but in a way that we know the other process actually wrote sth before we read from it. This is the synchronization I am talking about – Bober02 Mar 04 '14 at 10:24
  • It is a file not a pipe. Using mmap() alone will not allow you to synchronize. The sample code does (ugly) busy polling. – eckes May 26 '14 at 20:07

3 Answers3

13

I do a lot of work with memory-mapped files for interprocess communication. I would not recommend Holger's #1 or #2, but his #3 is what I do. But a key point is perhaps that I only ever work with a single writer - things get more complicated if you have multiple writers.

The start of the file is a header section with whatever header variables you need, most importantly a pointer to the end of the written data. The writer should always update this header variable after writing a piece of data, and the reader should never read beyond this variable. A thing called "cache coherency" that all mainstream CPU's have will guarantee that the reader will see memory writes in the same sequence they are written, so the reader will never read uninitialised memory if you follow these rules. (An exception is where the reader and writers are on different servers - cache coherency doesn't work there. Don't try to implement shared memory across different servers!)

There is no limit to how frequently you can update the end-of-file pointer - it's all in memory and there won't be any i/o involved, so you can update it each record or each message you write.

ByteBuffer has versions of 'getInt()' and 'putInt()' methods which take an absolute byte offset, so that's what I use for reading & writing the end-of-file marker...I never use the relative versions when working with memory-mapped files.

There's no way you should use the file size or yet another interprocess method to communicate the end-of-file marker and no need or benefit when you already have shared memory.

Tim Cooper
  • 10,023
  • 5
  • 61
  • 77
  • +1. You could state how you prevent two writers from trying to extend the file at the same place at the same time. I've also seen the header used to implement the locks themselves. – user207421 Apr 12 '14 at 00:48
  • Store order will be respected on x86, but not any other "mainstream" CPU. Also your compiler/JVM may be allowed to re-order your stores (if they're not volatile or ordered.) – Eloff May 29 '14 at 22:52
  • Store order will be respected on all mainstream compilers. It's called "cache coherency". There were some experimental CPU's developed that don't respect "cache coherency" but they never became mainstream. I've used this technique for many years on a great many computers, Windows and Unix. – Tim Cooper May 31 '14 at 03:10
  • Remember we're not storing POJO's in the shared memory, we're using 'getInt()' etc. methods, and everything here is implicitly volatile. – Tim Cooper May 31 '14 at 03:23
  • I think Eloff is right on this. @TimCooper, cache coherency refers to the different cache units on a machine having the same view of main memory. The cache coherency protocol (MESI and others based on it) ensures values are consistent. What Eloff is writing about is read/write ordering where the compiler or CPU can reorder reads and writes as long as single-thread correctness is maintained. Memory barriers are what is needed to fix that (or using volatile which includes implicit barriers). – JasonN Dec 21 '14 at 20:48
  • Agreed, you need the keyword "volatile" to have store order respected. But anyway, we're talking about memory-mapped files in Java, right? With .getInt() et al? I'd be pretty surprised if you said those calls can be reordered. – Tim Cooper Dec 22 '14 at 23:11
  • @TimCooper, you gusy are both right but on two different topics. For linux, writes to mmap shared memory is visible to other processes *immediately* with cache coherence, if you just read a flag by getInt(), it is perfectly fine, it's atomic and always most update to date, I guess this is what you talk about. BUT, if you write two flags and the order matters, you need a memory fence to drain CPU load/store/WC buffers, otherwise reordering happens. Cache coherence ensures *immediate* visibility, but it does not ensure reordering, that's Eloff/JasonN talk about. – Daniel May 14 '15 at 10:43
  • 1
    The question specifically mentions "java" and "interprocess". Therefore we're talking about MappedByteBuffer and getInt(). Why is everyone talking about issues relevant only to C++ and/or threads? – Tim Cooper May 16 '15 at 03:32
5

Check out my library Mappedbus (http://github.com/caplogic/mappedbus) which enables multiple Java processes (JVMs) to write records in order to the same memory mapped file.

Here's how Mappedbus solves the synchronization problem between multiple writers:

  • The first eight bytes of the file make up a field called the limit. This field specifies how much data has actually been written to the file. The readers will poll the limit field (using volatile) to see whether there's a new record to be read.

  • When a writer wants to add a record to the file it will use the fetch-and-add instruction to atomically update the limit field.

  • When the limit field has increased a reader will know there's new data to be read, but the writer which updated the limit field might not yet have written any data in the record. To avoid this problem each record contains an initial byte which make up the commit field.

  • When a writer has finished writing a record it will set the commit field (using volatile) and the reader will only start reading a record once it has seen that the commit field has been set.

(BTW, the solution has only been verified to work on Linux x86 with Oracle's JVM. It most likely won't work on all platforms).

MikaelJ
  • 331
  • 3
  • 5
3

There are several ways.

  1. Let the writer acquire an exclusive Lock on the region that has not been written yet. Release the lock when everything has been written. This is compatible to every other application running on that system but it requires the reader to be smart enough to retry on failed reads unless you combine it with one of the other methods

  2. Use another communication channel, e.g. a pipe or a socket or a file’s metadata channel to let the writer tell the reader about the finished write.

  3. Write at a position in the file a special marker (being part of the protocol) telling about the written data, e.g.

    MappedByteBuffer bb;
    …
    // write your data
    
    bb.force();// ensure completion of all writes
    bb.put(specialPosition, specialMarkerValue);
    bb.force();// ensure visibility of the marker
    
Holger
  • 285,553
  • 42
  • 434
  • 765