寻找最快的大文件拷贝方法

  众所周知微软的操作系统自带的拷贝是很“弱智”的,速度不高,无断点续传,而且拷贝会拖累其他的应用程序,占用大量的文件缓存。所以很多高级的拷贝工具孕育而生,用过最好的是FastCopy。FastCopy的拷贝速度基本上可以达到磁盘的极限,还因为他开源,所以可以看到其实现。但是很可惜他的工程是VC6的,而且源代码注释都是日文的,不仅如此,其源代码风格很让人迷惑。证实了我的那句话:开源软件的最高境界就是,我开源了,你看不懂;等你看懂了,已经过时了。
  要达到最快的拷贝速度和减少对内存的占用,需要对拷贝的过程有一个了解。拷贝无非就是将文件的数据读出来,然后再写进去的一个过程。XP操作系统自带的拷贝工具会首先打开文件句柄,然后将一块数据读取到缓存中,然后再写入到磁盘中。打开“Windows任务管理器”,进程,查看,选择列,打开I/O读取字节,I/O写入字节。拷贝一个文件,注意explorer.exe进程即可看到整个读写过程。基本上可以看到XP对于文件拷贝几乎是属于同时进行的,换句话说其开的缓存比较小,但其效率可能并不见得很高。在我的200G Seagate 7200.8硬盘上,复制速度在15M/s左右。而这个硬盘的平均读取速度在40M/s,平均写入速度也在35M/s以上。
  在Vista下面文件拷贝做了一些优化,虽然一些BUG导致复制小文件会感觉很慢,但是复制大文件的思路已经不同于XP了。还是打开任务管理器,进行同样的操作。会发现Vista的会读取将近100M后,再将文件写入磁盘。explorer.exe进程也会在拷贝的瞬间内存占用飙升到100M以上,我的电脑商测试是120M左右,而复制完成以后内存占用将恢复正常。Vista的状态显示复制速度在18M/s左右。还是没有达到硬盘的极限速度。
观察Vista和XP的拷贝过程可以得出一个结论,Vista试图对磁盘的拷贝做优化了,但是其无论XP的分小块的复制,还是Vista的大缓存大块复制,都不能达到磁盘的最快速度。
  在两个操作系统复制的过程中,你会发现一个有趣的现象。XP的“任务管理器”的“性能”页面种的“物理内存”种的“系统缓存”的值会不断的增大,大到一个值以后就不会在增长。系统缓存主要用于缓存使用过的一些程序的内存、缓存打开并读写过的文件,已达到更快的读写速度。Win32 API的CreateFile函数默认是使用系统缓存的读写,所以简单的用CreateFile打开的文件是要先到系统缓存的。explorer也是这样,所以当你打开了比较多的后台程序,复制完一个大文件以后,再打开这些后台程序就变得十分缓慢,硬盘不停的读取。这是因为文件缓存占用了太多的内存空间的缘故,将一些程序缓存占用了,所以后台程序会变得十分缓慢。Vista下面这种情况要好一些。虽然这样的设计可以加速很多文件操作的应用,但是对于文件拷贝这样的一次性操作,使用系统缓存固然就是浪费资源了。
  我还发现当使用FastCopy的拷贝大文件的时候会出现另一个现象,就是“系统缓存”会骤降,磁盘读写速度基本达到极限。在XP下面能够改善后台程序的性能,因为此时FastCopy使用的是不使用操作系统缓存的读写操作,软件自生打开了一个32M的缓存(可自定义)。Vista下面的行为有些古怪,“系统缓存”也会减少,但是当复制完以后,硬盘会不断的读取,直到达到复制之前的大小,XP无此现象。
那么怎么样才能达到极限速度呢?是需要缓存还是不需要缓存呢?要缓存需要多大的缓存才好呢?为此我做了一个小实验。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;


