C# I/O异步(copyAsync):如何避免文件碎片?

4
在一个用于磁盘间拷贝大文件的工具中,我将 System.IO.FileInfo.CopyTo 方法替换成了 System.IO.Stream.CopyToAsync。这样可以实现更快的拷贝和更好的控制,例如我可以停止拷贝。但这会导致被拷贝文件更加碎片化。当我拷贝数百兆字节大小的文件时,这尤其令人烦恼。
如何避免拷贝过程中的磁盘碎片化?
使用 xcopy 命令,/j 开关可以无缓存地拷贝文件。对于非常大的文件,TechNet 推荐使用该开关。它似乎确实可以避免文件碎片化(而在 Windows 10 资源管理器中进行简单的文件拷贝却会导致文件碎片化!)
无缓存的拷贝似乎与异步拷贝背道而驰。是否有办法实现异步拷贝且不使用缓存?
以下是我的当前异步拷贝代码。我使用默认的缓冲区大小,即 81920 字节,即 10*1024*size(int64)。
我正在使用 NTFS 文件系统,因此每个簇的大小为 4096 字节。

编辑:根据建议,我更新了代码并使用SetLength添加了FileOptions Async,同时在创建destinationStream时修复了设置属性之后设置时间的问题(否则,只读文件会抛出异常)。

        int bufferSize = 81920; 
        try
        {
            using (FileStream sourceStream = source.OpenRead())
            {
                // Remove existing file first
                if (File.Exists(destinationFullPath))
                    File.Delete(destinationFullPath);

                using (FileStream destinationStream = File.Create(destinationFullPath, bufferSize, FileOptions.Asynchronous))
                {
                    try
                    {                             
                        destinationStream.SetLength(sourceStream.Length); // avoid file fragmentation!
                        await sourceStream.CopyToAsync(destinationStream, bufferSize, cancellationToken);
                    }
                    catch (OperationCanceledException)
                    {
                        operationCanceled = true;
                    }
                } // properly disposed after the catch
            }
        }
        catch (IOException e)
        {
            actionOnException(e, "error copying " + source.FullName);
        }

        if (operationCanceled)
        {
            // Remove the partially written file
            if (File.Exists(destinationFullPath))
                File.Delete(destinationFullPath);
        }
        else
        {
            // Copy meta data (attributes and time) from source once the copy is finished
            File.SetCreationTimeUtc(destinationFullPath, source.CreationTimeUtc);
            File.SetLastWriteTimeUtc(destinationFullPath, source.LastWriteTimeUtc);
            File.SetAttributes(destinationFullPath, source.Attributes); // after set time if ReadOnly!
        }

我担心我的代码末尾的File.SetAttributes和Time可能会增加文件碎片化。
有没有一种正确的方法来创建一个1:1的异步文件复制而不会产生任何文件碎片,即请求HDD使文件流仅获得连续扇区?
关于文件碎片化的其他主题,例如如何在使用.NET时限制文件碎片化建议以较大的块递增文件大小,但这似乎并不是我问题的直接答案。

在复制之前,您是否尝试过destinationStream.Length = sourceStream.Length;吗? - Lucas Trzesniewski
好主意,Length只是一个getter,但SetLength方法可以完成任务。在快速测试中似乎确实避免了碎片化!我还注意到在创建destinationStream时有FileOptions选项。不知道异步或WriteThrough是否是一个好选择。 - EricBDev
3个回答

4
但是SetLength方法并没有完成任务。它只是更新目录条目中的文件大小,而不分配任何簇。你可以在非常大的文件上尝试这个方法,比如100GB的文件,你会发现调用立即完成,这只有在文件系统不进行簇的分配和写入时才能实现。从文件中读取数据实际上是可能的,即使文件中没有实际数据,文件系统只是返回二进制零。
这也会误导任何报告碎片的工具。因为文件没有簇,所以不可能有碎片。所以看起来只是解决了问题。
唯一能做的事情就是强制分配簇的是实际写入文件。实际上,使用单个写操作就可以分配100GB的簇。你必须使用Seek()将位置定位到Length-1,然后使用Write()写入一个字节。这在非常大的文件上需要一段时间,实际上不再是异步的。
这样做减少碎片的可能性并不大。你只是稍微降低了写入过程中与其他进程的写入交错的风险。实际上,写入是由文件系统缓存懒惰地完成的。核心问题是在你开始写入之前,卷已经被分段了,写完后它永远不会减少碎片。
最好的方法就是不要担心它。在Windows上自动进行碎片整理,自Vista以来一直如此。也许你想玩一下调度,或者在superuser.com上询问更多信息。

