Skip to content

Commit 37d2514

Browse files
Follow up on Scatter/Gather API changes (#58447)
* Allocate an array of memory handles only if needed. * Remove an unnecessary variable in the multiple-syscall write gather. * Actually verify the content read by the read scatter operation. * Delay allocating native memory. * Verify that the whole file was read in the scatter/gather test. * Test the case when the scatter/gather buffers are acceptable by the Windows API. * Avoid null pointer dereferences when passing an empty segment array. * Test performing scatter/gather I/O with an empty segment array. Co-authored-by: Stephen Toub <[email protected]>
1 parent f77e586 commit 37d2514

File tree

2 files changed

+59
-31
lines changed

2 files changed

+59
-31
lines changed

src/libraries/System.IO.FileSystem/tests/RandomAccess/NoBuffering.Windows.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,17 @@ public async Task WriteAsyncUsingMultipleBuffers(bool asyncOperation, bool async
180180
Assert.Equal(content, File.ReadAllBytes(filePath));
181181
}
182182

183-
[Fact]
184-
public async Task ReadWriteAsyncUsingNonPageSizedMultipleBuffers()
183+
[Theory]
184+
[InlineData(true)]
185+
[InlineData(false)]
186+
public async Task ReadWriteAsyncUsingMultipleBuffers(bool memoryPageSized)
185187
{
186188
string filePath = GetTestFilePath();
187-
// The Windows scatter/gather APIs accept segments that are exactly one page long.
188-
// Combined with the FILE_FLAG_NO_BUFFERING's requirements, the segments must also
189-
// be aligned at page size boundaries and have a size of a multiple of the page size.
190-
// Using segments with a length of twice the page size adheres to the second requirement
191-
// but not the first. The RandomAccess implementation will see it and issue sequential
192-
// read/write syscalls per segment, instead of one scatter/gather syscall.
193-
// This test verifies that fallback behavior.
194-
int bufferSize = Environment.SystemPageSize * 2;
189+
// We test with buffers both one and two memory pages long. In the former case,
190+
// the I/O operations will issue one scatter/gather API call, and in the latter
191+
// case they will issue multiple calls; one per buffer. The buffers must still
192+
// be aligned to comply with FILE_FLAG_NO_BUFFERING's requirements.
193+
int bufferSize = Environment.SystemPageSize * (memoryPageSized ? 1 : 2);
195194
int fileSize = bufferSize * 2;
196195
byte[] content = RandomNumberGenerator.GetBytes(fileSize);
197196

@@ -205,10 +204,22 @@ public async Task ReadWriteAsyncUsingNonPageSizedMultipleBuffers()
205204
await RandomAccess.WriteAsync(handle, new ReadOnlyMemory<byte>[] { firstHalf, secondHalf }, 0);
206205

207206
buffer.GetSpan().Clear();
208-
await RandomAccess.ReadAsync(handle, new Memory<byte>[] { firstHalf, secondHalf }, 0);
207+
long nRead = await RandomAccess.ReadAsync(handle, new Memory<byte>[] { firstHalf, secondHalf }, 0);
208+
209+
Assert.Equal(buffer.GetSpan().Length, nRead);
210+
AssertExtensions.SequenceEqual(buffer.GetSpan(), content.AsSpan());
209211
}
212+
}
213+
214+
[Fact]
215+
public async Task ReadWriteAsyncUsingEmptyBuffers()
216+
{
217+
string filePath = GetTestFilePath();
218+
using SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous | NoBuffering);
210219

211-
Assert.Equal(content, await File.ReadAllBytesAsync(filePath));
220+
long nRead = await RandomAccess.ReadAsync(handle, Array.Empty<Memory<byte>>(), 0);
221+
Assert.Equal(0, nRead);
222+
await RandomAccess.WriteAsync(handle, Array.Empty<ReadOnlyMemory<byte>>(), 0);
212223
}
213224

