Friday, May 27, 2016

Memory Mapped Files Study


In this Post I face a new argument: Memory Mapped Files.

Why this choice?


Simply because I need a shared memory between processes where to store data, and MMF is a really good way to do this. I have a software that needs to read and write data to and from external devices, and the devices registries are global variables in my programs.

This solution guaranties a fast and easy storage, no database mechanisms, between data and applications (logic layer). I don’t pretend to be understood but simply share my studies about MMF.
Just to be clear, I’m not an expert of MMF but only a new user.


Window offers three groups of functions for managing memory in applications: memory-mapped file functions, heap memory functions, and virtual-memory functions.



Memory Mapped Files


A memory-mapped file contains the contents of a file in virtual memory. This mapping between a file and memory space enables an application, including multiple processes, to modify the file by reading and writing directly to the memory. Starting with the .NET Framework 4, you can use managed code to access memory-mapped files in the same way that native Windows functions access memory-mapped files, as described in Managing Memory-Mapped Files in Win32 in the MSDN Library. 

A good article is also: Managing Memory-Mapped Fileds (https://msdn.microsoft.com/en-us/library/ms810613.aspx).

There are two types of memory-mapped files:
  • Persisted memory-mapped files
Persisted files are memory-mapped files that are associated with a source file on a disk. When the last process has finished working with the file, the data is saved to the source file on the disk. These memory-mapped files are suitable for working with extremely large source files.
  • Non-persisted memory-mapped files
Non-persisted files are memory-mapped files that are not associated with a file on a disk. When the last process has finished working with the file, the data is lost and the file is reclaimed by garbage collection. These files are suitable for creating shared memory for inter-process communications (IPC).




This and more can be read in MSDN (https://msdn.microsoft.com/en-us/library/dd997372.aspxhttps://msdn.microsoft.com/en-us/library/dd997372.aspx). 


There are two types of views: 

Stream access view 
Random access view. 

Use stream access views for sequential access to a file; this is recommended for non-persisted files and IPC. Random access views are preferred for working with persisted files.

Functions


Memory-mapped file functions can be thought of as second cousins to the virtual-memory management functions in Windows. Like the virtual-memory functions, these functions directly affect a process's address space and pages of physical memory. No overhead is required to manage the file views, other than the basic virtual-memory management that exists for all processes. These functions deal in reserved pages of memory and committed addresses in a process. The entire set of memory-mapped file functions are not here discussed but only what I need for my example.


Example Project


In my example I will use a Random access view because I need to move up and down the variables list. I defined a class named MemoryManager which encapsulate all the code usefull for the MMF management.



public class MemoryManager
    {
        string msMutexName = "map_mutex";
        string msName = ""; //Memory Area Name
        MemoryMappedFile moMem = null;
        MemoryMappedViewAccessor moAncestor = null;
        
        public string Name { get { return msName;  } set { msName = value; } }

        int miObjSize = 0;
        int miCount = 0; //number of elements\objects

        public MemoryManager()
        {


I image to have a list of objects to map in a MMF, and a list of methods to query and writes values in this list.

No Generics or Classes


In general my list could be a generic list of object, but it is not possible to create standard method that manage Generics like this:

public void Create(List lListOfObjects)

because T or Objects can be nullable! So a Struct is the right choice. In My example I called it MyStruct.


/// /// In order to use MMF it is neccessary using not nullable types (16Byte)
/// 
public struct MyStruct
{
    public int Key; //Primary Key
    public int ValueInt; //Current In Value
    public float ValueFloat; //Current Float Value
    public int EditInt; //New Int value to write
    public float EditFloat; //New Float value to write
    public int Edit; //There is at least one value to write: Int or Float or both

    public MyStruct(int key, int value_int = 0, float value_float = 0, int edit_int = 0, float edit_float = 0, int edit = 0)
    {
        Key = key;
        ValueInt = value_int;
        ValueFloat = value_float;
        EditInt = edit_int;
        EditFloat = edit_float;
        Edit = edit;
    }
}}


public void Create(List lListOfObjects)
{
if (lListOfObjects == null) return;
            
bool mutexCreated;

miObjSize = Marshal.SizeOf(typeof(MyStruct));
miCount = lListOfObjects.Count;

int iBufferSize = miObjSize * miCount;

moMem = MemoryMappedFile.CreateNew(msName, miObjSize * miCount);
moAncestor = moMem.CreateViewAccessor(0, iBufferSize);

Mutex mutex = new Mutex(true, msMutexName, out mutexCreated);

int iIndex = 0;
for (int i = 0; i < miCount; i++)
{
        MyStruct o = lListOfObjects[i];
moAncestor.Write(iIndex, ref o);
iIndex += miObjSize;
       }               

       mutex.ReleaseMutex();
}


The method Create allocate a memory map with CreateNew(), assigning a name and a size to a map. While CreateViewAnchestor() create a random access view. The for section initialize the memory with the list.

From this moment ahead this memory space is available from other process that wants read and write into it.


public string Name { get { return msName;  } set { msName = value; }
public bool Open(int iCount)
{
try
{
miCount = iCount;
moMem = MemoryMappedFile.OpenExisting(msName);
return true;
}
catch (FileNotFoundException)
{
return false;
}            
}
public MyStruct Read(int iIndex)
{
    if (moMem == null)
        return new MyStruct(-1);

    miObjSize = Marshal.SizeOf(typeof(MyStruct));

    if (moAncestor == null)
    {
        int iBufferSize = miObjSize * miCount;
        moAncestor = moMem.CreateViewAccessor(0, iBufferSize);
    }
                

    int iBufferIndex = iIndex * miObjSize;

    MyStruct oRet;
    moAncestor.Read(iBufferIndex, out oRet);

    return oRet;
}

public void Write(int iIndex, MyStruct oValue)
{
    if (moMem == null)
        return;

    bool mutexCreated;
    Mutex mutex = new Mutex(true, msMutexName, out mutexCreated);
    mutex.WaitOne();

    miObjSize = Marshal.SizeOf(typeof(MyStruct));

    if (moAncestor == null)
    {
        int iBufferSize = miObjSize * miCount;
        moAncestor = moMem.CreateViewAccessor(0, iBufferSize);
    }

    int iBufferIndex = iIndex * miObjSize;
            
    if (moAncestor.CanWrite)
    {
        moAncestor.Write(iBufferIndex, ref oValue);
        Debug.Print(string.Format("Write index: {0} -> {1}, {2}, {3}", oValue.Key, oValue.ValueInt, oValue.ValueFloat, oValue.Edit));
    }
    mutex.ReleaseMutex();
}

Shortcut methods

public int ReadInt(int iIndex)
{
    MyStruct oValue = Read(iIndex);
    return oValue.ValueInt;
}

public void WriteInt(int iIndex, int iValue)
{
    MyStruct oValue = Read(iIndex);
    oValue.ValueInt = iValue;
    Write(iIndex, oValue);
}


download the example