这也会误导任何报告碎片的实用程序。由于文件没有簇,因此不可能存在碎片。但是文件最终会被写入。刚刚进行了一个测试,使用了占用16k簇的4 GB文件:在Defrag工具的ClusterView中,所有内容似乎都是连续的。 - EricBDev
请查看我的回答,这就是您想表达的意思。按照写法,它似乎与SetLength()一样“即时”,不会产生性能损失。但是它不能保证所有聚集体都是连续的。我刚才测试了一下,在只有90 GB可用的分区上复制了一个60 GB的文件。60 GB被复制了,但分成了3个片段,因为我的磁盘没有60 GB的连续空间!(一些簇在中间被占用) - EricBDev
正如我的回答中所述,与SetLength相比,在复制100 GB VM时,使用“寻找+写入”策略效果更好:使用“寻找+写入”只需一次操作即可完成,而使用SetLength()需要分成3个片段进行操作! - EricBDev

3

1
我也是在Lucas的评论下得出了这个解决方案。它很大程度上减少了碎片化。然而,还是有一些文件在复制后出现了碎片。与以前相比不算什么大问题,但我想知道能否做得更好。我们能保证没有碎片吗? - EricBDev
1
只有在每次复制操作之前格式化磁盘,才能保证数据的安全性。 - H H
@HenkHolterman 您是对的,但另一方面,在多个并行写入的情况下,可以减少碎片化。 - Yury Glushkov
@HansPassant 但在提供的情况下,它将在异步语句的第一次写入时发生。 - Yury Glushkov
1
查看 https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,d6c30590c2fd88be 中 SetLengthCore 的实现,可以通过 SeekCore 调用和 Win32Native.SetEndOfFile(_handle) 调用得到一些提示。 但我真的不明白为什么 SetLength(Length - 1) 比 SetLength(Length) 更好。 - EricBDev
显示剩余2条评论

1
考虑到Hans Passant的回答,在我的上述代码中,有一个替代方案是:
destinationStream.SetLength(sourceStream.Length);

如果我理解正确的话,应该是这样的:

byte[] writeOneZero = {0};
destinationStream.Seek(sourceStream.Length - 1, SeekOrigin.Begin);
destinationStream.Write(writeOneZero, 0, 1);
destinationStream.Seek(0, SeekOrigin.Begin);

看起来确实巩固了副本。

但是查看FileStream.SetLengthCore的源代码,它几乎做了相同的事情,在结尾处寻找但没有写入一个字节:

    private void SetLengthCore(long value)
    {
        Contract.Assert(value >= 0, "value >= 0");
        long origPos = _pos;

        if (_exposedHandle)
            VerifyOSHandlePosition();
        if (_pos != value)
            SeekCore(value, SeekOrigin.Begin);
        if (!Win32Native.SetEndOfFile(_handle)) {
            int hr = Marshal.GetLastWin32Error();
            if (hr==__Error.ERROR_INVALID_PARAMETER)
                throw new ArgumentOutOfRangeException("value", Environment.GetResourceString("ArgumentOutOfRange_FileLengthTooBig"));
            __Error.WinIOError(hr, String.Empty);
        }
        // Return file pointer to where it was before setting length
        if (origPos != value) {
            if (origPos < value)
                SeekCore(origPos, SeekOrigin.Begin);
            else
                SeekCore(0, SeekOrigin.End);
        }
    }

无论如何,这些方法并不能保证不会出现碎片,但至少对大多数情况进行了避免。因此,自动碎片整理工具将以较低的性能开销完成工作。 没有这些 Seek 调用的初始代码为 1 GB 文件创建了成千上万个碎片,在碎片整理工具启动时减慢了我的机器速度。

2
我昨天复制了一个100 GB的虚拟机文件到目标驱动器,该驱动器有足够的空间(但是,目标驱动器是SSD,碎片化不相关,因此可能会改变Windows内核中的结果)。a)使用Windows 10资源管理器/复制:目标文件有3个片段 b)使用SetLength():相同的3个片段 c)使用上述代码/ writeOneZero / seek + write:仅1个片段因此,这种seek + write确实有意义! - EricBDev

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