2.3 访问文件
在实际编程中,程序大部分时间在与数据打交道,要么是读写数据库,要么是读写文件。.NET也提供了丰富的类库来方便程序员处理文件的读写。在编程中,文件读写一般称为I/O(Input/Output)操作。相关的类库基本在System.IO命名空间中。在.NET中,处理文件的类库大致可以分为以下几种类型。
- 文件和文件夹处理类:主要包含文件和文件夹的创建、复制、移动、删除甚至基础读写等日常操作。
- 流处理:处理文件的读写。
- 隔离存储:在保护计算机安全的基础上,对网络下载等非信任环境下的程序提供安全的文件存储和读写服务。
- 管道:经常用在多进程编程场景中。
- 内存映射文件:经常用在高性能文件读写的场景中。
2.3.1 文件和文件夹基本操作
在System.IO命名空间里,File类包含了大部分与文件相关的操作,Directory类则包含了大部分与文件夹相关的操作。不过,File和Directory类都是通过静态方法来操作的,因此.NET还提供了相应的对象版FileInfo和DirectoryInfo。其在实例化后通过实例方法操作文件夹和文件类型。
代码清单2-15实现了一个简化版的xcopy命令,用来将源文件夹srcdir中符合searchPattern模式的文件保留文件夹结构并复制到dstdir文件夹中。第9行的Directory.GetFiles静态方法用于在srcdir文件夹中寻找匹配searchPattern模式的文件名列表,SearchOptions.AllDirectories参数表明是递归查找。如果srcdir里有符合searchPattern模式的文件,其绝对路径保存在files数组中。第13~14行分别使用Path的GetDirectory-Name和GetFileName方法从文件的绝对路径中获取文件夹的路径和文件名。第18行和第20行将源文件的文件夹路径替换成目标文件夹路径,然后在第23行和第24行判断目标文件夹是否已存在,如果不存在则创建它。最后,第26行使用File.Copy静态方法将源文件复制到目标文件夹中。
代码清单2-15 C#简化版的xcopy命令
// 源码位置:第2章\csxcopy.cs 01 static void DoXCopy(string srcdir, string dstdir, string searchPattern) 02 { 03 // 省略一些参数验证代码 04 if (srcdir[srcdir.Length - 1] == Path.DirectorySeparatorChar) 05 srcdir = srcdir.Substring(0, srcdir.Length - 1); 06 if (dstdir[dstdir.Length - 1] == Path.DirectorySeparatorChar) 07 dstdir = dstdir.Substring(0, dstdir.Length - 1); 08 09 var files = Directory.GetFiles( 10 srcdir, searchPattern, SearchOption.AllDirectories); 11 foreach (var file in files) 12 { 13 var directory = Path.GetDirectoryName(file); 14 var filename = Path.GetFileName(file); 15 var newdirectory = dstdir; 16 if (directory.Length > srcdir.Length) 17 { 18 var relativePath = directory.Substring( 19 srcdir.Length + 1, directory.Length - srcdir.Length - 1); 20 newdirectory = Path.Combine(dstdir, relativePath); 21 } 22 23 if (!Directory.Exists(newdirectory)) 24 Directory.CreateDirectory(newdirectory); 25 26 File.Copy(file, Path.Combine(newdirectory, filename)); 27 } 28 }
由于Windows操作系统和其他操作系统的路径分隔符不一致,Windows操作系统使用“\”作为分隔符。而其他操作系统大部分使用“/”作为分隔符。.NET使用Path.DirectorySeparatorChar字段来保存这个差异,如代码清单2-15在第4行、第6行使用该字段来判断用户输入的文件夹是否以分隔符结尾。在代码清单2-15里笔者还演示了.NET中处理文件路径的方法,如要拼接文件夹路径和文件名,与其使用字符串拼接方法,不如使用Path.Combine方法。这是因为不同操作系统上路径分隔符不同,Combine方法会自动处理。
2.3.2 流处理
File类型的ReadAllText、ReadAllBytes、WriteAllText和WriteAllByte等方法实现了简单的文件读写。如果程序只是为了读写文件的部分数据的话,很明显这些方法的效率就低多了。这种情况下,我们一般采用流式数据处理方法来读写文件。
在计算机中,所有的数据都是以字节形式存储的,文件也不例外。计算机除了从文件中读写数据以外,还可以从内存、网络等地方读写数据。为了统一多种数据源的读写操作,计算机科学里将这些读写抽象成流(Stream)处理。流处理有点像水流,即字节流,程序可以从字节流里读取一些字节,也可以向字节流里写入一些字节。对于一些类型的字节流,我们还可以寻址到流的某个位置进行读写操作。流处理方法主要包含3类API。
1)读取(Read):用来从流中读取数据到指定的数据结构中,如将字节数据存储到数组中。
2)写入(Write):将字节数据从某个数据源写入流中。
3)寻址(Seek):查询和修改流中当前的读写位置。
代码清单2-16演示了流处理的基本操作。由于文件是一个不受CLR管理的系统资源,打开文件并使用完毕后需要在操作系统里及时关闭,所以第1行使用using语句来管理这种非托管资源。我们可以通过实例化FileStream类的方式来打开文件,也可以使用File.Open静态方法打开文件。与C/C++等语言类似,FileMode的OpenOrCreate枚举限定了打开方式——如果文件不存在的话则创建它。第4行使用一个循环将a~z字母写入文件filestream.demo,WriteByte说明是以字节的方式写入的。在流处理中,流的内部会保存一个指针来跟踪目前在流中读写的位置。这个指针默认情况下只会向前推进,如第5行每次写入1字节,指针就会前进1字节。只有Seek方法能重新调整指针的位置,如第7行将指针重新定位到流的开头位置,再在第9行逐个读取字节。ReadByte返回0,说明流的所有字节已经读取完。在流里逐个读写字节是一种效率非常低下的操作,通常情况下使用批量读写的方法,即事先定义一个缓存用的字节数组(数组的大小是固定的,如第13行的bytes数组),然后使用Read或者Write方法批量读写字节。因此,这两个方法都要求明确读写的起始位置——第二个参数,和每次读写的数量——第三个参数。Read方法执行完毕后会返回成功读写的字节数,通过判断这个返回值是否与缓存数组的长度相等,即可获知流中是否还有未处理的数据,如第15行。每次读取成功后,缓存数组bytes存放读取到的新字节,然后根据预设的编码方式解析字节即可。
代码清单2-16 使用流处理模式读写文件
// 源码位置:第2章\StreamDemo.cs 01 using (var fs = new FileStream("filestream.demo", FileMode.OpenOrCreate)) 02 //using (FileStream fs = File.Open("filestream.demo", FileMode.OpenOrCreate)) 03 { 04 for (var i = 0; i< 26; ++i) 05 fs.WriteByte((byte)(i + 'a')); 06 07 fs.Seek(0, SeekOrigin.Begin); 08 int b = 0; 09 while ((b = fs.ReadByte()) > 0) 10 Console.Write((char) b); 11 Console.WriteLine(); 12 fs.Seek(0, SeekOrigin.Begin); 13 byte[] bytes = new byte[20]; 14 int count = 0; 15 while((count = fs.Read(bytes, 0, bytes.Length)) > 0) 16 { 17 Console.Write(System.Text.Encoding.ASCII.GetString(bytes)); 18 Array.Clear(bytes, 0, bytes.Length); 19 } 20 Console.WriteLine(); 21 }
流处理很好地体现了编程中抽象这个概念,而且非常贴合面向对象编程的抽象、继承和封装等编程模式。代码清单2-17是一个缩放图片大小的命令行程序。程序的逻辑是读取源图片到输入流,即在inputStream中执行源图片缩放大小操作,并将结果图片的字节写入输出流,即写入outputStream。程序演示了几种读取图片的方式。
- 被注释的第5行是从文件系统中读取图片文件,这时inputStream的类型是FileStream。
- 第1~4行则是通过WebRequest直接从网络上下载图片文件到内存,这时inputStream的类型是更为抽象和通用的流处理。
- 第7行和第8行都是将结果文件写入文件系统,outputStream的类型都是FileStream。
- 第10行则是将结果文件写入内存,outputStream的类型是MemoryStream,这也意味着如果进程结束,结果文件也就被操作系统废弃了。
代码清单2-17中演示了无论来源数据和结果文件输出是什么,流处理都能封装并提供统一的编程体验。然而并不是所有的数据源都支持流处理封装的基本方法,如第7行中的FileStream是采用File.OpenWrite方式打开的,只能用在修改模式下。将第8行与第7行互换的话,程序在运行到第18行时会抛出异常,这是因为只写模式不支持读操作。另外,有些数据源(如硬盘)是支持对移动流的位置进行读写的,而有些数据源不支持,因此流处理提供了CanRead、CanWrite和CanSeek三个属性辅助我们实现相应的功能。
代码清单2-17 使用不同的流读写图片文件
// 源码位置:sample-code/第2章/ResizeImage/ResizeImage // 编译方法:使用Visual Studio打开工程编译 01 WebRequest request = WebRequest.CreateHttp( 02 "https://cn.bing.com//az/hprichbg/rb/GoldenEagle_EN-CN5621882775_1920x1080.jpg"); 03 var response = request.GetResponse(); 04 using (var inputStream = response.GetResponseStream()) 05 // using(FileStream inputStream=File.OpenRead("BingFeedImage_1920x1080.jpg")) 06 { 07 // using (FileStream outputStream = File.OpenWrite("Resized.jpg")) 08 using (FileStream outputStream = File.Open( 09 "Resized.jpg", FileMode.OpenOrCreate)) 10 // using (MemoryStream outputStream = new MemoryStream()) 11 { 12 var img = Image.Load(inputStream, out IImageFormat format); 13 img.Mutate(data => 14 data.Resize(img.Width / 2, img.Height / 2) 15 .Grayscale()); 16 img.Save(outputStream, format); 17 outputStream.Seek(0, SeekOrigin.Begin); 18 Console.WriteLine(outputStream.ReadByte()); 19 } 20 }
流处理也可以使用设计模式里的组合模式,即一个流可以包含其他流或复用其他流。代码清单2-18演示了这种编程模式,其通过.NET框架内置的GZipStream实现压缩和解压功能。首先第8行打开压缩后文件的输出字节流outputStream,然后第10行的GZipStream实现了压缩的算法,但并没有将压缩后的字节保存,而是依赖其他如outputStream的FileStream类型实现数据保存,最后在第13行从输入流一边读取数据,一边压缩数据,同时写入输出流完成整个压缩操作。
代码清单2-18 流处理的组合模式
// 源码位置:第2章\csgzip.cs // 编译命令:csc csgzip.cs 01 static void Compress(string file) 02 { 03 using (var fileStream = File.OpenRead(file)) 04 { 05 if ((File.GetAttributes(file) & FileAttributes.Hidden) 06 == FileAttributes.Hidden) 07 return; 08 using (var outputStream = File.Create(file + ".gz")) 09 { 10 using (var gzstream = new GZipStream( 11 outputStream, CompressionMode.Compress)) 12 { 13 fileStream.CopyTo(gzstream); 14 } 15 } 16 } 17 }
表2-6列出了常用的流类型以及相应的使用场景,具体的使用方法请读者自行参阅微软的.NET官方文档。
表2-6 常用的流类型说明
通常,流处理都是基于字节的,将字节转换成程序能够使用的具体数据还要经过编码处理。为了便于编程,.NET提供了额外的读写类封装流处理。下面是常用的读写类说明,具体的使用方法请读者查阅.NET官方文档。
- BinaryReader和BinaryWriter类:用来从字节流里读写原生数据类型。
- StreamReader和StreamWriter类:用来从流中根据不同的编码来读写字符串。
- StringReader和StringWriter类:在字符串中读写字符。
- TextReader和TextWriter类:读写字符的基类。
2.3.3 管道
与将数据读写抽象成流处理的概念类似,数据存储(Data Store)在操作系统中也被抽象成文件,即操作系统将所有可以存储或输出数据的设备抽象成文件处理。如命令行程序在屏幕上打印(Console.Write)文本数据,操作系统将文本数据输出到屏幕文件中处理,而命令行程序从键盘读取(Console.Read)数据,被抽象成从键盘文件中读取数据。所有的进程都可以向屏幕文件输出数据,也都可以从键盘文件中读取数据,因此这些文件如屏幕文件被称作标准输出(Standard Output)文件,而键盘(或相似)文件被称作标准输入(Standard Input)文件。在.NET中,Console.In静态字段可以获取进程的标准输入文件,Console.Out则获取进程的标准输出文件。除此之外,Console.Error字段获取进程的标准错误输出,以便将进程的错误信息与普通输出区分开来。Console.Write字段就是往标准输出文件中写入数据,Console.Read字段则从标准输入文件中读取数据。
由于标准输入和标准输出都被抽象成一个文件,所以操作系统的确允许将标准输出写到文件系统,也允许从文件系统读取数据到标准输入,这个功能称为重定向(Redirect)。在操作系统中,符号“>”表示将标准输出重定向到某个文件,如将目录列表的结果输出到文件demo.txt中:
dir > demo.txt
符号“<”表示重定向标准输入到某个文件,如从文件demo.txt读入文本行,而不是从键盘终端读取:
sort < demo.txt
既然可以将标准输入和输出重定向到文件,那可不可以将一个程序的标准输出重定向到另一个程序的标准输入呢?答案是可以的。这个特性称为管道(Pipe Line),其符号是“|”,如将前面的dir和sort命令通过管道连接起来:
dir | sort
管道是一个非常有用的编程模式,与其将所有功能都集成到一个大而全的程序中,导致功能耦合性过高和代码维护代价大,不如考虑将不同的功能拆分到几个小程序里,以分而治之的方式来简化代码结构。
代码清单2-19是一个将图片转换成字符画的代码。
代码清单2-19 将图片转换成字符画
// 源码位置:\第2章\Image2Ascii\Image2Ascii\Program.cs // 使用Visual Studio IDE编译 01 static string _ASCIICharacters = "##@%=+*:-. "; 02 03 static string Convert(string file, int width, int height) 04 { 05 var img = Image.Load(Path.GetFullPath(file)); 06 width = Math.Min(width, img.Width); 07 height = Math.Min(height* img.Height / img.Width, img.Height); 08 09 img.Mutate(data => 10 data.Resize(width, height).Grayscale()); 11 var sb = new StringBuilder(); 12 for (var h = 0; h<height; ++h) 13 { 14 for (var w = 0; w<width; ++w) 15 { 16 var pixel = img[w, h]; 17 var idx = pixel.R * _ASCIICharacters.Length / 255; 18 idx = Math.Max(0, Math.Min(_ASCIICharacters.Length - 1, idx)); 19 var c = _ASCIICharacters[idx]; 20 sb.Append(c); 21 } 22 sb.AppendLine(); 23 } 24 25 return sb.ToString(); 26 }
代码逻辑是先在第9~10行将图片转换成黑白图片。由于需要在命令行终端(分辨率过低导致宽度和高度都有限)显示,笔者添加了缩小图片的功能。第12~23行循环遍历图片中的每一个像素点。第16~19行根据像素点的红色部分(pixel.R)来确定使用哪个字符代表这个像素点,最后将选中的字符添加到结果字符串中。第22行的换行对应处理完的一行像素。运行程序,打印的结果如图2-9左图所示,是一个由黑色字符组成的画。
图2-9 将图片转换成字符画的结果
如果想给字符串添加一些色彩,可以在代码清单2-19的程序中添加支持彩色的代码,但如果在一个程序里不断添加功能,必然会导致程序越来越臃肿,维护不便。而使用管道的话,只需要实现一个从标准输入不停读入字符并根据预定的规则添加功能的小程序即可。如代码清单2-20所示,其工作只是不断地从标准输入里读取输入字符,如果添加字符在0~255之间的话,则判断字符是否在预定规则的位置,即第9行。如果匹配高亮规则,则执行第11行(设定输出字符的色彩),否则进入第13行(使用默认色彩输出)。
代码清单2-20 从标准输入读取字符并添加色彩的程序
// 源码位置:第2章\Image2Ascii\colorful\Program.cs // 使用Visual Studio IDE编译 01 static string _ASCIICharacters = "#@%=+*:-. "; 02 static void Main(string[] args) 03 { 04 var originColor = Console.ForegroundColor; 05 var colors = (ConsoleColor[])Enum.GetValues(typeof(ConsoleColor)); 06 char c = (char)Console.Read(); 07 while (c > 0 && c< 255) 08 { 09 var idx = _ASCIICharacters.IndexOf(c); 10 if (idx >= 0) 11 Console.ForegroundColor=(ConsoleColor)(colors[colors.Length-idx-1]); 12 else 13 Console.ForegroundColor = originColor; 14 Console.Write(c); 15 16 c = (char) Console.Read(); 17 } 18 }
在执行时,只需要将两个程序使用管道连接起来就可以获取图2-9右图的彩色图像了。
除了命令行终端提供的连接多个进程的标准输入/输出的管道以外,操作系统本身也提供了进程间通信的管道API,由于这个知识跟多进程编程相关,我们放在后面的章节里讨论。
2.3.4 内存映射文件
现代操作系统通常是多用户、多任务的。通过虚拟内存管理机制,每个进程都可以运行在独立的内存沙盒中。这个沙盒被称为虚拟地址空间。在32位操作系统中,这个地址空间是4GB,即32位系统的最大寻址空间。从进程的角度看,其独占整个内存。而实际上,操作系统通过页映射表(Page Table)将虚拟地址空间中的地址映射到物理内存地址上。每个进程都有自己的页映射表。一旦启用虚拟地址,机器上所有的代码都会受影响,包括内核代码,因此进程必须为内核保留一部分虚拟地址空间。如图2-10所示,Linux内核一般占用1GB的地址空间,Windows默认占用2GB的地址空间,如果打开大地址开关(Large Address Aware)可以将Windows内核占用的地址空间压缩到1GB。
图2-10 Windows和Linux操作系统的虚拟地址空间分配情况
虽然操作系统内核保留了不小的地址空间,但并不意味着内核会实际占用如此多的物理内存。只有在映射发生时,内核才会占用匹配的地址空间。在操作系统中,内核占用的地址空间(Kernel Space)在页映射表中会加上特权代码(Privileged Code)标志。当用户模式(User Mode)代码访问到内核地址空间时,会触发页错误(Page Fault)异常。早期的操作系统里,内核代码和数据的虚拟内存地址都是固定的,即可以事先推算出来,而且所有的电脑都是一样的,这就导致黑客很容易猜出内核代码和数据的位置,再通过缓冲区溢出等漏洞进行破坏。因此,新版本的操作系统通常会做一些随机化和保护处理。用户模式的虚拟内存地址的物理内存映射部分会随着进程切换而不同。如图2-11所示,用户态空间中浅灰色部分代表映射到物理内存地址的虚拟内存地址,即进程实际使用到的内存部分。
图2-11 用户态不同进程的虚拟内存映射
用户态里的虚拟内存也不是随意分配的。用户模式的地址空间通常会分成几个大的内存片段。图2-12展示了Linux进程的标准内存布局。除了进程的代码、静态变量等事先就已经占用的内存以外,剩下的内存主要用来保存函数调用时需要的参数和局部变量的栈、进程运行时为变量动态分配内存的堆(Heap),以及内存映射段。这些内存随着程序的运行动态增减。
图2-12 Linux进程的用户模式地址空间的内存划分
无论是内核地址空间还是用户地址空间,整个4GB的地址空间按页拆分,虽然32位的x86处理器支持4KB、2MB和4MB的页大小,但Windows和Linux都使用4KB来映射用户态的地址空间。3GB的用户态地址空间按4KB页进行布局(见图2-13),类似的物理内存也是按4KB的页来划分。
图2-13 3GB的用户态地址空间页布局
虚拟内存技术极大地便利了编程。对于32位进程来说,整个4GB的地址空间都由其专属使用,这将程序员从繁重的物理内存管理编程中解脱了出来,也极大地提高了物理内存的使用效率。例如内核可以将多个进程用到的内核代码(如libc.so)都映射到同一段物理地址,避免每个进程都将内核代码重新加载一遍。另外,在32位机器流行的时代,物理内存实际上都很小,比如256MB内存的机器就是高级配置了。虚拟内存技术可以放大计算机上可用的物理内存,将物理内存里暂时不用且容纳不了的数据放到硬盘上,在进程使用的时候再从硬盘加载回物理内存。这个过程分为两种情况:页命中(Page Hit)和页缺失(Page Fault)。
- 页命中:如果进程访问的虚拟内存映射的地址在物理内存里,称为页命中。这时,操作系统几乎不需要做工作,如访问图2-14中左边的灰色部分。
图2-14 虚拟内存页映射表
- 页缺失:如果要访问的地址不在物理内存里,称为页缺失,如访问图2-14中左边的白色部分。页缺失会导致CPU通知操作系统的页缺失处理程序从硬盘中将丢失的页加载回物理内存。如果物理内存已经满了,操作系统还会按照一定的算法选择一部分物理内存页并移到硬盘来腾出空间。
既然可以自动移动物理内存和虚拟内存中的文件,那为什么不能指定物理内存和文件之间的映射呢,这就是内存映射文件的来历。内存映射文件允许程序像直接使用物理内存这样来处理文件。而且在很多时候,这种处理方式的性能要高于文件I/O处理。如代码清单2-21中的MemMapDemo方法,其使用内存映射技术将要打开的图片文件直接当作系统的虚拟内存处理,也就是说程序读取图片里的数据时,直接把图片当作一个已经加载到虚拟内存的字节类型的大数组。当访问这个大数组中的某个字节,也就是图片的某个像素时,虽然图片数据尚未加载到物理内存,但操作系统会自动触发内存页缺失机制,将映射到虚拟内存地址的相应图片数据从硬盘中加载到物理内存,并返给进程。整个过程对于进程是不可见的。进程所需要做的就是普通的内存寻址操作,因此极大地便利了程序读取数据。
代码清单2-21 使用文件I/O和内存映射文件处理图片
// 源码位置:第2章\MMapDemo\MMapDemo\Program.cs // 使用Visual Studio IDE编译 01 static void FileIoDemo(string source, string destination) 02 { 03 var input = Image.Load(Path.GetFullPath(source)); 04 for (var i = 0; i < input.Height; i += 50) 05 { 06 for (var j = 0; j < input.Width; ++j) 07 input[j, i] = Rgba32.White; 08 } 09 input.Save(Path.GetFullPath(destination)); 10 } 11 12 static void MemMapDemo(string source, string destination) 13 { 14 File.Copy(source, destination, true); 15 var (offset, width, height) = ReadHeaders(source); 16 using (var mm = MemoryMappedFile.CreateFromFile(destination)) 17 { 18 var whiteRow = new byte[width]; 19 for (var i = 0; i < width; ++i) whiteRow[i] = 255; 20 using (var writer = mm.CreateViewAccessor(offset, width * height)) 21 { 22 for (var i = 0; i < height; i += 50) 23 { 24 writer.WriteArray(i * width, whiteRow, 0, whiteRow.Length); 25 } 26 } 27 } 28 }
代码清单2-21演示了.NET中内存映射文件的用法以及与文件I/O处理的性能对比。为了演示方便,程序只能处理bmp格式的位图,这是因为bmp格式的图片相对来说好处理。
第1~10行采用文件I/O的方式处理图片,每隔50行像素就在图片上加一条白线。第6~7行的循环负责逐个对像素点赋值来添加白线。第12行的内存映射版本首先将源文件复制到目标文件,然后在第16行使用MemeoryMappedFile类型在目标文件上创建内存映射文件。由于创建白线需要知道图片的宽度和高度信息,因此第15行调用ReadHeaders方法读取位图的实际数据的开始位置offset、宽度width和高度height,并通过元组返回。第20行的CreateViewAccessor方法创建被映射文件的修改视图,最后在第22~25的循环里每隔50个像素点,将预定的白线像素数组whiteRow直接写入内存,进而持久化到目标文件中。
图2-15是两个版本的性能对比,使用文件I/O的版本耗时0.298558s,而使用内存映射文件的版本虽然看上去代码多,但是运行速度相对来说快很多,只耗时0.119186s。
图2-15 文件I/O和内存映射文件性能对比
与大部分技术类似,内存映射文件也是一把双刃剑。其优点如下。
1)对于编程来说,其避免了文件I/O流操作不停地从硬盘读取数据到内存,以及清空缓存等烦琐的代码。而且文件I/O的读写实际上使用的是read()和write()系统调用函数,每次系统调用都涉及从内核态到用户态来回切换,这种切换对性能的损耗很大。而内存映射文件只有在开始映射时用到系统调用函数。除了页缺失,读写内存映射文件不会用到系统调用函数。
2)当多个进程将相同的文件映射到内存时,文件的数据对多个进程都是可见的,因此一个进程写入数据,其他进程可以立即看到,这在多进程间通信非常有用,也是内存映射文件的另一个使用场景,如图2-16所示。
图2-16 多进程间内存映射
3)文件的读写仅仅是指针的操作,相对于I/O流的寻址操作来说,方便太多。
内存映射文件缺点如下。
1)内存映射文件受地址空间的大小限制。因为其只能映射到用户态地址空间,所以操作系统的内存配置不能超过2GB或者3GB。对于64位操作系统,建议读者先调研一下。
2)被映射文件的I/O错误,如硬盘被拔出或者硬盘已经写满,这些错误视操作系统不同需要不同的错误处理逻辑。
3)由于内存映射文件被操作系统当作内存处理,数据何时被操作系统实际写入文件系统严重依赖操作系统的内存管理程序。如果程序崩溃,不能保证数据被实际写入文件系统。
在实际编程中,如果对程序运行的硬件环境有把握的话,使用内存映射文件可以提高程序性能和编程效率,否则建议读者先调研清楚具体的使用场景再考虑内存映射文件的适用性。