214225
// when using FileOptions.Asynchronous we are testing Scatter&Gather APIs on Windows (FILE_FLAG_OVERLAPPED requirement)

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Buffers;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.IO.Strategies;
89
using System.Numerics;
910
using System.Runtime.InteropServices;
@@ -437,8 +438,8 @@ private static bool CanUseScatterGatherWindowsAPIs(SafeFileHandle handle)
437438
// The pinned MemoryHandles and the pointer to the segments must be cleaned-up
438439
// with the CleanupScatterGatherBuffers method.
439440
private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnlyList<T> buffers,
440-
THandler handler, out MemoryHandle[] handlesToDispose, out IntPtr segmentsPtr, out int totalBytes)
441-
where THandler: struct, IMemoryHandler<T>
441+
THandler handler, [NotNullWhen(true)] out MemoryHandle[]? handlesToDispose, out IntPtr segmentsPtr, out int totalBytes)
442+
where THandler : struct, IMemoryHandler<T>
442443
{
443444
int pageSize = s_cachedPageSize;
444445
Debug.Assert(BitOperations.IsPow2(pageSize), "Page size is not a power of two.");
@@ -447,13 +448,11 @@ private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnly
447448
long alignedAtPageSizeMask = pageSize - 1;
448449

449450
int buffersCount = buffers.Count;
450-
handlesToDispose = new MemoryHandle[buffersCount];
451+
handlesToDispose = null;
451452
segmentsPtr = IntPtr.Zero;
452453
totalBytes = 0;
453454

454-
// "The array must contain enough elements to store nNumberOfBytesToWrite bytes of data, and one element for the terminating NULL. "
455-
long* segments = (long*) NativeMemory.Alloc((nuint)(buffersCount + 1), sizeof(long));
456-
segments[buffersCount] = 0;
455+
long* segments = null;
457456

458457
bool success = false;
459458
try
@@ -469,36 +468,55 @@ private static unsafe bool TryPrepareScatterGatherBuffers<T, THandler>(IReadOnly
469468
return false;
470469
}
471470

472-
MemoryHandle handle = handlesToDispose[i] = handler.Pin(in buffer);
473-
long ptr = segments[i] = (long)handle.Pointer;
471+
MemoryHandle handle = handler.Pin(in buffer);
472+
long ptr = (long)handle.Pointer;
474473
if ((ptr & alignedAtPageSizeMask) != 0)
475474
{
475+
handle.Dispose();
476476
return false;
477477
}
478+
479+
// We avoid allocations if there are no
480+
// buffers or the first one is unacceptable.
481+
(handlesToDispose ??= new MemoryHandle[buffersCount])[i] = handle;
482+
if (segments == null)
483+
{
484+
// "The array must contain enough elements to store nNumberOfBytesToWrite
485+
// bytes of data, and one element for the terminating NULL."
486+
segments = (long*)NativeMemory.Alloc((nuint)buffersCount + 1, sizeof(long));
487+
segments[buffersCount] = 0;
488+
}
489+
segments[i] = ptr;
478490
}
479491

480492
segmentsPtr = (IntPtr)segments;
481493
totalBytes = (int)totalBytes64;
482494
success = true;
483-
return true;
495+
return handlesToDispose != null;
484496
}
485497
finally
486498
{
487499
if (!success)
488500
{
489-
CleanupScatterGatherBuffers(handlesToDispose, (IntPtr) segments);
501+
CleanupScatterGatherBuffers(handlesToDispose, (IntPtr)segments);
490502
}
491503
}
492504
}
493505

494-
private static unsafe void CleanupScatterGatherBuffers(MemoryHandle[] handlesToDispose, IntPtr segmentsPtr)
506+
private static unsafe void CleanupScatterGatherBuffers(MemoryHandle[]? handlesToDispose, IntPtr segmentsPtr)
495507
{
496-
foreach (MemoryHandle handle in handlesToDispose)
508+
if (handlesToDispose != null)
497509
{
498-
handle.Dispose();
510+
foreach (MemoryHandle handle in handlesToDispose)
511+
{
512+
handle.Dispose();
513+
}
499514
}
500515

501-
NativeMemory.Free((void*) segmentsPtr);
516+
if (segmentsPtr != IntPtr.Zero)
517+
{
518+
NativeMemory.Free((void*)segmentsPtr);
519+
}
502520
}
503521

504522
private static ValueTask<long> ReadScatterAtOffsetAsync(SafeFileHandle handle, IReadOnlyList<Memory<byte>> buffers,
@@ -510,7 +528,7 @@ private static ValueTask<long> ReadScatterAtOffsetAsync(SafeFileHandle handle, I
510528
}
511529

512530
if (CanUseScatterGatherWindowsAPIs(handle)
513-
&& TryPrepareScatterGatherBuffers(buffers, default(MemoryHandler), out MemoryHandle[] handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
531+
&& TryPrepareScatterGatherBuffers(buffers, default(MemoryHandler), out MemoryHandle[]? handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
514532
{
515533
return ReadScatterAtOffsetSingleSyscallAsync(handle, handlesToDispose, segmentsPtr, fileOffset, totalBytes, cancellationToken);
516534
}
@@ -607,7 +625,7 @@ private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOn
607625
}
608626

609627
if (CanUseScatterGatherWindowsAPIs(handle)
610-
&& TryPrepareScatterGatherBuffers(buffers, default(ReadOnlyMemoryHandler), out MemoryHandle[] handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
628+
&& TryPrepareScatterGatherBuffers(buffers, default(ReadOnlyMemoryHandler), out MemoryHandle[]? handlesToDispose, out IntPtr segmentsPtr, out int totalBytes))
611629
{
612630
return WriteGatherAtOffsetSingleSyscallAsync(handle, handlesToDispose, segmentsPtr, fileOffset, totalBytes, cancellationToken);
613631
}
@@ -671,13 +689,12 @@ private static unsafe ValueTask WriteFileGatherAsync(SafeFileHandle handle, IntP
671689

672690
private static async ValueTask WriteGatherAtOffsetMultipleSyscallsAsync(SafeFileHandle handle, IReadOnlyList<ReadOnlyMemory<byte>> buffers, long fileOffset, CancellationToken cancellationToken)
673691
{
674-
long bytesWritten = 0;
675692
int buffersCount = buffers.Count;
676693
for (int i = 0; i < buffersCount; i++)
677694
{
678695
ReadOnlyMemory<byte> rom = buffers[i];
679-
await WriteAtOffsetAsync(handle, rom, fileOffset + bytesWritten, cancellationToken).ConfigureAwait(false);
680-
bytesWritten += rom.Length;
696+
await WriteAtOffsetAsync(handle, rom, fileOffset, cancellationToken).ConfigureAwait(false);
697+
fileOffset += rom.Length;
681698
}
682699
}
683700

0 commit comments

Comments
 (0)