namespace csharp
{
    
class Program
    {
        
public const short FILE_ATTRIBUTE_NORMAL = 0x80;
        
public const short INVALID_HANDLE_VALUE = -1;
        
public const uint GENERIC_READ = 0x80000000;
        
public const uint GENERIC_WRITE = 0x40000000;
        
public const uint CREATE_NEW = 1;
        
public const uint CREATE_ALWAYS = 2;
        
public const uint OPEN_EXISTING = 3;
        
public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
        
public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;
        
public const uint FILE_SHARE_READ = 0x00000001;
        
public const uint FILE_SHARE_WRITE = 0x00000002;

        
// Use interop to call the CreateFile function.
        
// For more information about CreateFile,
        
// see the unmanaged MSDN reference library.
        [DllImport("kernel32.dll", SetLastError = true)]
        
static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess,
          
uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition,
          
uint dwFlagsAndAttributes, IntPtr hTemplateFile);

       
static void Main(string[] args)
        {
            
bool useBuffer = false;
            SafeFileHandle fr 
= CreateFile("d:\\source", GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, useBuffer? 0:FILE_FLAG_NO_BUFFERING , IntPtr.Zero);
            SafeFileHandle fw 
= CreateFile("d:\\dest", GENERIC_WRITE, FILE_SHARE_READ, IntPtr.Zero, CREATE_ALWAYS,useBuffer? 0:(FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH), IntPtr.Zero);

            
int bufferSize = useBuffer? 1024*1024*32:1024 * 1024 * 32;

            FileStream fsr 
= new FileStream(fr, FileAccess.Read);
            FileStream fsw 
= new FileStream(fw, FileAccess.Write);

            BinaryReader br 
= new BinaryReader(fsr);
            BinaryWriter bw 
= new BinaryWriter(fsw);

            
byte[] buffer = new byte[bufferSize];
            Int64 len 
= fsr.Length;
            DateTime start 
= DateTime.Now;
            TimeSpan ts;
            
while (fsr.Position < fsr.Length)
            {
                
int readCount = br.Read(buffer, 0, bufferSize);
                bw.Write(buffer, 
0, readCount);
                ts 
= DateTime.Now.Subtract(start);
                
double speed=(double)fsr.Position / ts.TotalMilliseconds * 1000 / (1024 * 1024);
                
double progress=(double)fsr.Position / len * 100;
                Console.WriteLine(
"Speed:{0},  Progress:{1}",speed ,progress );
            }
            br.Close();
            bw.Close();
            sw.Close();
            Console.WriteLine(
"End");
            Console.ReadLine();
        }
    }
}

  整个程序的思路比较简单,打开文件,读取数据到自定义的缓存,然后写入数据,关闭文件。.NET默认的FileStream默认是缓存的读写,而且没有参数指定非缓存的读写。但好在FileStream的一个构造函数能够很方便的传递一个句柄并为之所用。查阅MSDN的SafeFileHandle时发现了CreateFile的用法,只需要传递一个FILE_FLAG_NO_BUFFERING给CreateFile,就可以实现不使用系统缓存的读写。但使用非缓存的读写有一些操作上的限制,详细的可以见MSDN的相关文档。
  运行以上程序,对一个Segate 80G的硬盘的D盘(硬盘自身缓存8M,其拷贝极限速度在26M/s左右)的一个大文件进行复制操作,得到的结果如下:
缓存大小

不使用系统缓存
拷贝速度(MB/s)

使用系统缓存
拷贝速度(MB/s)
1M 11.99 n/a
2M 15.19 n/a
4M 20.42 n/a
8M 23.87 n/a
16M 25.09 n/a
32M 25.93 11.31
64M n/a 15.89
128M n/a 17.02
由于将在程序中将bufferSize设置为64M会导致出现异常,所以64M没有数据。对于使用缓存的拷贝的速度小于32M缓存的读取速度很慢,没有进行更多的测试。
  结果很明显,在同样的自定义缓存大小的同时,不使用系统缓存的拷贝速度明显要高于使用系统缓存的拷贝速度。当不使用系统缓存的拷贝时,当缓存大小等于磁盘物理缓存大小的时候拷贝速度就达到了90%的最大速度;当等于磁盘物理缓存2倍时基本达到磁盘存取极限。由于上述原因,这也就是为什么FastCopy不使用系统缓存的缘故了。
  当然如果大家有什么更好的看法或想法,欢迎留言探讨,谢谢!
posted @ 2007-08-18 14:35 Dream world 梦想天空 阅读(4198) 评论(19)  编辑 收藏

  回复  引用    
#1楼 2007-08-18 16:28 | 560889223 [未注册用户]
但是,据说XP和Vista的复制是自带校检的?
  回复  引用  查看    
#2楼 2007-08-18 16:40 | 补丁      
当然自带校验咯,不然还不得整天崩溃,呵呵
  回复  引用  查看    
#3楼 [楼主]2007-08-18 17:33 | Dream world 梦想天空      
@补丁
@560889223
这不清楚~~~我觉得应该是不带的。
  回复  引用  查看    
#4楼 2007-08-18 20:13 | 地狱门神      
我上个月做了一个文件复制器
http://www.cnblogs.com/Rex/archive/2007/07/27/832953.html
是关于文件同步的。

对于这种既有一些大文件又有大量小文件的情况,有没有什么好的办法来解决?

  回复  引用  查看    
#5楼 2007-08-18 20:19 | hesicong      
@地狱门神
使用一定的策略即可。
大文件用不缓存的。
小文件用缓存的。
  回复  引用  查看    
