Skip to content

Commit 44b4450

Browse files
tmdsjozkee
andauthored
File preallocationSize: align Windows and Unix behavior. (#59338)
* File preallocationSize: align Windows and Unix behavior. This aligns Windows and Unix behavior of preallocationSize for the intended use-case of specifing the size of a file that will be written. For this use-case, the expected FileAccess is Write, and the file should be a new one (FileMode.Create*) or a truncated file (FileMode.Truncate). Specifing a preallocationSize for other modes, or non-writable files throws ArgumentException. The opened file will have a length of zero, and is ready to be written to by the user. If the requested size cannot be allocated, an IOException is thrown. When the OS/filesystem does not support pre-allocating, preallocationSize is ignored. * fix pal_io preprocessor checks * pal_io more fixes * ctor_options_as.Windows.cs: fix compilation * Update tests * tests: use preallocationSize from all public APIs * pal_io: add back FreeBSD, fix OSX * tests: check allocated is zero when preallocation is not supported. * Only throw for not enough space errors * Fix compilation * Add some more tests * Fix ExtendedPathsAreSupported test * Apply suggestions from code review Co-authored-by: David Cantú <dacantu@microsoft.com> * Update System.Private.CoreLib Strings.resx * PR feedback * Remove GetPathToNonExistingFile * Fix compilation * Skip checking allocated size on mobile platforms. Co-authored-by: David Cantú <dacantu@microsoft.com>
1 parent 0976372 commit 44b4450

File tree

25 files changed

+420
-387
lines changed

25 files changed

+420
-387
lines changed

src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs renamed to src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ internal static partial class Interop
99
internal static partial class Sys
1010
{
1111
/// <summary>
12-
/// Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned.
12+
/// Returns -1 on error, 0 on success.
1313
/// </summary>
14-
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PosixFAllocate", SetLastError = false)]
15-
internal static extern int PosixFAllocate(SafeFileHandle fd, long offset, long length);
14+
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FAllocate", SetLastError = true)]
15+
internal static extern int FAllocate(SafeFileHandle fd, long offset, long length);
1616
}
1717
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
internal static partial class Interop
5+
{
6+
internal static partial class Kernel32
7+
{
8+
// Value taken from https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle#remarks:
9+
internal const int FileAllocationInfo = 5;
10+
11+
internal struct FILE_ALLOCATION_INFO
12+
{
13+
internal long AllocationSize;
14+
}
15+
}
16+
}

src/libraries/Native/Unix/Common/pal_config.h.in

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
#cmakedefine01 HAVE_STRLCAT
3737
#cmakedefine01 HAVE_SHM_OPEN_THAT_WORKS_WELL_ENOUGH_WITH_MMAP
3838
#cmakedefine01 HAVE_POSIX_ADVISE
39-
#cmakedefine01 HAVE_POSIX_FALLOCATE
40-
#cmakedefine01 HAVE_POSIX_FALLOCATE64
39+
#cmakedefine01 HAVE_FALLOCATE
4140
#cmakedefine01 HAVE_PREADV
4241
#cmakedefine01 HAVE_PWRITEV
4342
#cmakedefine01 PRIORITY_REQUIRES_INT_WHO

src/libraries/Native/Unix/System.Native/entrypoints.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ static const Entry s_sysNative[] =
9292
DllImportEntry(SystemNative_FTruncate)
9393
DllImportEntry(SystemNative_Poll)
9494
DllImportEntry(SystemNative_PosixFAdvise)
95-
DllImportEntry(SystemNative_PosixFAllocate)
95+
DllImportEntry(SystemNative_FAllocate)
9696
DllImportEntry(SystemNative_Read)
9797
DllImportEntry(SystemNative_ReadLink)
9898
DllImportEntry(SystemNative_Rename)

src/libraries/Native/Unix/System.Native/pal_io.c

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,83 +1008,33 @@ int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, i
10081008
#endif
10091009
}
10101010

