Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 95fad11

Browse files
committed
Improve reliability of System.IO.MemoryMappedFiles
When a MemoryMappedFile is created backed by memory (rather than by a file), in most cases we create a temporary backing store for it to be used by mmap when creating the views. That backing store may be either a POSIX shared memory object or a temporary file, depending on the platform, and it enables us to better match the exposed API, e.g. enabling multiple views over the same data where some have copy-on-write access, etc. However, the current implementation only unlinks that backing store when it's Disposed; this means that if the process crashes, the backing store could be left around indefinitely (in the case of a memory object, until restart, and in the case of a temporary file, until tmps are cleared out). This change moves the unlink up to be done immediately after the backing store is created and opened, and then cleans up a bunch of cruft left over that's no longer necessary. This has multiple advantages: in particular, we'll clean up the backing store much more reliably in the face of crashes, and the name will only be visible to others on the system for the briefest of moments. As I was changing these files, I took the opportunity to rename them away from the initial platform they were targeting and instead based on their characteristics. This should make it easier for other systems, e.g. FreeBSD, to use whichever implementation best suits its need.
1 parent 96bb8e0 commit 95fad11

File tree

7 files changed

+84
-93
lines changed

7 files changed

+84
-93
lines changed

src/System.IO.MemoryMappedFiles/src/Microsoft/Win32/SafeMemoryMappedFileHandle.Unix.cs

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ namespace Microsoft.Win32.SafeHandles
1313
{
1414
public sealed partial class SafeMemoryMappedFileHandle : SafeHandle
1515
{
16-
/// <summary>Indicates where the FileHandle came from, which then controls if/how it should be cleaned up.</summary>
17-
internal enum FileStreamSource
18-
{
19-
Provided,
20-
ManufacturedFile,
21-
ManufacturedSharedMemory,
22-
}
23-
2416
/// <summary>Counter used to produce a unique handle value.</summary>
2517
private static long s_counter = 0;
2618

@@ -31,15 +23,8 @@ internal enum FileStreamSource
3123
/// </summary>
3224
internal readonly FileStream _fileStream;
3325

34-
/// <summary>Indication as to where the file stream came from, if it exists.</summary>
35-
internal readonly FileStreamSource _fileStreamSource;
36-
37-
/// <summary>
38-
/// The name of the map, currently used to give internal names to anonymous,
39-
/// memory-backed maps. This will be null if the map is file-backed or in
40-
/// some corner cases of memory-backed maps, e.g. read-only.
41-
/// </summary>
42-
internal readonly string _mapName;
26+
/// <summary>Whether this SafeHandle owns the _fileStream and should Dispose it when disposed.</summary>
27+
internal readonly bool _ownsFileStream;
4328

4429
/// <summary>The inheritability of the memory-mapped file.</summary>
4530
internal readonly HandleInheritability _inheritability;
@@ -54,24 +39,23 @@ internal enum FileStreamSource
5439
internal readonly long _capacity;
5540

5641
/// <summary>Initializes the memory-mapped file handle.</summary>
57-
/// <param name="mapName">The name of the map; may be null.</param>
5842
/// <param name="fileStream">The underlying file stream; may be null.</param>
59-
/// <param name="fileStreamSource">The source of the file stream.</param>
43+
/// <param name="ownsFileStream">Whether this SafeHandle is responsible for Disposing the fileStream.</param>
6044
/// <param name="inheritability">The inheritability of the memory-mapped file.</param>
6145
/// <param name="access">The access for the memory-mapped file.</param>
6246
/// <param name="options">The options for the memory-mapped file.</param>
6347
/// <param name="capacity">The capacity of the memory-mapped file.</param>
6448
internal SafeMemoryMappedFileHandle(
65-
string mapName,
66-
FileStream fileStream, FileStreamSource fileStreamSource, HandleInheritability inheritability,
49+
FileStream fileStream, bool ownsFileStream, HandleInheritability inheritability,
6750
MemoryMappedFileAccess access, MemoryMappedFileOptions options,
6851
long capacity)
6952
: base(new IntPtr(-1), ownsHandle: true)
7053
{
54+
Debug.Assert(!ownsFileStream || fileStream != null, "We can only own a FileStream we're actually given.");
55+
7156
// Store the arguments. We'll actually open the map when the view is created.
72-
_mapName = mapName;
7357
_fileStream = fileStream;
74-
_fileStreamSource = fileStreamSource;
58+
_ownsFileStream = ownsFileStream;
7559
_inheritability = inheritability;
7660
_access = access;
7761
_options = options;
@@ -84,26 +68,17 @@ internal SafeMemoryMappedFileHandle(
8468

8569
protected override void Dispose(bool disposing)
8670
{
87-
if (disposing && _fileStream != null && _fileStreamSource != FileStreamSource.Provided)
71+
if (disposing && _ownsFileStream)
8872
{
89-
// Clean up the file if we created it
73+
// Clean up the file descriptor (either for a file on disk or a shared memory object) if we created it
9074
_fileStream.Dispose();
9175
}
9276
base.Dispose(disposing);
9377
}
9478

9579
protected override unsafe bool ReleaseHandle()
9680
{
97-
if (_fileStreamSource == FileStreamSource.ManufacturedSharedMemory)
98-
{
99-
Debug.Assert(_mapName != null);
100-
Debug.Assert(_fileStream != null);
101-
return Interop.libc.shm_unlink(_mapName) == 0;
102-
}
103-
104-
// For _fileHandleSource == File, there's nothing to clean up, as it's either the caller's responsibility
105-
// or it was created as DeleteOnClose (if it was a temporary backing store).
106-
81+
// Nothing to clean up. We unlinked immediately after creating the backing store.
10782
return true;
10883
}
10984

src/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,6 @@
122122
<Compile Include="$(CommonPath)\Interop\Unix\Interop.IOErrors.cs">
123123
<Link>Common\Interop\Unix\Interop.IOErrors.cs</Link>
124124
</Compile>
125-
<Compile Include="$(CommonPath)\Interop\Unix\libc\Interop.ftruncate.cs">
126-
<Link>Common\Interop\Unix\Interop.ftruncate.cs</Link>
127-
</Compile>
128125
<Compile Include="$(CommonPath)\Interop\Unix\libc\Interop.gnu_get_libc_version.cs">
129126
<Link>Common\Interop\Unix\Interop.gnu_get_libc_version.cs</Link>
130127
</Compile>
@@ -156,8 +153,11 @@
156153
</ItemGroup>
157154
<!-- Linux -->
158155
<ItemGroup Condition="'$(TargetsLinux)' == 'true'">
159-
<Compile Include="System\IO\MemoryMappedFiles\MemoryMappedFile.Linux.cs" />
160-
<Compile Include="System\IO\MemoryMappedFiles\MemoryMappedView.Linux.cs" />
156+
<Compile Include="System\IO\MemoryMappedFiles\MemoryMappedFile.Unix.BackingObject_Memory.cs" />
157+
<Compile Include="System\IO\MemoryMappedFiles\MemoryMappedView.Unix.MADV_DONTFORK.cs" />
158+
<Compile Include="$(CommonPath)\Interop\Unix\libc\Interop.ftruncate.cs">
159+
<Link>Common\Interop\Unix\Interop.ftruncate.cs</Link>
160+
</Compile>
161161
<Compile Include="$(CommonPath)\Interop\Linux\Interop.Errors.cs">
162162
<Link>Common\Interop\Linux\Interop.Errors.cs</Link>
163163
</Compile>
@@ -185,7 +185,10 @@
185185
</ItemGroup>
186186
<!-- OSX -->
187187
<ItemGroup Condition="'$(TargetsOSX)' == 'true'">
188-
<Compile Include="System\IO\MemoryMappedFiles\MemoryMappedFile.OSX.cs" />
188+
<Compile Include="System\IO\MemoryMappedFiles\MemoryMappedFile.Unix.BackingObject_File.cs" />
189+
<Compile Include="$(CommonPath)\Interop\Unix\libc\Interop.unlink.cs">
190+
<Link>Common\Interop\Unix\Interop.unlink.cs</Link>
191+
</Compile>
189192
<Compile Include="$(CommonPath)\Interop\OSX\Interop.Errors.cs">
190193
<Link>Common\Interop\OSX\Interop.Errors.cs</Link>
191194
</Compile>
@@ -204,9 +207,6 @@
204207
<Compile Include="$(CommonPath)\Interop\OSX\libc\Interop.SysConfNames.cs">
205208
<Link>Common\Interop\OSX\Interop.SysConfNames.cs</Link>
206209
</Compile>
207-
<Compile Include="$(CommonPath)\Interop\OSX\libsystem_kernel\Interop.shm_open.cs">
208-
<Link>Common\Interop\OSX\Interop.shm_open.cs</Link>
209-
</Compile>
210210
</ItemGroup>
211211
<!-- Resource files -->
212212
<ItemGroup>

src/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.OSX.cs

Lines changed: 0 additions & 30 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.Win32.SafeHandles;
5+
6+
namespace System.IO.MemoryMappedFiles
7+
{
8+
public partial class MemoryMappedFile
9+
{
10+
private static FileStream CreateSharedBackingObject(Interop.libc.MemoryMappedProtections protections, long capacity)
11+
{
12+
string path = TmpPathPrefix + Guid.NewGuid().ToString("N") + ".tmp";
13+
14+
FileAccess access =
15+
(protections & (Interop.libc.MemoryMappedProtections.PROT_READ | Interop.libc.MemoryMappedProtections.PROT_WRITE)) != 0 ? FileAccess.ReadWrite :
16+
(protections & (Interop.libc.MemoryMappedProtections.PROT_WRITE)) != 0 ? FileAccess.Write :
17+
FileAccess.Read;
18+
19+
// Create the backing file, then immediately unlink it so that it'll be cleaned up when no longer in use.
20+
// Then enlarge it to the requested capacity.
21+
const int DefaultBufferSize = 0x1000;
22+
var fs = new FileStream(path, FileMode.CreateNew, TranslateProtectionsToFileAccess(protections), FileShare.ReadWrite, DefaultBufferSize);
23+
Interop.CheckIo(Interop.libc.unlink(path));
24+
fs.SetLength(capacity);
25+
return fs;
26+
}
27+
28+
// -----------------------------
29+
// ---- PAL layer ends here ----
30+
// -----------------------------
31+
32+
private static readonly string TmpPathPrefix = Path.Combine(Path.GetTempPath(), MemoryMapObjectFilePrefix);
33+
}
34+
}

src/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Linux.cs renamed to src/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.BackingObject_Memory.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@ namespace System.IO.MemoryMappedFiles
88
public partial class MemoryMappedFile
99
{
1010
private static FileStream CreateSharedBackingObject(
11-
Interop.libc.MemoryMappedProtections protections, long capacity,
12-
out string mapName, out SafeMemoryMappedFileHandle.FileStreamSource fileStreamSource)
11+
Interop.libc.MemoryMappedProtections protections, long capacity)
1312
{
1413
// The POSIX shared memory object name must begin with '/'. After that we just want something short and unique.
15-
mapName = "/" + MemoryMapObjectFilePrefix + Guid.NewGuid().ToString("N");
16-
fileStreamSource = SafeMemoryMappedFileHandle.FileStreamSource.ManufacturedSharedMemory;
14+
string mapName = "/" + MemoryMapObjectFilePrefix + Guid.NewGuid().ToString("N");
1715

1816
// Determine the flags to use when creating the shared memory object
1917
Interop.libc.OpenFlags flags = (protections & Interop.libc.MemoryMappedProtections.PROT_WRITE) != 0 ?
@@ -30,15 +28,24 @@ private static FileStream CreateSharedBackingObject(
3028
if ((protections & Interop.libc.MemoryMappedProtections.PROT_EXEC) != 0)
3129
perms |= Interop.libc.Permissions.S_IXUSR;
3230

33-
// Create the shared memory object. Then enlarge it to the requested capacity.
31+
// Create the shared memory object.
3432
int fd;
3533
Interop.CheckIo(fd = Interop.libc.shm_open(mapName, flags, (int)perms), mapName);
3634
SafeFileHandle fileHandle = new SafeFileHandle((IntPtr)fd, ownsHandle: true);
3735

38-
// Wrap the handle in a stream and return it.
39-
var fs = new FileStream(fileHandle, TranslateProtectionsToFileAccess(protections));
40-
fs.SetLength(capacity);
41-
return fs;
36+
// Unlink the shared memory object immediatley so that it'll go away once all handles
37+
// to it are closed (as with opened then unlinked files, it'll remain usable via
38+
// the open handles even though it's unlinked and can't be opened anew via its name).
39+
Interop.CheckIo(Interop.libc.shm_unlink(mapName));
40+
41+
// Give it the right capacity. We do this directly with ftruncate rather
42+
// than via FileStream.SetLength after the FileStream is created because, on some systems,
43+
// lseek fails on shared memory objects, causing the FileStream to think it's unseekable,
44+
// causing it to preemptively throw from SetLength.
45+
Interop.CheckIo(Interop.libc.ftruncate(fd, capacity));
46+
47+
// Wrap the file descriptor in a stream and return it.
48+
return new FileStream(fileHandle, TranslateProtectionsToFileAccess(protections));
4249
}
4350
}
4451
}

src/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ private static unsafe SafeMemoryMappedFileHandle CreateCore(
2121
{
2222
if (mapName != null)
2323
{
24-
// TODO: We currently do not support named maps. We could possibly support
25-
// named maps in the future by using shm_open / shm_unlink, as we do for
26-
// giving internal names to anonymous maps. Issues to work through will include
27-
// dealing with permissions, passing information from the creator of the
28-
// map to another opener of it, etc.
24+
// Named maps are not supported in our Unix implementation. We could support named maps on Linux using
25+
// shared memory segments (shmget/shmat/shmdt/shmctl/etc.), but that doesn't work on OSX by default due
26+
// to very low default limits on OSX for the size of such objects; it also doesn't support behaviors
27+
// like copy-on-write or the ability to control handle inheritability, and reliably cleaning them up
28+
// relies on some non-conforming behaviors around shared memory IDs remaining valid even after they've
29+
// been marked for deletion (IPC_RMID). We could also support named maps using the current implementation
30+
// by not unlinking after creating the backing store, but then the backing stores would remain around
31+
// and accessible even after process exit, with no good way to appropriately clean them up.
32+
// (File-backed maps may still be used for cross-process communication.)
2933
throw CreateNamedMapsNotSupportedException();
3034
}
3135

32-
var fileStreamSource = SafeMemoryMappedFileHandle.FileStreamSource.Provided;
36+
bool ownsFileStream = false;
3337
if (fileStream != null)
3438
{
3539
// This map is backed by a file. Make sure the file's size is increased to be
@@ -50,15 +54,16 @@ private static unsafe SafeMemoryMappedFileHandle CreateCore(
5054
// of an object that has read-only permissions, but we also don't need to worry about sharing
5155
// views over a read-only, anonymous, memory-backed map, because the data will never change, so all views
5256
// will always see zero and can't change that. In that case, we just use the built-in anonymous support of
53-
// the map by leaving fileHandle as null.
57+
// the map by leaving fileStream as null.
5458
Interop.libc.MemoryMappedProtections protections = MemoryMappedView.GetProtections(access, forVerification: false);
5559
if ((protections & Interop.libc.MemoryMappedProtections.PROT_WRITE) != 0 && capacity > 0)
5660
{
57-
fileStream = CreateSharedBackingObject(protections, capacity, out mapName, out fileStreamSource);
61+
ownsFileStream = true;
62+
fileStream = CreateSharedBackingObject(protections, capacity);
5863
}
5964
}
6065

61-
return new SafeMemoryMappedFileHandle(mapName, fileStream, fileStreamSource, inheritability, access, options, capacity);
66+
return new SafeMemoryMappedFileHandle(fileStream, ownsFileStream, inheritability, access, options, capacity);
6267
}
6368

6469
/// <summary>
@@ -109,7 +114,7 @@ private static Exception CreateNamedMapsNotSupportedException()
109114
return new PlatformNotSupportedException(SR.PlatformNotSupported_NamedMaps);
110115
}
111116

112-
private const string MemoryMapObjectFilePrefix = "CoreFX_MMF_";
117+
private const string MemoryMapObjectFilePrefix = "corefx_mmf_";
113118

114119
private static FileAccess TranslateProtectionsToFileAccess(Interop.libc.MemoryMappedProtections protections)
115120
{

0 commit comments

Comments
 (0)