如何动态扩展内存映射文件

30
我将使用C#来解决以下需求: - 创建一个可以快速接收大量数据的应用程序 - 在接收更多数据时,必须能够分析已接收的数据。 - 尽可能少地使用CPU和磁盘。
我的算法想法是...
SIZE = 10MB
Create a mmf with the size of SIZE
On data recived:
  if data can't fit mmf: increase mmf.size by SIZE
  write the data to mmf

-> 当前“房间/空间”使用完毕后,磁盘上的大小以10MB为增量进行增加。

如何在C#中执行“通过SIZE增加mmf.size”? 我找到了很多关于创建mmfs和视图的简单示例,但唯一一个我看到实际增加mmfs区域的地方(link)使用的代码无法编译。任何帮助将不胜感激。

编辑 这会导致异常:

private void IncreaseFileSize()
{
    int theNewMax = this.currentMax + INCREMENT_SIZE;
    this.currentMax = theNewMax;

    this.mmf.Dispose();

    this.mmf = MemoryMappedFile.CreateFromFile(this.FileName, FileMode.Create, "MyMMF", theNewMax);
    this.view = mmf.CreateViewAccessor(0, theNewMax);            
}

抛出此异常:由于另一个进程正在使用,无法访问文件'C:\Users\moberg\Documents\data.bin'。


1
为什么那个页面上的代码无法编译?在我看来它是有效的。 - Edwin de Koning
它使用了一个不存在的重载 - “MemoryMappedFile.CreateFromFile(file,null,1000);” - Moberg
由于另一个进程正在使用该文件,因此无法访问'C:\Users\molsgaar\Documents\data.bin'。 - Moberg
这个任务一定要使用MMF吗?你不能只用常规的文件访问方式吗——创建或打开一个文件进行追加,然后将数据写入文件末尾(这样文件会自动增长)。你能否提供更多关于数据分析的上下文信息,或者是谁将对其进行分析? - Gavi Lock
5个回答

28

一旦将文件映射到内存中,您无法增加其大小。这是内存映射文件的已知限制。

...您必须计算或估计完成文件的大小,因为文件映射对象在大小上是静态的;一旦创建,它们的大小就不能增加或减少。

一种策略是使用存储在给定大小的非持久化内存映射文件中的块, 比如1GB或2GB。您可以通过自己设计的顶层ViewAccessor来管理它们(可能需要从MemoryMappedViewAccessor中传递您需要的基本方法)。

编辑:或者您可以只创建一个非持久化内存映射文件,其最大大小为您预期使用的大小(例如8GB开始,具有用于调整应用程序启动时的参数),并检索每个逻辑块的MemoryMappedViewAccessor。除非请求每个视图,否则非持久性文件不会使用物理资源。


谢谢。我有一种感觉,我在一个对我来说有点陌生的领域里摸索。 - Moberg
你可以使用NtExtendSection函数来增加它们的大小。https://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Section/NtExtendSection.html - Matt
@Matt 看起来你的链接特别提到了 ntdll.dll 中的 'shared (memory) section' API 不适用于文件系统支持的内存映射文件... "如果该部分是一个映射文件,则函数失败。" 更多信息请参见此(可疑的)链接 - Glenn Slayden

7

你可以实现可增长的内存映射文件!

这是我的一个可增长的内存映射文件的实现:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.MemoryMappedFiles;

namespace MmbpTree
{
    public unsafe sealed class GrowableMemoryMappedFile : IDisposable
    {

        private const int AllocationGranularity = 64 * 1024;

        private class MemoryMappedArea
        {
            public MemoryMappedFile Mmf;
            public byte* Address;
            public long Size;
        }


        private FileStream fs;

        private List<MemoryMappedArea> areas = new List<MemoryMappedArea>();
        private long[] offsets;
        private byte*[] addresses;

        public long Length
        {
            get {
                CheckDisposed();
                return fs.Length;
            }
        }

        public GrowableMemoryMappedFile(string filePath, long initialFileSize)
        {
            if (initialFileSize <= 0 || initialFileSize % AllocationGranularity != 0)
            {
                throw new ArgumentException("The initial file size must be a multiple of 64Kb and grater than zero");
            }
            bool existingFile = File.Exists(filePath);
            fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
            if (existingFile)
            {
                if (fs.Length <=  0 || fs.Length % AllocationGranularity != 0)
                {
                    throw new ArgumentException("Invalid file. Its lenght must be a multiple of 64Kb and greater than zero");
                }
            }
            else
            { 
                fs.SetLength(initialFileSize);
            }
            CreateFirstArea();
        }

