Memory Mapped File的WIKI
所謂的memory-mapped file可視為一種虛擬記憶體 (Virtual Memory)的區段 (Segment) 映射,通常都是映射至硬碟或是SSD的非揮發性的記憶裝置上。一但映射好了之後, 就可以如同一般記憶體般地來使用。
好處在於使用這種技術可以提高I/O的效率尤其是對於比較大的檔案,至於對於小檔案來說可能就不是那麼地適合。為啥? 因為作業系統的paging size大約是4 K bytes, 如果我們只映射了5 K bytes的話¸ 那麼就有3 K bytes的paging的空間是浪費了。另外一個好處就是對於一個Size很大的檔案 (比記憶體大), 我們可以使用一小塊記憶體就可以把它映射進來處理。
當然這世上是沒有白吃的午餐的, 使用memory-mapped file有可能會因為page fault (當一段資料被載入到page cache中但是映射到virtual memory的過程還沒完成)而導致速度比正常I/O還差 (還好這種機率不高---擦汗!!!!)
另外要小心的是, 在32位元的作業系統中也不適合使用, 因為32位元的記憶體定址空間有限(大約是4GiB)。
範例程式
以下我將使用兩支Java 的程式來示範如何用memory mapped file來共享資料 (IPC – Inter Process Communication)。一支程式專門用來生產message物件 (producer)及別外一支程式用來消費message物件(consumer), 因為是範例啦, 所以我沒有作太多例外的處理, 理論上兩支程式是可以是時運行的喔!!
IMemoryRepository.java
package ew.blog.memmapipc; /** * 這個interface定義了一個簡單的基於記憶體的訊息存取的方法。 * 為了便利演繹出blog文章的主軸,在訊息(message)的結構上是固定為: * 1.Int - 4 bytes * 2.Long - 8 bytes * 3.Byte - 1 bytes * * 要存放一個message時要先哷叫navigate(index)方法來移動指標到對應的位址, * 然後呼叫setInt, setLong及setByte來存放訊息資料。若要取回訊息資料時, * 也是要先哷叫navigate(index)方法來移動指標到對應的位址, 然後再使用 * getInt, getLong及getByte方法來取回資料 * * @author ErhWen,Kuo (erhwenkuo@gmail.com) * */ public interface IMemoryRepository { void navigate(int index); void setInt(int value); void setLong(long value); void setByte(byte value); int getInt(); long getLong(); byte getByte(); }
MappedByteBufferRepository.java
package ew.blog.memmapipc; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; public class MappedByteBufferRepository implements IMemoryRepository{ // 由於ByteBuffer是一個連續的Memroy區塊, 所以必須要先定義 // 儲放的順序與大小以便存取 // 在本範例中, StoreValue有: // 1: intValue (int: 4 bytes) // 2: longValue (long: 8 bytes) // 3: byteValue (byte: 1 bytes) // 我們會依序來儲放StoreValue的properties private final static int intValue_Offset = 0; private final static int longValue_Offset = 4; private final static int byteValue_Offset = 12; // 在本範例message物件的長度(int:4 + long:8 + byte:1 = 13) private final static int storageValue_Size = 13; private ByteBuffer objectStore; //用來作為定位每個一物件在memory中的位置(position) private int pos; public MappedByteBufferRepository(File memMappedFile, int capacity){ RandomAccessFile raf = null; FileChannel fc = null; try { raf = new RandomAccessFile(memMappedFile, "rw"); fc = raf.getChannel(); objectStore = fc.map(MapMode.READ_WRITE, 0, capacity * storageValue_Size); objectStore.order(ByteOrder.nativeOrder()); } catch (Exception e) { e.printStackTrace(); } finally{ //把FileChannel與RandomAccessFile物件給Close起來 try{ fc.close(); raf.close(); } catch (IOException e) {} } } public void navigate(int index) { pos = index * storageValue_Size; } public void setInt(int value) { objectStore.putInt(pos + intValue_Offset, value); } public void setLong(long value) { objectStore.putLong(pos + longValue_Offset, value); } public void setByte(byte value) { objectStore.put(pos + byteValue_Offset, value); } public int getInt() { return objectStore.getInt(pos + intValue_Offset); } public long getLong() { return objectStore.getLong(pos + longValue_Offset); } public byte getByte() { return objectStore.get(pos + byteValue_Offset); } }
MemMappedMsgWriter.java
package ew.blog.memmapipc; import java.io.File; public class MemMappedMsgWriter { public static void main(String[] args) throws Exception { // 取得temp的目錄路徑 String tmpDir = System.getProperty("java.io.tmpdir"); // 為這個memory mapped的檔案給名 String memMappedFileName = tmpDir + "/memmappedfile.dat"; // 產生一個標準的Java File物件 File memMappedFile = new File(memMappedFileName); // 預計產生一千萬個Message (每個Message長度是13 bytes) int messageSize = 10000000; // 初始一個Memory Mapped File的ByteBuffer來儲存資料 IMemoryRepository memStore = new MappedByteBufferRepository(memMappedFile, messageSize); long start = System.currentTimeMillis(); for(int i=0; i<messageSize; i++){ // 移動指標 memStore.navigate(i); // 儲放資料 memStore.setInt(i); memStore.setLong((long)i); memStore.setByte((byte)i); } long end = System.currentTimeMillis(); System.out.println( String.format("Persist %s Messages, total spends %s ms", messageSize, (end-start))); } }
MemMappedMsgReader.java
package ew.blog.memmapipc; import java.io.File; public class MemMappedMsgReader { public static void main(String[] args) throws Exception { // 取得temp的目錄路徑 String tmpDir = System.getProperty("java.io.tmpdir"); // 為這個memory mapped的檔案給名 String memMappedFileName = tmpDir + "/memmappedfile.dat"; // 產生一個標準的Java File物件 File memMappedFile = new File(memMappedFileName); // 預計讀進一千萬個Messages (每個Message長度是13 bytes) int messageSize = 10000000; // 初始一個Memory Mapped File的ByteBuffer來讀取資料 IMemoryRepository memStore = new MappedByteBufferRepository(memMappedFile, messageSize); long start = System.currentTimeMillis(); for(int i=0; i<messageSize; i++){ // 移動指標 memStore.navigate(i); // 讀取資 料 int messageIntData = memStore.getInt(); long messageLongData = memStore.getLong(); byte messageByteDataq = memStore.getByte(); } long end = System.currentTimeMillis(); System.out.println( String.format("Read %s Messages, total spends %s ms", messageSize, (end-start))); } }
結論
Memory mapped file的技術對於開發IPC (Inter-Process Communication)相關的需求來說應該是一種很不錯的選項。而且讀寫的速度相對使用Socket的方法來說是有效率的多了。
範例的源碼在這裡