【摘要】本文介绍了在高速并行数据采集系统中(WIN32环境下),实现上位机控制软件对物理内存进行分配的三种方法:一是在DOS环境下分配,二是使用DDK(Device Driver Kit)实现分配,三是利用WinDriver实现,对这三种方法进行了比较。并给出利用LabWindows/CVI实现直接访问物理内存的方法。
关键词: WIN32 物理内存DOS DDK WinDriver LabWindows/CVI
这里的高速并行数据采集系统是指由我所研制开发的16通道并行数据采集系统,包括上位机、数据采集板、信号调理机箱;如图1:

图1、 PHD系统本地模式组成结构
上位机与数据采集板之间需要通过物理内存来实现数据的传输与存储;首先上位机要获得一段(例如64KDWORD)物理地址连续的物理内存,然后将此物理内存首地址通知数据采集模板,数据采集模板再通过一定方式(DMA等)将采集数据送往上位机内存,上位机再从指定内存地址读取数据进行诸如实时显示、实时存盘等处理。所以物理内存在上位机和数据采集模板之间起着数据链路的衔接作用。
一些常用的开发语言如Visual C++虽然提供了诸如_inp()、_outp()之类的函数,但运用这些函数明显存在一些不足:一是这些函数只能从端口一个一个字节、字或双字地读写数据,速度相对较慢;二是读取的数据的共享度不高,只能为上位机某一部分程序所用。这不能满足高速并行数据采集系统高速、高共享度的要求。而物理内存的直接访问以其速度快共享度高的优点,恰好弥补了这些不足。但在Win32下,系统管理内存采取的是虚拟地址空间,而不是实际物理内存,该地址空间只不过是一段内存地址,在能正确访问数据而不引起访问冲突之前,物理内存要被分配或映射到地址空间的分区。所以,一般情况下,程序得到的地址是物理内存映射后的虚拟内存地址,而无法得到其实际物理内存地址。因而,大块物理地址连续的物理内存的分配并得到所分配的内存的物理首地址,且实现物理内存的直接访问就成了整个系统性能好坏的关键。
一、WIN32虚拟内存的工作方式
Windows 是多任务操作系统,其每个应用程序称为一个进程,每个进程都有独享4GB的内存地址空间。各种程序和数据元素以4KB为单位分散在这个4GB的地址空间中。每个4KB单元叫做一页,它可以保存代码或数据。当使用一页时,它占用物理空间,但不知道它的物理地址。Intel CPU 可以有效地把一个32位虚拟地址映射为物理页以及该页内的偏移量,它使用的只是如下图所示的2级4KB分页表。每个进程都有它自己的分页表。芯片的CR3寄存器所保持的是指向该目录页的指针,因此,当Windows从一个进程切换到另一个进程时,只需更新CR3即可。每个分页表中有一个“现场”位,它的作用是指示该4KB页是否位于当前RAM中。如果访问一个不在当前RAM中的页,那末将触发一个中断,Windows 通过检查它的内部分区表来对当前形式进行分析。如果内存应用错误,将给出“页面错误”消息,退出程序。否则,Windows 将该页读入RAM中,并通过加载物理地址和设置现场位来更新分页表。这就是Win32虚拟内存的实质。从中我们可以看出,Windows 中的内存地址是不确定的,内存块在不停的“漂移”着,而且内存的物理地址是不可见的。强行分配并固定内存很容易引起应用程序的退出,甚至系统的崩溃。