#6楼 2007-08-18 20:35 | 地狱门神      
@hesicong
有没有研究过分界线取多少比较好?
  回复  引用  查看    
#7楼 [楼主]2007-08-18 21:14 | Dream world 梦想天空      
@地狱门神
这个要做实验~~~我正在对文件操作相关做研究。
  回复  引用  查看    
#8楼 2007-08-18 22:12 | Jonny Yu      
用OVERLAPPED I/O应该会更快一些的吧?
还有开缓存的时候一定要不能被交换方式,并且不要开太大,否则Windows 不得不把一些内存写到交换文件才能创建内存块。
  回复  引用  查看    
#9楼 2007-08-18 22:19 | 地狱门神      

  回复  引用  查看    
#10楼 2007-08-19 01:17 | hesicong      
@Jonny Yu
关于文件的读写的过程,我还正在逐步探讨当中。OVERLAPPED I/O是异步读取,实现起来要麻烦一些,没有试过,但我猜测应该比此法要慢,因为文中的做法几乎完全占用磁盘的I/O。决定速度的因素还是在于如何读取磁盘。如果操作系统要管理缓存的话,无论用何种方法都难以控制缓存的大小,当文件大到一定程度的时候,操作系统会缓存一定数量的文件块到内存当中,势必会造成很多已经关闭的程序的缓存丢失,造成系统整体变慢。
  回复  引用  查看    
#11楼 2007-08-19 11:56 | Lostinet      
OVERLAPPED I/O是用来做并发编程的.
例如服务器针对超多人在线访问数据.

如果不用OVERLAPPED IO,则服务器需要启动上千条线程同时执行,等待,,,服务器不能承受得了.

使用了OVERLAPPED IO,在有数据的时候才执行具体的方法,就能节省很多线程数. 所以服务器的SOCKET编程一般都是使用完成端口.

而对于桌面应用, 没必要.

  回复  引用  查看    
#12楼 2007-08-19 12:42 | hesicong      
@Lostinet
学习了:)
  回复  引用  查看    
#13楼 2007-08-20 09:53 | 亚历山大同志      
复制文件的速度跟计算机的硬件环境是密切相关的,和操作系统的关系也比较密切,涉及到CPU缓存和内存页之间的调度,硬盘缓存容量,操作系统对磁盘的调度算法等一系列条件的制约,没有哪个单独的算法是最快的,所一要做到最快的复制文件程序需要能够自动识别硬件的性能(比如硬盘缓存的大小,是IDE的硬盘还是SCSI的硬盘)和操作系统版本,从而选择最优化的算法,不然没有一种算法是通吃的
  回复  引用    
#14楼 2007-08-20 10:10 | 神州软件园 [未注册用户]
写得不错
  回复  引用  查看    
#15楼 [楼主]2007-08-20 16:52 | Dream world 梦想天空      
@亚历山大同志
谢谢评论。确实如此。但我觉得现在操作系统提供的函数对于大多数个人电脑的单硬盘速度已经足够了。主要的问题集中在是否需要系统缓存。不管哪种函数,我想,经过了系统缓存的处理以后,是肯定要拖慢系统的,效率应该是没有使用自管理小缓存(32M左右)高。
总之这是一个值得探讨的话题,万用药是没有的:)
  回复  引用    
#16楼 2007-08-21 06:41 | 拷贝 [未注册用户]
@Dream world 梦想天空

you are right,

曾经碰到这样的问题,如果你的内存有坏块,文件的内容读到内存后,再从内存读到另外的盘或目录,文件就会损坏。

  回复  引用    
#17楼 2007-08-21 11:40 | 吕昆 [未注册用户]
不错,记得很早以前国外就出了一个替换WINDOWS复制文件的软件,那个可以调整目前复制速度,在需要运行其他软件的同时复制,调低些更好:)你的这个也不错,说明得很全面,支持下!

也欢迎您到我的博客做客!http://www.10000o.com.cn
  回复  引用  查看    
#18楼 [楼主]2007-08-21 13:27 | Dream world 梦想天空      
@拷贝
对于这个问题可能操作系统会比程序还先崩溃,呵呵。
如果确实有必要检查文件的正确性,可以验证HASH码,如果有错误就重新分配一块内存(以前那个内存块废弃不用)。
@吕昆
我正在尝试做相关的软件,功能更强大的软件。
  回复  引用    
#19楼 2007-09-07 10:20 | k4me [未注册用户]
能在你的环境下测试一下这个嘛? 自己做的,没什么技术,原本只是测试程序能达到的文件读写速度,后来发现令人惊讶,就把它扩展了下.只是开20M内存读满了再写到磁盘.
http://www.fileden.com/files/2007/5/8/1059086/k4mecopy_2005.rar

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-08-19 13:50 编辑过
 
另存  打印