1011-
int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length)
1011+
int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length)
10121012
{
10131013
assert_msg(offset == 0, "Invalid offset value", (int)offset);
10141014

10151015
int fileDescriptor = ToFileDescriptor(fd);
10161016
int32_t result;
1017-
#if HAVE_POSIX_FALLOCATE64 // 64-bit Linux
1018-
while ((result = posix_fallocate64(fileDescriptor, (off64_t)offset, (off64_t)length)) == EINTR);
1019-
#elif HAVE_POSIX_FALLOCATE // 32-bit Linux
1020-
while ((result = posix_fallocate(fileDescriptor, (off_t)offset, (off_t)length)) == EINTR);
1017+
#if HAVE_FALLOCATE // Linux
1018+
while ((result = fallocate(fileDescriptor, FALLOC_FL_KEEP_SIZE, (off_t)offset, (off_t)length)) == EINTR);
10211019
#elif defined(F_PREALLOCATE) // macOS
10221020
fstore_t fstore;
1023-
fstore.fst_flags = F_ALLOCATECONTIG; // ensure contiguous space
1024-
fstore.fst_posmode = F_PEOFPOSMODE; // allocate from the physical end of file, as offset MUST NOT be 0 for F_VOLPOSMODE
1021+
fstore.fst_flags = F_ALLOCATEALL; // Allocate all requested space or no space at all.
1022+
fstore.fst_posmode = F_PEOFPOSMODE; // Allocate from the physical end of file.
10251023
fstore.fst_offset = (off_t)offset;
10261024
fstore.fst_length = (off_t)length;
10271025
fstore.fst_bytesalloc = 0; // output size, can be > length
10281026

10291027
while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR);
1030-
1031-
if (result == -1)
1032-
{
1033-
// we have failed to allocate contiguous space, let's try non-contiguous
1034-
fstore.fst_flags = F_ALLOCATEALL; // all or nothing
1035-
while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR);
1036-
}
1037-
#elif defined(F_ALLOCSP) || defined(F_ALLOCSP64) // FreeBSD
1038-
#if HAVE_FLOCK64
1039-
struct flock64 lockArgs;
1040-
int command = F_ALLOCSP64;
1041-
#else
1042-
struct flock lockArgs;
1043-
int command = F_ALLOCSP;
1044-
#endif
1045-
1046-
lockArgs.l_whence = SEEK_SET;
1047-
lockArgs.l_start = (off_t)offset;
1048-
lockArgs.l_len = (off_t)length;
1049-
1050-
while ((result = fcntl(fileDescriptor, command, &lockArgs)) == -1 && errno == EINTR);
1028+
#else
1029+
(void)offset; // unused
1030+
(void)length; // unused
1031+
result = -1;
1032+
errno = EOPNOTSUPP;
10511033
#endif
10521034

1053-
#if defined(F_PREALLOCATE) || defined(F_ALLOCSP) || defined(F_ALLOCSP64)
1054-
// most of the Unixes implement posix_fallocate which does NOT set the last error
1055-
// fctnl does, but to mimic the posix_fallocate behaviour we just return error
1056-
if (result == -1)
1057-
{
1058-
result = errno;
1059-
}
1060-
else
1061-
{
1062-
// align the behaviour with what posix_fallocate does (change reported file size)
1063-
ftruncate(fileDescriptor, length);
1064-
}
1065-
#endif
1035+
assert(result == 0 || errno != EINVAL);
10661036

1067-
// error codes can be OS-specific, so this is why this handling is done here rather than in the managed layer
1068-
switch (result)
1069-
{
1070-
case ENOSPC: // there was not enough space
1071-
return -1;
1072-
case EFBIG: // the file was too large
1073-
return -2;
1074-
case ENODEV: // not a regular file
1075-
case ESPIPE: // a pipe
1076-
// We ignore it, as FileStream contract makes it clear that allocationSize is ignored for non-regular files.
1077-
return 0;
1078-
case EINVAL:
1079-
// We control the offset and length so they are correct.
1080-
assert_msg(length >= 0, "Invalid length value", (int)length);
1081-
// But if the underlying filesystem does not support the operation, we just ignore it and treat as a hint.
1082-
return 0;
1083-
default:
1084-
assert(result != EINTR); // it can't happen here as we retry the call on EINTR
1085-
assert(result != EBADF); // it can't happen here as this method is being called after a succesfull call to open (with write permissions) before returning the SafeFileHandle to the user
1086-
return 0;
1087-
}
1037+
return result;
10881038
}
10891039

