2. 设备I/O及线程间通讯
我不会特别的强调这一章的重要性,本章包含二个当您在实作高效能、可伸缩性应用程序时的基本主题:设备I/O及线程间通讯。一个具有可伸缩性的应用程序在处理大量同时发生的操作时,就像在处理少量同时发生的操作一样有效率。对一个服务应用程序来说,这些操作处理在不可预知的时间到达且需要无法预测之处理能力总数的客户端请求。这些请求通常从如网路介面卡的I/O设备而来;频繁地处理这些请求需要如磁盘文件般的额外I/O设备。在Microsoft Windows应用程序中,线程是帮助您划分工作的最佳可用工具。每个线程被分派给一个允许多重处理器同时执行多重操作的处理行程,以增加其总处理能力。当一个线程因设备I/O请求而产生时,线程会暂时停止执行,直到该设备完成I/O请求为止。因为线程无法执行初始化另一个客户端处理请求之类的工作,所以这个悬置状态会造成效能的损失。总之,您需要让您的线程一直执行着有帮助的工作。
为了让线程保持忙碌的状态,您必须使线程与其所执行的另一个相关操作互相通讯。Microsoft花了多年的时间在研究和测试这个领域,且开发了一个协调良好的机制来建立此通讯。这个机制称作I/O完成端口(I/O completion port),它能帮助您建立高效能、可调节性的应用程序。使用I/O完成端口,您可以不需等待设备的回应就读写设备,使得应用程序的线程达到惊人的总处理能力。
I/O完成端口起初被设计用在处理设备I/O的部份,但经过数年以后,Microsoft已经设计了一套越来越适合I/O完成连接模组的作业系统。一个例子是在Microsoft Windows 2000中采用了新的工作核心物件:像工作物件监视它的行程一样,它会传送事件通知给I/O完成端口。在《Programming Applications for Microsoft Windows, Fourth Edition》(Jeffrey Richter, Microsoft Press, 1999)中可以找到JobLab范例应用程序,它说明了I/O完成端口和工作物件如何一起工作的方式。
身为一个Windows开发人员,基于多年的经验,我发现越来越多人开始使用I/O完成端口,且觉得每位Windows开发人员都必须完全地了解I/O完成连接的工作方式。本书中有许多范例应用程序皆使用了I/O完成端口。虽然我在这章提出了关于设备I/O之I/O完成端口,但我察觉到I/O完成端口并不一定要与设备I/O一起使用-简单地说,它是一个无限制使用线程间通讯的可怕机制。
由于以上的夸耀,您或许会说我是I/O完成端口的狂热爱好者。希望在这章节结束时,您也会和我一样。跳过I/O完成端口的细节部份。我将说明Windows当初在线程间通讯和设备I/O的部份提供了什么给开发人员,这将使您更正确的评价I/O完成端口。本章的最后部分〈I/O完成端口〉将讨论I/O完成端口的内容。
启动与关闭设备
Windows的其中一个长处就是它支援所有的设备。以这个讨论为背景,我替设备所下的定义是任何允许通讯的东西。表2-1列出了一些设备及它们最常见的使用方式:
| 表2-1 各种设备和其常见的使用方式 |
| 设备 | 最常见的使用方式 |
|---|---|
| 文件 | 持久储存多变的资料。 |
| 目录 | 属性和文件压缩设定。 |
| 逻辑磁盘驱动程序 | 格式化的驱动程序。 |
| 实体磁盘驱动程序 | 存取分割表格。 |
| 序列埠 | 经由电话线传送资料。 |
| 串列埠 | 将资料传送到打印机。 |
| Mailslot | 一对多传送资料,通常经由网路连接到执行Windows的机器上。 |
| 命名管道(Named pipe) | 一对一传送资料,通常经由网路连接到执行Windows的机器上。 |
| 匿名管道(Anonymous pipe) | 在单机上一对一传送资料(不经由网路)。 |
| Socket套接字 | 资料包或资料流传送资料,通常经由一个网路到任何一台支援套接字的机器(机器不需要执行Windows)上。 |
| 控制台 | 一个文字视窗萤幕缓冲器。 |
本章讨论在没有等待设备回应的情况下,应用程序的线程如何和它们通讯。Windows试着尽可能对软件开发人员隐藏这些设备的差异部份,也就是一旦您启动了一个设备,不管您正在和什么设备通讯,对您来说,允许您读写资料到设备的Windows函数都是相同的。例如,为序列埠指定传输速率是有意义的,但是当您使用命名管道通过网路(或透过本地机器)来通讯时,传输速率就没有意义了。设备彼此间有巧妙的不同处,我不会提出所有的细微差别,不过,会花些时间在文件上,因为它比较常见。
为了要执行任何类型的I/O,您首先必须启动所需的设备且获得对它的handle。获得设备handle的方法依据特定的设备而有所不同。表2-2列出各种设备和启动它们所应该呼叫的函数。
| 表2-2 开始各种设备的函数 |
| 设备 | 启动设备所使用的函数 |
|---|---|
| 文件 | CreateFile(pszName是路径名称或UNC路径名称)。 |
| 目录 | CreateFile(pszName是目录名称或UNC目录名称)。假如您在呼叫CreateFile时指定了FILE_FLAG_BACKUP_SEMANTICS标记,Windows 2000将允许您去开启一个目录,并允许您改变目录的属性(标准、隐藏等)及时间戳记(time stamp)。 |
| 逻辑磁盘设备 | CreateFile(pszName是「\\.\x:」)。假如您您以「\\.\x:」的形式指定一个字串,x即是设备代号,Windows 2000允许您启动一个逻辑设备。例如,您指定「\\.\A:」来启动设备A。启动设备允许您格式化驱动程序或决定该设备的适当大小。 |
| 实体磁盘设备 | CreateFile(pszName是「\\.\PHYSICALDRIVEx」)。假如您以「\\.\PHYSICALDRIVEx」的形式指定一个字串,x即是代表实体设备编号,Windows 2000允许您启动一个实体设备。例如,您指定「\\.\PHYSICALDRIVE0」以读写使用者的第一个实体硬盘之实体区段。启动实体设备允许您直接存取硬盘的资料分割资料表。启动实体设备可能是危险的;对设备不正确的写入可能会使磁盘的内容无法经由作业系统的文件系统存取。 |
| 序列埠 | CreateFile(pszName是「COMx」)。 |
| 串列埠 | CreateFile(pszName是「LPTx」)。 |
| Mailslot伺服端 | CreateMailslot(pszName是「\\.\mailslot\mailslotname」)。 |
| Mailslot客户端 | CreateFile(pszName是「\\servername\mailslot\mailslotname」)。 |
| 命名管道(Named pipe) 伺服端 | CreateNamedPipe(pszName是「\\.\pipe\pipename」)。 |
| 命名管道(Named pipe)客户端 | CreateFile(pszName是「\\servername\pipe\pipename」)。 |
| 匿名管道(Anonymous pipe) | CreatePipe客户端和伺服端。 |
| 套接字socket | socket,接受或AcceptEx。 |
| 控制台 | CreateConsoleScreenBuffe或GetStdHandle。 |
每个在表2-2中的函数皆会回传一个识别设备的handle。您可以传递handle到与设备通讯的各种函数中。例如,您呼叫SetCommConfig以设定序列埠的传输速率:
BOOL SetCommConfig(
HANDLE hCommDev,
LPCOMMCONFIG pCC,
DWORD dwSize);
并且当等待读取资料时,使用SetMailslotInfo设定逾时值:
BOOL SetMailslotInfo(
HANDLE hMailslot,
DWORD dwReadTimeout);
如您所见,这些函数需要使用设备的handle来当作它们的第一个参数。
当您要停止使用设备时,您必须关闭它。对大部分的设备来说,您可以经由呼叫一个很受欢迎的CloseHandle函数来关闭设备:
BOOL CloseHandle (HANDLE hObject);
然而,假如这个设备是一个套接字,您就必须呼叫closesocket来替代CloseHandle:
int closesocket (SOCKET s);
如果您有某个设备的handle,则您可以经由呼叫GetFileType来找出该设备的类型:
DWORD GetFileType (HANDLE hDevice);
所有您该做的事即是传递设备的handle到GetFileType函数中,该函数会传回表2-3所列出的其中一个值。
| 表2-3 经由GetFileType函数的回传值 |
| 值 | 描述 |
|---|---|
| FILE_TYPE_UNKNOWN | 不知道所指定文件的类型。 |
| FILE_TYPE_DISK | 所指定的文件是磁盘文件。 |
| FILE_TYPE_CHAR | 所指定的文件是字元文件,通常是一个LPT设备或控制台。 |
| FILE_TYPE_PIPE | 所指定的文件不是具名就是匿名的管道。 |
细看CreateFile
当然,CreateFile函数会建立和开始磁盘文件,但不要让这个名字愚弄您-它同样可以开始很多其他的设备:
HANDLE CreateFile( PCTSTR pszName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttrs,
HANDLE hfileTemplate);
如您所见,CreateFile需要相当多参数,当要启动一个设备时需要考虑到许多弹性。关于这点,我将详细地讨论这些参数。
当您呼叫CreateFile,pszName参数代表设备的类型和特定实体。
dwDesiredAccess参数具体指定您如何从设备来回地传送资料。您可以传送表2-4中叙述的四个可能的值。
| 表2-4 可以传递给CreateFile的dwDesiredAccess参数值 |
| 值 | 意义 |
|---|---|
| 0 | 您不想在设备中读写资料。当您只想改变设备的安装设定时,即传递0-例如,您只想改变文件的时间戳记的情形。 |
| GENERIC_READ | 允许对设备做唯读存取。 |
| GENERIC_WRITE | 允许只能写入到设备。举例来说,这个值可被用来传送资料到打印机和经由备份软件传送资料。注意,GENERIC_WRITE并不意味着GENERIC_READ。 |
| GENERIC_READ | GENERIC_WRITE | 允许对设备的读写存取。这个值是最常见的,因为它允许自由的交换资料。 |
dwShareMode参数指定分享设备的权利。它可能是单一设备在同一时间内能被数台电脑(在一个网路环境里)或是数个行程存取(在一个多线程环境里)的情形。设备分享可能意味着您必须考虑到是否应该且如何限定其他电脑或行程来存取设备的资料。表2-5叙述可以传递给dwShareMode参数的值。
| 表2-5 可以被传递给CreateFile之dwShareMode参数的相关I/O值 |
| 值 | 意义 |
|---|---|
| 0 | 要求没有其他的行程正在读写设备。假如另一个行程已经启动了这个设备,则您在呼叫CreateFile时会失败。假如您已经成功地启动了这个设备,则另一个行程对CreateFile的呼叫会一直失败。 |
| FILE_SHARE_READ | 要求没有其他行程正在写入设备。假如另一个行程已经以写入或独占存取的方式启动了这个设备时,则您对CreateFile的呼叫失败。假如您已成功地启动了这个设备,而另一个行程要求以GENERIC_WRITE存取该设备,则呼叫CreateFile会失败。 |
| FILE_SHARE_WRITE | 要求没有其他行程正在读取设备。假如另一个行程已经以读取或独占存取的方式启动了这个设备时,则您对CreateFile的呼叫会失败。假如您已成功地启动了这个设备,而另一个行程要求GENERIC_READ存取,则呼叫CreateFile失败。 |
| FILE_SHARE_READ | FILE_SHARE_WRITE | 不管另一个形式是否正在读写设备。假如另一个行程已经以独占存取的方式启动了设备,您对CreateFile的呼叫会失败。假如 您已成功地启动设备,当要求唯一的读、写、或读/写存取时,另一个呼叫CreateFile的行程会失败。 |
说明
假如您开启了一个文件,其最长的路径名称是 _MAX_PATH(规定为260)的字元长度。然而,您可以经由呼叫CreateFileW(CreateFile的Unicode版本)并且可在路径名称前加上「\\?\」来超越这个限制。呼叫CreateFileW可移除前缀字元,并允许您传递一个差不多32,000个Unicode字元长度的路径。然而,当您使用前缀字元时,必须记得使用完全有效的路径;系统不处理相对路径如「.」 及「..」。同样地,每个路径的个别元件仍旧限制为 _MAX_PATH字元。
psa参数指向一个SECURITY_ATTRIBUTES结构,它可允许您指定安全资讯且无论您是否喜欢,CreateFile会回传一个可被继承的handle。在这个结构里的安全描述符号只有在如NTFS的安全文件系统上建立文件时使用;在其他所有的实例中,安全描述符号是被忽略的。您通常只需传递NULL给psa参数,指示文件以预设的安全方式被建立,其回传的handle是不可继承的。
当您呼叫CreateFile用来开启与另一个设备型态相反的文件时,dwCreationDistribution参数是很有意义的。表2-6列出可传递给这个参数的值。
| 表2-6 可以传递给CreateFile的dwCreationDistribution参数值 |
| 值 | 意义 |
|---|---|
| CREATE_NEW | 命令CreateFile建立一个新的文件,假如有一个相同名称的文件已经存在,则建立失败。 |
| CREATE_ALWAYS | 命令CreateFile建立一个新的文件,不管是否已有一个相同名称的文件存在。假如已经存在一个相同名称的文件,则CreateFile会覆盖现存的文件。 |
| OPEN_EXISTING | 命令CreateFile开启一个已存在的文件或设备,假如该文件或设备不存在则失败。 |
| OPEN_ALWAYS | 如果文件存在则命令CreateFile开启它,如果不存在则建立一个新的文件。 |
| TRUNCATE_EXISTING | 命令CreateFile开启一个已存在的文件,并将它的文件大小缩短为0位元组,假如文件不存在则失败。 |
说明
当您呼叫CreateFile去启动一个除了文件以外的设备时,您必须传递OPEN_EXISTING给dwCreationDistribution参数。
CreateFile的dwFlagsAndAttrs有两个目的:它允许您设定标记以使与设备的通讯稳定,假如该设备是一个文件,您也能设定文件的属性。这些通讯标记大部分是告诉系统您想要如何存取设备的讯号。于是系统能有效地执行它的快取演算法来帮助您的应用程序工作得更有效率。我首先将会叙述通讯标记,接着再讨论文件属性。
CreateFile快取标记
FILE_FLAG_NO_BUFFERING 这个标记指出当存取文件时,不使用任何的资料缓冲。为了增进效能,系统会来回快取磁盘资料。通常不会指定这个标记,且快取管理者保有最近存取文件系统的部分在内存中。这样一来,假如您从文件读取了几个位元组,然后又读取了更多的位元组,该文件的资料很有可能已经被载入内存,则磁盘原本要存取二次的动作现在只需一次,大大地改进了效能。然而,这个程序意味着在内存中有二份文件资料:快取管理者有缓冲器,及您所呼叫的某些函数(例如ReadFile )也从快取管理者的缓冲器中复制一些资料到您自己的缓冲器上。
当快取管理者(cache manager)正在缓冲资料时,它可能也会事先读取以使您可能要读取的下一个位元组已经存在于内存中。再者,执行速度是藉由从文件中读取比所需要还更多的位元组来改进的。假如您从不试图对文件做更进一步地读取,则有可能会浪费内存。(请参阅〈FILE_FLAG_SEQUENTIAL_SCAN和 FILE_FLAG_RANDOM_ACCESS标记〉,以获得更多有关事先读取的讨论内容)。
经由指定FILE_FLAG_NO_BUFFERING标记,您可以命令快取管理者不要缓冲任何资料-即您自己承担这个责任!依您正在执行的事,这个标记可以改进应用程序的速度和内存的使用情形。由于文件系统的设备驱动程序直接将文件的资料写入您所提供的缓冲器中,所以您必须采用某些规则:
- 您必须要使用位移(offsets)—即磁盘容量区段大小的正确倍数—来存取文件(使用GetDiskFreeSpace函数来决定磁盘容量的区段大小)。
- 您必须读/写的位元组即是正确倍数的区段大小。
- 您必须确定行程位址空间中的缓冲器之所在的位址是可被区段大小所整除的。
FILE_FLAG_SEQUENTIAL_SCAN及FILE_FLAG_RANDOM_ACCESS 这些标记只在您允许系统为您缓冲文件资料时有帮助。假如您指定了FILE_FLAG_NO_BUFFERING标记,则这两个标记会被忽略。
假如您指定了FILE_FLAG_SEQUENTIAL_SCAN标记,则系统会认为您正在连续地存取文件。当您从文件读取一些资料时,系统实际上读取的文件资料会比您要求的数量还多。这个行程会减少与硬盘碰撞的次数,且增加应用程序的速度。假如您在文件上执行任何的直接搜寻动作,系统已经花费了一些额外的时间和内存来快取您不存取的资料。这是绝对可以的,但是假如您常这样做,最好能指定FILE_FLAG_RANDOM_ACCESS标记。这个标记会告诉系统不要预先读取文件资料。
为了管理文件,快取管理者必须为文件维持某些内部的资料结构-越大的文件越需要资料结构。当与非常大的文件一起工作时,快取管理者可能会无法分配它所需要的内部资料结构,且开启文件的动作将会失败。为了要存取非常大的文件,您必须使用FILE_FLAG_NO_BUFFERING标记来开启文件。
FILE_FLAG_WRITE_THROUGH 这是与快取相关的最后一个标记。它会使文件写入的中间快取操作失效以减少资料流失的可能性。
当您指定这个标记时,系统会直接将所有修改过的文件写入磁盘。然而,系统仍然维持内部快取的文件资料,并且以在文件读取操作时使用快取的资料(假如可得到)取代直接从磁盘读取资料的方式。当这个标记在网路服务器上被用来开启一个文件时,Windows文件写入函数不会返回呼叫的线程,直到资料被写入服务器的磁盘为止。
这就是与缓冲器相关的通讯标记。现在,让我们来讨论剩馀的通讯标记。
各种的CreateFile标记
FILE_FLAG_DELETE_ON_CLOSE 使用这个标记来命令文件系统在所有使用它的handles关闭后删除文件。这个标记最常与FILE_ATTRIBUTE_TEMPORARY属性一起使用。当这二个标记一起使用时,您的应用程序可以建立一个暂存档,对它写入、读取然后关闭它。当文件被关闭后,系统会自动地删除文件-多么方便啊!
FILE_FLAG_BACKUP_SEMANTICS 使用这个标记来备份和回存软件。在开启或建立任何文件前,系统通常会执行安全检查以确保试图开启或建立文件的行程拥有存取权。然而,备份和回存软件是特殊的,它可能会覆盖某些文件的安全检查。当您指定FILE_FLAG_BACKUP_SEMANTICS标记时,系统会检查呼叫者的存取权杖,看看备份/回存文件及目录的权限是否有效。假如有适当的权限,系统将会允许该文件被开启。您也可以使用FILE_FLAG_BACKUP_SEMANTICS标记来开启一个指向目录的handle。
FILE_FLAG_POSIX_SEMANTICS 在Windows中,文件名称是保留大小写的,反之,文件名称的搜寻则是不分大小写的。然而,POSIX子系统要求对文件名称搜寻还是有大小写之分的。当您建立或开启一个文件时,FILE_FLAG_ POSIX_SEMANTICS标记会让CreateFile使用有大小写之分的文件名称搜寻方式。使用FILE_FLAG_POSIX_ SEMANTICS标记要非常的小心,一旦您在建立文件时使用它,则该文件有可能会发生无法被Windows应用程序存取的情形。
FILE_FLAG_OPEN_REPARSE_POINT 依我的看法,这个标记应该有呼叫FILE_FLAG_IGNORE_REPARSE_POINT,因为它命令系统忽略文件的重新分析属性(假如它存在)。重新分析属性允许文件系统过滤器可以做修改开启、读取、写入和关闭文件的行为。通常修改的行为都会被要求使用,所以不建议使用FILE_FLAG_OPEN_REPARSE_POINT标记。
FILE_FLAG_OPEN_NO_RECALL 这个标记命令系统不要从离线的储存设备(例如磁带)回存文件的内容到线上储存设备(例如硬盘)上。当文件并没有被长时间的存取时,系统便可以转换文件的内容到离线的储存设备,以释放硬盘空间。当系统这样做时,硬盘上的文件并不会被毁坏;只有文件中的资料会被毁坏。当文件被开启时,系统会自动地从离线储存设备回存资料。FILE_FLAG_OPEN_NO_RECALL标记指示系统不要回存资料及使离线储存设备媒体的I/O操作被执行。
FILE_FLAG_OVERLAPPED 这个标记告诉系统您想要使用非同步的方式存取一个设备。您会注意到预设开启设备的方式是使用同步I/O(未指定FILE_FLAG_OVERLAPPED),大部分的开发人员习惯使用同步I/O。当您从文件读取资料时,您的线程会被延迟,并等待被读取的资讯。一旦资讯被读取,线程便会收回控制权并继续执行。与大部分其他的操作比较时,由于设备I/O是迟缓的,您可能想考虑与一些设备使用非同步的通讯方式。它是这样运作的:基本上,您呼叫一个函数去命令作业系统读取或写入资料,除了等待I/O的完成以外,您的呼叫会立即回传且作业系统会代表您使用它自己的线程来完成I/O。当操作系统已经完成了您的I/O要求,您便会收到通知。非同步I/O是建立高效能服务应用程序的关键。Windows提供了几个非同步I/O的不同方法,它们都会在这章被讨论到。
文件属性标记
现在是为CreateFile's dwFlagsAndAttrs参数检查属性标记的时候了,如表2-7所述,这些标记完全地被系统忽视,除非您建立一个新文件且传递NULL值给CreateFile的hfileTemplate参数。大部分的属性应该是您所熟悉的。
| 表2-7 可以传递给CreateFile之dwFlagsAndAttrs参数的文件属性标记 |
| 标记 | 意义 |
|---|---|
| FILE_ATTRIBUTE_ARCHIVE | 一个文件档。应用程序使用这个标记来标记文件,以备份或移除。当CreateFile建立一个新文件时,会自动设定此标记。 |
| FILE_ATTRIBUTE_ENCRYPTED | 一个被加密的文件。 |
| FILE_ATTRIBUTE_HIDDEN | 被隐藏的文件。它不会被包括在一般的的目录清单。 |
| FILE_ATTRIBUTE_NORMAL | 文件没有其他的属性设定。这个属性只在它单独被使用时有效。 |
| FILE_ATTRIBUTE_NOT_CONTENT_INDEXED | 文件不会被内容索引服务索引。 |
| FILE_ATTRIBUTE_OFFLINE | 文件存在,但它的资料已经被移到离线的储存设备上。这个标记对阶层储存系统是有帮助的。 |
| FILE_ATTRIBUTE_READONLY | 文件是唯读的。应用程序可以读文件但不能写入或删除它。 |
| FILE_ATTRIBUTE_SYSTEM | 文件是操作系统的一部份或是操作系统专用的文件。 |
| FILE_ATTRIBUTE_TEMPORARY | 文件资料只能在短时间内被使用。文件系统会试图在RAM而不是磁盘中保存文件的资料,以维持最小的存取时间。 |
假如您要建立一个暂存档,可以使用FILE_ATTRIBUTE_TEMPORARY。当CreateFile建立了一个拥有暂存属性的文件时,CreateFile会试图在内存中保有文件的资料以代替磁盘,这样会使存取文件内容的速度更快。假如您持续对文件写入且系统不再于RAM中保有资料,则操作系统将会被迫开始将资料写入硬盘。您可以经由结合FILE_ATTRIBUTE_TEMPORARY标记和FILE_FLAG_DELETE_ ON_CLOSE标记(先前所讨论的)来增进系统的效能。当文件被关闭时,系统通常会注入文件的快取资料。然而,假如它在关闭时文件被删除,系统就不需要注入文件的快取资料。
除了这些通讯和属性标记外,当开启一个命名管道(Named-Pipe)设备时,有一些标记允许您去控制服务的安全等级。由于这些标记为命名管道所独有,所以我不在这里讨论它们。若您想学习它们,请阅读Platform SDK说明文件中有关CreateFile函数的部份。
CreateFile的最后一个参数hfileTemplate表示为被开启文件的handle或是NULL。假如hfileTemplate确认一个文件之handle,CreateFile完全忽略参数中的属性标记,并且使用与经由hfileTemplate指示之文件相关属性。由hfileTemplate所指示的文件必须已经被该工作之GENERIC_READ标记开启。假如CreateFile开启了一个已存在的文件(相对于建立一个新文件),则hfileTemplate参数被忽略。
如果CreateFile成功地建立或开启了一个文件或设备,该文件或设备的handle会被回传。假如失败,则INVALID_HANDLE_VALUE被回传。
说明
当函数执行失败时,大部分的Windows函数会传回一个NULL的handle。然而,CreateFile则传回INVALID_HANDLE_VALUE(定义为 -1)来取代。我常看到如下的错误程序代码:
HANDLE hfile = CreateFile(...); If (hfile == NULL)
// 我们永远不会进入这里
} else {
// 文件不一定会建立成功
} . . .
以下是检查一个无效文件handle之正确方式:
HANDLE hfile = CreateFile(...);
if (hfile == INVALID_HANDLE_VALUE) // 文件不会建立
} else {
// 文件建立成功
} . . .
与文件设备一起工作
与文件一起工作是很常见的,所以我想花些时间来说明文件设备之特别应用的议题。本节会说明如何放置文件指标及改变文件大小的方法。
第一个您必须知道的议题是Windows被设计来与非常大的文件一起运作。新的Microsoft设计者选择使用64位元值来代替以32位元值表示文件大小的方式,这表示理论上一个文件可以达到16 EB(exabytes)。
在32位元作业系统中处理64位元的值会使得在与文件一起工作时感到有点讨厌,因为有许多Windows函数皆要求您将64位元值分割成二个32位元值传递。不过如您即将看到的一般,在标准的正常操作时,这些工作并不会很难,您可能不会需要与一个大于4 GB的文件一起工作。这意味着在64位元文件大小中,其高32位元的内含值皆会是0。
获得文件的大小
当与文件一起工作时,您时常会需要取得文件的大小。最容易的方式是呼叫GetFileSizeEx:
BOOL GetFileSizeEx( HANDLE hfile,
PLARGE_INTEGER pliFileSize);
第一个hfile参数是一个已开启文件的handle,而pliFileSize参数则是LARGE_INTEGER等位(union)的位址。这个等位允许参考一个有号的64位元值,如同二个32位元值或是单一的64位元值一般,而且在与文件大小及位移(offset)一起工作时,它可以相当方便。以下就是基本等位看起来的样子:
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart; //Low 32-bit unsigned value
LONG HighPart; //High 32-bit signed value
};
LONGLONG QuadPart; //Full 64-bit signed value
} LARGE_INTEGER, *PLARGE_INTEGER;
除了LARGE_INTEGER之外,有一个ULARGE_INTEGER结构代表一个无号的64位元值:
typedef union _ULARGE_INTEGER {
struct {
DWORD LowPart; // 低32位元之无号值
DWORD HighPart; //高32位元之无号值
};
ULONGLONG QuadPart; // 完整的64位元无号值
} ULARGE_INTEGER, *PULARGE_INTEGER;另一个能获得文件大小的有用函数是GetCompressedFileSize:
DWORD GetCompressedFileSize( PCTSTR pszFileName,
PDWORD pdwFileSizeHigh);
这个函数会回传文件的实际大小值;反之,GetFileSizeEx则会回传文件的逻辑大小。例如,一个100 KB的文件可被压缩为85 KB的大小。呼叫GetFileSizeEx以回传文件之逻辑大小-即100 KB-反之,GetCompressedFileSize会回传磁盘上被文件占用的实际位元组数量-即85 KB。
不像GetFileSizeEx一般,GetCompressedFileSize会将文件名称以字串的方式传递给第一个参数,而非使用handle。GetCompressedFileSize函数会以特别的方式回传文件大小之64个位元:文件大小之低32位元式函数回传值。文件大小之高32位元被放置在DWORD内,由pdwFileSizeHigh参数表示。此时就会用到ULARGE_INTEGER结构:
ULARGE_INTEGER ulFileSize;
ulFileSize.LowPart =GetCompressedFileSize("SomeFile.dat",
&ulFileSize.HighPart);
// 64位元文件大小现在位于ulFileSize.QuadPart内适当的放置文件指标
呼叫CreateFile会使系统在管理文件的操作中建立一个文件核心物件。在这个核心物件内部即是一个文件指标。此文件指标指出在文件中下一个将被执行的同步读取或写入之64位元位移(offset)。一开始这个文件指标会被设定为0,所以若是在您呼叫CreateFile后立即呼叫ReadFile,您会从offset 0开始读取文件。假如您读取文件中的10个位元组到内存中,则系统会更新与文件handle有关的指标,以便下一次呼叫ReadFile时,从文件的第10个位元组开始读取。举例来说,以下这个程序代码从文件读取第一个10位元组到缓冲器中,接着读取下一个10位元组:
BYTE pb [10];
DWORD dwNumBytes;
HANDLE hfile = CreateFile("MyFile.dat", ...); // 指标设为0
ReadFile(hfile, pb, 10, &dwNumBytes, NULL); // 读取0-9位元组
ReadFile(hfile, pb, 10, &dwNumBytes, NULL); // 读取10-19位元组每个文件核心物件皆有自己的文件指标,所以如果开启相同的文件二次,会产生稍微不同的结果:
BYTE pb[10];
DWORD dwNumBytes;
HANDLE hfile1 = CreateFile("MyFile.dat", ...); // 指标设为0
HANDLE hfile2 = CreateFile("MyFile.dat", ...); // 指标设为0
ReadFile(hfile1, pb, 10, &dwNumBytes, NULL); // 读取0-9位元组
ReadFile(hfile2, pb, 10, &dwNumBytes, NULL); // 读取0-9位元组
在这个范例中,两个不同的核心物件管理着相同的文件。由于每个核心物件皆有自己的文件指标,所以使用一个文件物件来操作文件并不会影响其他物件所维持的文件指标,该文件的第一个10位元组会被读取两次。
以下范例会使这个观念更清楚:
BYTE pb[10];
DWORD dwNumBytes;
HANDLE hfile1 = CreateFile("MyFile.dat",...); // 指标设为0
HANDLE hfile2;
DuplicateHandle(
GetCurrentProcess(), hfile1,
GetCurrentProcess(), &hfile2,
0, FALSE, DUPLICATE_SAME_ACCESS);
ReadFile(hfile1, pb, 10, &dwNumBytes, NULL); // 读取0-9位元组
ReadFile(hfile2, pb, 10, &dwNumBytes, NULL); // 读取10-19位元组
在这个范例中,一个文件核心物件同时被两个文件handles参考。不管使用哪个handle来操作文件,该文件指标皆会被更新。就像第一个范例一样,每次都会读取不同的位元组。
若您需要随机存取文件,就必须改变与文件核心物件有关的文件指标,可以经由呼叫SetFilePointerEx达成。
BOOL SetFilePointerEx(
HANDLE hfile,
LARGE_INTEGER liDistanceToMove,
PLARGE_INTEGER pliNewFilePointer,
DWORD dwMoveMethod);
hfile参数指出您希望改变的文件核心物件之文件指标;liDistanceToMove参数则告诉系统您想要将指标移动多少个位元组。您所指定的数量会被加到文件指标的当前值,所以负数在文件中为向后的作用。SetFilePointerEx的最后一个参数dwMoveMethod告诉SetFilePointerEx该如何解译liDistanceToMove参数。表2-8叙述了叁个您可以经由dwMoveMethod传递的值,用来指定移动的起始点。
| 表2-8 可以传递给SetFilePointerEx之dwMoveMethod参数值 |
| 值 | 意义 |
|---|---|
| FILE_BEGIN | 文件物件之文件指标设为liDistanceToMove参数所指定的值。请注意,liDistanceToMove被视为一个无号之64位元值。 |
| FILE_CURRENT | 文件物件的文件指标是加上liDistanceToMove以后的值。请注意,liDistanceToMove被视为一个有号之64位元值,允许您在文件中向后搜寻。 |
| FILE_END | 文件物件的文件指标值设为逻辑文件大小加上liDistanceToMove参数值。请注意,liDistanceToMove被视为一个有号之64位元值,允许您在文件中向后搜寻。 |
在SetFilePointerEx更新文件物件的文件指标后,文件指标的新值会指向pliNewFilePointer参数,并在LARGE_INTEGER中被传回。如果您对新的指标值没有兴趣,可以传递NULL给pliNewFilePointer。
这里有一些关于SetFilePointerEx必须注意的事项:
- 一个文件指标的设定超出文件当前大小的末端是无效的,这样做并不会增加磁盘上文件的大小,除非您在这个位置写入文件或呼叫SetEndOfFile。
- 当SetFilePointerEx与一个具有FILE_FLAG_NO_BUFFERING标记的已开启文件一起使用时,该文件指标只能被放置在对齐磁区之边界。在本章稍后的FileCopy范例应用程序中将会示范如何正确地做这件事。
- Windows没有提供GetFilePointerEx函数,但您可以使用SetFilePointerEx来得到想要的效果: