首先看10.2.1节的WordCount例子中读文件的操作,与在DOS操作系统下所有文件操作代码的结构一样,例子中用分块读入的办法读取文件,每次读取的内容受限于缓存区的大小,对上一次读入的内容处理完毕后,程序才能继续读入下一块内容,代码结构如下:
.while TRUE
.if 缓冲区中还有未处理的数据
;从缓冲区中取出数据并处理
;递减未处理字节数
.else
;读取文件,并重新设置未处理字节数
.break .if 到达文件尾部
.endif
.endw
经常使用这种结构处理文件的读者一定知道它的严重缺陷就是缓冲区边界的处理问题。假如需要每次先把单词提取出来后再处理(在这个例子中用每次处理单个字符的办法回避了这个问题),当一个单词刚好跨越缓冲区边界的时候,就要先保存单词的上半部分,等继续将后续数据读到缓冲区后再将单词的下半部分连接在一起;把这个问题再扩展开来,假如每次要提取是整个行的内容,而遇到某个行的长度等于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.7所示,对于每个进程,系统将可执行的代码页提交到磁盘中的可执行文件中,而数据页(包括进程的静态数据段以及动态分配的内存)则被提交到虚拟内存中。
除了加载文件,使用内存映射文件也可以在同一台计算机上运行的多个进程之间共享数据,而且内存映射文件是多个进程互相进行通信的最有效的方法。那么如何实现数据共享呢,其实原理很简单,如图10.8所示,对于不同进程间共享的数据页,只要将它们提交到虚拟内存的同样页面就可以了,这样,当一个进程改变了数据页的内容时,通过分页映射机制,其他进程的共享数据区的内容就会同时改变,因为它们实际上存储在同一个地方。
10.4.2 使用内存映射文件
1. 内存映射文件函数
内存映射文件函数包括:CreateFileMapping,OpenFileMapping,MapViewOfFile,UnmapViewOfFile和FlushViewOfFile。
使用内存映射文件的步骤分为两步,第一步是使用CreateFileMapping创建一个内存映射文件对象。这个步骤决定了使用内存映射文件的用途——究竟是在磁盘文件上建立内存映射文件还是在页文件中建立进程间共享的映射。CreateFileMapping函数的用法是:
invoke CreateFileMapping,hFile,lpFileMappingAttributes,
flProtect,dwMaximumSizeHigh,dwMaximumSizeLow,lpName
eax
mov 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 eax
mov 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
eax
mov 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。
现在,将WordCount例子中的读文件部分改成使用内存映射文件的方法,将其中的_WordCount子程序改成下面的样子后,使用效果是一样的:
_CountWord proc
local @hFile,@hFileMap,@lpMemory,@dwFileSize
local @szLogFile[MAX_PATH]:byte
local @szBuffer
invoke RtlZeroMemory,addr stWordCount,sizeof stWordCount
;********************************************************************
; 打开文件
;********************************************************************
invoke CreateFile,addr szFileName,GENERIC_READ,
FILE_SHARE_READ,0,
OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
.if eax == INVALID_HANDLE_VALUE
jmp _Error
.endif
mov @hFile,eax
invoke GetFileSize,@hFile,NULL
mov @dwFileSize,eax
;********************************************************************
; 建立内存映射文件并处理每个字节
;********************************************************************
invoke CreateFileMapping,@hFile,NULL,PAGE_READONLY,0,0,NULL
.if eax
mov @hFileMap,eax
invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
.if eax
mov @lpMemory,eax
jmp @F
.endif
invoke CloseHandle,@hFileMap
.endif
jmp _Error
@@:
;********************************************************************
; 处理文件内容
;********************************************************************
xor eax,eax
mov dwCount,eax
mov esi,@lpMemory
mov edi,offset stWordCount
.while @dwFileSize
lodsb
dec @dwFileSize
invoke _CountLetter,eax
.endw
invoke _CountLetter,0
;********************************************************************
; 关闭内存映射文件
;********************************************************************
invoke UnmapViewOfFile,@lpMemory
invoke CloseHandle,@hFileMap
invoke CloseHandle,@hFile
;********************************************************************
; 输出记录文件
;********************************************************************
...
读者可以在所附光盘的Chapter10WordCountFileMap目录中找到修改后的源程序。对比原来的程序可以看出,除了多了几句建立内存映射文件的代码外,程序中真正用于处理数据的代码结构要简单很多。
3. 使用内存映射文件在进程间共享数据
先来看一个例子文件,例子在所附光盘的Chapter10MMFShare目录下。首先多次执行目录中的MMFShare.exe文件,然后尝试在不同执行副本中的编辑框中输入字符,读者马上可以发现,不管在哪个副本中输入字符,所有副本的文本框中都会被设置成刚刚输入的内容(如图10.9所示),这就是用内存映射文件实现的。
使用内存映射文件在进程间共享数据的步骤如下:
(1)调用OpenFileMapping打开一个命名的内存映射文件对象,得到hFileMap。如果打开成功则跳到步骤(3),如果打开不成功,则表示本进程是执行的第一个副本,那么继续执行步骤(2)。
(2)调用CreateFileMapping函数创建一个命名的内存映射对象,得到hFileMap。
(3)调用MapViewOfFile函数映射对象的一个视图,得到指向映射到内存的第一个字节的指针lpMemory。
(4)用该指针来读写共享的内存区域。
(5)调用UnmapViewOfFile来解除视图映射,传入参数为lpMemory
(6)调用CloseHandle来关闭内存映射文件,传入参数为hFileMap。
上面的步骤与映射普通的磁盘文件相比,少了打开和关闭文件的步骤,但多了一个OpenFileMapping的步骤。还有一个区别在于,建立内存映射文件对象的时候使用的不是文件句柄,而是使用命名的方法。
具体的实现方法参见例子文件MMFShare.asm中的源代码:
.386
flat, stdcall
option casemap :none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Equ 等值定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN equ 1000
DLG_MAIN equ 100
IDC_TXT equ 101
IDC_INFO equ 102
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
hWinMain dd ?
hFileMap dd ?
lpMemory dd ?
.const
szErr db '无法建立内存共享文件',0
szMMFName db 'MMF_Share_Example',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_CreateMMF proc
invoke OpenFileMapping,FILE_MAP_READ or FILE_MAP_WRITE,
0,addr szMMFName
! eax
invoke CreateFileMapping,-1,NULL,PAGE_READWRITE,0,
4096,addr szMMFName
! eax
jmp @F
.endif
.endif
mov hFileMap,eax
invoke MapViewOfFile,eax,FILE_MAP_READ or FILE_MAP_WRITE,
0,0,0
eax
mov lpMemory,eax
mov dword ptr [eax],0
ret
.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,0
ret
_CloseMMF endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
local @szBuffer[4096]:byte
mov eax,wMsg
eax == WM_TIMER
invoke SetDlgItemText,hWnd,IDC_INFO,lpMemory
eax == WM_CLOSE
invoke KillTimer,hWnd,1
invoke _CloseMMF
invoke EndDialog,hWinMain,0
;********************************************************************
eax == WM_INITDIALOG
push hWnd
pop hWinMain
invoke LoadIcon,hInstance,ICO_MAIN
invoke SendMessage,hWnd,WM_SETICON,ICON_BIG,eax
invoke SendDlgItemMessage,hWnd,IDC_TXT,
EM_SETLIMITTEXT,100,0
invoke _CreateMMF
invoke SetTimer,hWnd,1,200,NULL
;********************************************************************
eax == WM_COMMAND
mov eax,wParam
ax == IDC_TXT && lpMemory
invoke GetDlgItemText,hWnd,IDC_TXT,
lpMemory,4096
.endif
;********************************************************************
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,DLG_MAIN,NULL,
offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
对应的资源脚本文件MMFShare.rc如下:
ICO_MAIN ICON "Main.ico"
DLG_MAIN DIALOG 229, 208, 211, 55
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "内存映射文件共享"
FONT 9, "宋体"
{
LTEXT "请执行本程序的多个拷贝,并尝试在下面输入文本:", -1, 7, 8, 196, 8
EDITTEXT IDC_TXT, 7, 22, 197, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
LTEXT "", IDC_INFO, 8, 41, 196, 8
}
程序的结构异常简单,首先在用做主界面的对话框的初始化消息中创建内存映射文件(在_CreateMMF子程序中完成),并建立一个定时器,用来不停地将共享区域的内容设置到文本框中。在这里,内存映射文件对象的名称是“MMF_Share_Example”,别的进程只要知道这个名称,就可以使用它。
程序在WM_COMMAND消息中检测编辑框的输入动作,如果用户在编辑框中输入了字符,那么马上将编辑框的内容取到共享区域中,这样,其他进程在定时器消息中就可以马上将这个内容在自己窗口的文本框中反映出来
在退出的时候,程序在_CloseMMF子程序中调用UnmapViewOfFile和CloseHandle函数来关闭内存映射文件,并在撤销定时器后退出。
当程序运行多个副本的时候,内存映射文件是由首先运行的副本建立的,但是在退出的时候,即使首先运行的副本先退出(也就是说创建内存映射文件的副本首先退出),其他副本之间的通信也不受影响,这就是说,这时内存映射文件还是存在的。实际上,系统为进程间共享的内存映射文件对象维护一个计数器,每次有进程打开内存映射文件对象的时候,计数器加1,关闭的时候减1,只有当计数器减到零的时候,内存映射文件才真正被释放,所以程序中关闭内存映射文件的时候并不需要考虑别的程序是否还在使用它。
原文始发于微信公众号(汇编语言):第10章 内存管理和文件操作
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论