3

I am using a MemoryMappedFile to exchange data between two processes. So I created/opened the file like this in both processes:

MemoryMappedFile m_MemoryMappedFile = MemoryMappedFile.CreateOrOpen("Demo", 8);

The file access itself is protected with a global mutex in both processes. Now when I write data to the file which is bigger than the defined length of 8 bytes I do NOT get an exception.

var random = new Random();
var testData = new byte[55];
random.NextBytes(testData);
using (var contentAccessor = m_MemoryMappedFile.CreateViewStream())
{
    contentAccessor.Write(testData, 0, testData.Length);
}

So perhaps I am getting something wrong here, but I thought if I create a non persistent memory mapped file with a specified capacity (in my case 8 bytes) it is not allowed to write more data than 8 bytes? Or do I corrupt the memory with my call above? Any explanation would be great?

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Franz Gsell
  • 1,425
  • 2
  • 13
  • 22
  • You create an MMF, which is 8 bytes long. Then write 55 bytes there... so it is apparently a problem. The allocated memory is not self-expanding. Neither is the view, which is between you and MMF here. – Roman R. Feb 08 '15 at 09:02
  • I am not sure it this is your case, but searching in the Native API called by CreateOrOpen and CreateViewStream you stumble on this remarks in the MSDN about CreateFileMapping function : _If an application specifies a size for the file mapping object that is larger than the size of the actual named file on disk and if the page protection allows write access (that is, the flProtect parameter specifies PAGE_READWRITE or PAGE_EXECUTE_READWRITE), then the file on disk is increased to match the specified size of the file mapping object_ – Steve Feb 08 '15 at 09:45
  • Yes I thought there is a problem as well, but it seems it works and I see the 55 bytes in the other application which makes me wonder. And for the documentation stuff from @Steve I have created a non persistent memory mapped file, so I am not sure if this applies here as well? It is really a pitty that the MSDN documentation is so bad on this area. – Franz Gsell Feb 08 '15 at 11:45
  • @FranzGsell yes the documentation is a bit vague, but look at the source code of [MemoryMappedFile](http://referencesource.microsoft.com/#System.Core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,298) – Steve Feb 08 '15 at 11:55

2 Answers2

3

This is specifically mentioned in the documentation for CreateViewStream():

To create a complete view of the memory-mapped file, specify 0 (zero) for the size parameter. If you do this, the size of the view might be larger than the size of the source file on disk. This is because views are provided in units of system pages, and the size of the view is rounded up to the next system page size.

It is indeed rounded up to the page size. Best thing to do is to use the method overload that lets you set the view size:

using (var contentAccessor = m_MemoryMappedFile.CreateViewStream(0, 8))
{
    contentAccessor.Write(testData, 0, testData.Length);
}
Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • yes I know that I can create a view stream with only a capacity of 8 bytes. The question is why my example above is working and I do not get an exception. The other application is able to see the 55 bytes. So I would expect an error here? – Franz Gsell Feb 08 '15 at 18:12
  • Read the quoted MSDN remark again. If you don't know what "system pages" means then you can ask a question about it. – Hans Passant Feb 08 '15 at 18:24
  • Yes @HansPassant it seems I do not really understand the underlying mechanism. When the view is rounded to the next system page size, does it mean that if I write more bytes than the next system page size I will get an exception or is the view than again extended with another system page size? – Franz Gsell Feb 08 '15 at 18:48
  • You get an exception. Use `var testData = new byte[4097];` to see it, a page is 4096 bytes. – Hans Passant Feb 08 '15 at 18:53
0

tl;dr: Yes - a C# MemoryMappedFile will expand - but only to the next largest multiple of Environment.SystemPageSize (typically 4,096 bytes). Once its size exceeds that multiple an exception will be thrown.


I came across this question because I was looking into ways to use MemoryMappedFile to get around the fact that MemoryStream (whose backing field is a byte array) is limited to int.MaxValue values.

I saw the same behaviour that you did where data could be written/read beyond the specified capacity, and noted that the documentation for MemoryMappedFile.CreateOrOpen(string mapName, long capacity) clearly says (emphasis mine):

capacity

Int64

The maximum size, in bytes, to allocate to the memory-mapped file.

which clearly doesn't tally with what we saw.

As Hans Passant points out, the size of the view stream is effectively a multiple of Environment.SystemPageSize which, according to the Wikipedia page for Page Size traditionally had a fixed value of 4,096 bytes.

You can confirm this by inspecting some of the properties on MemoryMappedViewStream, e.g.:

var pageSize = Environment.SystemPageSize;
Console.WriteLine($"System page size: {pageSize} bytes");
Console.WriteLine();

var capacity = 1;

Console.WriteLine($"Creating MemoryMappedFile with capacity of {capacity} byte(s)");
using ( var memoryMappedFile = MemoryMappedFile.CreateOrOpen("Demo", capacity) )
using ( var viewStream = memoryMappedFile.CreateViewStream() )
{
    Console.WriteLine($"View stream capacity: {viewStream.Capacity} bytes");
    Console.WriteLine($"View stream length: {viewStream.Length} bytes");
    Console.WriteLine();

    var randomBytes = new byte[pageSize];
    new Random().NextBytes(randomBytes);
    viewStream.Write(randomBytes, offset: 0, count: randomBytes.Length);

    Console.WriteLine($"{randomBytes.Length} bytes written to view stream");

    viewStream.Position = 0;
    for ( int index = 0; index < randomBytes.Length; index++ )
    {
        var writtenByte = randomBytes[index];
        var readByte = viewStream.ReadByte();

        if ( readByte != writtenByte )
            throw new Exception($"Read byte at index {index} ({readByte}) was not the same as written ({writtenByte})");
    }

    Console.WriteLine($"{randomBytes.Length} bytes successfully read and verified from view stream");
    Console.WriteLine();

    // Attempt to write another byte (this should throw a NotSupportedException )
    try
    {
        viewStream.WriteByte(0);
        throw new InvalidOperationException("An extra byte was written to the view stream when it should not have");
    }
    catch ( NotSupportedException ex )
    {
        Console.WriteLine($"Unable to write additional bytes to view stream:{Environment.NewLine}    {ex.Message}");
    }
}

which outputs:

System page size: 4096 bytes

Creating MemoryMappedFile with capacity of 1 byte(s)
View stream capacity: 4096 bytes
View stream length: 4096 bytes

4096 bytes written to view stream
4096 bytes successfully read and verified from view stream

Unable to write additional bytes to view stream:
    Unable to expand length of this stream beyond its capacity.

Note: The types in the C# System.IO.MemoryMappedFiles namespace are effectively C# wrappers for Windows API calls, e.g. CreateFileMappingW and OpenFileMappingW, for which there is a decent amount of documentation in the Microsoft documentation.

For instance, on Creating a File Mapping Object it says (emphasis mine):

File Mapping Size

The size of the file mapping object is independent of the size of the file being mapped. However, if the file mapping object is larger than the file, the system expands the file before CreateFileMapping returns. If the file mapping object is smaller than the file, the system maps only the specified number of bytes from the file.

The dwMaximumSizeHigh and dwMaximumSizeLow parameters of CreateFileMapping allow you to specify the number of bytes to be mapped from the file:

  • When you do not want the size of the file to change (for example, when mapping read-only files), call CreateFileMapping and specify zero for both dwMaximumSizeHigh and dwMaximumSizeLow. Doing this creates a file mapping object that is exactly the same size as the file. Otherwise, you must calculate or estimate the size of the finished file because file mapping objects are static in size; once created, their size cannot be increased or decreased.

On Managing Memory-Mapped Files it says:

What Do Memory-Mapped Files Have to Offer?

One advantage to using MMF I/O is that the system performs all data transfers for it in 4K pages of data.

This agrees with the documentation for MemoryMappedFile.CreateViewStream which says (emphasis mine):

To create a complete view of the memory-mapped file, specify 0 (zero) for the size parameter. If you do this, the size of the view might be larger than the size of the source file on disk. This is because views are provided in units of system pages, and the size of the view is rounded up to the next system page size.

In other words, as an implementation detail of MemoryMappedFile (i.e. it being a wrapper for Windows API calls), the capacity passed to the static constructors is effectively rounded up to the next largest multiple of the system page size because the virtual address space is split into pages (of typically 4,096 bytes).


Related questions:

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92