图2、WIN32虚拟内存管理(Intel)
二、物理内存分配方法的比较
自从Windows实行虚拟内存管理机制以来,物理内存的分配和直接访问就成为一个棘手的问题,因此也产生了不少的实现方法。现将各种方法列举一下并做以比较:
(1)原始的解决方法
在Windows使用初期,物理内存分配与访问的一般方法是:先在DOS下分配一定的物理地址连续的内存,并将该部分内存的句柄、物理首地址及数量存储到一个文件中;然后进入Windows进行该内存访问;最后返回到DOS下释放此内存。此方法是借助DOS来实现的,明显存在着不足:一是,PC机启动后首次分配的扩展内存在物理地址上是连续的,一旦释放后再分配内存就不能保证其物理地址还是连续的;二是,对大部分数据采集系统来说,要多次不确定量的分配内存,这样在DOS和Windows之间来回显然是不现实的。
(2)使用DDK来实现的方法
MS-DDK(Device Driver Kit)是微软公司开发的设备驱动程序开发软件。使用它的资源可以开发出与Win32操作系统兼容的独立的虚拟设备程序。利用其库函数:
ULONG EXTERN _PageAllocate(
ULONG nPages, ULONG pType, ULONG VM,
ULONG AlignMask, ULONG minPhys, ULONG maxPhys, ULONG *PhysAddr,
ULONG flags);
可以实现物理内存的分配锁存并得到物理首地址。此函数各参数含义如下:
nPages:所分配内存块的页数;
pType:所分配内存页的类型。
= PG_SYS,在系统级上分配内存;
= PG_HOOKED 或PG_VM:在Ring-3分配内存,二者无实质区别;
VM:设备句柄。在pType =PG_HOOKED 或 PG_VM有用;当pType= PG_SYS时,为0。
AlignMask:内存块开始页数目标志,只有在flags=PAGEUSEALIGN时有用。
=00H,物理地址是4K的倍数;
=01H,物理地址是8K的倍数;
=03H,物理地址是16K的倍数;
=07H,物理地址是32K的倍数;
=0FH,物理地址是64K的倍数;
=1FH,物理地址是128K的倍数;
MinPhys:内存块最小页数,只有在flags=PAGEUSEALIGN时有用。
MaxPhys:内存块最大页数,只有在flags=PAGEUSEALIGN时有用。
PhysAddr:内存块的物理首地址;
flags:操作标志。
=PAGECONTIG:分配连续的内存块;
=PAGEFIXED:锁定所分配内存的线性地址,并禁止内存被解锁或被移动;
=PAGELOCKED:锁定所分配内存;
=PAGELOCKEDIFDP:锁定所分配内存;为MS-DOS或BIOS专用;
=PAGEUSEALIGN:利用AlignMask、minPhys、maxPhys,、PhysAddr参数值分配内 存。
=PAGEZEROINIT:把所分配的内存初始化为0。
例如要分配并锁定64KDWORD内存,然后将其初始化为0:
include vmm.inc
{ _PageAllocate(64, PG_SYS, 0, 0, 16,16, PhysAddr, PAGEFIXED|| PAGEUSEALIGN|| PAGEZEROINIT);//分配64*16Kbytes内存
test eax, eax ; 错误返回0
jz error
mov [Address], eax ; 分配内存块的线性地址
……
}
采用此方法必须十分精通Windows内部体系结构、Microsoft MASM汇编语言和设备驱动程序结构体系方法以及具有丰富的经验,否则会造成软件极不稳定甚至系统崩溃。这种方法在开发设备虚拟驱动程序时非常实用,但需要花费的时间和精力也比较多。
(3)利用WinDriver实现的方法
利用WinDriver也可以实现物理内存的分配,用这中方法的好处有:
Ú WinDriver的函数以文件形式独立出来,开发时只需将其文件加入模板接口API 动态链接库(DLL)源文件中即可,使用方便而且独立性较强。
Ú 由于模板接口API DLL主要是利用WinDriver实现的,这样使整个DLL有较强的统一性和稳定性。
Ú 在整个DLL中,每个数据采集模板只有一个句柄,避免了有其他方法实现时的每个数据采集模板多句柄问题。
如上所诉,利用KRFTech’s公司开发的WinDriver是可以实现物理内存分配的。WinDriver本来是硬件驱动程序开发工具,运用它很容易完成访问I/O端口、存储区域、处理中断、执行DMA操作及访问PCI和自定义寄存器的工作。利用WinDriver 库函数WD_DMALOCK()可以动态地分配一段物理内存。这个函数的作用是:分配和锁定物理内存区,并返回已分配内存的物理地址。下面对此函数做一详细介绍。
WD_DMALOCK(HANDLE Hwd,WD_DMA *pDma)
这里用到WD_DMA结构,它的成员如下:
pUserAddr – 内存的基地址
dwBytes – 内存的大小
dwOptions – 内存分配方式,常为 0。
=DMA_KERNEL_BUFFER_ALLOC 时,内存地址是连续的,内 存地址放在pUserAddr中
=DMA_LARGE_BUFFER 将分配大于1MB 的内存
Page[] – 内存地址数组
Page[i].pPhysicalAddr – 第i页的物理地址
比如向上位机申请64KDWORD物理内存,则:
{ WD_DMA Dma;
BZERO(Dma);
Dma.dwBytes = 262144;//64KDWORD
Dma.pUserAddr = NULL;
Dma.dwOptions= DMA_KERNEL_BUFFER_ALLOC;
WD_DMALock (hWD, &Dma);
if (Dma.hDma==0)
printf (“Could not lock down memory”); }
当不用此段内存时,必须用WD_DMAUnLock ()将此次释放,以免引起内存泄漏。
三、物理内存直接访问
上位机在内存分配完毕并且获得内存的物理地址后,将该内存物理地址通知数据采集模板,数据采集模板就可以以DMA方式向此段内存存取数据,上位机就可以获取共享数据采集模板存储在此的数据,实现数据采集模板向上位机传输采集数据,大大提高了上位机获取数据采集模板采集数据的速度。而且,上位机也可以通过程序方式将处理后的数据直接放回该段物理内存,实现上位机各进程之间的数据共享和交换。但由于Win32的应用程序对内存的访问都是用内存的逻辑地址,内存的逻辑地址和物理地址的转换是自动的,不允许直接操作物理内存,所以上位机对物理内存的访问比较困难。是运用LabWindows/CVI 5.0中的两个函数来直接访问物理内存区。这两个函数是:
ReadFromPhysicalMemoryEx(unsigned int physicalAddress,void*destinationBuffer,unsigned int numberOfBytes, int bytesAtATime)
WriteToPhysicalMemoryEx (unsigned int physicalAddress, void *sourceBuffer, unsigned int numberOfBytes, int bytesAtATime)
参数具体含义如下:
physicalAddress:内存物理地址,可由上述方法得到
destinationBuffer:上位机从内存读取数据后的存放缓冲
sourceBuffer: 上位机要写入内存的数据缓冲
numberOfBytes:读取或存储到内存的数据总量
bytesAtATime:一次读取或写入内存的数据量,可以是1,2或4bytes
如果是在LabWIndows/CVI下,可直接用着两个函数来访问物理内存,例如:
ReadFromPhysicalMemoryEx(Dma.pUserAddr,&data,2,2):可实现从上面分配的物理内存中一次读取一个字放在data变量中;
WriteToPhysicalMemoryEx(Dma.pUserAddr,&data,4,4):可实现向物理内存地址Dma.pUserAddr 中写入一个双字。
在实践中工作中,应用Visual C++等开发平台,脱离了LabWindows/CVI环境。而仍想用这两个函数,可以将它们在LabWindows/CVI环境下编辑成动态链接库,然后可以在Visual C++或别的开发工具中调用该动态库,可以实现相同的功能。将ReadFromPhysicalMemoryEx()和WriteToPhysicalMemoryEx()在LabWindows/CVI下封装成动态链接库(DLL),共四个函数分别为:
phymemreadw(unsigned int phyAdd):从物理内存读取一个字(16BIT)
phymemreaddw(unsigned int phyAdd):从物理内存读取双字(32BIT)
phymemwritew(unsigned int phyAdd,void *data): 向物理内存写字(16BIT)
phymemwritedw(unsigned int phyAdd,void *data): 向物理内存写双字(32BIT)
这样一来,即满足了软件设计的要求,又使物理内存的直接访问具有更广泛性。在软件的实际应用中,函数phymemreadw(unsigned int phyAdd)用的比较多,因为上位机主要是从内存获取采集数据。
四、小结
本文讨论了三种物理内存的分配方法,其中第三种方法具有较强的灵活性,本系统采用这种方法实现数据流由采集板到上位机硬盘的高速存储,该方法是可以广泛应用的。物理内存分配和直接访问问题的解决,为整个数据采集系统功能的实现扫清了障碍,也使数据采集软件成功的应用在Windows系统中。
参考文献
1. Visual C++6.0技术内幕(第五版)
北京希望电子出版社,1999年
2. Win9X虚拟设备驱动程序编程指南
清华大学出版社,1999年
3. LabWindows/CVI User Manual、NI公司
1997年