10901040
int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize)

src/libraries/Native/Unix/System.Native/pal_io.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -620,11 +620,11 @@ PALEXPORT int32_t SystemNative_Poll(PollEvent* pollEvents, uint32_t eventCount,
620620
PALEXPORT int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, int32_t advice);
621621

622622
/**
623-
* Ensures that disk space is allocated.
623+
* Preallocates disk space.
624624
*
625-
* Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned.
625+
* Returns 0 for success, -1 for failure. Sets errno on failure.
626626
*/
627-
PALEXPORT int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length);
627+
PALEXPORT int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length);
628628

629629
/**
630630
* Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor.

src/libraries/Native/Unix/configure.cmake

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,9 @@ check_symbol_exists(
232232
HAVE_POSIX_ADVISE)
233233
234234
check_symbol_exists(
235-
posix_fallocate
235+
fallocate
236236
fcntl.h
237-
HAVE_POSIX_FALLOCATE)
238-
239-
check_symbol_exists(
240-
posix_fallocate64
241-
fcntl.h
242-
HAVE_POSIX_FALLOCATE64)
237+
HAVE_FALLOCATE)
243238
244239
check_symbol_exists(
245240
preadv

src/libraries/System.IO.FileSystem/src/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@
173173
<data name="Argument_InvalidAppendMode" xml:space="preserve">
174174
<value>Append access can be requested only in write-only mode.</value>
175175
</data>
176+
<data name="Argument_InvalidPreallocateAccess" xml:space="preserve">
177+
<value>Preallocation size can be requested only in write mode.</value>
178+
</data>
179+
<data name="Argument_InvalidPreallocateMode" xml:space="preserve">
180+
<value>Preallocation size can be requested only for new files.</value>
181+
</data>
176182
<data name="Argument_InvalidFileModeAndAccessCombo" xml:space="preserve">
177183
<value>Combining FileMode: {0} with FileAccess: {1} is invalid.</value>
178184
</data>

src/libraries/System.IO.FileSystem/tests/File/Open.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,14 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA
4242
}
4343
}
4444

45-
public class File_Open_str_options_as : FileStream_ctor_options_as
45+
public class File_Open_str_options : FileStream_ctor_options
4646
{
4747
protected override FileStream CreateFileStream(string path, FileMode mode)
4848
{
4949
return File.Open(path,
5050
new FileStreamOptions {
5151
Mode = mode,
52-
Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite,
53-
PreallocationSize = PreallocationSize
52+
Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite
5453
});
5554
}
5655

@@ -59,12 +58,23 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA
5958
return File.Open(path,
6059
new FileStreamOptions {
6160
Mode = mode,
62-
Access = access,
63-
PreallocationSize = PreallocationSize
61+
Access = access
6462
});
6563
}
6664

6765
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
66+
{
67+
return File.Open(path,
68+
new FileStreamOptions {
69+
Mode = mode,
70+
Access = access,
71+
Share = share,
72+
Options = options,
73+
BufferSize = bufferSize
74+
});
75+
}
76+
77+
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize)
6878
{
6979
return File.Open(path,
7080
new FileStreamOptions {
@@ -73,7 +83,7 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA
7383
Share = share,
7484
Options = options,
7585
BufferSize = bufferSize,
76-
PreallocationSize = PreallocationSize
86+
PreallocationSize = preallocationSize
7787
});
7888
}
7989
}

src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,24 @@
77
namespace System.IO.Tests
88
{
99
// to avoid a lot of code duplication, we reuse FileStream tests
10-
public class File_OpenHandle : FileStream_ctor_options_as
10+
public class File_OpenHandle : FileStream_ctor_options
1111
{
1212
protected override string GetExpectedParamName(string paramName) => paramName;
1313

1414
protected override FileStream CreateFileStream(string path, FileMode mode)
1515
{
1616
FileAccess access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite;
17-
return new FileStream(File.OpenHandle(path, mode, access, preallocationSize: PreallocationSize), access);
17+
return new FileStream(File.OpenHandle(path, mode, access), access);
1818
}
1919

2020
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access)
21-
=> new FileStream(File.OpenHandle(path, mode, access, preallocationSize: PreallocationSize), access);
21+
=> new FileStream(File.OpenHandle(path, mode, access), access);
2222

2323
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
24-
=> new FileStream(File.OpenHandle(path, mode, access, share, options, PreallocationSize), access, bufferSize, (options & FileOptions.Asynchronous) != 0);
24+
=> new FileStream(File.OpenHandle(path, mode, access, share, options), access, bufferSize, (options & FileOptions.Asynchronous) != 0);
2525

26-
[Fact]
27-
public override void NegativePreallocationSizeThrows()
28-
{
29-
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
30-
() => File.OpenHandle("validPath", FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize: -1));
31-
}
26+
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize)
27+
=> new FileStream(File.OpenHandle(path, mode, access, share, options, preallocationSize), access, bufferSize, (options & FileOptions.Asynchronous) != 0);
3228

3329
[ActiveIssue("https://github.com/dotnet/runtime/issues/53432")]
3430
[Theory, MemberData(nameof(StreamSpecifiers))]

src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,14 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA
7474
}
7575
}
7676

77-
public class FileInfo_Open_options_as : FileStream_ctor_options_as
77+
public class FileInfo_Open_options : FileStream_ctor_options
7878
{
7979
protected override FileStream CreateFileStream(string path, FileMode mode)
8080
{
8181
return new FileInfo(path).Open(
8282
new FileStreamOptions {
8383
Mode = mode,
84-
Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite,
85-
PreallocationSize = PreallocationSize
84+
Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite
8685
});
8786
}
8887

@@ -91,12 +90,23 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA
9190
return new FileInfo(path).Open(
9291
new FileStreamOptions {
9392
Mode = mode,
94-
Access = access,
95-
PreallocationSize = PreallocationSize
93+
Access = access
9694
});
9795
}
9896

9997
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
98+
{
99+
return new FileInfo(path).Open(
100+
new FileStreamOptions {
101+
Mode = mode,
102+
Access = access,
103+
Share = share,
104+
Options = options,
105+
BufferSize = bufferSize
106+
});
107+
}
108+
109+
protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize)
100110
{
101111
return new FileInfo(path).Open(
102112
new FileStreamOptions {
@@ -105,7 +115,7 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA
105115
Share = share,
106116
Options = options,
107117
BufferSize = bufferSize,
108-
PreallocationSize = PreallocationSize
118+
PreallocationSize = preallocationSize
109119
});
110120
}
111121
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.IO.Tests
5+
{
6+
public partial class FileStream_ctor_options
7+
{
8+
private static long GetAllocatedSize(FileStream fileStream)
9+
{
10+
return 0;
11+
}
12+
13+
private static bool SupportsPreallocation => false;
14+
15+
private static bool IsGetAllocatedSizeImplemented => false;
16+
}
17+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Runtime.InteropServices;
6+
7+
namespace System.IO.Tests
8+
{
9+
public partial class FileStream_ctor_options
10+
{
11+
private static long GetAllocatedSize(FileStream fileStream)
12+
{
13+
bool isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
14+
// Call 'stat' to get the number of blocks, and size of blocks.
15+
using var px = Process.Start(new ProcessStartInfo
16+
{
17+
FileName = "stat",
18+
ArgumentList = { isOSX ? "-f" : "-c",
19+
isOSX ? "%b %k" : "%b %B",
20+
fileStream.Name },
21+
RedirectStandardOutput = true
22+
});
23+
string stdout = px.StandardOutput.ReadToEnd();
24+
25+
string[] parts = stdout.Split(' ');
26+
return long.Parse(parts[0]) * long.Parse(parts[1]);
27+
}
28+
29+
private static bool SupportsPreallocation =>
30+
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ||
31+
RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
32+
33+
// Mobile platforms don't support Process.Start.
34+
private static bool IsGetAllocatedSizeImplemented => !PlatformDetection.IsMobile;
35+
}
36+
}

0 commit comments

Comments
 (0)