《WINDOWS 环境下32位汇编语言程序设计》第10章 内存管理和文件操作(2)
10.2 文件操作
10.2.1 Windows的文件I/O
在DOS操作系统下,最早的文件操作方法是使用FCB(文件控制块),FCB是一个数据结构,为了存取一个文件,必须建立一个FCB并在其中填写好驱动器名、文件名和要读写的记录号等,然后调用int 21h中对应的功能。使用FCB方式的缺点很多,如每次只能按记录为单位读取数据,无法随意指定数据块大小,无法直接指定一个全路径的文件名,文件的操作位置不会自动调整,每次操作都必须指定记录号等,归纳起来就是功能简单,操作复杂。
于是在2.0以上的DOS版本中,开始使用更方便的文件句柄方式,这种方式不再需要文件控制块,程序指定一个包含全路径的文件名后,就可以要求操作系统打开这个文件并返回一个文件句柄,以后就可以用这个句柄来读写文件,直到关闭文件为止。操作系统在内部为每个文件句柄维护一个读写指针。读写指针总是指向文件下一次要存取的位置,每次对文件的读写操作完成以后,读写指针会自动调整到本次操作的最后一个字节后面的位置,这样顺序读写文件就不必每次重新指定位置。读写指针可以被移动到文件的任意位置,以便满足随机存取的要求。
Windows操作系统中,文件操作沿用了这种句柄方式,保留了文件句柄和读写指针等概念,同时又根据Windows操作系统的新特征对文件I/O进行了很多的扩展,下面列出了Win32中文件函数经过扩展的一些功能:
● 文件函数的操作对象有了很大的扩展,除了普通的文件,对串口、磁盘设备、网络文件、控制台和目录等的操作都可以使用文件函数来完成。
● 支持异步文件操作,文件函数可以不必等待到操作完成才能返回。
● Windows是多用户的操作系统,可能发生多个程序同时对文件操作的现象,文件函数中增强了对共享和锁定的支持。
● 文件操作函数和内存映射文件函数配合可以实现将文件当做内存的一部分来存取的功能。
● 增加了拷贝文件和移动文件等函数来实现常用的功能。
另外,在文件的命名中有长短文件名之分,众所周知,DOS操作系统使用8.3结构的文件命名方式,在这种命名方式下,用文件名来简单地说明文件的用途显得比较困难,因为仅用8个字符是表达不了什么复杂的含义。
而在长文件名系统中,文件名的长度可以长达255个字符,这样在文件名中就可以清晰地表达出文件的用途,长文件名在磁盘的目录区中占用了多个连续的目录项,其中的一个目录项用做8.3结构的短文件名,其他的目录项存放其他名字字符。在8.3文件名中不合法的一些字符,如小数点与空格等在长文件名中都可以使用,只有/ \:*?"<>|等9个字符不能用于长文件名。
长文件名需要文件系统的支持,从DOS到Windows,使用过的有FAT,VFAT,FAT32,NTFS与HPFS等多种文件系统,在这些文件系统中,只有FAT系统不支持长文件名。
各种操作系统对文件系统的支持是不同的。Windows 3.x和DOS操作系统一直使用的是文件分配表(FAT)系统;Windows 95开始使用扩展FAT文件系统(VFAT),FAT系统和VFAT系统都是16位的文件系统,也称为FAT16。Windows NT在支持FAT16的同时,还支持两种32位的文件系统:NT文件系统(NTFS)和高性能文件系统(HPFS),NTFS支持文件的安全性,能够指定谁能访问某一文件或目录和对它做什么操作。Windows系列操作系统对文件系统的支持如表10.2所示。
那么在Win32的文件操作函数中,如何处理长、短文件名,又如何处理不同的文件系统呢?答案很简单:就是不要去考虑它们,不管要操作的文件名是长是短,不管文件位于什么样的文件系统中,只要指定了正确的文件名,文件操作函数就能正确地处理它。
表10.2 Windows系列操作系统对文件系统的支持
10.2.2 创建和读写文件
在开始讨论文件I/O的函数之前,先来看一个例子,这是一个将UNIX文件格式的文本转换到PC格式文本的小程序FormatText,由于UNIX系统保存的文本文件以0ah作为一行的结束而不是使用0dh+0ah,造成这种格式的文本在Windows的记事本上无法看到正常的换行,所有的内容将被显示在同一行内。本程序将读取UNIX格式的文本文件,转换到PC格式并保存到一个新的文本文件中。文件的源程序可以在所附光盘的Chapter10\FormatText目录中找到。汇编源文件FormatText.asm的内容如下:
;FormatText.asm
;文件读写例子 —— 将 Unix 格式的文本文件(以0ah换行)转换成 PC 格式
;的文本文件(以0dh,0ah换行),读写文件操作使用文件操作函数。
;-------------------------------------------------------------------
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff FormatText.asm
;rc FormatText.rc
;Link /subsystem:windows FormatText.obj FormatText.res
.386
.model flat, stdcall
option casemap:none ;include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib
include c:/masm32/include/comdlg32.inc
includelib c:/masm32/lib/comdlg32.lib ;equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 100
IDC_FILE equ 101
IDC_BROWSE equ 102;数据段
.data?
hInstance dword ?
hWinMain dword ?
szFileName byte MAX_PATH dup(?).const
szFileExt byte '文本文件',0,'*.txt',0,0
szNewFile byte '.new.txt',0
szErrOpenFile byte '无法打开源文件!',0
szErrCreateFile byte '无法创建新的文本文件!',0
szSuccess byte '文件转换成功,新的文本文件保存为',0dh,0ah,'%s',0
szSuccessCap byte '提示',0;代码段
.code
;在缓冲区中找出一行数据,处理换行并保存
_FormatText proc uses esi _lpData, _dwSize, _hFile local @szBuffer[128]:byte, @dwBytesWrite mov esi, _lpData mov ecx, _dwSize lea edi, @szBuffer xor edx, edx cld
_LoopBegin:or ecx, ecx jz _WriteLine lodsb dec ecx cmp al, 0dh ;遇到0dh则丢弃jz _LoopBegin cmp al, 0ah ;遇到0ah则扩展为0dh,0ahjz _LineEnd stosb inc edx cmp edx, sizeof @szBuffer - 2 jae _WriteLine ;行缓冲区满则保存jmp _LoopBegin
_LineEnd:mov ax, 0a0dh stosw inc edx inc edx
_WriteLine:push ecx .if edx invoke WriteFile, _hFile, addr @szBuffer, edx, addr @dwBytesWrite, NULL .endif lea edi, @szBuffer xor edx, edx pop ecx or ecx, ecx jnz _LoopBegin ret
_FormatText endp ;------------------------------------------------------------------------------
_ProcFile proc local @hFile, @hFileNew, @dwBytesRead local @szNewFile[MAX_PATH]:byte local @szReadBuffer[512]:byte ;打开文件invoke CreateFile, addr szFileName, GENERIC_READ, FILE_SHARE_READ, 0, \OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0.if eax == INVALID_HANDLE_VALUE invoke MessageBox, hWinMain, addr szErrOpenFile, NULL, MB_OK or MB_ICONEXCLAMATION ret .endif mov @hFile, eax ;创建输出文件invoke lstrcpy, addr @szNewFile, addr szFileName invoke lstrcat, addr @szNewFile, addr szNewFile invoke CreateFile, addr @szNewFile, GENERIC_WRITE, FILE_SHARE_READ, \0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0.if eax == INVALID_HANDLE_VALUE invoke MessageBox, hWinMain, addr szErrCreateFile, NULL, MB_OK or MB_ICONEXCLAMATION invoke CloseHandle, @hFile ret .endif mov @hFileNew, eax ;循环读出文件并处理每个字节xor eax, eax mov @dwBytesRead, eax .while TRUE lea esi, @szReadBuffer invoke ReadFile, @hFile, esi, sizeof @szReadBuffer, addr @dwBytesRead, 0.break .if !@dwBytesRead invoke _FormatText, esi, @dwBytesRead, @hFileNew .endw invoke CloseHandle, @hFile invoke CloseHandle, @hFileNew invoke wsprintf, addr @szReadBuffer, addr szSuccess, addr @szNewFile invoke MessageBox, hWinMain, addr @szReadBuffer, addr szSuccessCap, MB_OK ret
_ProcFile endp ;--------------------------------------------------------------------
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam local @stOpenFileName:OPENFILENAME mov eax, wMsg .if eax == WM_CLOSE invoke EndDialog, hWnd, NULL .elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke LoadIcon, hInstance, ICO_MAIN invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax invoke SendDlgItemMessage, hWnd, IDC_FILE, EM_SETLIMITTEXT, MAX_PATH, 0.elseif eax == WM_COMMAND mov eax, wParam .if ax == IDC_BROWSE invoke RtlZeroMemory, addr @stOpenFileName, sizeof OPENFILENAME mov @stOpenFileName.lStructSize, sizeof @stOpenFileName mov @stOpenFileName.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST push hWinMain pop @stOpenFileName.hwndOwner mov @stOpenFileName.lpstrFilter, offset szFileExt mov @stOpenFileName.lpstrFile, offset szFileName mov @stOpenFileName.nMaxFile, MAX_PATH invoke GetOpenFileName, addr @stOpenFileName .if eax invoke SetDlgItemText, hWnd, IDC_FILE, addr szFileName .endif .elseif ax == IDC_FILE invoke GetDlgItemText, hWnd, IDC_FILE, addr szFileName, MAX_PATH mov ebx, eax invoke GetDlgItem, hWnd, IDOK invoke EnableWindow, eax, ebx .elseif ax == IDOK call _ProcFile .endif .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp ;main函数
main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, 0
main endp
end main
程序在_ProcFile子程序中打开输入的UNIX文本文件,并创建用于输出的新文件,然后分多次将文件数据读入到缓冲区中并调用_FormatText子程序处理字节流,_FormatText子程序找出每行的结尾字符0ah并在前面加上0dh后存盘。
【编译运行效果:】
接下来转入正题:结合例子中的代码讨论读写文件的函数。
1.打开和关闭文件
要对文件进行操作的第一个步骤是打开文件,文件打开成功后将返回一个文件句柄,以后所有对文件的操作都要用到这个文件句柄。
打开文件使用的函数是CreateFile,读者一定认为是搞错了,这是创建文件,打开文件不是OpenFile吗?没有错!在Win32中不能使用OpenFile函数(Win16中倒是有OpenFile函数),不管是打开文件还是创建文件使用的都是CreateFile函数。CreateFile函数的参数又多又复杂,读者千万不要被下面的内容吓住了。
CreateFile函数的用法是:
invoke CreateFile,lpFileName,dwDesiredAccess,\dwShareMode,lpSecurityAttributes,dwCreationDisposition,\dwFlagsAndAttributes,hTemplateFile
各参数的含义如下:
lpFileName指向存放有文件名的缓冲区,文件名是一个以0结尾的字符串,字符串的最大长度为MAX_PATH(这个值在Windows.inc文件中定义为260),Win32可以用文件函数处理多种对象,所以CreateFile函数可以打开多种对象,包括:
● 控制台(Consoles)——Windows中类似于MS-DOS方式的界面窗口。
● 通信设备(Communications resources)——包括串口等设备。
● 目录(Directories)和文件(Files)。
● 邮件槽(Mailslots)——在网络环境下,在同一网段中可以用Mailslots提供的一条单向的通信路径,将信息由一个发送者传送到一个或者多个接收者中。
● 管道(Pipes)——两个进程可以通过管道交换数据,也可以用管道在网络中的服务器和工作站之间交换数据。
CreateFile以不同格式的文件名来区分操作的对象,普通的“驱动器:\路径\文件名”之类的格式指的就是普通的文件;当指定“COM1”为文件名的时候,操作的对象是第一个串口;如果文件名是“\\.\mailslot\filename”格式,那么要操作的就是邮件槽了;要打开的文件也可以是网络上其他主机中的文件,这时可以用“\\服务器名\共享目录名\文件名”方式来指定文件名。在本章中,只讨论对普通文件的操作。
dwDesiredAccess参数是存取方式,通过这个参数可以指定要对打开的文件进行何种操作,指定GENERIC_READ标志表示需要读取文件数据,指定GENERIC_WRITE标志表示需要向文件写数据,如果要对一个文件进行读写,需要同时指定这两个标志。
dwShareMode参数是共享属性,表明文件被打开后是否允许其他进程以某种方式再次打开文件,它可以是一些取值的组合:
● 0——不允许文件再被打开。
● FILE_SHARE_DELETE——许其他进程同时对文件进行删除。
● FILE_SHARE_READ——允许其他进程同时以读方式打开文件。
● FILE_SHARE_WRITE——允许其他进程同时以写方式打开文件。
lpSecurityAttributes参数为安全属性,通过这个参数可以指定返回的文件句柄是否可以被子进程继承,如果参数设置为NULL,则表明无法被继承,否则需要将参数指向一个SECURITY_ATTRIBUTES结构,该结构的定义为:
SECURITY_ATTRIBUTES STRUCTnLength DWORD ? ;本结构的长度lpSecurityDescriptor DWORD ?bInheritHandle DWORD ? ;是否允许继承SECURITY_ATTRIBUTES ENDS
nLength字段需要设置为结构的长度,将bInheritHandle字段设置为TRUE就可以使句柄能够被子进程继承。
dwCreationDisposition参数用来设置文件已经存在或不存在时系统采取的动作,在这里指定不同的标志就可以决定函数执行的功能究竟是创建文件还是打开文件,参数可能的取值为:
● CREATE_NEW——创建新文件,如果文件已经存在函数会返回失败。
● CREATE_ALWAYS——创建新文件,如果文件已经存在则清除原文件。
● OPEN_EXISTING——打开存在的文件,当文件不存在时函数会返回失败。
● OPEN_ALWAYS——如果文件已经存在,则打开,不存在则创建新文件。
● TRUNCATE_EXISTING——打开文件并将文件截断为零,当文件不存在时返回失败。
dwFlagsAndAttributes参数用来指定新建文件的属性,文件属性可以是下面这些值的组合:
● FILE_ATTRIBUTE_NORMAL——普通文件,设置这个属性时其他属性都不会生效。
● FILE_ATTRIBUTE_ARCHIVE——设置归档属性。
● FILE_ATTRIBUTE_HIDDEN——设置隐藏属性。
● FILE_ATTRIBUTE_READONLY——设置只读属性。
● FILE_ATTRIBUTE_SYSTEM——设置系统属性。
● FILE_ATTRIBUTE_TEMPORARY——临时文件,系统会尽量把所有的文件内容保持在内存中以加快存取速度,程序在不再使用文件的时候需尽快将它删除。
此外该参数还可同时指定对文件操作的方式,常用的方式有:
● FILE_FLAG_WRITE_THROUGH——使用WriteThrough模式,系统不会对文件使用缓存,文件的改变马上会被写入到磁盘中。
● FILE_FLAG_OVERLAPPED——使用异步文件操作模式。
● FILE_FLAG_DELETE_ON_CLOSE——文件被关闭后立即被系统自动删除。
● FILE_FLAG_RANDOM_ACCESS——对文件进行随机读写操作(操作系统对该文件的缓存进行优化)。
hTemplateFile指定了一个文件模板的句柄,该文件模板的所有属性都会被复制到当前创建的文件中。Windows 95不支持本参数,为了保持程序的兼容性,建议在参数中使用NULL。
当打开或创建文件成功的时候,函数返回一个文件句柄,失败的话,函数的返回值是INVALID_HANDLE_VALUE,注意:这个值被定义为−1而不是NULL。如果想再详细地了解失败的原因,可以继续调用GetLastError函数。
用不同参数的组合调用CreateFile可以完成不同的功能,比如在例子中打开输入的文本文件的时候,使用的是下面的代码:
invoke CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ,\0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
.if eax == INVALID_HANDLE_VALUE;输出出错信息
.endif
mov @hFile,eax
为什么要这样使用呢?因为如果文件不存在的话,就没有必要继续处理,所以要使用OPEN_EXISTING标志,而且程序仅需要读取文件的内容,不需要写入数据,所以存取方式使用GENERIC_READ就可以了,为了让其他程序能同时使用这个文件,共享方式要指定为FILE_SHARE_READ。
而创建用于输出的文件时就不同了,这次使用的代码如下:
invoke CreateFile,addr @szNewFile,GENERIC_WRITE,FILE_SHARE_READ,\0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
.if eax == INVALID_HANDLE_VALUE;输出出错信息
.endif
mov @hFileNew,eax
因为要写文件,所以存取方式使用GENERIC_WRITE,打开方式使用OPEN_ALWAYS,告诉函数如果记录文件已经存在,则打开它并将它的内容清除,不存在的话则新建一个文件。
与打开或创建文件相比,关闭文件就简单得多了,关闭文件使用CloseHandle函数:
invoke CloseHandle,hFile
2.移动文件指针
系统为每个打开的文件维护一个文件指针,指定在文件中下一个读操作或写操作在什么位置进行,随着数据的读出或写入,文件指针随之移动。当文件刚被打开的时候,文件指针处于文件的头部。
顺序读取或写入文件数据的时候,由于文件指针总是被调整到上一次操作以后的地方,所以循环进行读取或写入就能将全部的文件数据处理完毕。但使用中常常需要随机读取文件内容,这些就需要先调整文件指针,再进行读写操作。
调整文件指针使用SetFilePointer函数:
invoke SetFilePointer,hFile,lDistanceToMove,lpDistanceToMoveHigh,dwMoveMethod
hFile是用CreateFile函数返回的文件句柄。
lDistanceToMove指定要移动的距离。lpDistanceToMoveHigh是一个指针,指向一个32位的变量,变量存放有移动距离的高32位,它和lDistanceToMove中的32位一起组成一个64位的距离,使用64位距离的原因是某些平台上的Windows支持64位的文件长度,但在运行于80x86平台上的Windows版本中,文件长度不会超过4 GB,只需32位就能够覆盖全部的文件长度,所以lpDistanceToMoveHigh参数一般设置为NULL(其他的文件操作函数也使用两个参数来指定64位的文件长度,同样也可以只使用低32位)。当移动距离是正值的时候,文件指针向文件尾部移动;如果移动距离是负值,那么文件指针向文件头部方向移动。
dwMoveMethod指定了移动的模式,也就是指明从什么地方开始移动,它可以是以下的取值:
● FILE_BEGIN——不管文件指针当前位于什么地方,总是从文件头部开始移动,这时的位置参数相当于指定了一个绝对位置。
● FILE_CURRENT——从当前的文件指针处开始移动,这时的位置参数相当于指定了一个相对位置。
● FILE_END——从文件尾开始移动,如果要从文件尾往回移动一段位置,那么位置参数指定的就应该是负值。
Win32文件操作函数可以支持很多对象,有些对象并不支持文件指针,对它们就不能使用SetFilePointer函数,能想象对打开的串口进行移动指针操作是什么意思吗?
函数的返回值根据lpDistanceToMoveHigh参数的取值不同而不同,由于这个参数一般设置为NULL,这里仅讨论参数为NULL时的情况。这时如果函数执行失败,返回值是−1,否则函数返回新的文件指针位置。
既然文件指针可以设置,那么如何获取当前的文件指针呢?实际上并没有一个专用的函数可以完成这个功能(并没有GetFilePointer函数),但是既然SetFilePointer函数的返回值是新的文件指针位置,那么可以巧妙地使用该函数来获得当前的文件指针:
invoke SetFilePointer,hFile,0,NULL,FILE_CURRENT
从当前的文件指针处移动0字节,文件指针并没有真正移动,但是返回值就是当前的文件指针了!
文件指针也可以移动到文件所有数据的后面,比如,现在文件的长度是100 B,但还是可以成功地把文件指针移到1000 B的位置,这样的操作有什么用途呢?用途是可以将文件扩展到需要的长度,可以接着用WriteFile写入数据,系统会将文件从100 B扩展到1000 B后再从1000 B处写入数据。
使用SetEndOfFile函数也可以扩展文件长度,SetEndOfFile总是将文件的长度调整到当前的文件指针指向的长度,所以这个函数还有截断文件的功能,当文件指针位于文件中间的时候,函数将文件指针后面的内容截断,当文件指针位于文件尾以后位置的时候,函数将文件长度扩展。SetEndOfFile函数的用法是这样的:
invoke SetEndOfFile,hFile
当文件被扩展的时候,被扩展部分的内容是不确定的(不过这是MSDN说的,试验的结果好像这部分内容总是0)。
SetEndOfFile函数总是要和SetFilePointer函数配合使用,比如,要写一个杀毒程序,需要将附在带毒软件尾部的病毒去掉,就可以先用SetFilePointer函数将文件指针移到文件原来长度的地方,再用SetEndOfFile函数将文件截断就可以了。另外,要建立一个内容为空的文件(并不是指文件长度为0),可以在创建文件后,马上将文件指针移到需要的地方,再用SetEndOfFile函数扩展文件就可以了。网络蚂蚁在下载前总是创建一个和目标文件同样尺寸的空文件,然后再逐步下载其中的数据,用这种方法就可以完成创建空文件的功能。
3.读写文件
读写文件可以使用ReadFile/WriteFile函数,这两个函数读写的方式可以是同步的也可以是异步的;也可以使用ReadFileEx/WriteFileEx函数,这两个函数只用于异步读写文件。
读文件函数ReadFile的使用方法是:
invoke ReadFile,hFile,lpBuffer,nNumberOfBytesToRead,\lpNumberOfBytesRead,lpOverlapped
其中hFile是文件句柄,用来指明要读取的文件;lpBuffer指向一个缓冲区,函数会将读出的数据传送到这里;nNumberOfBytesToRead参数指定需要读入的字节数,由于函数并不能总是读到用户要求的字节数(原因是多种多样的,比如,遇到了文件尾),所以下面一个参数lpNumberOfBytesRead指向一个dword类型的变量,函数将在这里返回实际读入的字节数;lpOverlapped参数指向一个OVERLAPPED结构,供函数在异步读取文件时使用,在同步读写中这个参数设置为NULL。由于Windows 95的ReadFile函数并不支持对文件的异步读写,所以在Windows 95中使用时,这个参数总是设置为NULL。
如果读取文件失败,则函数返回0,成功则函数返回非0值。当函数返回非0值而lpNumberOfBytesRead中返回的已读取字节数却是0时,表示已经读到了文件尾。在例子中读文件使用:
invoke ReadFile,@hFile,addr @szReadBuffer,\sizeof @szReadBuffer,addr @dwBytesRead,0
这样读出的字节数保存在@dwBytesRead变量中,然后根据@dwBytesRead中的计数循环处理文件内容。
向文件写数据的函数WriteFile的使用方法与ReadFile函数类似:
invoke WriteFile,hFile,lpBuffer,nNumberOfBytesToWrite,\lpNumberOfBytesWritten,lpOverlapped
同样,hFile参数是文件句柄;lpBuffer指向一个缓冲区,缓冲区中包含有要写入文件的数据;nNumberOfBytesToWrite参数指定需要写入的字节数,函数在lpNumberOfBytesRead指出的dword类型变量中返回成功写入的字节数。
当用WriteFile写文件的时候,写入的数据可能被Windows暂时保存在内部的高速缓存中,等合适的时候再一并写入磁盘;如果WriteFile函数写的是串口或者一个管道,大块数据同样是暂时保留在缓冲区中等待逐步发送出去。虽然这些数据一般不会丢失,但并不能保证它们总是不会丢失的,比如,在文件关闭之前计算机断电了,或者数据发送之前对端断开了串口连接等情况。
如果一定要保证数据正确地写入了或者传输了,可以强制使用FlushFileBuffers函数来清空数据缓冲区,FlushFileBuffers函数的参数仅是一个文件句柄:
invoke FlushFileBuffers,hFile
如果hFile代表一个文件,函数会将所有缓冲区的数据马上写入文件;如果hFile代表一个串口,那么函数发送缓冲区中所有的数据;如果hFile代表一个管道的话,函数一直等待直到管道的另一方读取全部数据。总之,如果这个函数执行成功的话,就能够保证所有的数据已经被传送。如果函数执行成功,返回值是非0值,执行失败则函数返回0。
4.文件的共享
如果对文件数据的一致性要求比较高,为了防止程序在写入的过程中其他进程刚好在读取写入区域的内容,可以对已打开文件的某个部分进行加锁,加锁后可以防止其他进程对该区域进行读取或写入的操作。加锁和解锁使用LockFile和UnlockFile函数,读者也可以使用LockFileEx和UnlockFileEx函数,这两个函数可以以异步方式执行。
LockFile函数和UnlockFile函数的使用方法是:
invoke LockFile,hFile,dwFileOffsetLow,dwFileOffsetHigh,\nNumberOfBytesToLockLow,nNumberOfBytesToLockHigh
invoke UnlockFile,hFile,dwFileOffsetLow,dwFileOffsetHigh,\nNumberOfBytesToLockLow,nNumberOfBytesToLockHigh
dwFileOffsetLow和dwFileOffsetHigh参数组合起来指定了加锁区域的开始位置,dwNumberOfBytesToLockLow和dwNumberOfBytesToLockHight参数组合则指定加锁区域的大小,这两组参数都指定了一个64位的值,在Windows中,可以只使用低32位。
文件锁是排他性的,不能对一个区域重复加锁,两个不同的加锁区域也不能重叠;在对文件加锁和解锁的时候操作的区域也必须一一对应,对一个区域加锁后不能分几次对区域中的不同部分解锁,而必须一次全部解锁。
当程序读取一个文件的时候,如果文件有可能被其他进程加锁的话,那么在读取失败的时候就必须调用GetLastError函数来获取失败的原因,如果失败的原因是因为锁定造成的共享错误的话,那么可以等待一段时间后继续读取。
10.2.3 查找文件
在DOS中可以使用int 21h中断的4eh和4fh功能查找一个目录中的指定文件,Win32中也可以进行类似的操作,方法是用两个函数分别实现查找第一个文件和继续查找文件的功能。
当要开始查找文件的时候,首先使用FindFirstFile函数,如果函数执行成功,返回一个句柄hFindFile来对应这个寻找操作,接下来可以利用这个句柄循环调用FindNextFile函数继续查找其他文件,一直到FindNextFile函数返回失败为止,最后必须关闭hFindFile句柄。使用这几个函数查找文件的代码一般使用下面的结构:
invoke FindFirstFile,lpFindFile,lpFindFileData
.if eax != INVALID_HANDLE_VALUEmov hFindFile,eax.repeat;处理本次找到的文件invoke FindNextFile,hFindFile,addr lpFindFileData.until eax == FALSEinvoke FindClose,hFindFile
.endif
如果FindFirstFile函数执行失败,则下面的循环就不必执行了,如果FindFirstFile执行成功,那么程序保存返回的hFindFile并开始一个 .repeat循环,使用 .repeat语句而不是.while的原因是.repeat构成的循环先执行循环体内的内容,这样第一次循环时首先处理FindFirstFile找到的文件,然后再根据FindNextFile执行的结果决定是否继续循环。在结束循环后,需要用FindClose函数将查找句柄关闭。
FindFirstFile的参数lpFindFile指向一个字符串,代表要寻找的文件名,如果文件名中不包含路径,那么在当前目录中查找文件,包含路径的话将在指定路径中查找。在文件名中可以用“*”或“?”通配符指定查找特定的文件。下面是文件名格式的几个例子:
c:\Windows\*.* ;在c:\Windows目录中查找所有文件
c:\Windows\System32\*.dll ;在c:\Windows\System32目录中查找所有dll文件
c:\Windows\System.ini ;在c:\Windows目录中查找System.ini文件
c:\Windows\a???.* ;在c:\Windows目录中查找所有以a开头的文件名;长度为4个字符的文件Test.dat ;在当前目录查找Test.dat文件*.* ;在当前目录查找所有文件
FindFirstFile函数和FindNextFile函数中的lpFindFileData参数则指向一个缓冲区,函数会在缓冲区中返回一个WIN32_FIND_DATA结构,结构中包括了Windows查找过程中临时使用的数据和找到的文件名与文件属性等数据。该结构的定义如下:
WIN32_FIND_DATA STRUCTdwFileAttributes DWORD ? ;文件属性ftCreationTime FILETIME <> ;文件的创建日期ftLastAccessTime FILETIME <> ;文件的最后存取日期ftLastWriteTime FILETIME <> ;文件的最后修改日期nFileSizeHigh DWORD ? ;文件长度的高32位nFileSizeLow DWORD ? ;文件长度的低32位dwReserved0 DWORD ? ;内部使用dwReserved1 DWORD ? ;内部使用cFileName BYTE MAX_PATH dup(?) ;本次找到的文件名cAlternate BYTE 14 dup(?) ;文件的8.3结构的短文件名WIN32_FIND_DATA ENDS
dwFileAttributes字段可以是下面取值的组合,通过这个字段可以检查找到的究竟是一个文件还是一个子目录,以及其他的文件属性:
● FILE_ATTRIBUTE_ARCHIVE——文件包含归档属性。
● FILE_ATTRIBUTE_COMPRESSED——文件和目录被压缩。
● FILE_ATTRIBUTE_DIRECTORY——找到的是一个目录。
● FILE_ATTRIBUTE_HIDDEN——文件包含隐含属性。
● FILE_ATTRIBUTE_NORMAL——文件没有其他属性。
● FILE_ATTRIBUTE_READONLY——文件包含只读属性。
● FILE_ATTRIBUTE_SYSTEM——文件包含系统属性。
● FILE_ATTRIBUTE_TEMPORARY——文件是一个临时文件。
cFileName字段中包括了找到的文件名,这个文件名中并不包含路径,仅文件名而已,如果需要全路径的文件名还需要由我们自己将它和路径字符串相连接。
在所附光盘中有一个全盘搜索的例子,一般杀毒软件都可以对指定目录或者盘符下的所有文件,以及所有子目录下的文件做扫描,使用上面的文件查找函数可以很方便地完成全盘搜索的功能,源代码位于Chapter10\FindFile目录下。FindFile.asm文件的内容如下:
;FindFile.asm 全盘文件搜索程序 —— 指定一个起始目录,
;查找所有文件(包括子目录下的文件)
;--------------------------------------------------------------------
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff FindFile.asm
;rc FindFile.rc
;Link /subsystem:windows FindFile.obj FindFile.res
.386
.model flat,stdcall
option casemap:none ;include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib
include c:/masm32/include/ole32.inc
includelib c:/masm32/lib/ole32.lib
include c:/masm32/include/shell32.inc
includelib c:/masm32/lib/shell32.lib ;equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 100
IDC_PATH equ 101
IDC_BROWSE equ 102
IDC_NOWFILE equ 103;数据段
.data?
hInstance dword ?
hWinMain dword ?
dwFileSizeHigh dword ?
dwFileSizeLow dword ?
dwFileCount dword ?
dwFolderCount dword ?
szPath byte MAX_PATH dup(?)
dwOption byte ?
F_SEARCHING equ 0001h
F_STOP equ 0002h .const
szStart byte '开始(&S)',0
szStop byte '停止(&S)',0
szFilter byte '*.*',0
szSearchInfo byte '共找到 %d 个文件夹,%d 个文件,共 %luK 字节',0;代码段
.code
include _BrowseFolder.asm ;处理找到的文件
_ProcessFile proc _lpszFile local @hFile inc dwFileCount invoke SetDlgItemText, hWinMain, IDC_NOWFILE, _lpszFile invoke CreateFile, _lpszFile, GENERIC_READ, FILE_SHARE_READ, 0, \OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 .if eax != INVALID_HANDLE_VALUE mov @hFile, eax invoke GetFileSize, eax, NULL add dwFileSizeLow, eax adc dwFileSizeHigh, 0invoke CloseHandle, @hFile .endif ret
_ProcessFile endp _FindFile proc _lpszPath local @stFindFile:WIN32_FIND_DATA local @hFindFile local @szPath[MAX_PATH]:byte ;用来存放“路径\”local @szSearch[MAX_PATH]:byte ;用来存放“路径\*.*”local @szFindFile[MAX_PATH]:byte ;用来存放“路径\找到的文件”pushad invoke lstrcpy, addr @szPath, _lpszPath ;在路径后面加上\*.*@@:invoke lstrlen, addr @szPath lea esi, @szPath add esi, eax xor eax, eax mov al, '\'.if byte ptr[esi-1] != al mov word ptr[esi], ax .endif invoke lstrcpy, addr @szSearch, addr @szPath invoke lstrcat, addr @szSearch, addr szFilter ;---------------------------------------------------------------- .;寻找文件invoke FindFirstFile, addr @szSearch, addr @stFindFile .if eax != INVALID_HANDLE_VALUE mov @hFindFile, eax .repeat invoke lstrcpy, addr @szFindFile, addr @szPath invoke lstrcat, addr @szFindFile, addr @stFindFile.cFileName .if @stFindFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY .if @stFindFile.cFileName != '.'inc dwFolderCount invoke _FindFile, addr @szFindFile .endif .else invoke _ProcessFile, addr @szFindFile .endif invoke FindNextFile, @hFindFile, addr @stFindFile .until (eax == FALSE) || (dwOption & F_STOP)invoke FindClose, @hFindFile .endif popad ret
_FindFile endp ;---------------------------------------------------------------------
_ProcThread proc uses ebx ecx edx esi edi,lParam local @szBuffer[256]:byte ;设置标志位,并灰化“浏览”按钮和路径输入栏and dwOption, not F_STOP or dwOption, F_SEARCHING invoke GetDlgItem, hWinMain, IDC_PATH invoke EnableWindow, eax, FALSE invoke GetDlgItem, hWinMain, IDC_BROWSE invoke EnableWindow, eax, FALSE invoke SetDlgItemText, hWinMain, IDOK, addr szStop xor eax, eax mov dwFileSizeHigh, eax mov dwFileSizeLow, eax mov dwFileCount, eax mov dwFolderCount, eax invoke _FindFile, addr szPath ;退出时显示找到文件的总大小mov edx, dwFileSizeHighmov eax, dwFileSizeLow mov ecx, 1000div ecx invoke wsprintf, addr @szBuffer, addr szSearchInfo, dwFolderCount, dwFileCount, eax invoke SetDlgItemText, hWinMain, IDC_NOWFILE, addr @szBuffer ;设置标志位,并启用“浏览”按钮和路径输入栏invoke GetDlgItem, hWinMain, IDC_BROWSE invoke EnableWindow, eax, TRUE invoke GetDlgItem, hWinMain, IDC_PATH invoke EnableWindow, eax, TRUE invoke SetDlgItemText, hWinMain, IDOK, addr szStart invoke SetDlgItemText, hWinMain, IDC_PATH, addr szPath and dwOption, not F_SEARCHING ret
_ProcThread endp ;-----------------------------------------------------------------------
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam local @dwTemp, @szBuffer[MAX_PATH]:byte mov eax, wMsg .if eax == WM_CLOSE .if !(dwOption & F_SEARCHING)invoke EndDialog, hWnd, NULL .endif .elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke LoadIcon, hInstance, ICO_MAIN invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax invoke SendDlgItemMessage, hWnd, IDC_PATH, EM_SETLIMITTEXT, MAX_PATH, 0.elseif eax == WM_COMMAND mov eax, wParam .if ax == IDC_BROWSE invoke _BrowseFolder, hWnd, addr szPath .if eax invoke SetDlgItemText, hWnd, IDC_PATH, addr szPath .endif .elseif ax == IDC_PATH invoke GetDlgItemText, hWnd, IDC_PATH, addr @szBuffer, MAX_PATH mov ebx, eax invoke GetDlgItem, hWnd, IDOK invoke EnableWindow, eax, ebx ;----------------------------------------------------------------------------;按下开始按钮,如果在寻找中则设置停止标志,如果没有开始寻找则建立一个寻找文件的线程.elseif ax == IDOK .if dwOption & F_SEARCHING or dwOption, F_STOP .else invoke GetDlgItemText, hWnd, IDC_PATH, addr szPath, MAX_PATH invoke CreateThread, NULL, 0, offset _ProcThread, NULL, \NULL, addr @dwTemp invoke CloseHandle, eax .endif .endif .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp ;main函数
main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, 0
main endp
end main
例子程序查找指定目录,以及所有下层子目录中的所有文件,然后累计文件的长度,以此计算目录中所有文件的总长度,当单击对话框中的“浏览”按钮的时候,程序调用附录C中介绍的“浏览目录”通用对话框获取需要查找的目录,源程序中用到了附录C中的_BrowseFolder子程序。
; _BrowseFolder.asm -------------- “选择目录”通用对话框子程序;定义几个基本的 COM 接口
;IUnknown interface
;---------------------------------------------------------------------
externdef IID_IUnknown:IID
LPUNKNOWN typedef dword
LPPUNKNOWN typedef ptr LPUNKNOWN IUnknown_QueryInterfaceProto typedef proto:dword, :dword, :dword
IUnknown_AddRefProto typedef proto:dword
IUnknown_ReleaseProto typedef proto:dword
IUnknown_QueryInterface typedef ptr IUnknown_QueryInterfaceProto
IUnknown_AddRef typedef ptr IUnknown_AddRefProto
IUnknown_Release typedef ptr IUnknown_ReleaseProto IUnknown struct dword QueryInterface IUnknown_QueryInterface ?AddRef IUnknown_AddRef ?Release IUnknown_Release ?
IUnknown ends ;IMalloc Interface
externdef IID_IMalloc:IID
LPMALLOC typedef dword
LPPMALLOC typedef ptr LPMALLOC IMalloc_AllocProto typedef proto :dword, :dword
IMalloc_ReallocProto typedef proto :dword, :dword
IMalloc_FreeProto typedef proto :dword, :dword
IMalloc_GetSizeProto typedef proto :dword, :dword
IMalloc_DidAllocProto typedef proto :dword, :dword
IMalloc_HeapMinimizeProto typedef proto :dword IMalloc_Alloc typedef ptr IMalloc_AllocProto
IMalloc_Realloc typedef ptr IMalloc_ReallocProto
IMalloc_Free typedef ptr IMalloc_FreeProto
IMalloc_GetSize typedef ptr IMalloc_GetSizeProto
IMalloc_DidAlloc typedef ptr IMalloc_DidAllocProto
IMalloc_HeapMinimize typedef ptr IMalloc_HeapMinimizeProto IMalloc struct dword QueryInterface IUnknown_QueryInterface ?AddRef IUnknown_AddRef ?Release IUnknown_Release ?Alloc IMalloc_Alloc ?Realloc IMalloc_Realloc ?Free IMalloc_Free ?GetSize IMalloc_GetSize ?DidAlloc IMalloc_DidAlloc ?HeapMinimize IMalloc_HeapMinimize ?
IMalloc ends .data?
_BrowseFolderTmp dword ?
.const
_szDirInfo byte '请选择目录:',0.code
;弹出选择目录的对话框
_BrowseFolderCallBack proc hWnd, uMsg, lParam, lpData local @szBuffer[260]:byte mov eax, uMsg .if eax == BFFM_INITIALIZED invoke SendMessage, hWnd, BFFM_SETSELECTION, TRUE, _BrowseFolderTmp .elseif eax == BFFM_SELCHANGED invoke SHGetPathFromIDList, lParam, addr @szBuffer invoke SendMessage, hWnd, BFFM_SETSTATUSTEXT, 0, addr @szBuffer .endif xor eax, eax ret
_BrowseFolderCallBack endp ;------------------------------------------------------------------------
_BrowseFolder proc _hWnd, _lpszBuffer local @stBrowseInfo:BROWSEINFO local @stMalloc local @pidlParent, @dwReturnpushad invoke CoInitialize, NULL invoke SHGetMalloc, addr @stMalloc .if eax == E_FAIL mov @dwReturn, FALSE jmp @F .endif invoke RtlZeroMemory, addr @stBrowseInfo, sizeof @stBrowseInfo ;---------------------------------------------------------------------;SHBrowseForFolder 选择一个目录,把不含路径的目录名放入;stBrowseInfo.pszDisplayName 中,SHGetPathFromIDList 把;stBrowseInfo.pszDisplayName 转换成含全部路径的目录名;---------------------------------------------------------------------push _hWnd pop @stBrowseInfo.hwndOwner push _lpszBuffer pop _BrowseFolderTmp mov @stBrowseInfo.lpfn, offset _BrowseFolderCallBack mov @stBrowseInfo.lpszTitle, offset _szDirInfo mov @stBrowseInfo.ulFlags, BIF_RETURNONLYFSDIRS or BIF_STATUSTEXT invoke SHBrowseForFolder, addr @stBrowseInfo mov @pidlParent, eax .if eax != NULL invoke SHGetPathFromIDList, eax, _lpszBuffermov eax, TRUE .else mov eax, FALSE .endif mov @dwReturn, eax mov eax, @stMalloc mov eax, [eax]invoke (IMalloc ptr[eax]).Free, @stMalloc, @pidlParent mov eax, @stMalloc mov eax, [eax]invoke (IMalloc ptr[eax]).Release, @stMalloc @@:invoke CoUninitialize popad mov eax, @dwReturn ret
_BrowseFolder endp
运行结果:
在这个例子中,用到了多线程编程的方法,因为通盘查找文件需要的时间比较多,不适合在处理单条消息的过程中完成,有关多线程的内容读者可以参考第12章。总之,当用户按下了“开始”按钮后,程序在WM_COMMAND消息中对IDOK按钮进行处理,处理的方法是调用CreateThread函数建立一个新的线程来执行_ProcThread子程序,在这个子程序中来进行查找工作,子程序的开始和结束部分对对话框中的按钮进行灰化和重新激活,在中间则调用_FindFile子程序来进行真正的查找工作。
在_FindFile子程序中,使用了前面推荐的循环格式:
invoke FindFirstFile,addr @szSearch,addr @stFindFile
.if eax != INVALID_HANDLE_VALUEmov @hFindFile,eax.repeatinvoke lstrcpy,addr @szFindFile,addr @szPathinvoke lstrcat,addr @szFindFile,addr @stFindFile.cFileName.if @stFindFile.dwFileAttributes &\FILE_ATTRIBUTE_DIRECTORY.if @stFindFile.cFileName != '.'inc dwFolderCountinvoke _FindFile,addr @szFindFile.endif.elseinvoke _ProcessFile,addr @szFindFile.endifinvoke FindNextFile,@hFindFile,addr @stFindFile.until (eax == FALSE) || (dwOption & F_STOP)invoke FindClose,@hFindFile
.endif
注意:这个子程序是递归调用的,这样不管下层子目录的深度有多少,程序都不需要去考虑它。如果不使用递归调用的方法,那么程序必须将找到的子目录先保存起来,等处理完当前目录的所有文件后再继续处理保存的目录。
由于WIN32_FIND_DATA结构中的cFileName字段不包括路径,所以在找到一个文件后,程序用lstrcpy和lstrcat函数将保存的路径和文件名连接到一起组成一个全路径的文件名,然后,程序测试dwFileAttributes中的属性。如果找到的是目录,则递归调用_FindFile子程序寻找下一层目录;如果不是目录,则调用_ProcessFile子程序处理找到的文件。
在_ProcessFile子程序中打开文件并获取文件长度进行累计。如果要把这个全盘搜索程序用在别的地方(比如,编写杀毒程序的时候),只要将_ProcessFile子程序替换成对文件进行病毒检查的子程序就可以了。
当找到一个目录以后,还要判断目录名是否是“.”和“..”,如果是这两个名称则不进行处理,因为“.”目录代表本目录,“..”目录代表上一层目录,如果对它们进行递归查找,那么这个循环可就真的出不来了。
.repeat循环中多了一个对dwOption是否是F_STOP的测试,这是为了随时终止查找过程。因为找完整个磁盘可能需要很长的时间,让用户有机会终止查找过程是程序人性化的表现。
对这个程序进行简单的修改,读者就可以将它用在任何需要全盘查找文件的地方,唯一需要注意的地方是:如果要查找的是“*.exe”之类的文件而不是“*.*”的话,在输入查找文件名的时候不能使用“*.exe”,因为这样会漏过子目录,正确的做法是使用“*.*”当做要查找的文件名,并在处理找到的文件时再对文件名进行判别,对扩展名不是.exe的文件直接忽略掉就可以了。
10.2.4 文件属性
一个文件有多种相关属性,如文件的类型、长度、日期,文件是只读的或隐含的等,Win32中有一些函数可以对这些属性进行操作,在本节中将对此进行讨论。
1.获取文件类型
Win32的文件函数可以操作多种对象,如果需要知道一个文件句柄究竟对应什么对象,可以使用GetFileType函数:
invoke GetFileType,hFile
函数的返回值可能是下面的一种:FILE_TYPE_UNKNOWN,FILE_TYPE_DISK,FILE_TYPE_CHAR或者FILE_TYPE_PIPE,分别代表文件类型未知,文件句柄对应磁盘文件,文件句柄对应字符设备(如控制台或并行口等)和对应管道这4种情况。
2.获取文件长度
如果需要得知文件当前的长度,可以使用GetFileSize函数:
invoke GetFileSize,hFile,lpFileSizeHigh
lpFileSizeHigh指向一个用来接收高32位长度的变量,一般设置为NULL,长度的低32位在返回值中返回,用GetFileSize函数得到的文件长度是“当前”长度,也就是说,假如现在文件长度是100字节,我们又在文件尾写了100字节,那么再一次调用函数得到的文件长度就是200字节。
3.获取和修改文件日期
如果要获取文件的日期,可以使用GetFileTime函数:
invoke GetFileTime,hFile,lpCreationTime,lpLastAccessTime,lpLastWriteTime
其中,最后的3个参数指向3个缓冲区,函数会将创建日期、最后存取日期与最后写入日期分别返回到3个缓冲区中,如果不需要某个日期数据,可以把对应的输入参数设置为NULL。返回的日期数据是个FILETIME结构,结构的定义为:
FILETIME STRUCTdwLowDateTime DWORD ? ;文件日期低32位dwHighDateTime DWORD ? ;文件日期高32位
FILETIME ENDS
可以看到,在这个结构中无法直接得到日期的年、月、日、时、分与秒等数据,为了将它转换成我们熟悉的SYSTEMTIME结构,需要调用FileTimeToSystemTime函数进行转换:
invoke FileTimeToSystemTime,lpFileTime,lpSystemTime
与GetFileTime函数相对应,也可以使用SetFileTime函数把文件日期设置成希望的日期,同样,输入的参数指向3个FILETIME结构,结构中预先填写好期望的日期参数,不需要修改的日期属性可以在参数中使用NULL。函数用法如下:
invoke SetFileTime,hFile,lpCreationTime,lpLastAccessTime,lpLastWriteTime
同样,填写FILETIME结构的时候,最简便的方法是使用SystemTimeToFileTime函数将预先填写好的SYSTEMTIME结构转换为FILETIME结构:
invoke SystemTimeToFileTime,lpSystemTime,lpFileTime
4.获取和修改文件属性
在创建文件的时候,可以在CreateFile的dwFlagsAndAttributes参数中指定文件属性,除了用这种方法设置文件属性外,也可以在以后使用SetFileAttributes参数修改文件属性:
invoke SetFileAttributes,lpFileName,dwFileAttributes
调用SetFileAttributes函数的时候不需要打开文件,只需要指定全路径的文件名就可以了,dwFileAttributes参数的定义与CreateFile函数中的dwFileAttributes参数是一样的。
要获取文件的只读、隐含与系统等属性的话,可以使用GetFileAttributes函数:
invoke GetFileAttributes,lpFileName
如果函数执行失败,返回值是−1,否则返回文件的属性。
10.2.5 其他文件操作
在DOS操作系统中,并没有一个中断功能对应拷贝文件和移动文件功能,如果要实现这些功能,需要自己编写代码读取一个文件的内容并写入另一个文件,而在Win32中实现这些功能很简单,因为系统提供了对应的API函数。
在这一小节中,将讨论这些函数。
1.拷贝文件
拷贝文件使用CopyFile或CopyFileEx函数,CopyFile函数的用法如下:
invoke CopyFile,lpExistingFileName,lpNewFileName,bFailIfExists
函数将lpExistingFileName指定的文件拷贝为lpNewFileName指定的文件,最后一个参数bFailIfExists指定目标文件存在时的动作,如果指定为TRUE,则拷贝失败;指定为FALSE,则函数继续拷贝并覆盖掉原来的文件,拷贝出来的新文件的属性(如只读或隐含等属性)与原来文件的属性一样。
CopyFileEx函数可以完成同样的功能,只不过函数可以指定一个回调函数,在拷贝的过程中,函数在每拷贝完一部分数据以后会调用回调函数,在回调函数中程序可以指定继续拷贝还是停止,或者显示一个进度条来指示拷贝的进度,所以CopyFileEx函数只在拷贝特大型文件的时候比较有用。
2.移动文件
移动文件使用MoveFile或MoveFileEx函数。MoveFile函数的用法如下:
invoke MoveFile,lpExistingFileName,lpNewFileName
函数将lpExistingFileName指定的文件移动到lpNewFileName指定的目标位置,文件名可以同时改变或者不改变,目标文件必须不存在,否则函数调用失败。当函数执行成功的时候返回非0值,否则函数返回0。
MoveFile函数可以移动文件或者目录。当移动一个文件的时候,源文件和目标文件既可以处于相同的驱动器上也可以处于不同的驱动器上,函数会自动决定是否需要进行拷贝工作(如果处在相同的驱动器中,函数仅移动了一下目录项,并不进行数据拷贝);当移动的对象是一个目录时,那么目标目录和源目录必须处在同一个驱动器中,函数会将目录中的所有文件包括所有的下层子目录一并移动到新的位置上。
与MoveFile函数对应,系统中也有一个MoveFileEx函数,不过不要受CopyFileEx函数的影响而认为MoveFileEx函数只是提供了一个回调函数,事实上完全不是这回事,MoveFileEx函数是MoveFile函数的扩展。它的使用方法是:
invoke MoveFileEx,lpExistingFileName,lpNewFileName,dwFlags
MoveFileEx函数增加了一个dwFlags参数来进行移动控制,参数可以是下面取值的组合:
● MOVEFILE_COPY_ALLOWED——允许拷贝数据,如果不指定这个标志,则移动文件的时候不能将文件移动到不同的驱动器上(当然即使指定了这个标志,将目录移动到不同驱动器上也是不支持的),本标志不能与MOVEFILE_DELAY_UNTIL_REBOOT标志合并使用。
● MOVEFILE_DELAY_UNTIL_REBOOT——在Windows NT中执行时,函数并不马上进行移动操作,而是等下一次系统启动的时候再进行移动操作。
● MOVEFILE_REPLACE_EXISTING ——如果目标文件已经存在的话则将它替换掉,相比之下,MoveFile函数无法替换已存在的目标文件。
MoveFileEx函数还有个特殊用途:当标志指定MOVEFILE_DELAY_UNTIL_REBOOT的时候,lpNewFileName参数可以指定为NULL,在这种情况下,当下一次启动的时候,系统会删除lpExistingFileName指定的文件。MOVEFILE_DELAY_UNTIL_REBOOT标志在Windows 95下不予支持。
3.删除文件
删除文件使用DeleteFile函数,函数的语法很简单:
invoke DeleteFile,lpFileName
lpFileName指向一个包含要删除文件名的字符串。当函数在Windows 95下执行的时候,即使文件处在打开状态也可以成功删除,而在Windows NT下执行的时候,不能对一个已打开的文件进行删除,必须首先用CloseHandle函数关闭文件后才能执行成功。
10.3 驱动器和目录
先简单复习一下Windows中的逻辑驱动器、目录和路径等基本概念。
Windows中的文件组织方式与DOS操作系统类似,也是采用分层次的结构:计算机中可以安装有多个物理驱动器,每个物理驱动器可以分为多个主分区和扩展分区,每个主分区就是一个逻辑驱动器,而每个扩展分区可以划分成多个逻辑驱动器,逻辑驱动器组成了我们熟悉的C盘与D盘等盘符。
如图10.4所示,假如计算机上安装了2个硬盘和2个光驱,每个硬盘都分为一个主分区和一个扩展分区,其中第一个硬盘的扩展分区中又分为3个逻辑驱动器,第二个硬盘的扩展分区分为2个逻辑驱动器,那么计算机中的逻辑驱动器就会从C盘一直排列到K盘为止。
图10.4 逻辑分区的分配
对于每个逻辑驱动器,可以给它取一个标号叫做“卷标”(Volume Label),卷标是当做一个目录项存放在逻辑驱动器的根目录中的。在每个逻辑驱动器中可以有多个文件,文件可以存放在各个目录中,目录是按照多层树状结构来安排的,每个逻辑驱动器中有个顶层目录叫做“根目录”,根目录下可以安排多个子目录,每个子目录中也可以包含多个下层子目录,一个逻辑驱动器中能够存放文件和子目录的数量只受限于驱动器的空间大小。
虽然同一个子目录中的文件名必须是唯一的,但不同的子目录中可以存在同名的文件,所以只有文件名的话并不能唯一确定一个文件,要唯一确定文件还需要指出文件的位置,除了指出文件位于的逻辑驱动器外,还要指出从根目录开始一直到文件所在目录为止的所有子目录名,这就是路径。
对一个进程来说,Windows维护一个当前驱动器,并为每个逻辑驱动器维护一个当前路径,如果不指定路径,表示要操作的文件就位于当前驱动器的当前路径下。如果要操作非当前路径下的文件,就必须明确指出包含全路径的文件名。比如,指定一个文件System.ini,如果当前目录下有这个文件,那么操作的对象就是这个文件,如果当前目录下并没有这个文件,即使其他目录中存在多个同名的文件,程序也无法知道它究竟对应哪个文件。
Win32中有一部分函数专门用来完成与逻辑驱动器及目录有关的操作,在本节中将具体讨论这些函数的使用方法。
10.3.1 逻辑驱动器操作
1.卷标操作
为一个驱动器创建、修改,以及删除卷标都使用SetVolumeLabel函数。如果要创建或修改卷标(如果原来没有卷标则为创建,原来存在卷标则为修改)可以这样使用:
szPath db 'c:\',0
szVolume db 'System',0
invoke SetVolumeLabel,addr szPath,szVolume
本例中,C盘的卷标会被设置为System,第一个参数指出了要设置卷标的逻辑驱动器的根目录,如果要设置C盘的卷标,目录名既不能写为“c:”也不能写为“c:\windows”,必须写为“c:\”,否则函数调用会失败;第二个参数则指向包含卷标字符串的缓冲区。
删除一个逻辑驱动器的卷标有两种方法。
方法一:
szPath db 'c:\',0
szVolume db 0
invoke SetVolumeLabel,addr szPath,szVolume
方法二:
szPath db 'c:\',0
invoke SetVolumeLabel,addr szPath,NULL
这两种方法的执行结果是一样的。如果函数执行成功,返回值是TRUE,否则返回FALSE。
获取卷标可以使用下面介绍的GetVolumeInformation函数。
2.逻辑驱动器的检测
要检测系统中当前存在多少个逻辑驱动器可以使用GetLogicalDrives函数,函数返回了所有可用的盘符。GetLogicalDrives函数没有输入参数,它返回一个32位的整数,用其中的每一位代表是否存在一个逻辑驱动器。由于系统中可用的盘符仅有26个(A:~Z:),所以32位已经可以反映出所有的逻辑驱动器,以及它们的盘符分布情况了,返回值的第0位到第25位分别代表驱动器A:~Z:是否存在,如系统中存在A,C,D,E和F 5个逻辑驱动器的时候,返回值的二进制数值为00000000000000000000000000111101b,也就是十六进制的0000003dh。
如果认为GetLogicalDrives函数返回的数据要进行位测试比较麻烦,可以使用另一个函数:GetLogicalDriveStrings,这个函数返回字符串类型的逻辑驱动器列表:
invoke GetLogicalDriveStrings,dwBufferSize,lpBuffer
lpBuffer指向一个缓冲区,函数在这里返回“A:\”,0,“B:\”,0,“C:\”,0,0格式的字符串,凡是存在的逻辑驱动器都会列在这个字符串中,字符串列表以一个附加的0结束;dwBufferSize指出缓存区的大小,如果缓冲区不够大,后面的数据会被截尾。
获取了逻辑驱动器的分布情况后,有时候还必须了解某个逻辑驱动器的类型,因为它可能是各种类型的盘——软盘、硬盘、光驱和内存中的虚拟盘等都是以逻辑驱动器的模样出现的。虽然文件操作函数中可以不必理会文件究竟位于什么样的驱动器上,只要指定全路径的文件名就可以透明地工作,但有时候必须检测盘的类型。比如,希望对软件进行保护,要求文件必须位于光盘上,那么就需要检查文件所在的逻辑驱动器是否是光盘;另外,需要建立一个临时文件的时候,如果建立在只读的光盘上是不会成功的,为了保证创建成功,需要预先检测一下程序是否运行于硬盘上。
检测驱动器类型的工作可以用GetDriveType函数来完成:
szPath db 'c:\',0
invoke GetDriveType,addr szPath
该函数唯一的一个参数指向存放有逻辑驱动器根目录的字符串的缓冲区,函数的返回值是逻辑驱动器的类型,它可能是下面取值中的一种:
● 0—驱动器类型无法检测。
● 1—指定的根目录不存在。
● DRIVE_REMOVABLE—可移动介质,如软盘。
● DRIVE_FIXED—固定盘,如硬盘中的逻辑驱动器。
● DRIVE_REMOTE—远程驱动器,如网络上映射的驱动器。
● DRIVE_CDROM—光盘。
● DRIVE_RAMDISK—内存虚拟盘。
如果需要更详细的情况,可以使用GetVolumeInformation函数,这个函数可以返回逻辑驱动器的卷标、序列号和文件系统类型等属性:
invoke GetVolumeInformation,lpRootPathName,\lpVolumeNameBuffer,dwVolumeNameSize,\lpVolumeSerialNumber,lpMaximumComponentLength,\lpFileSystemFlags,\lpFileSystemNameBuffer,dwFileSystemNameSize
参数lpRootPathName指向需要检测的驱动器根目录字符串,如果要检测的是网络上的驱动器,那么字符串可以是“\\服务器名\共享名”格式。
后面的各个参数指向一些用来返回数据的缓冲区。
lpVolumeNameBuffer指向一个字符串缓冲区,用来返回驱动器的卷标,缓冲区的长度由dwVolumeNameSize参数指出。
lpVolumeSerialNumber指向一个双字变量,函数在这里返回逻辑驱动器的序列号。序列号是驱动器被格式化的时候由系统随机生成的一个32位数,它保存在位于驱动器第一个扇区的引导记录中。在程序的运行中检测并记录软盘的序列号就可以检测到软盘是否被更换。
lpMaximumComponentLength指向一个双字变量,函数在这里返回最大允许的文件名长度,在Windows系统中,一般这个数值是255。
lpFileSystemFlags也指向一个双字变量,函数在这里返回一些逻辑驱动器的属性标志,返回值可能是下面数值的组合:
● FS_CASE_IS_PRESERVED——文件系统在保存文件名的时候保持它的大小写(如DOS就不是这样,它把所有的文件名转换成大写后保存到目录区)。
● FS_CASE_SENSITIVE——支持区分大小写的文件名(在Windows中文件名不区分大小写,如Abc.exe和aBc.ExE指的是同一个文件)。
● FS_UNICODE_STORED_ON_DISK——允许存放Unicode格式的文件名。
● FS_PERSISTENT_ACLS——支持ACL(访问控制列表),ACL用于安全性管理,它是一个为个人或组委派或否认特定访问权限的条目列表。NTFS文件系统支持ACL,而FAT系统不支持。
● FS_FILE_COMPRESSION——支持文件压缩。
● FS_VOL_IS_COMPRESSED——支持卷压缩。
lpFileSystemNameBuffer指向一个字符串缓冲区,用来接收文件系统字符串,函数在这里返回类似于“FAT”、“FAT32”或“NTFS”类型的字符串,dwFileSystemNameSize参数指出了这个缓冲区的长度。检测逻辑驱动器剩余空间的GetDiskFreeSpace函数也是一个常用的函数,它的用法如下:
invoke GetDiskFreeSpace, lpRootPathName,\lpSectorsPerCluster,lpBytesPerSector,\lpNumberOfFreeClusters,lpTotalNumberOfClusters
同样,参数lpRootPathName指向需要检测的驱动器根目录字符串,后面的参数指向一些双字变量,用来接收返回的数据:
● lpSectorsPerCluster参数——返回每簇的扇区数。
● lpBytesPerSector参数——返回每扇区的字节数。
● lpNumberOfFreeClusters参数——返回驱动器中未使用的簇的数量。
● lpTotalNumberOfClusters参数——返回驱动器中簇的总数。
驱动器的总容量可以通过算式计算出来:簇总数×每簇扇区数×每扇区字节数,驱动器中空闲的字节数则等于:未使用的簇×每簇扇区数×每扇区字节数。
10.3.2 目录操作
1.创建和删除目录
创建目录使用CreateDirectory函数,例如:
szDir db 'c:\dir1\dir2',0...
invoke CreateDirectory,addr szDir,NULL
这两句代码在c:\dir1目录下创建一个名为dir2的新子目录。如果创建成功,函数返回TRUE,否则返回FALSE。
在创建目录的时候要注意几个要点:首先是要创建目录的上层目录必须存在,上面的例子中,假如c:\dir1目录不存在,函数不会创建dir2目录;其次是与新建目录同名的目录或文件不能存在,假如c:\dir1目录中已经存在一个名为dir2的目录或文件,那么创建工作就会失败。由于文件名和目录名在磁盘目录区中的存放格式是一样的,唯一的不同是目录项的属性不同(在10.2.3节中,已经发现判别找到的目录项是文件还是目录的唯一办法就是检测FILE_ATTRIBUTE_DIRECTORY属性),所以,连同名文件的存在也是不允许的。
删除目录使用RemoveDirectory函数,删除上面创建的dir2目录的方法是:
szDir db 'c:\dir1\dir2',0...
invoke RemoveDirectory,addr szDir
如果删除成功,函数返回TRUE,否则返回FALSE。
删除目录也要注意几个要点:首先,被删除的是参数中指出的最后一级目录,如前面代码只删除dir2目录,而不会将dir1目录和dir2目录一起删掉;其次,在删除目录之前必须删除目录中的所有文件,以及子目录,函数无法删除一个不为空的目录;最后,函数执行以后目录是被“真正”删除掉了,不会像用手工删除一样还可以在回收站中恢复回来。
2.一些特殊目录
这里列出了Windows操作系统中的一些特殊目录:
● 当前目录——所有未指定路径的文件名均默认使用这个目录。
● Windows目录——Windows操作系统的安装目录。
● 系统目录——Windows安装目录下存放系统文件的目录,Windows 9x下是System目录,Windows NT下是System32目录。
● 临时目录——存放临时文件的目录,系统可以在磁盘空间不足的时候自动删除里面的文件。
这些目录在编程的时候是经常需要检测的,如要编写系统程序的时候常常要把dll文件拷贝到系统目录或Windows目录中去;而要建立临时文件的时候最好使用系统指定的临时目录,以便自动回收使用的空间。Win32中专门设置了几个函数来获取这些目录的位置:
invoke GetCurrentDirectory,dwBufferSize,lpBuffer ;获取当前目录
invoke GetTempPath,dwBufferSize,lpBuffer ;获取临时目录
invoke GetWindowsDirectory,lpBuffer,dwBufferSize ;获取Windows目录
invoke GetSystemDirectory,lpBuffer,dwBufferSize ;获取系统目录
参数lpBuffer指向一个缓冲区,用来接收返回的路径字符串,dwBufferSize指出了缓冲区的大小,一般把缓冲区的大小设置为MAX_PATH。
这几个函数有些奇怪是:GetWindowsDirectory和GetSystemDirectory函数的参数和通常的习惯一致,把缓冲区指针lpBuffer放在前面,而GetCurrentDirectory和GetTempPath函数却把dwBufferSize参数放在前面,不注意的话很容易搞错;另外,GetTempPath函数返回的路径的最后竟然包括“\”,在笔者的Windows XP操作系统中,它的返回值是“C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\”,而其他3个函数返回的路径最后并不带“\”,读者在路径后面添加文件名的时候最好首先检测一下字符串的最后是不是已经包含了一个“\”字符,否则会构造出一个类似于“c:\temp\\abc.dat”之类的无效的文件名。
当程序运行的时候,默认的当前路径是执行程序所在的目录,但这不是绝对的,比如使用GetOpenFileName等函数弹出一个打开文件的系统对话框,用户在里面指定了其他目录后,当前路径就会被设置到那个目录中。
读者也可以自己调用SetCurrentDirectory函数修改当前路径:
szDir db 'c:\dir1\dir2',0...
invoke SetCurrentDirectory,addr szDir
这几句代码将当前路径设置到“c:\dir1\dir2”目录中。
10.4 内存映射文件
首先回过头来看看10.2.1节的FormatText例子中读文件的操作,与在DOS操作系统下所有文件操作代码的结构一样,例子中用分块读入的办法读取文件,每次读取的内容受限于缓存区的大小,对上一次读入的内容处理完毕后,程序才能继续读入下一块内容,代码结构如下:
.while TRUE调用ReadFile读取文件.break .if读取的字节数为0 (到达文件尾部)从缓冲区中取出数据并处理
.endw
使用这种结构处理文件的严重缺陷就是缓冲区边界的处理问题,读者可以尝试将FormatText程序的功能改造成清除每一行尾部的多余空格,例如:假如某一行的内容是“abc def ”则转换成“abc def”,这样,当缓冲区结束的地方刚好是连续的空格的时候,就必须先保存这部分内容,等继续将后续数据读到缓冲区后才能判别这是行尾的空格还是两个单词之间的空格;把这个问题再扩展开来,假如某一次遇到的连续空格特别多,长度等于3个、4个甚至很多个缓冲区的长度,又要如何处理呢?这就已经涉及有名的边界判断问题了,实际上程序中许许多多的错误就是由此引起的。
解决这个问题最简单的办法就是一次性把文件全部读进内存,这样就不存在边界问题了,但在DOS操作系统下,程序能用的最大内存一般只有几百KB,又有多少个程序能保证自己要处理的文件一定能够一次性全部读进内存呢?毕竟包括操作系统在内的所有可寻址的地址空间只有1 MB。
在Windows中,每个进程可以自由使用的地址空间达到了2 GB,这就为将整个文件读进内存打好了基础,但程序还是需要预先分配一块大小等于文件长度的内存块,所以限制还是不少,因为在内存分配一节中已经发现,当要分配的内存块大小远远超过可用物理内存的时候,分配工作并不一定会成功。
Win32中内存映射文件的引入,使这一类问题得到较好的解决,更使Win32程序员们信心大增,笔者也认为,内存映射文件是Win32中最有实用价值的新特征之一。
10.4.1 内存映射文件简介
1.内存映射文件的概念
内存映射文件提供了一组独立的函数,使应用程序能够通过内存指针像访问内存一样对磁盘上的文件进行访问。通过内存映射文件函数可以将磁盘上文件的全部或部分映射到进程虚拟地址空间的某个位置,一旦完成了映射,对文件内容的访问就如同在该地址区域内直接对内存访问一样简单。这样,向文件中写入数据的操作就是直接对内存进行赋值,而从文件的某个特定位置读取数据也就是直接从内存中取数据。
当内存映射文件提供了对文件某个特定位置的直接读写时,真正对磁盘文件的读写操作是由系统底层处理的。而且在写操作时,数据也并非在每次操作时都即时写入到磁盘,而是通过缓冲处理来提高系统的整体性能。
使用内存映射文件的好处之一是系统对所有的数据传输都是通过4 KB大小的数据页面来实现的,这意味着一些小的文件操作将被缓冲入一次大的操作之中,也就是说首次存取文件中某段数据的时候,会引发一次磁盘操作并将数据所在的一个页面全部读入,到以后对附近的数据进行操作时,所需的数据已经被前一次的页面操作读入到内存,无需再进行一次磁盘操作,从而提高了系统的性能。
另一个好处是程序代码以标准的内存地址形式来访问文件数据,按页面大小周期性从磁盘读入数据的操作发生在后台,由操作系统底层来实现,这个过程对应用程序是完全透明的。虽然用内存映射文件最终还是要将文件从磁盘读入内存,实质上并没有省略掉什么操作,整体性能可能并没有获得什么提高,但是程序的结构将会从中受益,缓冲区边界等问题将不复存在。而且,对文件内容更新后的写入操作也由操作系统自动完成,由操作系统来判断内存中的页面是否为脏页面并仅将脏页面写入磁盘,比程序自己将全部数据写入文件的效率要高了很多。
2.内存映射文件的实现原理
Windows使用的是页式虚拟存储管理,在Windows中,地址空间中的每个页面在任一给定时刻都可以是三种状态之一:空闲的、保留的或者是已经提交物理内存的。这些页面根据需要由操作系统交换进内存或换出内存。当内存中的某个页面不再需要时,操作系统将取消原来拥用该页面的应用程序对它的控制权,并释放该页面以供其他应用程序使用;当该页面再次成为需求页面时,它将被从物理存储器中重新读入内存,物理存储器既可以是物理内存,也可以是磁盘上的页文件。
内存映射文件的实现基于同样的原理,内存映射文件是Windows内部已有的内存管理组件的一个扩充,与实现虚拟内存一样,内存映射文件保留了一个地址空间的区域,并根据需要将物理存储器提交给该区域。它们之间的区别在于,当内存映射文件用来存取一个磁盘文件的时候,它提交的物理存储器就来自于这个文件。
不仅应用程序使用内存映射文件来访问磁盘上的数据文件,Windows操作系统同样使用内存映射文件加载和执行exe和dll文件,这样可以大大节省页文件空间和应用程序启动运行所需的时间。如图10.5所示,对于每个进程,系统将可执行的代码页提交到磁盘中的可执行文件中,而数据页(包括进程的静态数据段以及动态分配的内存)则被提交到虚拟内存中。
图10.5 用内存映射文件加载执行文件
除了加载文件,使用内存映射文件也可以在同一台计算机上运行的多个进程之间共享数据,而且内存映射文件是多个进程互相进行通信的最有效的方法。那么如何实现数据共享呢,其实原理很简单,如图10.6所示,对于不同进程间共享的数据页,只要将它们提交到虚拟内存的同样页面就可以了,这样,当一个进程改变了数据页的内容时,通过分页映射机制,其他进程的共享数据区的内容就会同时改变,因为它们实际上存储在同一个地方。
图10.6 用内存映射文件实现进程间共享数据
10.4.2 使用内存映射文件
1.内存映射文件函数
内存映射文件函数包括:CreateFileMapping,OpenFileMapping,MapViewOfFile,UnmapViewOfFile和FlushViewOfFile。
使用内存映射文件的步骤分为两步,第一步是使用CreateFileMapping创建一个内存映射文件对象。这个步骤决定了使用内存映射文件的用途——究竟是在磁盘文件上建立内存映射文件还是在页文件中建立进程间共享的映射。CreateFileMapping函数的用法是:
invoke CreateFileMapping,hFile,lpFileMappingAttributes,\flProtect,dwMaximumSizeHigh,dwMaximumSizeLow,lpName
.if eaxmov hFileMap,eax
.endif
函数的第一个参数hFile指定一个文件句柄。如果句柄是属于一个已经打开的文件的,那么内存映射文件将在这个文件上面建立;如果需要建立存在于页文件中的内存映射文件供不同进程共享,那么hFile指定为-1。
lpFileMappingAttributes参数指向一个SECURITY_ATTRIBUTES结构,用来定义内存映射文件对象是否是可继承的。这个结构在文件打开函数中也曾经用到过,如果句柄不需要继承,可以把这个参数设置为NULL。
第三个参数flProtect指定该内存映射文件的保护类型,它可以是以下取值:
● PAGE_READONLY——内存映射文件提交的内存页面是只读的,为了使用此标志获得对应的读权限,在用CreateFile函数打开文件获得hFile句柄时必须相应指定GENERIC_READ标志。
● PAGE_READWRITE——内存映射文件提交的内存是可读写的。为了使用此标志,在用CreateFile函数打开文件获得hFile句柄时,必须同时指定GENERIC_READ标志和GENERIC_WRITE标志。
● PAGE_WRITECOPY——内存映射文件提交的内存可以有Copy on Write属性。为了使用此标志,在用CreateFile函数打开文件获得hFile句柄时,必须同时指定GENERIC_READ标志和GENERIC_WRITE标志。
dwMaximumSizeHigh和dwMaximumSizeLow参数则组合指定了一个64位的内存映射文件的长度。当内存映射文件用于磁盘文件的时候,如果这个长度大于磁盘文件的长度,那么磁盘文件将被扩展到这个长度;如果小于磁盘文件长度,那么只能存取磁盘文件的一部分。一种简单的方法是将这两个参数全部设置为0,那么内存映射文件的大小将被自动调整到磁盘文件的大小。
最后一个参数lpName指定一个字符串,用来给定内存映射文件的名字。当内存映射文件用于磁盘文件的时候,不需要给它起名;如果用于在进程间共享内存,那么必须为该对象命名,因为在其他进程中只有使用这个名称才能打开这个内存映射文件对象,该名字字符串不能和其他进程已创建的对象同名。
当一个进程创建内存共享文件用于和其他进程共享的时候,其他进程不能再使用CreateFileMapping函数去创建同样的内存映射文件对象,而是要用OpenFileMapping函数去打开已创建好的对象。OpenFileMapping函数的用法是:
invoke OpenFileMapping,dwDesiredAccess,bInheritHandle,lpName
.if eaxmov hFileMap,eax
.endif
这里的lpName参数指向的名字就是创建对象时使用的名字,dwDesiredAccess参数指定保护类型,它可以是以下的取值:
● FILE_MAP_WRITE(或FILE_MAP_ALL_ACCESS)——可写属性。
● FILE_MAP_READ——可读属性。
● FILE_MAP_COPY——Copy on write属性。
注意:FILE_MAP_ALL_ACCESS等于FILE_MAP_WRITE属性,并不同时包括FILE_MAP_READ属性。如果CreateFileMapping函数或OpenFileMapping函数执行成功,返回的是内存映射文件句柄,这个句柄可以用在后面的函数中,如果执行失败则返回NULL。
使用内存映射文件的第二个步骤是创建内存映射文件的一个视图。获得内存映射文件对象的句柄后,就可以使用它在进程的地址空间中映射该文件的一个视图,该操作可以视为给需要映射的文件内容分配线性地址空间,并将线性地址和文件内容对应起来,这样程序就可以通过存取线性地址来存取文件。
视图可以任意映射或取消映射。当一个文件的视图被映射时,系统仅为它分配足以覆盖文件视图的连续地址空间,并不马上将它提交到当做物理存储器的文件中去,当第一次读写内存页面中任一地址的时候,系统才真正分配一个对应于视图页面的物理内存页面,所以映射视图的速度是相当快的。
MapViewOfFile函数用来映射内存映射文件的一个视图。这个函数的用法是:
invoke MapViewOfFile,hFileMap,dwDesiredAccess,\dwFileOffsetHigh,dwFileOffsetLow,dwNumberOfBytesToMap
.if eaxmov lpMemory,eax
.endif
参数hFileMap就是前两个函数返回的内存映射文件对象的句柄,dwDesiredAccess参数指定保护类型,可能的取值同样是FILE_MAP_WRITE,FILE_MAP_READ或FILE_MAP_COPY。
一个视图可以映射到整个文件,也可以映射到磁盘文件的一部分。需要映射的起始位置可以由dwFileOffsetHigh和dwFileOffsetLow指定,这两个参数组合成一个64位的偏移量,用来指定视图的基地址是从文件的哪个位置开始映射。dwNumberOfBytesToMap参数指定要映射的字节数,如果dwNumberOfBytesToMap参数设置为0,那么映射的是整个文件,同时偏移地址被忽略。如果映射成功,函数返回一个地址,存取这个地址指定的内存块就相当于存取文件的内容了。如果映射失败,则函数返回NULL。
当不再使用内存映射文件后,可以通过UnmapViewOfFile函数撤销映射并使用CloseHandle函数关闭内存映射文件对象句柄:
invoke UnmapViewOfFile,lpMemory
invoke CloseHandle,hFileMap
当对视图中的内存进行修改后,系统会在视图撤销映射或文件映射对象被删除时自动将数据写到磁盘上,但程序也可以根据需要将对文件的修改立即写到磁盘上,该功能是由函数FlushViewOfFile提供的:
invoke FlushViewOfFile,lpMemory,dwFileSize
该函数将从指定地址开始、指定大小的数据块中的脏页面写到磁盘,指定的内存范围必须位于视图的边界之内。
2.使用内存映射文件读写文件
通过上一节的讨论,读者已经知道使用内存映射文件读写文件的步骤为:
(1)调用CreateFile打开想要映射的文件,得到hFile。
(2)调用CreateFileMapping函数生成一个建立在CreateFile函数创建的文件对象基础上的内存映射对象,得到hFileMap。
(3)调用MapViewOfFile函数把整个文件的一个区域或者整个文件映射到内存中。得到指向映射到内存的第一个字节的指针lpMemory。
(4)用该指针来读写文件。
(5)调用UnmapViewOfFile来解除文件映射,传入参数为lpMemory。
(6)调用CloseHandle来关闭内存映射文件,传入参数为hFileMap。
(7)调用CloseHandle来关闭文件,传入参数为hFile。
现在,将FormatText例子中的读文件部分改成使用内存映射文件的方法,将其中的_ProcFile子程序改成下面的样子后,使用效果是一样的:
_ProcFile proc local @hFile, @hFileNew, @hFileMap, @lpMemory, @dwFileSize local @szNewFile[MAX_PATH]:byte local @szBuffer[512]:byte ;打开文件invoke CreateFile, addr szFileName, GENERIC_READ, FILE_SHARE_READ, 0, \OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 .if eax == INVALID_HANDLE_VALUE invoke MessageBox, hWinMain, addr szErrOpenFile, NULL, MB_OK or MB_ICONEXCLAMATION ret .endif mov @hFile, eax invoke GetFileSize, @hFile, NULL mov @dwFileSize, eax ;建立内存映射文件invoke CreateFileMapping, @hFile, NULL, PAGE_READONLY, 0, 0, NULL .if !eax invoke MessageBox, hWinMain, addr szErrOpenFile, NULL, MB_OK or MB_ICONEXCLAMATION jmp _Ret1 .endif mov @hFileMap, eax invoke MapViewOfFile, eax, FILE_MAP_READ, 0, 0, 0.if !eax invoke MessageBox, hWinMain, addr szErrOpenFile, NULL, MB_OK or MB_ICONEXCLAMATION jmp _Ret2 .endif mov @lpMemory, eax ;创建输出文件invoke lstrcpy, addr @szNewFile, addr szFileName invoke lstrcat, addr @szNewFile, addr szNewFile invoke CreateFile, addr @szNewFile, GENERIC_WRITE, FILE_SHARE_READ, \0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0 .if eax == INVALID_HANDLE_VALUE invoke MessageBox, hWinMain, addr szErrCreateFile, NULL, MB_OK or MB_ICONEXCLAMATION jmp _Ret3 .endif mov @hFileNew, eax ;在映射的内存中处理文件invoke _FormatText, @lpMemory, @dwFileSize, @hFileNew invoke CloseHandle, @hFileNew invoke wsprintf, addr @szBuffer, addr szSuccess, addr @szNewFile invoke MessageBox, hWinMain, addr @szBuffer, addr szSuccessCap, MB_OK
_Ret3:invoke UnmapViewOfFile, @lpMemory
_Ret2:invoke CloseHandle, @hFileMap
_Ret1:invoke CloseHandle, @hFile ret
_ProcFile endp
读者可以在所附光盘的Chapter10\FormatText\FileMap目录中找到修改后的源程序。对比原来的程序可以看出,程序中分步将数据读取到缓冲区的循环不见了,取而代之的是对一个长度等于文件长度的大缓冲区进行的单次操作,这样,缓冲区边界判断之类的难题就无形中消失了。
3.使用内存映射文件在进程间共享数据
先来看一个例子,例子在所附光盘的Chapter10\MMFShare目录下。首先多次执行目录中的MMFShare.exe文件,然后尝试在不同执行副本中的编辑框中输入字符,读者马上可以发现,不管在哪个副本中输入字符,所有副本的文本框中都会被设置成刚刚输入的内容(如图10.7所示),这就是用内存映射文件实现的。
图10.7 使用MMF进行进程间共享
使用内存映射文件在进程间共享数据的步骤如下:
(1)调用OpenFileMapping打开一个命名的内存映射文件对象,得到hFileMap。如果打开成功则跳到步骤(3),如果打开不成功,则表示本进程是执行的第一个副本,那么继续执行步骤(2)。
(2)调用CreateFileMapping函数创建一个命名的内存映射对象,得到hFileMap。
(3)调用MapViewOfFile函数映射对象的一个视图,得到指向映射到内存的第一个字节的指针lpMemory。
(4)用该指针来读写共享的内存区域。
(5)调用UnmapViewOfFile来解除视图映射,传入参数为lpMemory。
(6)调用CloseHandle来关闭内存映射文件,传入参数为hFileMap。
上面的步骤与映射普通的磁盘文件相比,少了打开和关闭文件的步骤,但多了一个OpenFileMapping的步骤。还有一个区别在于,建立内存映射文件对象的时候使用的不是文件句柄,而是使用命名的方法。
具体的实现方法参见例子文件MMFShare.asm中的源代码:
;MMFShare.asm ---------- 使用内存映射文件进行进程间数据共享
;--------------------------------------------------------------------
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff MMFShare.asm
;rc MMFShare.rc
;Link /subsystem:windows MMFShare.obj MMFShare.res
.386
.model flat,stdcall
option casemap:none ;include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib;equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 100
IDC_TXT equ 101
IDC_INFO equ 102 ;数据段
.data?
hInstance dword ?
hWinMain dword ?
hFileMap dword ?
lpMemory dword ?
.const
szErr byte '无法建立内存共享文件',0
szMMFName byte 'MMF_Share_Example',0;代码段
.code
_CreateMMF proc invoke OpenFileMapping, FILE_MAP_READ or FILE_MAP_WRITE, 0, addr szMMFName .if !eax invoke CreateFileMapping, -1, NULL, PAGE_READWRITE, 0, 4096, addr szMMFName .if !eax jmp @F .endif .endif mov hFileMap, eax invoke MapViewOfFile, eax, FILE_MAP_READ or FILE_MAP_WRITE, 0, 0, 0.if eax mov lpMemory, eax mov dword ptr[eax], 0ret .endif invoke CloseHandle, hFileMap
@@:invoke MessageBox, hWinMain, addr szErr, NULL, MB_OK invoke EndDialog, hWinMain, -1 ret
_CreateMMF endp _CloseMMF proc invoke UnmapViewOfFile, lpMemory invoke CloseHandle, hFileMap mov lpMemory, 0 mov hFileMap , 0ret
_CloseMMF endp _ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam local @szBuffer[4096]:byte mov eax, wMsg .if eax == WM_TIMER invoke SetDlgItemText, hWnd, IDC_INFO, lpMemory .elseif eax == WM_CLOSEinvoke KillTimer, hWnd, 1 invoke _CloseMMF invoke EndDialog, hWinMain, 0.elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke LoadIcon, hInstance, ICO_MAIN invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax invoke _CreateMMF invoke SetTimer, hWnd, 1, 200, NULL .elseif eax == WM_COMMAND mov eax, wParam .if ax == IDC_TXT && lpMemory invoke GetDlgItemText, hWnd, IDC_TXT, lpMemory, 4096.endif .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp ;main函数
main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, 0
main endp
end main
对应的资源脚本文件MMFShare.rc如下:
#include <resource.h>#define ICO_MAIN 1000#define DLG_MAIN 100#define IDC_TXT 101#define IDC_INFO 102ICO_MAIN ICON "Main.ico"DLG_MAIN DIALOG 229, 208, 211, 55STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENUCAPTION "内存映射文件共享"FONT 9, "宋体"{LTEXT "请执行本程序的多个拷贝,并尝试在下面输入文本:", -1, 7, 8, 196, 8EDITTEXT IDC_TXT, 7, 22, 197, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOPLTEXT "", IDC_INFO, 8, 41, 196, 8}
运行效果:
程序的结构异常简单,首先在用做主界面的对话框的初始化消息中创建内存映射文件(在_CreateMMF子程序中完成),并建立一个定时器,用来不停地将共享区域的内容设置到文本框中。在这里,内存映射文件对象的名称是“MMF_Share_Example”,别的进程只要知道这个名称,就可以使用它。
程序在WM_COMMAND消息中检测编辑框的输入动作,如果用户在编辑框中输入了字符,那么马上将编辑框的内容取到共享区域中,这样,其他进程在定时器消息中就可以马上将这个内容在自己窗口的文本框中反映出来。
在退出的时候,程序在_CloseMMF子程序中调用UnmapViewOfFile和CloseHandle函数来关闭内存映射文件,并在撤销定时器后退出。
当程序运行多个副本的时候,内存映射文件是由首先运行的副本建立的,但是在退出的时候,即使首先运行的副本先退出(也就是说创建内存映射文件的副本首先退出),其他副本之间的通信也不受影响,这就是说,这时内存映射文件还是存在的。实际上,系统为进程间共享的内存映射文件对象维护一个计数器,每次有进程打开内存映射文件对象的时候,计数器加1,关闭的时候减1,只有当计数器减到零的时候,内存映射文件才真正被释放,所以程序中关闭内存映射文件的时候并不需要考虑别的程序是否还在使用它。