        private void CreateFirstArea()
        {
            var mmf = MemoryMappedFile.CreateFromFile(fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,  null, HandleInheritability.None, true);
            var address = Win32FileMapping.MapViewOfFileEx(mmf.SafeMemoryMappedFileHandle.DangerousGetHandle(), 
                Win32FileMapping.FileMapAccess.Read | Win32FileMapping.FileMapAccess.Write,
                0, 0, new UIntPtr((ulong) fs.Length), null);
            if (address == null) throw new Win32Exception();

            var area = new MemoryMappedArea
            {
                Address = address,
                Mmf = mmf,
                Size = fs.Length
            };
            areas.Add(area);

            addresses = new byte*[] { address };
            offsets = new long[] { 0 };

        }


        public void Grow(long bytesToGrow)
        {
            CheckDisposed();
            if (bytesToGrow <= 0 || bytesToGrow % AllocationGranularity != 0)  {
                throw new ArgumentException("The growth must be a multiple of 64Kb and greater than zero");
            }
            long offset = fs.Length;
            fs.SetLength(fs.Length + bytesToGrow);
            var mmf = MemoryMappedFile.CreateFromFile(fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite, null, HandleInheritability.None, true);
            uint* offsetPointer = (uint*)&offset;
            var lastArea = areas[areas.Count - 1];
            byte* desiredAddress = lastArea.Address + lastArea.Size;
            var address = Win32FileMapping.MapViewOfFileEx(mmf.SafeMemoryMappedFileHandle.DangerousGetHandle(), 
                Win32FileMapping.FileMapAccess.Read | Win32FileMapping.FileMapAccess.Write,
                offsetPointer[1], offsetPointer[0], new UIntPtr((ulong)bytesToGrow), desiredAddress);
            if (address == null) {
                address = Win32FileMapping.MapViewOfFileEx(mmf.SafeMemoryMappedFileHandle.DangerousGetHandle(),
                   Win32FileMapping.FileMapAccess.Read | Win32FileMapping.FileMapAccess.Write,
                   offsetPointer[1], offsetPointer[0], new UIntPtr((ulong)bytesToGrow), null);
            }
            if (address == null) throw new Win32Exception();
            var area = new MemoryMappedArea {
                Address = address,
                Mmf = mmf,
                Size = bytesToGrow
            };
            areas.Add(area);
            if (desiredAddress != address) {
                offsets = offsets.Add(offset);
                addresses = addresses.Add(address);
            }
        }

        public byte* GetPointer(long offset)
        {
            CheckDisposed();
            int i = offsets.Length;
            if (i <= 128) // linear search is more efficient for small arrays. Experiments show 140 as the cutpoint on x64 and 100 on x86.
            {
                while (--i > 0 && offsets[i] > offset);
            }
            else // binary search is more efficient for large arrays
            {
                i = Array.BinarySearch<long>(offsets, offset);
                if (i < 0) i = ~i - 1;
            }
            return addresses[i] + offset - offsets[i];
        }

        private bool isDisposed;

        public void Dispose()
        {
            if (isDisposed) return;
            isDisposed = true;
            foreach (var a in this.areas)
            {
                Win32FileMapping.UnmapViewOfFile(a.Address);
                a.Mmf.Dispose();
            }
            fs.Dispose();
            areas.Clear();
        }

        private void CheckDisposed()
        {
            if (isDisposed) throw new ObjectDisposedException(this.GetType().Name);
        }

        public void Flush()
        {
            CheckDisposed();
            foreach (var area in areas)
            {
                if (!Win32FileMapping.FlushViewOfFile(area.Address, new IntPtr(area.Size))) {
                    throw new Win32Exception();
                }
            }
            fs.Flush(true);
        }
    }
}

这里是 Win32FileMapping 类:

using System;
using System.Runtime.InteropServices;

namespace MmbpTree
{
    public static unsafe class Win32FileMapping
    {
        [Flags]
        public enum FileMapAccess : uint
        {
            Copy = 0x01,
            Write = 0x02,
            Read = 0x04,
            AllAccess = 0x08,
            Execute = 0x20,
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern byte* MapViewOfFileEx(IntPtr mappingHandle,
                                            FileMapAccess access,
                                            uint offsetHigh,
                                            uint offsetLow,
                                            UIntPtr bytesToMap,
                                            byte* desiredAddress);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool UnmapViewOfFile(byte* address);


        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool FlushViewOfFile(byte* address, IntPtr bytesToFlush);
    }
}

这里有一个名为 Extensions 的类:

using System;

namespace MmbpTree
{
    public static class Extensions
    {
        public static T[] Add<T>(this T[] array, T element)
        {
            var result = new T[array.Length + 1];
            Array.Copy(array, result, array.Length);
            result[array.Length] = element;
            return result;
        }

        public static unsafe byte*[] Add(this byte*[] array, byte* element)
        {
            var result = new byte*[array.Length + 1];
            Array.Copy(array, result, array.Length);
            result[array.Length] = element;
            return result;
        }
    }
}

正如您所见,我采用的是不安全的方法。这是获得内存映射文件性能优势的唯一途径。

要使用此功能,您需要考虑以下概念:

  • 页面。这是您工作的最小连续内存地址和存储空间区域。块或页面的大小必须是基础系统页大小(4Kb)的倍数。
  • 初始文件大小。它必须是块或页面大小的倍数,并且必须是系统分配粒度(64Kb)的倍数。
  • 文件增长。它必须是块或页面大小的倍数,并且必须是系统分配粒度(64Kb)的倍数。

例如,您可能希望使用1Mb的页面大小、64Mb的文件增长和1Gb的初始大小进行工作。您可以通过调用GetPointer获取页面指针,使用Grow增加文件大小,并使用Flush刷新文件:

const int InitialSize = 1024 * 1024 * 1024;
const int FileGrowth = 64 * 1024 * 1024;
const int PageSize = 1024 * 1024;
using (var gmmf = new GrowableMemoryMappedFile("mmf.bin", InitialSize))
{
    var pageNumber = 32;
    var pointer = gmmf.GetPointer(pageNumber * PageSize);

    // you can read the page content:
    byte firstPageByte = pointer[0];
    byte lastPageByte = pointer[PageSize - 1];

    // or write it
    pointer[0] = 3;
    pointer[PageSize -1] = 43;


    /* allocate more pages when needed */
    gmmf.Grow(FileGrowth);

    /* use new allocated pages */

    /* flushing the file writes to the underlying file */ 
    gmmf.Flush();

}

编译时针对.NET 4.0会抛出异常,但在其他情况下运行良好。 - Greg Mulvihill
1
MemoryMappedViewAccessor比FileStream还要慢。但是,您可以从MemoryMappedViewAccessor获取指针,这很快。但是,MemoryMappedFile.CreateViewAccessor不允许您请求所需的地址,这对于保持映射块尽可能连续至关重要。 - Jesús López
@GlennSlayden。谢谢您。目前我正在采用另一种方法,引入会话概念和重新映射,当无法分配连续内存时,以便我始终将整个文件映射到连续内存中。请看我正在开发的宠物项目https://github.com/jesuslpm/PersistentHashing - Jesús López
实际上,我已经这么做了。代码建议的正确方法是利用MEM_RESERVE_PLACEHOLDERMEM_REPLACE_PLACEHOLDERMapViewOfFile3VirtualAlloc2等函数中。 - Glenn Slayden
1
有没有Linux的等效外部调用?我知道mmap,只是不确定如何将其组合在一起。 - Bruno Zell
显示剩余2条评论

1
代码无法编译的原因是使用了不存在的重载。要么自己创建一个文件流并将其传递给正确的重载(假设2000是您的新大小):
FileStream fs = new FileStream("C:\MyFile.dat", FileMode.Open);
MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fs, "someName", 2000,
 MemoryMappedFileAccess.ReadWriteExecute, null, HandleInheritablity.None, false);

或者使用这个重载来跳过文件流的创建:

MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile("C:\MyFile.dat", 
          FileMode.Open, "someName", 2000);

当我这样做时,我会得到一个访问冲突错误 - 即使在重新分配之前处理旧的mmf。 - Moberg
@Moberg:你在使用这两种方法中的哪一种,异常信息告诉了你什么? - Edwin de Koning

1
我发现使用相同名称但新尺寸关闭并重新创建MMF在所有意图和目的上都有效。
                using (var mmf = MemoryMappedFile.CreateOrOpen(SenderMapName, 1))
                {
                    mmf.SafeMemoryMappedFileHandle.Close();
                }
                using (var sender = MemoryMappedFile.CreateNew(SenderMapName, bytes.Length))

而且它非常快。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接