来源:
Windows CE驱动程序结构概述
Windows CE的驱动程序可以从多种角度进行区分。
1.从加载以及接口方式来区分
可以分为本机设备驱动(Built-In Driver)、可加载驱动(Loadable Driver)以及混合型驱动。
(1)本机设备驱动
本机设备驱动即Native Device Drivers。这些驱动程序在系统启动时,在GWES的进程空间内被加载,因此它们不是以独立的DLL形式存在。这些驱动对应的设备通常在系统启动时就被要求加载,如果没有串口,也没有LCD的话,整个系统就不能和用户信息交流。另外,流驱动程序也能作为本机设备驱动而存在。
(2)可加载驱动
也被称为流驱动。
这些驱动可以在系统启动时或者和启动后的任何时候由设备管理器动态加载。通常它们以DLL动态链接库的形式存在,系统加载它们后,这些驱动程序也只是以用户态的角色运行。可加载驱动程序通过文件操作API来从设备管理器和应用程序获得命令。
在Windows CE中典型的可加载驱动有以下各类:
n PCMCIA driver(PCMCIA.dll)
n Serial driver(SERIAL.dll)
n ATAFLASH driver(ATA.dll)
n Ethernet driver(NE2000.dll,SMSC100FD.dll)
(3)混合型驱动
这类驱动综合了前两种驱动的特性。它同时使用了stream接口和custom-purpose接口。
混合型驱动主要是提供custom-purpose 接口,但是由于需要和系统中只允许使用stream接口的那些模块进行交互,因此也必须提供stream接口。例如,PC card socket驱动同时拥有两套接口。
2.从驱动层次上分
可以分为独立驱动和层次型驱动。图9-1是这两种驱动在系统中的位置。
图9-1 独立驱动和层次型驱动在系统中的位置
(1)独立驱动程序
可以将驱动程序编写成同时包含MDD和PDD层的独立驱动。独立驱动的代码应当包括中断服务例程和平台相关处理函数。使用独立驱动的好处在于可以省去MDD和PDD层驱动之间的信息传递,这一点在实时处理中非常重要。另外,如果设备的操作和MDD驱动层的接口描述相吻合,可以使用独立驱动程序提高处理性能。
(2)层次型驱动
层次型驱动分为两层,较上层的Model Device Driver(MDD)和比较下层的Platform Dependent Driver(PDD)。MDD实现的是和平台无关的功能,它描述了一个通用的驱动程序框架。而PDD是和硬件以及平台相关的代码组成。MDD调用PDD中特定的接口来获取硬件相关的信息。当使用层次型驱动的时候,一般只需要基于相近的样列驱动程序,针对特定的硬件修改PDD程序,MDD建立的框架可继续使用。由于层次间接口的层层调用以及消息的传递,使得处理速度相对独立驱动程序要慢,因此在时间要求苛刻的环境下,层次型驱动显得不是很适合。
一般MDD将完成以下任务。
n 连接PDD层,并且定义它要使用到的Device Driver Service Provider Interface(DDSI)函数集;
n 向设备管理器提供Device Driver Interface(DDI)接口集;
n 处理复杂的事件,如中断等等。
每一种MDD驱动都处理不同种类的设备。DDI是由MDD层驱动以及独立型驱动提供给设备管理器的一组接口集。DDSI是由PDD向MDD层提供的接口集。公司的设备可以用同样的DDI。
在开发过程中,MDD层驱动是不需要被修改的。微软公司不保证被修改的MDD能在系统中正确运行的。和MDD层驱动不同的是,PDD层驱动必须被修改成和特定硬件相匹配的代码。程序员可以自己开发一个PDD程序,多数情况下建议开发者在Platform Builder提供的样例驱动程序上进行修改。例如,Platform Builder提供了Wavedev驱动程序,它的代码位于%WINCEROOT%\public\common\oak\drivers\WAVEDEV下,这是一个容易理解的流接口层次型驱动程序。此样例audio驱动程序仅提供了播放及录音功能,只提供播放功能的结构框架,播放功能和音频设备的交互还需要PDD层来解决。
本机设备驱动程序通常只有OEMs才会对本机设备驱动程序进行修改,其他自由设备生产商由于只提供附加的硬件设备,对本机设备驱动程序不会有过多涉及。因此下面的本机设备驱动程序面向OEMs。
微软公司为每一种本机设备驱动程序设了一套custom接口。在此基础上,微软公司还为相同类型的设备驱动设计出了一组标准的接口。这样,Windows CE操作系统就能按照标准来操作同一类型的设备驱动,而不需要过多地去了解它们之间的硬件区别。如很多移动设备上都使用LCD来作为显示器,其中就有很多不同的LCD模块可以被使用到Windows CE的系统中。
Windows CE中提供了如下本机设备驱动程序实例。
n Display
n Battery
n Keyboard
n Touch screen
n Notification LED
如果在目标设备上,有以上列出之外的设备需要本机设备驱动程序,程序员就应当自己开发一套本机设备驱动程序。如果正在开发的目标系统上有上述的设备,开发人员应当考虑修改PlatformBuilder提供的样例驱动程序,尽可能不要重新编写驱动程序。样例驱动程序经过了Microsoft的测试,这比重新编写驱动程序省去一些验证上的麻烦。
流接口驱动程序的结构n 流接口驱动有一套标准的接口,这和本机驱动是不一样的。
n 对于I/O设备来说是非常适合的。
n 操作接口和文件系统API十分类似,比如ReadFile,IOControl等。
n 应用程序可以和流接口驱动进行交互,并且可以把流驱动当成文件来操作。
流驱动与驱动接口、提供设备的种类无关,因为这组接口有统一的接口规范。对于需要数据流的设备来说,这种驱动是十分适合的,如串口就是个典型的例子。可以把使用流驱动的设备近似地看作是文件,这样可以通过文件系统API来操作设备,如ReadFile,IOControl。由于采用了文件系统的API,使得驱动程序能通过文件系统进行访问,这点和独立驱动程序是不同的。
这种将设备看成是文件的做法在很多操作系统上都比较常见。包括Windows桌面系统和类Unix的操作系统。例如,开发Windows驱动的程序员通常将打印机设备贯以LPTx:前缀,窗口被贯以COMx:前缀,这些都是特殊的文件名。
图9-2是流驱动程序在整个系统中的结构示意图。
图9-2 流驱动程序在系统中的结构示意图
图9-2显示了作为本机驱动而存在的流驱动程序,在启动时被设备管理器所加载。上节9.1.1介绍过,一般本机驱动是指custom接口的驱动程序,但是流驱动也可以成为本机驱动,例如串口。
如图9-2所示,流驱动通过文件系统API来和应用程序交互,同时又通过流接口接受设备管理器的管理。无论流驱动管理的是本机设备还是动态加载的设备,它们自身是在启动时被加载还是启动后由设备管理器动态加载,这和系统中其他模块的交互模型是一样的。
流驱动程序入口的实现实现流驱动程序大致需要完成以下步骤。
(1)选择代表设备的文件名前缀;
(2)实现驱动的各个入口点;
(3)建立.DEF文件;
(4)在注册表中为驱动程序建立表项。
为了说明如何实现流驱动程序,我们将以platform builder提供的一个样例程序为参考来介绍。这个程序位于WINCE420\PLATFORM\SA11X0BD\DRIVERS\PWRBUTTON目录下,它是SA110XBD开发版上的power button驱动程序。
以下是创建流驱动的具体步骤。
(1)首先确定设备名的前缀。前缀非常重要,设备管理器在注册表中通过前缀来识别设备。同时,在流接口命名时,也将这个前缀作为入口点函数的前缀,如果设备前缀为XXX,那么流接口对应为XXX_Close,XXX_Init等。
(2)实现流接口的各个入口点。所谓入口点是指提供给设备管理器的标准文件I/O接口。
表9-1是对这些接口的介绍:
表9-1 接口的介绍
接 口 名 | 功能描述 |
XXX_Close | 关闭hOpenContext参数指定的设备上下文 |
XXX_Deinit | 通知设备管理器回收设备初始化时分配的资源 |
XXX_Init | 通知设备管理器为设备初始化时分配资源 |
XXX_Open | 打开设备,这个接口可以由应用程序直接调用createfile,然后通过文件系统映射为XXX_Open |
XXX_IOControl | I/O控制指令 |
XXX_PowerUp | 设备加电时,此接口会被自动调用,可以在这里分配资源等 |
XXX_PowerDown | 如果设备能由软件控制断电,则在设备断电前,设备管理器会调用这个接口做些安全性检查 |
XX_Read | 从打开的设备文件中读取数据 |
XXX_Seek | 文件定位,如果设备支持的话 |
XXX_Write | 写数据到设备文件 |
在样例提供的驱动代码中,WINCE400\PLATFORM\SA11X0BD\DRIVERS\PWRBUTTON
\pwrbuttonpdd.c可以找到以上对应的入口点。
(3)建立*.DEF文件。在这个例子中,文件名为PWRBUTTON.DEF,DEF文件中定义了DLL要导出的接口集。流驱动大多是以DLL形式存在的,所以应将DLL和DEF的文件名统一起来。如PWRBUTTON.DEF,PWRBUTTON.DLL。
下面是PWRBUTTON.DEF文件的内容:
;Copyright (c) Microsoft Corporation. All rights reserved.
/* Copyright _ 1999 Intel Corp. */
LIBRARY PWRBUTTON
EXPORTS
PWR_Init
PWR_Deinit
PWR_Open
PWR_Close
PWR_Read
PWR_Write
PWR_Seek
PWR_IOControl
PWR_PowerDown
PWR_PowerUp
PWR_PowerHandler
PWR_DllEntry
如上代码所示,EXPORTS标签下,列出了一系列的接口,它们都是以PWR为前缀。
(4)在注册表中建立驱动程序入口点,这样设备管理器才能识别和管理这个驱动。
下面是power button驱动在注册表中的信息:
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\PWRBUTTON]
"Prefix"="PWR"
"Dll"="PwrButton.Dll"
"Order"=dword:2
"Ioctl"=dword:4
此外,注册表中还能存储额外的信息,这些信息可以在驱动运行之后被使用到。DLL项是设备管理器在加载驱动时需要的DLL名称;Prefix代表了设备前缀;ORDER是驱动程序被加载的顺序。
1.Streams入口:Open和Close
(1)XXX_Open入口
用于读/写打开一个设备文件。当应用程序调用CreateFile的时候,文件系统会自动调用本接口,打开一个已经存在的设备文件。当这个接口被调用的时候,设备驱动可以向设备管理器申请分配资源,并且为读/写文件做好准备。
下面是接口的原形:
DWORD XXX_Open( DWORD hDeviceContext, DWORD AccessCode, DWORD ShareMode );
参数解释:
hDeviceContext
指向XXX_Init返回的设备句柄(上下文)。
AccessCode
打开设备的权限描述符,这些权限包括读设备、写设备、读/写等。
所谓的设备上下文指的是代表图形设备接口graphics device interface(GDI)的数据结构。它包含了在特定区域内设备显示图象的信息。设备上下文包含了图形对象(例如pen、brush、字体等),不断地修改和调用它们,从而达到显示不同图象的效果。
(2)XXX_Close入口
在关闭设备的时候被操作系统调用。对应于CloseHandle接口。
BOOL XXX_Close( DWORD hOpenContext );
参数解释:
hOpenContext
指向XXX_Open返回的已经打开的设备句柄(上下文)。
应用程序可以调用CloseHandle来关闭正在使用的流驱动程序,然后设备管理器会相应地调用本接口来关闭设备,当设备关闭后hOpenContext描述的设备句柄将不再有效。
2.Streams入口:Init and Deinit
(1)XXX_Init入口
XXX_Init要完成以下任务。
n 在驱动被系统加载时,本接口被调用;
n 初始化需要的资源在本接口处理中被分配;
n 创建内存映射。
下面是接口的原型:
DWORD XXX_Init( DWORD dwContext );
参数解释:
n dwContext
指向一个字符串,它描述了注册表中的一个流设备接口。
当调用设备ActivateDeviceEx函数后,设备管理器自动调用这个函数。当用户激活一个新的设备时,如插入USB设备后,当总线自检时,设备就会被激活,这个接口就会被调用,这个接口是不允许应用程序直接调用的。
当这个接口的处理结果返回时,设备管理器就在注册表中寻找驱动的Ioctl子键。如果这个子键存在,设备管理器将调用XXX_IOControl接口将dwCode参数传入驱动入口点。
(2)XXX_Deinit入口
在驱动被系统卸载的时候,本接口将被调用,它将释放所有占用的阻援,并且停止IST。
下面是接口的原型:
BOOL XXX_Deinit( DWORD hDeviceContext );
参数解释:
hDeviceContext
指向设备上下文的句柄。这个句柄应该是由XXX_Init返回的。
当程序调用DeactivateDevice时,设备管理器将自动调用本接口。流接口将释放全部它申请的资源,并且停止设备的运行。
3.Streams入口:Read,Write,Seek
(1)XXX_Read入口
当应用程序直接调用ReadFile函数时,设备管理器将调用这个接口。
下面是接口的原型:
DWORD XXX_Read( DWORD hOpenContext, LPVOID pBuffer, DWORD Count );
参数解释:
hOpenContext
指向XXX_Open接口返回的设备上下文。
pBuffer
指向缓冲区,这个缓冲区将用来存放从设备中读出的数据,以字节为单位。
Count
指定要从设备读取多少字节的数据存入pBuffer指向的缓冲区中。
这个从指定的设备中读取指定数量的字节数据,它对应的应用层API为ReadFile。ReadFile函数的参数hFile是指向设备的句柄,ReadFile函数的参数hFile将被填写到count参数中,ReadFile中的pSizeRead将存放实际读取数据的字节数。本接口的返回值是pSizeRead中填充的数值,若返回-1,代表发生了错误。
(2)XXX_Write入口
当应用程序调用WriteFile的时候,设备管理器将调用本接口。
下面是接口的原型:
DWORD XXX_Write( DWORD hOpenContext, LPCVOID pBuffer, DWORD Count );
参数解释:
hOpenContext
指向XXX_Open接口返回的设备上下文。
pBuffer
指向缓冲区,这个缓冲区将用来存放要向设备中写入的数据,以字节为单位。
Count
指定要从pBuffer指向的缓冲区向设备读取写入多少字节的数据。
(3)XXX_Seek入口
在定位I/O指针的时候被调用。
下面是接口的原型:
DWORD XXX_Seek( DWORD hOpenContext, long Amount, WORD Type );
参数解释:
hOpenContext
指向XXX_Open接口返回的设备上下文。
Amount
指定指针要移动多少距离,以字节为单位。正值代表向文件尾端移动,负值则相反。
Type
描述了起始点的位置。当应用程序调用了SetFilePointer函数后,设备管理器就会调用本接口。
如果设备是可以重复打开的,本接口用到的指针只是针对hOpenContext的。
4.Streams入口:IOControl
XXX_IOControl入口
允许应用程序进行非文件的操作。
I/O控制字可以用来识别命令类别,普通的读写操作通常是不能完全满足程序要求的,I/O控制字是和设备相关的。
下面是接口的原型:
BOOL XXX_IOControl( DWORD hOpenContext, DWORD dwCode, PBYTE
pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD
pdwActualOut );
参数解释:
hOpenContext
指向XXX_Open接口建立并且返回的设备上下文。
dwCode
指定驱动程序要操作的I/O操作的标识码。这是由设备特定的,一般在头文件中有I/O操作的标识码的定义。
pBufIn
指向存放要向设备传输的数据的缓冲区。
dwLenIn
指定要从pBufIn指向的缓冲区向设备读取写入多少字节的数据。
pBufOut
指向缓冲区,这个缓冲区将用来存放从设备中读出的数据,以字节为单位。
dwLenOut
指定要从设备读取多少字节的数据存入pBuffer指向的缓冲区中。
pdwActualOut
DWORD型指针,指向的内容反映了从设备中读取的实际字节数。
这个接口主要是传递了包括读写在内的I/O控制命令给设备。和Windows桌面平台类似,应用程序可以通过直接调用DeviceIOControl函数使设备管理器激活本接口。dwCode参数指定了命令的类型,这些命令类型是由驱动程序指定的,并且通过头文件的形式提供给应用程序。
如果注册表中有HKEY_LOCAL_MACHINE\Drivers\BuiltIn\YourDevice\Ioctl键,设备管理器在加载驱动的时候就会调用本接口,并且使用注册表中相应项的值作为dwCode的值,把NULL填写入pBufIn和pBufOut中。驱动程序可以在这个时候加载其他模块以及其他不适合在XXX_Init出现的功能,在设备交互的过程中,以上各个接口基本遵循如下的调用顺序:XXX_Init,XXX_Open,XXX_IOControl,XXX_Close。XXX_Open接口是要获得设备句柄所必须的操作,XXX_Close是释放资源所必须的操作。
5.Streams入口:PowerUp和PowerDown
(1)XXX_PowerDown入口
这个接口是在停止对设备供应电源时被调用。下面是接口的原型:
void XXX_PowerDown( DWORD hDeviceContext );
参数解释:
hDeviceContext
指向由XXX_Init返回的设备上下文。
这个函数应当执行停止设备供电的操作,此设备必须支持软关电的功能。在I/O control接口中,如果I/O命令字为IOCTL_POWER_XXX,那么就应该调用本接口。这个接口是对应与应用程序的,系统电源管理并不会用到这个接口。
设备管理器在将设备设置成节电模式之前将调用本接口,本接口将尽可能避免引起阻塞的操作,并尽快返回。
(2)XXX_PowerUp入口
恢复了设备的供电。
下面是接口的原型:
void XXX_PowerUp( DWORD hDeviceContext );
参数解释:
hDeviceContext
指向XXX_Open接口建立并且返回的设备上下文。
这个接口在需要恢复设备电源供应时被调用。在I/O control接口中,如果I/O命令字为IOCTL_POWER_XXX,那么就应该调用本接口。这个接口是对应与应用程序的,系统电源管理并不会用到这个接口。本接口应尽可能避免引起阻塞的操作,将尽快返回,并设置全局变量来表明电源已经被恢复,可以进行后续操作。
加载设备驱动所需要的接口设备管理器是如何加载驱动程序的呢?这里用到了一个重要的函数ActivateDeviceEx。
ActivateDeviceEx是指:
n 由设备管理器,也就是Device.exe来加载设备驱动;
n 注册表枚举时,用这个函数来读取启动时应当加载的驱动程序信息;
n ActivateDeviceEx将使用注册表中的Dll,Prefix,Index,Flags项。
ActivateDevice也能用来加载设备驱动,其实它内部也是调用了ActivateDevice
Ex。ActivateDeviceEx函数的主要功能就是用来加载驱动程序,它读取参数lpszDevKey中描述的注册表键来获取驱动程序的DLL名称、设备名前缀和其他相关信息(包括IOCTL等),这个函数可以用来替代ActivateDevice和RegisterDevice。
还以powerbutton为例,下面是注册表中它的相关内容。
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\PM]
"Prefix"="PWR"
"Dll"="pm.dll"
"Order"=dword:0
"Ioctl"=dword:4
"Index"=dword:0
"IClass"=multi_sz:"{A32942B7-920C-486b-B0E6-92A702A99B35}"
根据以上信息可以看出,在加载驱动程序时,调用ActivateDeviceEx函数的步骤为:
(1)ActivateDeviceEx(“\\HKEY_LOCAL_MACHINE\\Drivers\\BuiltIn\\PM”,…)
(2)ActivateDeviceEx函数将在\\HKEY_LOCAL_MACHINE\\Drivers\\BuiltIn\\PM键下寻找DLL的名称,在此也就是pm.dll,然后读取设备名前缀PWR以及index:0和其他信息。这里的index就是在前缀后面要添加上的序列号,如果是1,设备就是PWR1,它是用来妥善处理多重类似驱动设备在系统中被加载的情况。Index如果没有定义,设备管理器将为其指定一个空闲的索引号。
(3)ActivateDeviceEx将把设备添加到已激活设备列表中去。这时,如果没有定义index号,设备管理器必须指定一个空闲的索引号给设备,在添加完之后,设备管理器才将设备驱动加载到自己的进程上下文中。
下面是ActivateDeviceEx的函数原型:
HANDLE ActivateDeviceEx(
LPCWSTR lpszDevKey,
LPCVOID lpRegEnts,
DWORD cRegEnts,
LPVOID lpvParam
);
参数解释:
lpszDevKey
字符串指针,指向注册表中包含驱动信息的键,这个键应当包含驱动程序的DLL名、前缀、索引和入口点等信息。
lpRegEnts
指向结构体的数组,这个数组中定义了一些需要被添加到激活设备列表中的信息(ActivateDevice),这些信息填写后,驱动程序才被加载。如果是总线驱动的话,这里应该设置成NULL。
cRegEnts
lpRegEnts指向REGINI结构体的数组中元素的个数。
lpvParam
通过这个指针向已经加载的驱动程序传递参数,而不必将参数保留在注册表中,这个参数将以第2参数的角色被传递到XXX_Init(Device Manager)函数入口中。
返回值
如果返回具体设备的句柄,则代表操作成功,否则为失败。设备句柄可以在调DeactivateDevice函数的时候作为参数使用。
实现自己的流驱动程序由于在Windows CE中没有生成驱动程序框架的wizard,所以一切必须手动添加,能由wizard所生成的,只是一个dll工程而已。
本节将先建立一个通用的流驱动程序框架,实现应用程序和驱动程序的交互。在后续章节中,就可以在本节的基础上再添加具体传输控制和完全实现软件狗的驱动程序了。
其步骤如下。
n 选择Emulator platform——Internet Appliance建立一个新的平台,其他都选默认项;
n 新建一个dll文件作为驱动程序,命名为MyDriver;
n 在MyDriver.dll的项目目录下新建一个MyDriver.def文件,用于表示dll导出函数;
n 在Platform Builder中添加MyDriver.def文件;
n 新建一个WCE应用程序,命名为helo.exe;
n 在MyDriver.cpp文件中添加如下函数(具体见MyDriver.cpp文件);
n MDV_Init MDV_Deinit MDV_Open MDV_Close MDV_IOControl;
n MDV_PowerUp MDV_PowerDown MDV_Read MDV_Write MDV_Seek;
n 在驱动中除了对设备的读写,还有必要对设备进行控制,这样就需要额外的例程;
n 在应用程序和驱动程序中添加IO Control Function代码(具体见MyDriver.cpp文件);
n 实现应用程序调用devicecontrol函数时,系统会自动映射到MDV_IOControl例程;
n 此外还需要更改def文件中的内容,标识DLL导出的函数;
n 在helo.cpp中添加启动设备和调用流接口的函数(见helo.cpp文件):
n 在project.reg注册表文件中添加注册表项,将虚拟设备的Prefix和Index值写入HKEY_LOCAL_MACHINE\Drivers\BuiltIn键下,这样系统在启动时会自检注册表,从而自动激活(具体见project.reg);
n 编译平台;
n 单击“Build”菜单项->“Open release directory”菜单,弹出命令行界面。转到MyDriver.dll所在目录,键入命令:Dumpbin /exports mydriver.dll;
n 显示出如下信息,这其中包括了DLL文件的导出函数信息(图9-3所示);
图9-3 DLL文件的导出函数信息
n 下载系统kernel,选择“Target”命令->“Run programs”->helo.exe;
n 在Platform Builder的build区内就可以看到类似以下的调试信息。
通过以上步骤,一个流驱动程序的框架就构造完成了。
设备文件名如9.1.2节介绍,应用程序可以通过文件系统API来访问流驱动程序。在加载打开流设备驱动时,要用到设备标识,如COM1,PGR7等,文件系统ApI的调用被设备管理器重定向到流驱动入口点。
下面将介绍创建的文件格式、前缀和索引的内容。
1.设备文件的命名
如果要把设备当作文件来操作,那么有一点是必须要做的,就是文件名必须有特定的格式来标识文件是个设备,这种文件名应当由3个大写字母以及一位数字和冒号“:”组成,如串口可以被命名为“COM27:”、并口可以被命名“LPT1”。
2.设备文件的前缀
如果在注册表中Prefix子键没有被定义,那么流驱动程序的初始化和de-初始化入口点必须在注册表中定义,否则就应该在注册表中定义前缀。前缀由三个字符组成,它将出现在流驱动入口点的函数名称首部,这样就将设备文件名和特定的驱动入口点映射起来。
3.设备文件的索引
文件索引号是将同种设备的不同实例区分开来。索引是用跟在前缀之后的一位数字标示,默认情况下取值范围从0~9。如果需要有第10个设备实例,则用0作为索引值。
如果需要从非1的值开始记数设备的实例,那么应当在注册表中设置子键“Index”指定开始的索引值,这在某些情况下是有必要的,例如,如果系统Built-In驱动程序已经为某类设备占用了1-4的索引值,那么同种类的驱动再要被加载,索引值就应当从5开始。
注册表自举什么是注册表自举?它在系统中的作用是什么?
n 注册表自举是由设备管理器(Device.exe)在系统启动时加载的。
n 注册表自举时,通过读取注册表来寻找系统中的新设备。设置新设备的注册表信息应当是在安装驱动程序时,即InstallDriver入口中设置到注册表中去的。
n 实现为REGENUM.DLL动态链接库。
n 实现将代码存放在WINCE400\public\common\oak\DRIVERS\REGENUM目录下。
注
册表自举是设备驱动加载中的一部分。在系统启动时,初始化设备管理器完毕后,设备管理器将进入设备驱动加载阶段,在这个阶段的早期将进行注册表自举,这个
过程在注册表中不断寻找新的设备驱动(未加载,但是需要被加载的设备驱动)加载。注册表自举是可以重入的,通过软件函数调用可以重新进行注册表自举。
下面以一个例子来分析注册表自举的具体过程:
[HKEY_LOCAL_MACHINE\Drivers]
"RootKey"="Drivers\\BuiltIn"
"Dll"="RegEnum.dll“
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn]
"Dll"="RegEnum.dll"
[HKLM\Drivers\ BuiltIn \PCI]
"Dll"="PCIbus.dll"
"Order"=dword:4
"Flags"=dword:1
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]]
"Order"=dword:0
"Flags"=dword:0
"Dll"="RegEnum.dll"
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual \NDIS]
"Dll"="NDIS.dll"
"Order"=dword:1
"Prefix"="NDS"
1.在Device.exe进入注册表自举阶段,检查HKLM\Drivers\RootKey指向的子键。在这个例子中,RootKey的值被设置成Drivers\\BuiltIn,一般默认为Drivers。
2.在HKEY_LOCAL_MACHINE\Drivers\BuiltIn子键中检查DLL项的值,这里被设置成RegEnum.dll,也就是注册表自举模块,然后加载这个DLL。
3.将HKEY_LOCAL_MACHINE\Drivers\BuiltIn作为参数传递给RegEnum的Init函数。注意,Init函数不是一个流式接口,RegEnum只检查键下的一级子键中有无DLL需要加载,至于二级以下的子键,则必须递归调用RegEnum。
4.Init函数在HKLM\Drivers\RootKey键下基于“Order”值的顺序进行检查。逐个检查每个入口加载,为它们初始化驱动程序。在此,HKLM\Drivers\RootKey下有两个子键:[HKLM\Drivers\BuiltIn\PCI]和[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual],其中,[HKLM\Drivers\BuiltIn\PCI]的Order的值为4,而[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]]的Order值为0,因此RegEnum将优先检查[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]。
5.RegEnum进入[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]子键进行检查,发现此子键下的DLL项值为RegEnum.dll,则说明要递归使用RegEnum检查其下的子键。这里为[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual\NDIS],其Order项值为1,并且没有其他子键与其竞争加载顺序,所以它将被[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]下的RegEnum.dll加载。[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual \NDIS]下的DLL项值为NDIS.dll,因此NDIS驱动被加载。
6.完成[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]下一级子键的检查后,[HKEY_LOCAL_MACHINE\Drivers\BuiltIn]下的RegEnum.dll将检查[HKLM\Drivers\ BuiltIn \PCI]子键下的DLL,这里为PCIbus.dll,因此PCI驱动被加载。
通过以上步骤这个例子中的注册表自举完成。
在注册表自举的过程中,子键有表9-2中所示的各种形式:
表9-2 子键的形式
子 键 | 类 型 | 描 述 |
Order | DWORD | 最小的Order值将优先被加载 |
Dll | SZCHAR | 定义了要让注册表自举程序加载的驱动的DLL名,这个名称也将被写入ActiveDevice。只有DLL项是必须被设置的,如果Order项没有被设置,那么该驱动将在所有设置了Order项的驱动被加载后加载 |
Index | DWORD | 同类设备驱动的实例被加载时可以通过不同的索引号来区分。如COM0:COM1:,索引的取值范围从0到9,如果没有指定索引值,那么系统将会自动指定一个 |
Flags | DWORD | 描述注册表自举的默认操作。Flags的值是一系列位,他们将描述ActivateDevice的行为。如果不需要这个项的话,可以将其设置为0。位3~23为保留位,必须设置成0,注册表自举时可以使用位23~31来设置 |
表9-3是对Flag各个位功能的描述:
表9-3 Flag各个位功能
Flag | 值 | 功能描述 |
DEVFLAGS_NONE | 0x00000000 | 没有任何功能定义 |
DEVFLAGS_UNLOAD | 0x00000001 | 在调用驱动程序XXX_Init入口后,卸载驱动程序 |
DEVFLAGS_LOADLIBRARY | 0x00000002 | 使用LoadLibrary函数来加载驱动,而不是LoadDriver |
DEVFLAGS_NOLOAD | 0x00000004 | 不加载DLL |
注册表自举什么是注册表自举?它在系统中的作用是什么?
n 注册表自举是由设备管理器(Device.exe)在系统启动时加载的。
n 注册表自举时,通过读取注册表来寻找系统中的新设备。设置新设备的注册表信息应当是在安装驱动程序时,即InstallDriver入口中设置到注册表中去的。
n 实现为REGENUM.DLL动态链接库。
n 实现将代码存放在WINCE400\public\common\oak\DRIVERS\REGENUM目录下。
注
册表自举是设备驱动加载中的一部分。在系统启动时,初始化设备管理器完毕后,设备管理器将进入设备驱动加载阶段,在这个阶段的早期将进行注册表自举,这个
过程在注册表中不断寻找新的设备驱动(未加载,但是需要被加载的设备驱动)加载。注册表自举是可以重入的,通过软件函数调用可以重新进行注册表自举。
下面以一个例子来分析注册表自举的具体过程:
[HKEY_LOCAL_MACHINE\Drivers]
"RootKey"="Drivers\\BuiltIn"
"Dll"="RegEnum.dll“
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn]
"Dll"="RegEnum.dll"
[HKLM\Drivers\ BuiltIn \PCI]
"Dll"="PCIbus.dll"
"Order"=dword:4
"Flags"=dword:1
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]]
"Order"=dword:0
"Flags"=dword:0
"Dll"="RegEnum.dll"
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual \NDIS]
"Dll"="NDIS.dll"
"Order"=dword:1
"Prefix"="NDS"
1.在Device.exe进入注册表自举阶段,检查HKLM\Drivers\RootKey指向的子键。在这个例子中,RootKey的值被设置成Drivers\\BuiltIn,一般默认为Drivers。
2.在HKEY_LOCAL_MACHINE\Drivers\BuiltIn子键中检查DLL项的值,这里被设置成RegEnum.dll,也就是注册表自举模块,然后加载这个DLL。
3.将HKEY_LOCAL_MACHINE\Drivers\BuiltIn作为参数传递给RegEnum的Init函数。注意,Init函数不是一个流式接口,RegEnum只检查键下的一级子键中有无DLL需要加载,至于二级以下的子键,则必须递归调用RegEnum。
4.Init函数在HKLM\Drivers\RootKey键下基于“Order”值的顺序进行检查。逐个检查每个入口加载,为它们初始化驱动程序。在此,HKLM\Drivers\RootKey下有两个子键:[HKLM\Drivers\BuiltIn\PCI]和[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual],其中,[HKLM\Drivers\BuiltIn\PCI]的Order的值为4,而[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]]的Order值为0,因此RegEnum将优先检查[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]。
5.RegEnum进入[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]子键进行检查,发现此子键下的DLL项值为RegEnum.dll,则说明要递归使用RegEnum检查其下的子键。这里为[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual\NDIS],其Order项值为1,并且没有其他子键与其竞争加载顺序,所以它将被[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]下的RegEnum.dll加载。[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual \NDIS]下的DLL项值为NDIS.dll,因此NDIS驱动被加载。
6.完成[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Virtual]下一级子键的检查后,[HKEY_LOCAL_MACHINE\Drivers\BuiltIn]下的RegEnum.dll将检查[HKLM\Drivers\ BuiltIn \PCI]子键下的DLL,这里为PCIbus.dll,因此PCI驱动被加载。
通过以上步骤这个例子中的注册表自举完成。
在注册表自举的过程中,子键有表9-2中所示的各种形式:
表9-2 子键的形式
子 键 | 类 型 | 描 述 |
Order | DWORD | 最小的Order值将优先被加载 |
Dll | SZCHAR | 定义了要让注册表自举程序加载的驱动的DLL名,这个名称也将被写入ActiveDevice。只有DLL项是必须被设置的,如果Order项没有被设置,那么该驱动将在所有设置了Order项的驱动被加载后加载 |
Index | DWORD | 同类设备驱动的实例被加载时可以通过不同的索引号来区分。如COM0:COM1:,索引的取值范围从0到9,如果没有指定索引值,那么系统将会自动指定一个 |
Flags | DWORD | 描述注册表自举的默认操作。Flags的值是一系列位,他们将描述ActivateDevice的行为。如果不需要这个项的话,可以将其设置为0。位3~23为保留位,必须设置成0,注册表自举时可以使用位23~31来设置 |
表9-3是对Flag各个位功能的描述:
表9-3 Flag各个位功能
Flag | 值 | 功能描述 |
DEVFLAGS_NONE | 0x00000000 | 没有任何功能定义 |
DEVFLAGS_UNLOAD | 0x00000001 | 在调用驱动程序XXX_Init入口后,卸载驱动程序 |
DEVFLAGS_LOADLIBRARY | 0x00000002 | 使用LoadLibrary函数来加载驱动,而不是LoadDriver |
DEVFLAGS_NOLOAD | 0x00000004 | 不加载DLL |
服务与设备的比较本节将讨论有关Windows CE服务模块、服务模块和普通驱动程序的区别以及如何注册、激活/控制/停止一个服务。
Services.exe是一个和Device.exe有同等地位的进程,它的用途是为了弥补Device.exe的
不稳定。如果一个设备驱动由于某种原因处于异常状态或者不受设备管理器控制时,若让其继续存留在设备管理器中,则会影响其他正常运行的驱动程序,甚至使系
统崩溃,而使用服务这种机制能尽可能地避免这种情况发生,当系统中有类似服务操作失败的信号产生时,系统不会因此而受到影响。和驱动程序一样,服务也提供
了能控制其自身运行状态的函数接口集,如启动服务、暂停服务、其他I/O控制。
在Windows CE. NET中操作服务和操作设备驱动的方式十分类似。如果一个设备驱动实际上并不需要和硬件发生交互,也不需要进程间通信的时候,就应当将其实现为一个服务,以提高系统安全性。服务出错,不会引起系统的严重错误。
Services.exe同样可以被配置用来在特定的连接上等待信息。Socket端口接收到数据包时,Services.exe就会将数据分发到相应的服务中去。
有一点需要注意的是,Services.exe只是防止驱动程序因为服务失败而崩溃,但并不保证它不会引起其他服务的错误,因此服务应当认真调试。
在%WINCEROOT%\Public\Servers\Sdk\Samples\Telnetd目录下,Windows CE.NET提供了一个样例程序,里面包含了如何启动、加载和卸载一个服务,同时还包含了如何进行流接口服务的实现方式。
1.如何激活并且控制一个服务
所谓激活服务就是将服务加载到Service.exe的进程空间里来。可以使用以下方法来加载服务并且在加载后进行控制。
n 使用注册表中built-in键的功能,在系统启动时,通过注册表自举将服务加载;
n 使用ActivateService函数,通过程序来加载服务;
n 可以使用CreateFile函数来打开一个服务;
n 可以使用I/O control、ReadFile、WriteFile、SetFilePointer等函数来操作一个服务;
n 还可以使用GetServiceHandle来获得服务的句柄。
当用程序来加载一个服务时,代码加载的格式为:
ActivateService(L"TELNETD", 0);
和设备管理器能控制一个正在运行的设备驱动一样,Service.exe也能通过多种途径来控制一个正在运行的服务。
以下是实现控制服务的具体步骤。
(1)使用CreateFile函数,在参数中设置适当的前缀和索引值。这些值应当在注册服务的时候被写入注册表;
(2)向服务发送I/O控制字。利用DeviceIoControl函数,设备管理器重定向到该服务的XXX_IOControl入口。下面的代码段显示了如何获取一个服务的状态:
HANDLE hService = CreateFile(L"TEL0:",0,0,NULL,OPEN_EXISTING,0,NULL);
if(hService != INVALID_HANDLE_VALUE) {
DWORD dwState;
DeviceIoControl(hService, IOCTL_SERVICE_STATUS, NULL, 0,
&dwState, sizeof(DWORD), NULL, NULL);
CloseHandle(hService);
};
(3)如果服务还支持流式接口的话,那么可以通过额外的操作来控制一个服务。如ReadFile、WriteFile、SetFilePointer函数。
还可以使用GetServiceHandle 函数来控制一个服务的状态。需要注意的是,在调用流式接口之前,必须获取服务的有效句柄,否则这些操作不会成功。可以通过GetServiceHandle函数获取服务的有效句柄。
下面是使用GetServiceHandle的一个例子:
HANDLE hService = GetServiceHandle("TEL0:", NULL, NULL);
if(hService != INVALID_HANDLE_VALUE) {
DWORD dwState;
ServiceIoControl(hService, IOCTL_SERVICE_STATUS, NULL, 0,
&dwState, sizeof(DWORD), NULL, NULL);
}
2.Windows CE服务的目的
n 作为设备管理器的补充
n 处理不需要直接和系统有交互的请求
n 增强驱动的可靠性,降低系统崩溃的可能
n 提供超级服务
Services.exe是作为对Device.exe模块的补充,着重加强了其加载系统服务的可靠性。它将那些和系统内核交互不是很多或者不用直接和内核交互的请求与系统服务分割开来独立处理,这样就降低了内核的处理负担。
当有一个服务发生故障时,及时地将这个服务和系统服务分割开来是保障系统安全运行的重要手段之一。这个特性使得Windows CE中的驱动程序在系统服务发生故障的时候,仍然有较高的安全运行能力,降低系统崩溃风险。例如,很多嵌入式设备在运行之后可能无人监管,因此系统安全性要求比较高,不能象桌面Windows系统那样经常因为设备驱动的错误而崩溃。
所谓的超级服务是指一个所有端口都允许被Services.exe监
控的标准服务,即一个服务能监视所有其他的服务。使用超级服务有一个好处就是,即便许多的服务在使用,只要它们是被超级服务监视的,那么这些服务的处理线
程都只由超级服务的处理线程承担,即所有服务最终只有一个处理线程。如果不使用超级服务,那么每个服务都要创建一个接受线程。
Services.exe模块是通过注册表加载的。代码如下所示:
[HKEY_LOCAL_MACHINE\init]
"Launch60"="services.exe"
"Depend60"=hex:14,00
3.服务与设备的区别
n Device.exe加载驱动用来管理设备
n Sevice.exe加载驱动用来管理软件服务
n Services.exe象Device.exe一样可以加载多个服务
n 如果要同时使用Device.exe和Services.exe, Windows CE中32个进程槽就要被占用两个
Services.exe和Device.exe模块被设计用来互相补充。Device.exe即设备管理器用来加载驱动程序,以此来管理外围设备,Services.exe将加载那些不和设备打交道的服务。
Services.exe可以管理多个服务,正如设备管理器可以管理多个设备驱动那样。 Services.exe负责将和系统无关的服务的处理从设备管理器中分割出来。当然,设备管理器可以在Services.exe没有被加载的环境下运行,这样可以节省一个进程槽。但是,不加载Service.exe的后果就是驱动程序运行的安全性下降。
4.如何注册一个服务
n 使用RegisterService函数注册
n RegisterService函数功能和RegisterDevice函数功能类似,后者目前逐渐被ActivateDeviceEx所替代。
Services.exe 也可以通过编程来启动。代码如下所示:
HANDLE hService = RegisterService("TEL",0,"telnetd.dll",0);
这行代码启动了telnet server的服务例程。如果telnet server当前正在TEL0设备上运行,那么这个函数将返回false。如果调用GetlastError函数,将得到ERROR_DEVICE_IN_USE的错误信息。
RegisterService调用后,设备管理器将调用服务的xxx_Init入口,在调用完xxx_Init接口后,服务处于什么样的状态是依据服务各自不同dwContext设置而定的。在Services.h中,为这个参数预设置了以下值:
#define SERVICE_INIT_STARTED 0x00000000
#define SERVICE_INIT_STOPPED 0x00000001
如果dwContext参数中没有设置SERVICE_INIT_STOPPED值,那么服务在被注册后将建立一个线程,并且启动它作为服务的请求接收处理线程。
如果设置了SERVICE_INIT_STOPPED标志,那么代表本服务将接收超级服务的监督管理,自己将不拥有接收线程,而统一有超级服务进行处理。
下面是RegisterService函数的原型:
HANDLE RegisterService(
LPCWSTR lpszType,
DWORD dwIndex,
LPCWSTR lpszLib,
DWORD dwInfo
);
参数解释:
lpszType
指向一个字符串,这个字符串包含了服务的前缀名(由三个字母组成,和流驱动类似)
dwIndex
定义了0~9之间的服务的索引号。
lpszLib
指向一个字符串,该字符串包含了代表服务的DLL文件名。
dwInfo
这是代表在调用服务的
入口时,传入的参数。
返回值
如果返回值是被注册的服务的句柄,则代表操作成功。如果返回空值则代表操作失败。
RegisterService可以将一个未加载的服务加载到系统中,同样也可以为一个系统中已经存在的服务加载第2个实例。
RegisterService将先加载在参数lpszLib中定义的DLL链接库,然后检查该DLL链接库是否导出了、、接口。如果DLL链接库名出错,将导致系统寻找不到相应的文件,那么函数将返回ERROR_FILE_NOT_FOUND错误;如果DLL链接库中导出的接口集不正确的话,函数将返回ERROR_FILE_NOT_FOUND错误。
如果系统中已经有同类的服务在运行,即前缀名一致,那么就需要用索引值来区分实例。如果索引值也冲突的话,函数将返回ERROR_DEVICE_IN_USE错误。
在成功加载DLL文件后,Services.exe将调用xxx_Init入口,并且将dwInfo中的参数传递给它。如果init是成功的话,函数将xxx_Init的返回值保留,这将是后面xxx_IOControl、xxx_Deinit入口调用时的重要参数。
5.如何停止一个正在运行的服务
使用DeregisterService函数来停止并且卸载一个服务。DeregisterService将在系统中定位一个服务,并且将它标记为无效;然后DeregisterService将阻止任何企图打开该服务的函数。
下面的代码是实现关闭一个服务的例子:
HANDLE hServide = GetServiceHandle(L"TEL0: ",NULL,NULL);
if(hSerice != NULL)
DeregisterService(hService);
DeregisterService 需要使用从RegisterService返回的服务句柄,而不是使用CreateFile 返回的句柄。因此应用程序无法通过文件系统的流式API来获取服务的句柄,只能使用GetServiceHandle。
6.服务中所使用的API
Services.exe实现了如下的接口。
n XXX_Close
服务实现这个接口,并且由Services.exe 调用。当应用程序调用CloseHandle或者CloseService来关闭一个服务时,Services.exe会将操作重映射到本接口;
n XXX_Deinit
服务实现这个接口,并且由Services.exe 调用。当应用程序调用DeregisterService来取消注册一个服务时,Services.exe会将操作重映射到本接口;
n XXX_Init
初始化服务的接口;
n XXX_IOControl
向服务传递I/O控制字;
n XXX_Read
服务实现这个接口,并且由Services.exe调用。这个接口提供了类似读文件的功能,只能被流式的服务所实现;
n XXX_Seek
服务实现这个接口,并且由Services.exe调用。这个接口提供了类似文件定位的功能,只能被流式的服务所实现。
n XXX_Write
服务实现这个接口,并且由Services.exe调用。这个接口提供了类似写文件的功能,只能被流式的服务所实现。