一:寄存器的使用:
1、wince内部对物理地址的访问:
可以有3个途径。
1:直接使用g_oalAddressTable(oemaddrtab_cfg.inc)的已经定义好的,物理地址对应的虚拟地址。
如:
DCD 0x80000000, 0x30000000, 128 ;
访问虚拟地址0x80000000实际就是访问物理地址0x30000000。
2:在OAL层,使用OALPAtoVA函数。
如:
volatile S3C2410X_IOPORT_REG *pIOCTR;
pIOCTR = (volatile S3C2410X_IOPORT_REG *)OALPAtoVA(S3C2410X_BASE_REG_PA_IOPORT, FALSE);
那么在访问pIOCTR指向的首地址,实际就是访问被映射后S3C2410X_BASE_REG_PA_IOPORT定义的物理地址。
VOID* OALPAtoVA(
UINT32 pa, 参数1:需要映射的物理地址
BOOL cached 参数2:是否使用cache(驱动中要使用uncached)
)
3:在kernel里,使用MmMapIoSpace函数。
如:
pBaseAddress = (PUCHAR)MmMapIoSpace(ioPhysicalBase, Size, FALSE);
同上,访问pBaseAddress的指向地址,就为访问被映射后ioPhysicalBase定义的物理地址。
PVOID MmMapIoSpace(
PHYSICAL_ADDRESS PhysicalAddress, 参数1:需要映射的物理地址
ULONG NumberOfBytes, 参数2:映射的地址长度
BOOLEAN CacheEnable 参数3:是否使用cache(驱动中要使用uncached)
);
与OALPAtoVA不同,在使用MmMapIoSpace后,必须使用MmUnmapIoSpace。
VOID MmUnmapIoSpace(
PVOID BaseAddress, 参数1:被映射后的虚拟地址
ULONG NumberOfBytes 参数2:映射的地址长度
);
在一般的NK驱动编写中,为了规范编程风格,请勿直接使用g_oalAddressTable中的虚拟地址。统一使用MmMapIoSpace、MmUnmapIoSpace函数。
2、wince标准的寄存器访问
定义一个结构体。此结构包含某功能模块的寄存器地址。
如:
typedef struct {
UINT32 GPACON; // Port A - offset 0
UINT32 GPADAT; // Data
UINT32 PAD1[2];
……
……
UINT32 GSTATUS0; // external pin status
UINT32 GSTATUS1; // chip ID
UINT32 GSTATUS2; // reset status
UINT32 GSTATUS3; // inform register
UINT32 GSTATUS4; // inform register
} S3C2410X_IOPORT_REG, *PS3C2410X_IOPORT_REG;
volatile S3C2410X_IOPORT_REG *pIOCTR;
pIOCTR = (volatile S3C2410X_IOPORT_REG *)OALPAtoVA(S3C2410X_BASE_REG_PA_IOPORT, FALSE);
这样,访问pIOCTR的各个成员,就为访问被映射后S3C2410X_BASE_REG_PA_IOPORT定义的物理偏移地址。
为了统一、兼容平台的驱动代码,我们加入了寄存器操作宏(oal_io.h):
#define REG8(_register_) (*(volatile unsigned char *)(_register_))
#define REG16(_register_) (*(volatile unsigned short *)(_register_))
#define REG32(_register_) (*(volatile unsigned long *)(_register_))
访问模块中的功能寄存器,使用加上偏移地址的方式。
#define USB_REG_FADDR_OFFSET (0x0000)
#define USB_REG_POWER_OFFSET (0x0001)
例子:
要编写某个模块的驱动,首先使用MmMapIoSpace或OALPAtoVA建立物理地址映射关系。
volatile BYTE *pUSBCtrlAddr;
pUSBCtrlAddr= (volatile BYTE *)OALPAtoVA(AK3224_BASE_REG_PA_USB, FALSE);
这样,usb模块的首地址就为pUSBCtrlAddr的指向地址。然后,使用REGXX的宏来访问各个功能寄存器。
REG8(pUSBCtrlAddr + USB_REG_POWER_OFFSET) = USB_POWER_ENSUSPEND;
ucIntStatusR = REG16(pUSBCtrlAddr + USB_REG_INTRRX1_OFFSET);
REG32(pUSBCtrlAddr + USB_DMA_COUNT_1_OFFSET) = 256;
在驱动编写中,请统一使用REGXX的宏操作。
二:DMA的使用
1、 芯片DMA的使用要点:
AK3224芯片的DMA使用中,RAM的地址作为DMA传输的目标地址、源地址,必须要4字节对齐。而且DMA的操作长度以内的RAM地址,必须连续。
不过在使用中发现:Nandflash驱动中RAM地址作为目标地址时,只需要2字节对齐。RAM地址作为源地址可不需对齐。(其他情况需要逐一验证)
2、 wince中的DMA使用:
根据DMA一次操作的RAM地址必须连续的特性,在驱动DMA使用时,我们需要确保虚拟地址映射的物理地址是连续的。有3个途径:
1:数据区地址是由应用层或者其他进程、线程传入的,驱动并不知道其虚拟地址对应的物理地址是否一直连续。
由于wince的内存申请,是以4K字节为一个页,一段数据的内存申请可能跨越多个页。因此,只要数据区长度大于1字节,就有可能其物理地址是跨越的、不连续的。为了确保DMA操作,我们必须查询这段数据区在RAM上的物理分布。
首先,得到数据区所在的虚拟页
VirPageStart = (ULONG)pSourceBuffer & 0xFFFFF000;
其次,得到数据区在页内的偏移地址
offset = (ULONG)pSourceBuffer & 0x0FFF;
计算数据区是否跨越页段
if(offset + NumberOfBytes > 4096)
PageSize = WCE_UNIFORM_SIZE - offset; //整个数据跨越此页,则DMA传输需要分多个部分,一次一个页段的传
else
PageSize = NumberOfBytes; //数据区没有跨越页
由得到的页地址,查询映射的物理地址。
if(!LockPages((LPVOID)VirPageStart, 4096, &TransAddr, LOCKFLAG_READ))
{
//异常处理
}
UnlockPages((LPVOID)VirPageStart, 4096);
得到了映射的物理地址TransAddr后,根据RAM是目标地址还是源地址,做进一步的处理。
假设一个数据区作为DMA源地址,大小为9K。在虚拟地址首页的偏移为4K。那么它必然跨越3个页段。
如图,我们的DMA操作也要分解3次。
首先查询第一页的物理地址发送,第一个页的2K数据。然后查询第二页的物理地址,发送4K数据。最后查询第三页的物理地址,发送3K数据。
2:数据区的申请可以使用AllocPhysMem函数申请。
LPVOID AllocPhysMem(
DWORD cbSize, 参数1:数据区大小
DWORD fdwProtect, 参数2:保护标记
DWORD dwAlignmentMask, 参数3:0(default system)
DWORD dwFlags, 参数4:0(Reserved for future use)
PULONG pPhysicalAddress 参数5:得到数据区对应的物理地址
);
AllocPhysMem函数返回值为指向申请后的虚拟地址指针。
如:
pSerialHead->RxBufferInfo.RxCharBuffer = //alloc physical memory
AllocPhysMem(pSerialHead->RxBufferInfo.Length + 16, PAGE_READWRITE, 0, 0, &RX_PhyAddr);
由于此函数必定申请到一片连续的物理地址,因此pSerialHead->RxBufferInfo.RxCharBuffer的使用不再需要查询是否跨越多个页段。
但是,AllocPhysMem函数申请的物理地址可能会跨越多个RAM CHIP。因此,在使用1片以上RAM芯片的系统中,依然需要查询是否跨越CHIP。
AllocPhysMem函数使用后,需要使用FreePhysMem函数进行释放。
3:数据区可以在系统config.bib文件中,预先定义好一片连续、不跨越CHIP的RAM空间。
如下,系统保留了虚拟地址0x80024000开始,大小为0x3000的一段RAM。
SER_DMA 80024000 00003000 RESERVED
那么驱动DMA使用中,不再需要对这段内存,进行任何的查询动作。我们只需要在进程空间中做映射即可。
pSerialHead->RxBufferInfo.RxCharBuffer = VirtualAlloc(0, RX_PhySize,
MEM_RESERVE, PAGE_NOACCESS);
if (pSerialHead->RxBufferInfo.RxCharBuffer == NULL)
{
DEBUGMSG(ZONE_ERROR, (TEXT("COM_Init:: VirtualAlloc failed!/r/n")));
return(NULL);
}
else
{
if (!VirtualCopy((PVOID)pSerialHead->RxBufferInfo.RxCharBuffer, (PVOID)(RX_PhyAddr),
RX_PhySize, (PAGE_READWRITE | PAGE_NOCACHE)))
{
DEBUGMSG(ZONE_ERROR, (TEXT("COM_Init:: VirtualCopy failed!/r/n")));
return(NULL);
}
}
上面这段程序中,先使用函数VirtualAlloc,在进程空间中申请一段保留的虚拟地址空间。然后使用VirtualCopy,把需要使用的物理地址空间,映射到已经申请好的虚拟地址上。使用完毕,必须使用函数VirtualFree进行释放。
LPVOID VirtualAlloc(
LPVOID lpAddress,
DWORD dwSize,
DWORD flAllocationType,
DWORD flProtect
);
BOOL VirtualCopy(
LPVOID lpvDest,
LPVOID lpvSrc,
DWORD cbSize,
DWORD fdwProtect
);
BOOL VirtualFree(
LPVOID lpAddress,
DWORD dwSize,
DWORD dwFreeType
);
详细使用请查阅MSDN
三、中断的使用
1、wince中断简介
1: ISR的概念
ISR(interrupt service routine)是处理IRQs(interrupt request line)的程序。Windows CE用一个ISR来处理所有的IRQ请求。当一个中断发生时,内核的异常处理程序先调用内核ISR,内核ISR禁用所有具有相同优先级和较低优先级的中断,然后调用已经注册的OAL ISR程序,一般ISR有下列特征:
1) 执行最小的中断处理,最小的中断处理指能够检验、答复产生中断的硬件,而把更多的处理工作留给IST(interrupt service thread)。
2) 当ISR完成时返回中断ID(中断ID大部分是预定义的)。
2:中断注册步骤
1) 用SETUP_INTERRUPT_MAP宏关联SYSINTR和IRQ。以“SYSINTR_”为前缀的常量由内核使用,用于唯一标识发生中断的硬件。在Nkintr.h文件中预定义了一些SYSINTR,OEM可以在Oalintr.h文件中自定义SYSINTR。
2) 用HookInterrupt函数关联硬件中断号和ISR。这里的硬件中断号为物理中断号,而非逻辑中断号IRQ。
2、 驱动中IST使用
ISR是中断最小处理函数,因此各个驱动的中断处理函数称为IST。
系统中保留16个虚拟中断号,ak3224_intr.h已经定义好各个ISR的虚拟中断号28个。因此,驱动中的中断处理函数,只要与定义好的28个虚拟中断映射上即可。
例子:
首先,我们创建一个事件
pGPIOInfo->hGPIOEvent1 = CreateEvent(0,FALSE,FALSE,NULL);
其次创建一个处理事件的线程(IST)
pGPIOInfo->hGPIOThread1 = CreateThread(NULL, 0, GPIOFuncThread1,
pGPIOInfo, 0, NULL);
然后使用InterruptInitialize让虚拟中断号pGPIOInfo->dwIntID1与创建的事件pGPIOInfo->hGPIOEvent1挂钩。
InterruptInitialize(pGPIOInfo->dwIntID1, pGPIOInfo->hGPIOEvent1, NULL, 0)
那么,当GPIO的中断到来,与GPIO虚拟中断挂钩的事件pGPIOInfo->hGPIOEvent1就会被设为Active。线程pGPIOInfo->hGPIOThread1的语句
WaitForSingleObject(pGPIOInfo->hGPIOEvent1, INFINITE);
GPIOEventHandler1;
就会被唤醒,然后执行下一条指令。这里加入的函数GPIOEventHandler1(中断处理操作)就被执行。
当中断处理结束以后,必须使用
InterruptDone(pGPIOInfo->dwIntID1);
通知系统已经完成中断处理,那么下一次的中断到来,事件pGPIOInfo->hGPIOEvent1就才会再次被设为Active。
驱动卸载时,需要释放申请的事件及线程
CloseHandle(pGPIOInfo->hGPIOEvent1);
CloseHandle(pGPIOInfo->hGPIOThread1);
3、可安装ISR介绍
OEM在OEMInit函数中关联IRQ和SysIntr,当硬件设备发生中断时,ISR会禁止同级和低级中断,然后根据IRQ返回关联的SysIntr,内核根据ISR返回的SysIntr唤醒相应的IST(SysIntr与IST创建的Event关联),IST处理中断之后调用InterruptDone解除中断禁止。在OEMInit中关联的缺点是一旦编译了CE内核后就无法添加这种关联了,而一些硬件设备会随时插拔或者共享中断,要关联这样的硬件设备解决方法就是可安装ISR,可安装ISR专用于处理指定的硬件设备发出的中断,所以如果硬件设备需要可安装ISR必须在注册表中添加IsrDll、IsrHandler。多数硬件设备采用CE默认的可安装ISR giisr.dll,格式如下:
"IsrDll"="giisr.dll"
"IsrHandler"="ISRHandler"
如果一个硬件驱动程序需要可安装ISR而开发者又不想自己写一个,那么可以利用giisr.dll来实现。除了在注册表中添加如上所示外,还要在驱动程序中调用相关函数注册可安装ISR。如下:
g_IsrHandle = LoadIntChainHandler(IsrDll, IsrHandler, (BYTE)Irq);
GIISR_INFO Info;
Info.SysIntr = dwSysIntr;
Info.CheckPort = TRUE;
Info.PortIsIO = (dwIOSpace) ? TRUE : FALSE;
Info.UseMaskReg = TRUE;
Info.PortAddr = PhysAddr + 0x0C;
Info.PortSize = sizeof(DWORD);
Info.MaskAddr = PhysAddr + 0x10;
KernelLibIoControl(g_IsrHandle, IOCTL_GIISR_INFO, &Info, sizeof(Info), NULL, 0, NULL);
LoadIntChainHandler函数负责注册可安装ISR,参数1为DLL名称,参数2为ISR函数名称,参数3为IRQ。
如果要利用giisr.dll作为可安装ISR,必须先填充GIISR_INFO结构体,CheckPort=TRUE表示giisr要检测指定的寄存器来确定当前发出中断的是否是这个设备。
PortIsIO表示寄存器地址属于哪个地址空间,FALSE表示是内定空间,TRUE表示IO空间。
UseMaskReg=TRUE表示设备有一个掩码寄存器,专用于指定当前设备是否是中断源,也就是发出中断
而MaskAddr表示掩码寄存器的地址。
如果对Info.Mask赋值,那么PortAddr表示一个特殊的寄存器地址,这个寄存器的值与Mask的值&运算的结果如果为真,则证明当前设备是中断源,否则返回SYSINTR_CHAIN(表示当前ISR没有处理中断,内核将调用ISR链中下一个ISR),如果UseMaskReg=TRUE,那么MaskReg寄存器的值与PortAddr指定的寄存器的值&运算的结果如果为真,则证明当前设备是中断源。
可见,可安装ISR对与再次分解的中断处理是非常方便的。不过需要占用系统的一个虚拟中断号。而整个系统的虚拟中断号只有64个。
四、驱动的编写
1、 wince驱动介绍
1:WinCE毕竟是一个嵌入式系统,有其自身的特殊性,为了提高运行效率,所有驱动皆为动态链接库,驱动实现中可以调用所有标准的API。而在其他Windows系统中可能的驱动文件还有.vxd, .sys和动态链接库。
2:WinCE驱动从结构上讲分为本地驱动(Native Driver)和流接口驱动(Stream Driver)。
——本地驱动主要用于低级、内置的设备。实现它们的接口并不统一,而是针对不同类型的设备相应设计。因此开发过程相对复杂,没有固定的模式,一般做法是通过移植、定制现有的驱动样例来实现。
——流接口驱动是最基本的一种驱动结构,它的接口是一组固定的流接口函数,具有很高的通用性,WinCE的所有驱动程序都可以通过这种方式来实现。流接口驱动程序通过文件系统调用从设备管理器和应用程序接收命令。该驱动程序封装了将这些命令转换为它所控制的设备上的适当操作所需的全部信息。
流接口驱动是动态链接库,由一个叫做设备管理程序的特殊应用程序加载、管理和卸载。与本地驱动程序相比,所有流接口驱动程序使用同一组接口函数集,包括实现函数:XXX_Init、XXX_Deinit、XXX_Open、XXX_Close、XXX_Read、XXX_Write、XXX_PowerUp、XXX_PowerDown、XXX_Seek、XXX_IOControl,这些函数与硬件打交道。
用户函数:CreateFile、DeviceIoControl、 ReadFile、 WriteFile,这些函数方便用户使用驱动程序。
3:WinCE下驱动的加载方式:
——通过GWES(Graphics, Windowing, and Events
Subsystem):主要加载与显示和输入有关的驱动,如鼠标、键盘驱动等。这些驱动一般为本地驱动。
——通过设备管理器:两种结构的驱动都加载,加载的本地驱动主要由PCMCIA Host Controller,USB Host Controller driver,主要是总线类的驱动;流接口驱动主要有音频驱动,串并口驱动。
——动态加载:前两者都是系统启动时加载的,动态加载则允许设备挂载上系统时将驱动调入内核,主要有外接板卡驱动,USB设备驱动等。
2、流接口驱动函数介绍:
1:DWORD XXX_Init(LPCTSTR pContext, LPCVOID lpvBusContext);
pContext:指向一个字符串,包含注册表中该流接口活动键值的路径
lpvBusContext:
该函数是驱动挂载后第一个被执行的。主要负责完成对设备的初始化操作和驱动的安全性检查。由ActiveDeviceEx通过设备管理器调用。其返回值一般是一个数据结构指针,作为函数参数传递给其他流接口函数。
2:BOOL XXX_Deinit(DWORD hDeviceContext);
hDeviceContext:XXX_Init的返回值。
整个驱动中最后执行。用来停止和卸载设备。由DeactivateDevice触发设备管理器调用。成功返回TRUE。
3:DWORD XXX_Open(DWORD hDeviceContext, DWORD AccessCode , DWORD ShareMode);
hDeviceContext:XXX_Init的返回值。
AccessCode:访问模式标志,读、写或其他。
ShareMode:驱动的共享方式标志。
打开设备,为后面的操作初始化数据就够,准备相应的资源。应用程序通过CreateFile函数间接调用之。返回一个结构指针,用于区分哪个应用程序调用了驱动,这个值还作为参数传递给其他接口函数XXX_Read、XXX_Write、XXX_Seek、XXX_IOControl。
4:BOOL XXX_Close(DWORD hOpenContext);
hOpenContext:XXX_Open返回值。
关闭设备,释放资源。由CloseHandle函数间接调用。
5:DWORD XXX_Read(DWORD hOpenContext, LPVOID pBuffer, DWORD Count);
hOpenContext:XXX_Open返回值。
pBuffer:缓冲区指针,接收数据。
Count:缓冲区长度。
由ReadFile函数间接调用,用来读取设备上的数据。返回读取的实际数据字节数。
6:DWORD XXX_Write(DWORD hOpenContext, LPCVOID pBuffer, DWORD Count);
hOpenContext:XXX_Open返回值。
pBuffer:缓冲区指针,接收数据。
Count:缓冲区长度。
由WriteFile函数间接调用,把数据写到设备上,返回实际写入的数据数。
7:BOOL XXX_IOControl(DWORD hOpenContext, DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut);
hOpenContext:XXX_Open返回值。
dwCode:控制命令字。
pdwActualOut:实际输出数据长度。
用于向设备发送命令,应用程序通过DeviceIoControl调用来实现该功能。要调用这个接口还需要在应用层和驱动之间建立一套相同的命令,通过宏定义CTL_CODE(DeviceType, Function, Method, Access)来实现。如:
#define IOCTL_INIT_PORTS /
CTL_CODE(FILE_DEVICE_UNKNOWN,0X801,METHOD_BUFFERED,FILE_ANY_ACCESS)
8:void XXX_PowerDown(DWORD hDeviceContext);
hDeviceContext:XXX_Init的返回值。
负责设备的上电控制。
9:void XXX_PowerUp(DWORD hDeviceContext);
hDeviceContext:XXX_Init的返回值。
负责设备的断电控制
10: DWORD IOC_Seek(DWORD hOpenContext, long Amount, WORD Type)
hOpenContext:XXX_Open返回值。
Amount:指针的偏移量。
Type:指针的偏移方式。
将设备的数据指针指向特定的位置,应用程序通过SetFilePointer函数间接调用。不是所有设备的属性上都支持这项功能。
3、流接口驱动的加载和注册表设置:
系统启动时启动设备管理程序,设备管理程序读取HKEY_LOCAL_MACHINE/Drivers/BuiltIn键的内容并加载已列出的流接口驱动程序。因此注册表对于驱动的加载有着关键作用。下面是一个例子:
【HKEY_LOCAL_MACHINE/Drivers/BuiltI/IOControler】
"Prefix"="XXX"
"Dll"="drivername.dll"
"Order"=dword:0
"DeviceArrayIndex"=dword:0
"Prefix"="XXX"中的XXX要和XXX_Init等函数中的一样。CreateFile创建的驱动名前缀也必须和它们一致。
Dll是列出需要加载的驱动名称
注册表存在多个设备,如串口有3个,Order表明加载串口N的顺序
DeviceArrayIndex是应用使用CreatFile时打开设备的名字,如0为“COM1“,1为”COM2“…
注册表还可以加入许多驱动需要的信息,驱动中可以使用RegQueryValueEx读取。
4、程序的编写、编译及其相关目录、配置文件的格式和修改:
1:先必须在PB相应平台的的driver目录下建立要创建的驱动所在的目录。如在x: /WINCE500/PLATFORM/AK3224/SRC/DRIVERS/目录下建立一个GPIO
2:改Drivers目录下的dirs文件。
3:建驱动源文件XXX.c,在该文件中实现上述流接口函数。并且加入DLL入口函数:
BOOL DllEntry(HINSTANCE hinstDll, /*@parm Instance pointer. */
DWORD dwReason, /*@parm Reason routine is called. */
LPVOID lpReserved /*@parm system parameter. */
)
4:建Makefile和Sources和.def文件,控制编译。
5:用CEC Editor修改cec文件,编译添加的新特性。
6:修改注册表文件platform.reg和platform.bib文件。
7:Build & Make Image。
注:驱动也可以在PB中建立project来创建Dll
5、WINCE的内存配置
WINCE的内存(包括SDRAM及FLASH)的配置包含两个方面:源代码(包括C和汇编)中的定义,及系统配置文件CONFIG.BIB中的定义。源代码中需要定义内存的物理及虚拟地址,大小,并初始化名为OEMAddressTable的结构数组,以告知系统物理地址与虚拟地址的对应关系,系统根据其设置生成MMU页表。而CONFIG.BIB中一般会将内存定义成不同的段,各段用作不同的用途。
CONFIG.BIB文件:
CONFIG.BIB文件分两个部分,我们且称之为段,MEMORY段和CONFIG段。MEMORY段定义内存的分片方法,CONFIG段定义系统其它的一些属性。以下是一个CONFIG.BIB文件MEMORY段的例子:
MEMORY
; 名称 起始地址 大小 属性
RESERVED 80000000 00008000 RESERVED
DRV_GLB 80008000 00001000 RESERVED
CS8900 80010000 00030000 RESERVED
EDBG 80040000 00080000 RESERVED
NK 800C0000 00740000 RAMIMAGE
RAM 81000000 00800000 RAM
名称原则上可以取任意字符串,ROMIMAGE通过一个内存片的属性来判断它的用途。RESERVE属性表明该片内存是BSP自己使用的,系统不必关心其用途;RAMIMAGE说明它是一片存放OS IMAGE的内存;而RAM则表示些片内存为RAM,系统可以在其中分配空间,运行程序。
但存放ROM的这片内存的名称,即NK一般不要改动。因为BIB文件中定义将一个文件加入到哪个ROM片(WINCE支持将ROM IMAGE存放在不连续的几个内存片中)中时会用到这个名称,如下现这行BIB文件项就定义将touch.dll放在名称为NK这片ROM中,
touch.dll $(_FLATRELEASEDIR)/touch.dll NK SH
因而,如果将NK改为其它名称,则系统中所有的BIB文件中的这个NK串都需要改动。
注意:保证各片内存不要重叠;而且中间不要留空洞,以节约内存。
两种设备如果不能同时被加载,就应该只为其保留一片从而节约内存,例如,本例中的CS8950是为网卡驱动程序保留的,EDBG是为网卡作调试(KITL)用时保留的,而系统设计成这两个程序不会同时加载(CS8950在启动时判断如果EDBG在运行就会自动退出),这样为这两个驱动程序各保留一片内存实在浪费而且也没有必要。
RAM片必须在物理上是连续的,如果系统的物理内存被分成了几片,则在RAM片只能声明一片,其它的内存在启动阶段由OEMGetExtensionDRAM报告给系统,如果有多于一个的内存片,应该用OEMEnumExtensionDRAM报告。NK片则没有此限制,只是NK跨越两个以上物理内存片时,系统启动时会显示这个OS包跨越了多个物理内存片,认为是个错误,但并不影响系统的执行与稳定性,因为系统启动之时便会打开MMU而使用虚拟地址,从而看到连续的内存空间。当然,如果内核自己都被放在了两个内存片上,那系统应该就无法启动了。而其它保留起来的内存片是一般是给驱动程序DMA用,应该保证它们在物理上的连续性,因为DMA是直接用物理地址的。
CONFIG段中以下几个需要格外注意:
ROMSTART,它定义ROM的起始位置,应该和NK片的起始位置相同。
ROMSIZE,定义ROM的大小,应该和NK片的大小相同。
如果不需要NK。BIN文件,则可以不设这两个值。
ROMWIDTH,它只是定义ROMIMAG生成ROM包时如何组织文件,而非其字面含义:ROM的宽度,所以一般都应该为32
COMPRESSION,一般定义为ON,以打开压缩功能,从而减小BIN文件的尺寸。
AUTOSIZE,一般应该设为ON,以使系统将定义给ROM但没有用掉的内存当做RAM使用,而提高RAM的使用率。注意,如果ROM是FLASH,则不能设为ON,因为FLASH不能当作RAM使用。
ROMOFFSET,它定义OS起始位置(即ROMSTART)的物理地址和虚拟地址的差值,有些BSP中并没有使用这个定义。
OEMAddressTable及其它:
OEMAddressTable用来初始化系统中各种设备的虚拟地址与物理地址的对映关系。在我使用的BSP中,它是这样定义并初始化的:
typedef struct
{
ULONG ulVirtualAddress;
ULONG ulPhysicalAddress;
ULONG ulSizeInMegs;
} AddressTableStruct;
#define MEG(A) (((A - 1)>>20) + 1)
const AddressTableStruct OEMAddressTable[] =
{
{ SDRAM_VIRTUAL_MEMORY, //虚拟地址
PHYSICAL_ADDR_SDRAM_MAIN, //物理地址
MEG(SDRAM_MAIN_BLOCK_SIZE) //这段空间的大小,以M计
},
………………………
{
0,
0,
0
}
};
如例子所示,OEMAddressTable为一个结构数组,每项的第一个成员为虚拟地址,第二个成员为对应的物理地址,最后一个成员为该段空间的大小。这个数组的最后一项必须全部为0,以示整个数组的结束。内核启动时会读取这个数组的内容以初始化MMU页表,启用MMU,从尔使程序可以用虚拟地址来访问设备。当然,OEMAddressTable中所用到的每个物理地址及虚拟地址都需要在头文件中定义,每个BSP中定义这些值的文件不尽相同,所以,在此不能说明具体在哪个文件,读者朋友可以参考具体BSP的文档及代码。
(/WINCE500/PLATFORM/AK3224/SRC/BOOTLOADER/EBOOT/boot.bib)
不连续内存的处理:
如果内存在物理上是连续的,则OEMAddressTable中只需要一项就可以完成对内存的地址映射。但如果BSP运行在SDRAM物理上不连续的系统上时,OEMAddressTable中需要更多的项来将SDRAM映射到连续的虚拟地址上,当然也可以将它们映射到不连续的虚拟地址上,但似乎没有理由那么做。而且,当其物理地址不连续时系统需要做更多的工作。例如一个系统:32M SDRAM,16M FLASH,SDRAM在物理上不连续,被分成了4个8M的内存块CONFIG。BIB文件的MEMORY段如下所示:
MEMORY
RESERVED 80000000 00008000 RESERVED
DRV_GLB 80008000 00001000 RESERVED
CS8900 80010000 00030000 RESERVED
EDBG 80040000 00080000 RESERVED
NK 800C0000 00940000 RAMIMAGE
RAM 81800000 00800000 RAM
在这32M的空间中,BSP保留了前0x80000字节,接下来是NK,它占用了0x940000字节,而且它跨越了两个内存片,这些和其它BSP的设置都没有多大差别,接下来看RAM片,它只占用了最后的8M空间,前面说过,在这种物理内存不连续的系统中,RAM片不能跨越两个物理内存块,所以它被设计成只占用该系统中的最后一个物理内存片,而其它两片则由OEMEnumExtensionDRAM在运行时刻报告给系统,该函数的内容如下:
pMemSections[0].dwFlags=0;
pMemSections[0].dwStart=(SDRAM_VIRTUAL_MEMORY + 0x1000000);
pMemSections[0].dwLen=0x800000;
pMemSections[1].dwFlags=0;
pMemSections[1].dwStart=(SDRAM_VIRTUAL_MEMORY + 0x0A00000);
pMemSections[1].dwLen=0x600000;
return 2;
这样,系统所有的内存都被激活,系统可用内存就变成了8+8+6=24M,可以将RAM定义为这三片中的任意一片,而在OEMEnumExtensionDRAM中报告其它两片。但把RAM放在最后一片物理内存上有一个很大的好处,即如果NK变大,例如编译一个DEBUG版的系统时,这时,只需要将OEMEnumExtensionDRAM中的内容注释掉,CONFIG.BIB文件不用做任何改动,系统就可运行,只是在MAKEIMG时会有一个警告说系统包太大,可能无法运行,但实际不会影响系统的执行与稳定性,因为NK之后的那段内存并没有被使用,正好被涨大的系统占用,这在调试时极其方便。
而如果系统物理内存是连续的,所有者系统可用内存都可以定义在RAM片中。
对硬件知识了解不多请注意:SDRAM是否在物理上连续,与我们的板上有几片SDRAM没有关系,应该向硬件工程师了解SDRAM的地址分布情况。
6、驱动中注意的要点
1:CE下同名设备不能大于10
CE5.0中已经没有这个问题了,以前的版本可以这样做:只给上层输出一个设备,然后用一个IOCTL去打开一个个的物理设备这样就可以做到不受任何限制了。
2:MDD与PDD
一个驱动程序通常会被分成硬件相关(PDD)与硬件无关(MDD)层两部分。
当然,这种分层不是必须的,只是采用这种分层以后可以少写很多代码,因为微软提供了很多驱动程序的MDD。即使CE中没有我们所写的驱动程序的样例,采用这种结构以后,当需要写第二个程序时,就可以重用它的代码,就可以提高开发效率。
MDD是提供同类型的设备(比如串口)都会有的功能,这样PDD基本上就只有寄存器操作了。
像串口的中断处理,Read/Write函数,其大部分代码都是在MDD中实现的,不同的串口实现中只需要提供一些实际操作寄存器的函数。不同的驱动程序,其MDD与PDD的接口不尽相同,
3:XXX_Init函数的返回句柄
通常,这个句柄是驱动程序自己保存数据的一个指针,我们在Init返回时告诉上层程序,以后上层调用其它函数(例如Open)时,会将这个值传入,这样,我们就可以访问自己的一些私有数据。
当然,也可以返回一个任意的非0值对于一个设备驱动程序,系统不用的层会有不同的句柄。我们在XXX_Init中返回的句柄保存在设备管理器中,别的程序中应该是看不到的,而用CreateFile也会得到一个文件句柄,这个保存在哪我不知道,但和前者是不一样的。也就是说不同层的软件所关心的句柄也会不一样
4:DEBUGMSG与RETAILMSG的区别
它们都是输出调试信息用的,区别是:
DEBUGMSG只在DEBUG版中有效,RELEASE版中它被定义成了NULL
RETAILMSG在DEBUG和RELEASE版中都可以输出,
而且DEBUGMSG可以在运行时刻用DEBUZONE控制要不要输出信息。
在ship build 时,RETAILMSG 和DEBUGMSG都无效。
5:调试区与dpCurSettings
我们都是利用OutpubDebugString函数来实现调试信息的输出的,但是由于系统底层的调试信息非常繁多,如果这样大量的调试信息用于实时输出的话一定会影响到系统的性能和实时性,也就影响到了系统的运行。如果有一种方式能允许开发人员自己选择输出哪些调试信息,不输出哪些调试信息的话,那么就可以让开发人员只看到关心的调试信息,而把诸如键盘按键、鼠标移动等无用的调试信息隐去,则可以更好的提高开发效率。
调试区就是为了解决以上提出的问题的,对某一个驱动程序,它规定好自己向外输出的调试信息的分类,比如初始化时的信息,出错时的信息,释放时的信息,激活时的信息等,然后分成几个调试区,在现有的CE版本中最多允许16个调试区。
开发人员通过Platform Builder中Target菜单下的CE Debug Zones命令来决定想要得到哪一个或哪几个调试区的信息,在驱动程序中则可以根据开发人员的选择来输出指定调试区的信息。这就是调试区大体上的工作原理。
调试区的定义,声明,注册及使用。
在程序中使用调试区之前必须先定义它们,一个程序的16个调试区编号分别为0-15。代码样例如下所示:
#ifdef DEBUG
//
// For debug builds, use the real zones.
//
#define ZONE_TEST DEBUGZONE(0)
#define ZONE_PARAMS DEBUGZONE(1)
#define ZONE_VERBOSE DEBUGZONE(2)
……
#define ZONE_WARN DEBUGZONE(14)
#define ZONE_ERROR DEBUGZONE(15)
#else
//
// For retail builds, use forced messages based on the zones turned on below.
//
#define ZONE_TEST 0
#define ZONE_PARAMS 0
#define ZONE_VERBOSE 0
……
#define ZONE_WARN 0
#define ZONE_ERROR 0
#endif
这样,就可以程序的DEBUG版本中使用调试区了,而在RELEASE版本中则将其全部定义为0,调试信息即不再输出。
在程序中,除了以上的定义以外,还要声明几个专用的调试信息输出函数,这些函数与OutputDebugString函数的区别就在于在调用时需要指定对应的调试区,这些函数以及以上用到的DEBUGZONE宏的定义都在DbgApi.h头文件中,因此只要在源程序中包含此头文件即可。除此以外,还需要一个全局的DEBPARAM类型的变量命名为dpCurSettings,以供集成开发环境和调试信息输出函数使用。其代码样例如下:
#ifdef DEBUG
DBGPARAM dpCurSettings = {
TEXT("WaveDriver"), {
TEXT("Test") // 0
,TEXT("Params") // 1
,TEXT("Verbose") // 2
,TEXT("Interrupt") // 3
,TEXT("WODM") // 4
,TEXT("WIDM") // 5
,TEXT("PDD") // 6
,TEXT("MDD") // 7
,TEXT("Regs") // 8
,TEXT("Misc") // 9
,TEXT("Init") // 10
,TEXT("IOcontrol") // 11
,TEXT("Alloc") // 12
,TEXT("Function") // 13
,TEXT("Warning") // 14
,TEXT("Error") // 15
}
,
(1 << 15) // Errors
| (1 << 14) // Warnings
};
#endif
此例中还把ERROR和WARN调试区作为默认被开发人员选中的调试区。
要想使用调试区,还需要做的最后一件准备的事情就是在程序中进行注册,也就是在程序启动时通知集成开发环境本程序中要使用调试区,这个注册很简单,只要在程序的入口处使用DEBUGREGISTER宏即可,样例如下:
DllEntry (
HANDLE hinstDLL,
DWORD Op,
LPVOID lpvReserved
)
{
switch (Op) {
case DLL_PROCESS_ATTACH :
DEBUGREGISTER((HINSTANCE)hinstDLL);
break;
……
6:内核对象的传递
许多情况下,在不同进程中运行的线程需要共享内核对象。一般来说对于操作系统的内核对象句柄,比如线程句柄等等它们是不能在进程中进行共享的,也就是说这些句柄只能属于进程私有,句柄本身只是一个32位值,不同的句柄有不同的含义,HBRUSH
、HINSTANCE、HRESULT 都是句柄,但其含义大相径庭,但一般情况下句柄都是标识某对象的(HRESULT就不是),所以一般的句柄是内存空间相关的,而不同的exe其内存空间是无关的,所以共享句柄的值是无意义的,实际上返回的这些句柄的值是进程句柄表中的索引值。
那么如何在不同进程间共享内核对象句柄呢?一般有三种方法:
1 利用对象句柄的继承性
只有当进程具有父子关系时,才能使用对象句柄的继承性。在这种情况下,父进程可以使用一个或多个内核对象句柄,并且该父进程可以决定生成一个子进程,为子进程赋予对父进程的内核对象的访问权。。若要使这种类型的继承性能够实现,父进程必须执行若干个操作步骤。
首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄。这要求进程必须指定一个S E C U R I T Y _ AT T R I B U T E S结构并对它进行初始化,然后将该结构的地址传递给特定的C r e a t e函数。
然后,是让父进程生成子进程。这要使用C r e a t eP r o c e s s函数来完成。
BOOL CreateProcess(
LPCTSTR lpApplicationName, // name of executable module
LPTSTR lpCommandLine, // command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
BOOL bInheritHandles, // handle inheritance option
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // new environment block
LPCTSTR lpCurrentDirectory, // current directory name
LPSTARTUPINFO lpStartupInfo, // startup information
LPPROCESS_INFORMATION lpProcessInformation // process information);
可通过将bInheritHandles设置为True来实现。接下来通过这种方法生成的子进程就可以拥有父进程内核句柄的继承权了。
2 命名对象
共享跨越进程边界的内核对象的第二种方法是给对象命名。许多(虽然不是全部)内核对
象都是可以命名的。但当创建一个未命名的对象时,可以通过使用继承性(如上一方法介绍的那样)或DuplicateHandle (下一方法将要介绍)共享跨越进程的对象。若要按名字共享对象,必须为对象赋予一个名字。
3 复制对象句柄
共享跨越进程边界的内核对象的最后一个方法是使用DuplicateHandle函数
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, // handle to source process
HANDLE hSourceHandle, // handle to duplicate
HANDLE hTargetProcessHandle, // handle to target process
LPHANDLE lpTargetHandle, // duplicate handle
DWORD dwDesiredAccess, // requested access
BOOL bInheritHandle, // handle inheritance option
DWORD dwOptions // optional actions);
简单说来,该函数取出一个进程的句柄表中的项目,并将该项目拷贝到另一个进程的句柄表中。DuplicateHandle 函数配有若干个参数,但是实际上它是非常简单的。DuplicateHandle函数最普通的用法要涉及系统中运行的3个不同进程。
当调用DuplicateHandle函数时,第一和第三个参数hSourceProcessHandle和hTargetProcessHandle是内核对象句柄。这些句柄本身必须与调用DuplicateHandle函数的进程相关。此外,这两个参数必须标识进程的内核对象。如果将句柄传递给任何其他类型的内核对象,那么该函数运行就会失败。
第二个参数hSourceHandlee是任何类型的内核对象的句柄。但是该句柄值与调用DuplicateHandle的进程并无关系。相反,该句柄必须与hSourceProcessHandle句柄标识的进程相关。
第四个参数phTargetHandle是HANDLE变量的地址,它将接收获取源进程句柄信息拷贝的项目索引。返回的句柄值与hTargetProcessHandle标识的进程相关。DuplicateHandle的最后3个参数用于指明该目标进程的内核对象句柄表项目中使用的访问屏蔽值和继承性标志。DwOptions参数可以是0(零),也可以是下面两个标志的任何组合:DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE。如果设定了DUPLICATE_SAME_ACCESS标志,则告诉DuplicateHandle函数,你希望目标进程的句柄拥有与源进程句柄相同的访问屏蔽。使用该标志将使DuplicateHandle忽略它的dwDesiredAccess参数。如果设定了DUPLICATE_CLOSE_SOURCE标志,则可以关闭源进程中的句柄。该标志使得一个进程能够很容易地将内核对象传递给另一个进程。当使用该标志时,内核对象的使用计数不会受到影响。
上述三种方法的使用都有自己需要的条件,使用之前要先确定自己所具备的条件以及自己所要求达到的目的,以免使用时遇到不必要的错误。
7、同步
在多数情况下,线程之间难免要相互通信、相互协调才能完成任务。比如,当有多个线程共同访问同一个资源时,就必须保证一个线程正读取这个资源数据的时候,其它线程不能够修改它。这就需要线程之间相互通信,了解对方的行为。再有当一个线程要准备执行下一个任务之前,它必须等待另一个线程终止才能运行,这也需要彼此相互通信。实际开发过程中,线程间需要同步的情况非常多。Windows CE.NET给我们提供了很多的同步机制,熟练的掌握这些机制并合理运用会使线程之间的同步更合理、更高效。
Windows CE.NET具有两种运行模式:用户模式和内核模式。并且允许一个运行于用户模式的应用程序随时切换为内核模式,或切换回来。线程同步的有些解决办法运行在用户模式,有些运行在内核模式。《Windows核心编程》上说从用户模式切换到内核模式再切换回来至少要1000个CPU周期。查看CE下API函数SetKMode的源码,这个函数用于在两种模式间切换,改变模式只需修改一些标志,至于需要多少个CPU周期很难确定。但至少可以肯定来回切换是需要一定时间的。所以在选择同步机制上应该优先考虑运行在用户模式的同步解决办法。
1、互锁函数
互锁函数运行在用户模式。它能保证当一个线程访问一个变量时,其它线程无法访问此变量,以确保变量值的唯一性。这种访问方式被称为原子访问。互锁函数及其功能见如下列表:
函数 | 参数和功能 |
InterlockedIncrement | 参数为PLONG类型。此函数使一个LONG变量增1 |
InterlockedDecrement | 参数为PLONG类型。此函数使一个LONG变量减1 |
InterlockedExchangeAdd | 参数1为PLONG类型,参数2为LONG类型。此函数将参数2赋给参数1指向的值 |
InterlockedExchange | 参数1为PLONG类型,参数2为LONG类型。此函数将参数2的值赋给参数1指向的值 |
InterlockedExchangePointer | 参数为PVOID* 类型,参数2为PVOID类型。此函数功能同上。具体参见帮助 |
InterlockedCompareExchange | 参数1为PLONG类型,参数2为LONG类型,参数3为LONG类型。此函数将参数1指向的值与参数3比较,相同则把参数2的值赋给参数1指向的值。不相同则不变 |
InterlockedCompareExchangePointer | 参数1为PVOID* 类型,参数2为PVOID类型,参数3为PVOID。此函数功能同上。具体参见帮助 |
2、临界区
临界区对象运行在用户模式。它能保证在临界区内所有被访问的资源不被其它线程访问,直到当前线程执行完临界区代码。除了API外,MFC也对临界区函数进行了封装。临界区相关函数:
void InitializeCriticalSection ( LPCRITICAL_SECTION );
void EnterCriticalSection ( LPCRITICAL_SECTION );
void LeaveCriticalSection ( LPCRITICAL_SECTION );
void DeleteCriticalSection ( LPCRITICAL_SECTION );
举例如下:
void CriticalSectionExample (void)
{
CRITICAL_SECTION csMyCriticalSection;
InitializeCriticalSection (&csMyCriticalSection); ///初始化临界区变量
__try
{
EnterCriticalSection (&csMyCriticalSection); ///开始保护机制
///此处编写代码
}
__finally ///异常处理,无论是否异常都执行此段代码
{
LeaveCriticalSection (&csMyCriticalSection); ///撤销保护机制
}
}
MFC类使用更简单:
CCriticalSection cs;
cs.Lock();
///编写代码
cs.Unlock();
使用临界区要注意的是避免死锁。当有两个线程,每个线程都有临界区,而且临界区保护的资源有相同的时候,这时就要在编写代码时多加考虑。
3、事件对象
事件对象运行在内核模式。与用户模式不同,内核模式下线程利用等待函数来等待所需要的事件、信号,这个等待过程由操作系统内核来完成,而线程处于睡眠状态,当接收到信号后,内核恢复线程的运行。内核模式的优点是线程在等待过程中并不浪费CPU时间,缺点是从用户模式切换到内核模式需要一定的时间,而且还要切换回来。在讲解事件对象前应该先谈谈等待函数。等待函数有四个。具体参数和功能见下表:
函数 | 参数和功能 |
WaitForSingleObject | 参数1为HANDLE类型,参数2为DWORD类型。此函数等待参数1标识的事件,等待时间为参数2的值,单位ms。如果不超时,当事件成为有信号状态时,线程唤醒继续运行。 |
WaitForMultipleObjects | 参数1为DWORD类型,参数2为HANDLE * 类型,参数3为BOOL类型,参数4为DWORD类型。此函数等待参数2指向的数组中包含的所有事件。如果不超时,当参数3为FALSE时,只要有一个事件处于有信号状态,函数就返回这个事件的索引。参数3为TRUE时,等待所有事件都处于有信号状态时才返回。 |
MsgWaitForMultipleObjects | 参数1为DWORD类型,参数2为LPHANDLE类型,参数3为BOOL类型,参数4为DWORD类型,参数5为DWORD类型。此函数功能上同WaitForMultipleObjects函数相似,只是多了一个唤醒掩码。唤醒掩码都是和消息有关的。此函数不但能够为事件等待,还能为特定的消息等待。其实这个函数就是专为等待消息而定义的。 |
MsgWaitForMultipleObjectsEx | 参数1为DWORD类型,参数2为LPHANDLE类型,参数3为DWORD类型,参数4为DWORD类型,参数5为DWORD类型。此函数是MsgWaitForMultipleObjects函数的扩展。将原来函数的参数3除掉,添加参数5为标志。标志有两个值:0或MWMO_INPUTAVAILABLE。 |
如果一个线程既要执行大量任务同时又要响应用户的按键消息,这两个专用于等待消息的函数将非常有用。
和事件有关的函数有:
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPTSTR lpName);
BOOL SetEvent(HANDLE hEvent );
BOOL PulseEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);
HANDLE OpenEvent(DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName );
事件对象是最常用的内核模式同步方法。它包含一个使用计数和两个BOOL变量。其中一个BOOL变量指定这个事件对象是自动重置还是手工重置。另一个BOOL变量指定当前事件对象处于有信号状态还是无信号状态。
函数CreateEvent创建一个事件对象,参数1必须为NULL,参数2指定是否手工重新设置事件对象的状态。如果为FALSE,当等待函数接到信号并返回后此事件对象被自动置为无信号状态。这时等待此事件对象的其它线程就不会被唤醒,因为事件对象已经被置为无信号状态。如果参数2设置为TRUE,当等待函数接到信号并返回后事件对象不会被自动置于无信号状态,其它等待此事件对象的线程都能够被唤醒。用ResetEvent函数可以手工将事件对象置为无信号状态。相反SetEvent函数将事件对象置为有信号状态。PulseEvent函数将事件对象置为有信号状态,然后立即置为无信号状态,在实际开发中这个函数很少使用。OpenEvent函数打开已经创建的事件对象,一般用于不同进程内的线程同步。在调用CreateEvent创建一个事件对象时,传递一个名字给参数4,这样在其它进程中的线程就可以调用OpenEvent函数并指定事件对象的名字,来访问这个事件对象。
4、互斥对象
互斥对象运行在内核模式。它的行为特性同临界区非常相似,在一个线程访问某个共享资源时,它能够保证其它线程不能访问这个资源。不同的是,互斥对象运行在内核模式,从时间上比临界区要慢。由于内核对象具有全局性,不同的进程都能够访问,这样利用互斥对象就可以让不同的进程中的线程互斥访问一个共享资源。而临界区只能在一个进程内有效。
和互斥相关的函数有:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName);
BOOL ReleaseMutex(HANDLE hMutex);
互斥对象包含一个引用计数,一个线程ID和一个递归计数。引用计数是所有内核对象都含有的。线程ID表示哪个线程正在使用互斥资源,当ID为0时,互斥对象发出信号。递归计数用于一个线程多次等待同一个互斥对象。函数CreateMutex创建一个互斥对象,参数1必须设置为NULL,参数2如果设置为FALSE,表示当前线程并不占有互斥资源,互斥对象的线程ID和递归计数都被设置为0,互斥对象处于有信号状态。如果设置为TRUE,表示当前线程将占有互斥资源,互斥对象的线程ID被设置为当前线程ID,递归计数被设置为1,互斥对象处于无信号状态。当调用等待函数时,等待函数检验互斥对象的线程ID是否为0,如果为0,说明当前没有线程访问互斥资源,内核将线程唤醒,并且将互斥对象的递归计数加1。当一个线程被唤醒后,必须调用函数ReleaseMutex将互斥对象的递归计数减1。如果一个线程多次调用等待函数,就必须以同样的次数调用ReleaseMutex函数。与其它Windows不同的是,和互斥相关的函数中没有OpenMutex函数。要在不同进程中访问同一互斥对象,调用CreateMutex函数,参数传递互斥对象的名称,返回这个互斥对象的句柄。
5、信标对象
信标对象,也叫信号灯,用于限制资源访问数量,他包含一个引用计数,一个当前可用资源数,一个最大可用资源数。如果当前可用资源数大于0,信标对象处于有信号状态。当可用资源数等于0,信标对象处于无信号状态。
和信标对象相关的函数:
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName);
BOOL ReleaseSemaphore(HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount);
函数CreateSemaphore的参数1为NULL,参数2为当前可用资源初始值,参数3为最大可用资源数,参数4为名字。当参数2的值等于0时,信标对象处于无信号状态,这时内核将调用等待函数的线程置于睡眠状态,如果参数2的值大于0,信标对象处于有信号状态,这时内核将调用等待函数的线程置于运行状态,并将信标对象的当前可用资源数减1。函数ReleaseSemaphore的参数1为信标对象的句柄,参数2为要释放的资源数,参数3返回原来可用资源数,调用此函数将当前可用资源数加上参数2的值。当一个线程访问完可用资源后,应该调用ReleaseSemaphore函数使当前可用资源数递增。要在不同进程中访问同一信标对象,调用CreateSemaphore函数并传递信标对象的名称,得到已经在其它进程创建的信标对象的句柄。CE下没有OpenSemaphore函数。另外我还要说明一点,等待函数默认将信标对象的当前可用资源数减1,但线程可能一次使用多个资源,这就可能出现问题了。为避免问题出现,应该遵守一个线程只使用一个资源的原则。
6、消息队列
Windows CE.NET允许一个应用程序或驱动程序创建自己的消息队列。消息队列既可以作为在线程之间传递数据的工具,也可以作为线程之间同步的工具。它的优点是需要很小的内存,一般只用于点到点的通信。
和消息队列相关的函数:
HANDLE WINAPI CreateMsgQueue(LPCWSTR lpszName,
LPMSGQUEUEOPTIONS lpOptions);
BOOL WINAPI CloseMsgQueue(HANDLE hMsgQ);
BOOL GetMsgQueueInfo(HANDLE hMsgQ,
LPMSGQUEUEINFO lpInfo);
HANDLE WINAPI OpenMsgQueue(HANDLE hSrcProc,
HANDLE hMsgQ,
LPMSGQUEUEOPTIONS lpOptions);
BOOL ReadMsgQueue(HANDLE hMsgQ,
LPVOID lpBuffer,
DWORD cbBufferSize,
LPDWORD lpNumberOfBytesRead,
DWORD dwTimeout,
DWORD *pdwFlags);
BOOL WINAPI WriteMsgQueue(HANDLE hMsgQ,
LPVOID lpBuffer,
DWORD cbDataSize,
DWORD dwTimeout,
DWORD dwFlags);
使用CreateMsgQueue函数创建一个消息队列,传递一个MSGQUEUEOPTIONS结构指针。在这个结构中设置标志(允许队列缓冲区动态改变大小,允许直接读或者写操作而不管之前是否有过写操作或读操作)、队列允许的最大消息数、队列属性(只读或者只写)。使用WriteMsgQueue函数把一个消息写入到消息队列中。传递一个消息队列的缓冲区、消息数据的大小、写入缓冲区的超时值、标志。使用ReadMsgQueue函数把一个消息从消息队列中读出。使用CloseMsgQueue函数关闭消息队列缓冲区。使用OpenMsgQueue函数能够打开其它进程中创建的消息队列。另外可以用等待函数等待消息队列的变化。当消息队列由没有消息到有消息时,或由满消息到不满消息时唤醒调用等待函数的线程。关于消息队列MSDN上有几个简单的例子。
8、结构化异常处理(Structured Exception Handling,SEH)
微软在Windows中引入SEH的主要动机是为了便于操作系统本身的开发。操作系统的开发人员使用SEH,使得系统更加强壮。我们也可以使用SEH,使我们的自己的程序更加强壮。使用SEH所造成的负担主要由编译程序来承担,而不是由操作系统承担。当异常块(exception block)出现时,编译程序要生成特殊的代码。编译程序必须产生一些表( t a b l e)来支持处理SEH的数据结构。编译程序还必须提供回调( c a l l b a c k)函数,操作系统可以调用这些函数,保证异常块被处理。编译程序还要负责准备栈结构和其他内部信息,供操作系统使用和参考。在编译程序中增加SEH支持不是一件容易的事。不同的编译程序厂商会以不同的方式实现SEH,这一点并不让人感到奇怪。幸亏我们可以不必考虑编译程序的实现细节,而只使用编译程序的SEH功能。
SEH的异常处理模型主要由try-except语句来完成,它与标准C++所定义的异常处理模型非常类似,也都是可以定义出受监控的代码模块,以及定义异常处理模块等。看一个例子,代码如下:
//seh-test.c
#include <stdio.h>
void main()
{
puts("hello");
// 定义受监控的代码模块
try
{
puts("in try");
}
//定义异常处理模块
except(1)
{
puts("in except");
}
puts("world");
}
很简单的调用,而且与C++异常处理模型很相似。
下面有个例子,说明这个SEH机制处理过程。
// seh-test.c
// 异常处理模块的查找过程演示
#include <stdio.h>
int seh_filer()
{
return 0;
}
void test()
{
try
{
int* p;
puts("test()函数的try块中");
// 下面将导致一个异常
p = 0;
*p = 45;
}
// 注意,__except关键字后面的表达式是一个函数表达式
// 而且这个函数将返回0,所以控制流进入到上一层
// 的try-except语句中继续查找
except(seh_filer())
{
puts("test()函数的except块中");
}
}
void main()
{
puts("hello");
try
{
puts("main()函数的try块中"); // 注意,这个函数的调用过程中,有可能出现一些异常
test();
}
// 注意,这个表达式是一个逗号表达式
// 它前部分打印出一条message,后部分是
// 一个常量,所以这个值也即为整个表达式
// 的值,因此系统找到了except定义的异
// 常处理模块,控制流进入到except模块里面
except(puts("in filter"), 1)
{
puts("main()函数的except块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
main()函数的try块中
test()函数的try块中
in filter
main()函数的except块中
world
Press any key to continue
CE驱动中使用大量的SEH代码,保证系统健壮性。因此,我们在驱动编写过程中,最好也利用SEH这个有效的机制。
9、不同进程的内存空间访问
驱动编写过程中,往往使用由应用传递的内存空间缓存区。由于应用与驱动处于不同进程空间,因此不能之间读写其他进程空间的内容,这就必须做一些处理。
1:使用SetProcPermissions函数
首先获得当前线程的权限
pSerialHead->TxBufferInfo.Permissions = GetCurrentPermissions();
调用SetProcPermissions函数后,就可以访问外部进程的空间。
DWORD oldPerm = SetProcPermissions(pSerialHead->TxBufferInfo.Permissions);
使用完毕,恢复当前线程的访问权限。
SetProcPermissions(oldPerm);
SetProcPermissions(0xFFFFFFFF),这会让当前线程能访问CE系统中任何地址空间。
2:用MapCallerPtr映射地址,把应用程序传递过来的地址转换成当前线程能够访问的地址。
lpInBuf = MapCallerPtr(lpInBuf,nInBufSize);
一般是使用设备接口的驱动,设备管理层已经做了这个映射动作,驱动可以之间使用ReadFile、WriteFile传入的内存空间。不过如果传入是指向结构体的指针,那么这个结构体内的指针成员指向的地址,需要再次转换。
3:培养驱动编写习惯
先利用IsBadReadPtr 、IsBadWritePtr、IsBadCodePtr判断传递的指针是否是有效的地址。
10、WINAPI作用
Windows代码中经常使用WINAPI声明函数。WINAPI主要是声明函数的调用形式,即是函数调用时参数入栈的顺序,原型如下
#define WINAPI __stdcall
一般来WINDOW的API函数都是采用这种调用方式的。WINAPI的调用方式是比较保守的,就是采用PASCAL的调用方式,参数从右往左入栈。除此之外,还有经常使用的__cdecl调用。
__cdecl、__fastcall和__stdcall都是函数调用规范,规定了参数出入栈的顺序和方法,如果只用VC编程的话可以不用关心,但是要在C++和Pascal等其他语言通信的时候就要注意了,只有用相同的方法才能够调用成功。另外,像printf这样接受可变个数参数的函数只有用__cdecl才能够实现。
_stdcall区别于_cdecl:
前者(_stdcall)一般用于调用WIN32 API函数,被调函数本身(这个函数)从栈内取数;
后者是c/c++函数的默认调用方式,调用的函数(调用这个函数的函数)从栈内取数。
五、线程的基础知识
1. 进程与线程有那些区别和联系?
每个进程至少需要一个线程。
进程由两部分构成:进程内核对象,地址空间。线程也由两部分组成:线程内核对象,操作系统用它来对线程实施管理。线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量。
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。
如果在单进程环境中,有多个线程正在运行,那么这些线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。
进程使用的系统资源比线程多得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。因此始终都应该设法用增加线程来解决编程问题,避免创建新的进程。但是许多程序设计用多个进程来实现会更好些。
2. 如何使用_beginthreadex函数?
使用方法与CreateThread函数相同,只是调用参数类型需要转换。
3. 如何使用CreateThread函数?
当CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。使用时应当注意在不需要对线程内核进行访问后调用CloseHandle函数关闭线程句柄。因为CreateThread函数中使用某些C/C++运行期库函数时会有内存泄漏,所以应当尽量避免使用。参数含义:
lpThreadAttributes 如果传递NULL该线程使用默认安全属性。如果希望所有的子进程能够继承该线程对象的句柄,必须将它的bInheritHandle成员被初始化为TRUE。
dwStackSize 设定线程堆栈的地址空间。如果非0,函数将所有的存储器保留并分配给线程的堆栈。如果是0,CreateThread就保留一个区域,并且将链接程序嵌入.exe文件的/STACK链接程序开关信息指明的存储器容量分配给线程堆栈。
lpStartAddress 线程函数的地址。
lpParameter 传递给线程函数的参数。
dwCreationFlags 如果是0,线程创建后立即进行调度。如果是CREATE_SUSPENDED,系统对它进行初始化后暂停该线程的运行。
LpThreadId 用来存放系统分配给新线程的ID。
4. 如何终止线程的运行?
(1) 线程函数返回(最好使用这种方法)。
这是确保所有线程资源被正确地清除的唯一办法。
如果线程能够返回,就可以确保下列事项的实现:
在线程函数中创建的所有C++对象均将通过它们的撤消函数正确地撤消。
操作系统将正确地释放线程堆栈使用的内存。
系统将线程的退出代码设置为线程函数的返回值。
系统将递减线程内核对象的使用计数。
(2) 调用ExitThread函数(最好不要使用这种方法)。
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++类对象)将不被撤消。
(3) 调用TerminateThread函数(应该避免使用这种方法)。
TerminateThread能撤消任何线程。线程的内核对象的使用计数也被递减。TerminateThread函数是异步运行的函数。如果要确切地知道该线程已经终止运行,必须调用WaitForSingleObject或者类似的函数。当使用返回或调用ExitThread的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。
(4) 包含线程的进程终止运行(应该避免使用这种方法)。
由于整个进程已经被关闭,进程使用的所有资源肯定已被清除。就像从每个剩余的线程调用TerminateThread一样。这意味着正确的应用程序清除没有发生,即C++对象撤消函数没有被调用,数据没有转至磁盘等等。
一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调GetExitcodeThread来检查由hThread标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码。
5. 为什么不要使用_beginthread函数和_endthread函数?
与_beginthread函数相比参数少,限制多。无法创建暂停的线程,无法取得线程ID。_endthread函数无参数,线程退出代码必须为0。还有_endthread函数内部关闭了线程的句柄,一旦退出将不能正确访问线程句柄。
6. 如何对进程或线程的内核进行引用?
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
这两个函数都能返回调用线程的进程的伪句柄或线程内核对象的伪句柄。伪句柄只能在当前的进程或线程中使用,在其它线程或进程将不能访问。函数并不在创建进程的句柄表中创建新句柄。调用这些函数对进程或线程内核对象的使用计数没有任何影响。如果调用CloseHandle,将伪句柄作为参数来传递,那么CloseHandle就会忽略该函数的调用并返回FALSE。
DWORD GetCurrentProcessId();
DWORD GetCurrentThreadId();
这两个函数使得线程能够查询它的进程的唯一ID或它自己的唯一ID。
7. 如何将伪句柄转换为实句柄?
HANDLE hProcessFalse = NULL;
HANDLE hProcessTrue = NULL;
HANDLE hThreadFalse = NULL;
HANDLE hThreadTrue = NULL;
hProcessFalse = GetCurrentProcess();
hThreadFalse = GetCurrentThread();
取得线程实句柄:
DuplicateHandle( hProcessFalse, hThreadFalse, hProcessFalse, &hThreadTrue, 0, FALSE, DUPLICATE_SAME_ACCESS );
取得进程实句柄:
DuplicateHandle( hProcessFalse, hProcessFalse, hProcessFalse, &hProcessTrue, 0, FALSE, DUPLICATE_SAME_ACCESS );
由于DuplicateHandle会递增特定对象的使用计数,因此当完成对复制对象句柄的使用时,应该将目标句柄传递给CloseHandle,从而递减对象的使用计数。
线程的调度、优先级和亲缘性
1. 如何暂停和恢复线程的运行?
线程内核对象的内部有一个值指明线程的暂停计数。当调用CreateProcess或CreateThread函数时,就创建了线程的内核对象,并且它的暂停计数被初始化为1。因为线程的初始化需要时间,不能在系统做好充分的准备之前就开始执行线程。线程完全初始化好了之后,CreateProcess或CreateThread要查看是否已经传递了CREATE_SUSPENDED标志。如果已经传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。如果尚未传递该标志,那么该函数将线程的暂停计数递减为0。当线程的暂停计数是0的时候,除非线程正在等待其他某种事情的发生,否则该线程就处于可调度状态。在暂停状态中创建一个线程,就能够在线程有机会执行任何代码之前改变线程的运行环境(如优先级)。一旦改变了线程的环境,必须使线程成为可调度线程。方法如下:
hThread = CreatThread( ……,CREATE_SUSPENDED,…… );
或
bCreate = CreatProcess( ……,CREATE_SUSPENDED,……,pProcInfo );
if( bCreate != FALSE )
{
hThread = pProcInfo.hThread;
}
……
……
……
ResumeThread( hThread );
CloseHandle( hThread );
ResumeThread成功,它将返回线程的前一个暂停计数,否则返回0xFFFFFFFF。
单个线程可以暂停若干次。如果一个线程暂停了3次,它必须恢复3次。创建线程时,除了使用CREATE_SUSPENDED外,也可以调用SuspendThread函数来暂停线程的运行。任何线程都可以调用该函数来暂停另一个线程的运行(只要拥有线程的句柄)。线程可以自行暂停运行,但是不能自行恢复运行。与ResumeThread一样,SuspendThread返回的是线程的前一个暂停计数。线程暂停的最多次数可以是MAXIMUM_SUSPEND_COUNT次。SuspendThread与内核方式的执行是异步进行的,但是在线程恢复运行之前,不会发生用户方式的执行。调用SuspendThread时必须小心,因为不知道暂停线程运行时它在进行什么操作。只有确切知道目标线程是什么(或者目标线程正在做什么),并且采取强有力的措施来避免因暂停线程的运行而带来的问题或死锁状态,SuspendThread才是安全的。
2. 是否可以暂停和恢复进程的运行?
对于Windows来说,不存在暂停或恢复进程的概念,因为进程从来不会被安排获得CPU时间。不过Windows确实允许一个进程暂停另一个进程中的所有线程的运行,但是从事暂停操作的进程必须是个调试程序。特别是,进程必须调用WaitForDebugEvent和ContinueDebugEvent之类的函数。由于竞争的原因,Windows没有提供其他方法来暂停进程中所有线程的运行。
3. 如何使用sleep函数?
系统将在大约的指定毫秒数内使线程不可调度。Windows不是个实时操作系统。虽然线程可能在规定的时间被唤醒,但是它能否做到,取决于系统中还有什么操作正在进行。
可以调用Sleep,并且为dwMilliseconds参数传递INFINITE。这将告诉系统永远不要调度该线程。这不是一件值得去做的事情。最好是让线程退出,并还原它的堆栈和内核对象。可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。但是,系统可以对刚刚调用Sleep的线程重新调度。如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。
4. 如何转换到另一个线程?
系统提供了SwitchToThread函数。当调用这个函数的时候,系统要查看是否存在一个迫切需要CPU时间的线程。如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。如果存在一个迫切需要CPU时间的线程,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要CPU时间的线程可以运行一个时间段,然后系统调度程序照常运行。该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源。如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALSE,否则返回一个非0值。调用SwitchToThread与调用Sleep是相似的。差别是SwitchToThread允许优先级较低的线程运行;而即使有低优先级线程迫切需要CPU时间,Sleep也能够立即对调用线程重新进行调度。
5. 如何取得线程运行的时间?
(1) 简单取得线程大概运行时间:
DWORD dwStartTime = 0;
DWORD dwEndTime = 0;
DWORD dwRunTime = 0;
dwStartTime = GetTickCount();
……
……
……
dwEndTime = GetTickCount();
dwRunTime = dwEndTime – dwStartTime;
(2) 调用GetThreadTimes的函数:
参数含义:
hThread 线程句柄
lpCreationTime 创建时间:英国格林威治时间
lpExitTime 退出时间:英国格林威治时间,如果线程仍然在运行,退出时间则未定义
lpKernelTime 内核时间:指明线程执行操作系统代码已经经过了多少个100ns的CPU时间
lpUserTime 用户时间:指明线程执行应用程序代码已经经过了多少个100ns的CPU时间
GetProcessTimes是个类似GetThreadTimes的函数,适用于进程中的所有线程(甚至是已经终止运行的线程)。返回的内核时间是所有进程的线程在内核代码中经过的全部时间的总和。
GetProcessTimes这个函数在WINCE中不存在。
6. 进程的优先级类有哪些?
实时:REALTIME_PRIORITY_CLASS
立即对事件作出响应,执行关键时间的任务。会抢先于操作系统组件之前运行。
高:HIGH_PRIORITY_CLASS
立即对事件作出响应,执行关键时间的任务。
高于正常:ABOVE_NORMAL_PRIORITY_CLASS
在正常优先级与高优先级之间运行(Windows2000)。
正常:NORMAL_PRIORITY_CLASS
没有特殊调度需求
低于正常:BELOW_NORMAL_PRIORITY_CLASS
在正常优先级与空闲优先级之间运行(Windows2000)。
空闲:IDLE_PRIORITY_CLASS
在系统空闲时运行。
设置方法:
BOOL SetPriorityClass( HANDLE hProcess, DWORD dwPriority );
DWORD GetPriorityClass( HANDLE hProcess );
使用命令外壳启动一个程序时,该程序的起始优先级是正常优先级。如果使用Start命令来启动该程序,可以使用一个开关来设定应用程序的起始优先级。例如:
c:/>START /LOW CALC.EXE
Start命令还能识别/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH和/REALTIME等开关。
WINCE没有以上实现。
7. 线程的相对优先级有哪些?
关键时间:THREAD_PRIORITY_TIME_CRITICAL
对于实时优先级类线程在优先级31上运行,对于其他优先级类,线程在优先级15上运行。
最高:THREAD_PRIORITY_HIGHEST
线程在高于正常优先级上两级上运行。
高于正常:THREAD_PRIORITY_ABOVE_NORMAL
线程在正常优先级上一级上运行。
正常:THREAD_PRIORITY_NORMAL
线程在进程的优先级类上正常运行。
低于正常:THREAD_PRIORITY_BELOW_NORMAL
线程在低于正常优先级下一级上运行。
最低:THREAD_PRIORITY_LOWEST
线程在低于正常优先级下两级上运行。
空闲:THREAD_PRIORITY_IDLE
对于实时优先级类线程在优先级16上运行对于其他优先级类线程在优先级1上运行。
设置方法:
BOOL SetThreadPriority( HANDLE hThread, DWORD dwPriority );
DWORD GetThreadPriorityClass( HANDLE hThread );
8. 如何避免系统动态提高线程的优先级等级?
系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。或者当系统发现一个线程在大约3至4s内一直渴望得到CPU时间,它就将这个渴望得到CPU时间的线程的优先级动态提高到15,并让该线程运行两倍于它的时间量。当到了两倍时间量的时候,该线程的优先级立即返回到它的基本优先级。下面的函数可以对系统的调度方式进行设置:
BOOL SetProcessPriorityBoost( HANDLE hProcess, BOOL bDisableBoost );
BOOL GetProcessPriorityBoost( HANDLE hProcess, PBOOL pbDisableBoost );
BOOL SetThreadPriorityBoost( HANDLE hThread, BOOL bDisableBoost );
BOOL GetThreadPriorityBoost( HANDLE hThread, PBOOL pbDisableBoost );
SetProcessPriorityBoost负责告诉系统激活或停用进行中的所有线程的优先级提高功能,而SetThreadPriorityBoost则激活或停用各个线程的优先级提高功能。
WINCE没有提供这4个函数的有用的实现代码。
用户方式中线程的同步:
1. 仅一条语句用不用考虑线程同步的问题?
当使用高级语言编程时,我们往往会认为一条语句是最小的原子访问,CPU不会在这条语句中间运行其他的线程。这是错误的,因为即使非常简单的一条高级语言的语句,经编译器编译后也可能变成多行代码由计算机来执行。因此必须考虑线程同步的问题。任何线程都不应该通过调用简单的C语句来修改共享的变量。
2. 互锁函数有那些?
(1) LONG InterlockedExchangeAdd ( LPLONG Addend, LONG Increment );
Addend为长整型变量的地址,Increment为想要在Addend指向的长整型变量上增加的数值(可以是负数)。这个函数的主要作用是保证这个加操作为一个原子访问。
(2) LONG InterlockedExchange( LPLONG Target, LONG Value );
用第二个参数的值取代第一个参数指向的值。函数返回值为原始值。
(3) PVOID InterlockedExchangePointer( PVOID *Target, PVOID Value );
用第二个参数的值取代第一个参数指向的值。函数返回值为原始值。
(4) LONG InterlockedCompareExchange(
LPLONG Destination, LONG Exchange, LONG Comperand );
如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。
(5) PVOID InterlockedCompareExchangePointer (
PVOID *Destination, PVOID Exchange, PVOID Comperand );
如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。
3. 为什么单CPU的计算机不应该使用循环锁?
举例说明:
BOOL g_bResourceUse = FALSE;
……
void ThreadFunc1( )
{
BOOL bResourceUse = FALSE;
while( 1 )
{
bResourceUse = InterlockedExchange( &g_bResourceUse, TRUE );
if( bResourceUse == FALSE )
{
break;
}
Sleep( 0 );
}
……
……
……
InterlockedExchange( &g_bResourceUse, FALSE );
}
首先循环锁会浪费CPU时间。CPU必须不断地比较两个值,直到一个值由于另一个线程而“奇妙地”改变为止。而且使用该循环锁的线程都应该为同一优先级,并且应当使用SetProcessPriorityBoost函数或SetThreadPriorityBoost函数禁止线程优先级的动态提高功能,否则优先级较低的线程可能永远不能被调用。
4. 如何使用volatile声明变量?
如果是对共享资源的地址进行使用如&g_Resource那么可以不使用volatile,因为将一个变量地址传递给一个函数时,该函数必须从内存读取该值。优化程序不会对它产生任何影响。如果直接使用变量,必须有一个volatile类型的限定词。它告诉编译器,变量可以被应用程序本身以外的某个东西进行修改,这些东西包括操作系统,硬件或同时执行的线程等。volatile限定词会告诉编译器,不要对该变量进行任何优化,并且总是重新加载来自该变量的内存单元的值。否则编译器会把变量的值存入CPU寄存器,每次对寄存器进行操作。线程就会进入一个无限循环,永远无法唤醒。
5. 如何使用关键代码段实现线程的同步?
如果需要一小段代码以原子操作的方式执行,这时简单的互锁函数已不能满足需要,必须使用关键代码段来解决问题。不过使用关键代码段时,很容易陷入死锁状态,因为在等待进入关键代码段时无法设定超时值。关键代码段是通过对共享资源设置一个标志来实现的,就像厕所门上的“有人/没人”标志一样。这个标志就是一个CRITICAL_SECTION变量。该变量在任何一个线程使用它之前应当进行初始化。初始化可以有两种方法,使用InitializeCriticalSection函数和InitializeCriticalSectionAndSpinCount函数。然后在每个使用共享资源的线程函数的关键代码段前使用EnterCriticalSection函数或者使用TryEnterCriticalSection函数。在关键代码段使用之后调用LeaveCriticalSection函数。在所有的线程都不再使用该共享资源后应当调用DeleteCriticalSection函数来清除该标志。举例说明:
const int MAX_TIMES = 1000;
int g_intIndex = 0;
DWORD g_dwTimes[MAX_TIMES];
CRITICAL_SECTION g_cs;
void Init( )
{
……
InitializeCriticalSection( &g_cs );
……
}
DWORD WINAPI FirstThread( PVOID lpParam )
{
while ( g_intIndex < MAX_TIMES )
{
EnterCriticalSection( &g_cs );
g_dwTimes[g_intIndex] = GetTickCount( );
g_intIndex++;
LeaveCriticalSection( &g_cs );
}
return 0;
}
DWORD WINAPI SecondThread( PVOID lpParam )
{
while ( g_intIndex < MAX_TIMES )
{
EnterCriticalSection( &g_cs );
g_intIndex++;
g_dwTimes[g_intIndex - 1] = GetTickCount( );
LeaveCriticalSection( &g_cs );
}
return 0;
}
void Close( )
{
……
DeleteCriticalSection( &g_cs );
……
}
使用关键代码段应当注意一些技巧:
(1) 每个共享资源使用一个CRITICAL_SECTION变量。
这样在当前线程占有一个资源时,另一个资源可以被其他线程占有。
EnterCriticalSection( &g_cs );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_intArray[intLoop] = 0;
g_uintArray[intLoop] = 0;
}
LeaveCriticalSection( &g_cs );
改为:
EnterCriticalSection( &g_csInt );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_intArray[intLoop] = 0;
}
LeaveCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = 0;
}
LeaveCriticalSection( &g_csUint );
(2) 同时访问多个资源,必须始终按照完全相同的顺序请求对资源的访问。
这样才能避免死锁状态产生。离开的顺序没有关系。
Thread1:
EnterCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
Thread2:
EnterCriticalSection( &g_csUint );
EnterCriticalSection( &g_csInt );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
改为:
Thread1:
EnterCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
Thread2:
EnterCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
(3) 不要长时间运行关键代码段。
EnterCriticalSection( &g_cs );
SendMessage( hWnd, WM_SOMEMSG, &g_s, 0 );
LeaveCriticalSection( &g_cs );
改为:
EnterCriticalSection( &g_cs );
sTemp = g_s;
LeaveCriticalSection( &g_cs );
SendMessage( hWnd, WM_SOMEMSG, &sTemp, 0 );
6. InitializeCriticalSection和InitializeCriticalSectionAndSpinCount差别?
InitializeCriticalSection函数的返回值为空并且不会创建事件内核对象,比较节省系统资源,但是一旦发生两个或多个线程争用关键代码段的情况,如果内存不足,关键代码段可能被争用,同时系统可能无法创建必要的事件内核对象。这时EnterCriticalSection函数将会产生一个EXCEPTION_INVALID_HANDLE异常。这个错误非常少见。如果想对这种情况有所准备,可以有两种选择。可以使用结构化异常处理方法来跟踪错误。当错误发生时,既可以不访问关键代码段保护的资源,也可以等待某些内存变成可用状态,然后再次调用EnterCriticalSection函数。
另一种选择是使用InitializeCriticalSectionAndSpinCount,第二个参数dwSpinCount中,传递的是在使线程等待之前它试图获得资源时想要循环锁循环迭代的次数。这个值可以是0至0x00FFFFFF之间的任何数字。如果在单处理器计算机上运行时调用该函数,该参数被忽略,并且始终设置为0。使用InitializeCriticalSectionAndSpinCount函数创建关键代码段,确保设置了dwSpinCount参数的高信息位。当该函数发现高信息位已经设置时,它就创建该事件内核对象,并在初始化时将它与关键代码段关联起来。如果事件无法创建,该函数返回FALSE。可以更加妥善地处理代码中的这个事件。如果事件创建成功,EnterCriticalSection将始终都能运行,并且决不会产生异常情况(如果总是预先分配事件内核对象,就会浪费系统资源。只有当代码不能容许EnterCriticalSection运行失败,或者有把握会出现争用现象,或者预计进程将在内存非常短缺的环境中运行时,才能预先分配事件内核对象)。
7. TryEnterCriticalSection和EnterCriticalSection的差别是什么?
9、线程优先级:
线程除了能够访问进程的资源外,每个线程还拥有自己的栈。栈的大小是可以调整的,最小为1KB或4KB(也就是一个内存页。内存页的大小取决于CPU),一般默认为64KB,但栈顶端永远保留2KB为防止溢出。如果要改变栈初始时大小,在EVC"Project"-"Settings"-"Link"链接选项"/STACK"后的参数中指定大小。其中参数1为默认大小,参数2为一个内存页大小,都用十六进制表示。如果将栈的初始值设置太小,很容易导致系统访问非法并立即终止进程。使用UserKInfo[KINX_PAGESIZE]来获得系统页大小。
线程有五中状态,分别为运行、挂起、睡眠、阻塞、终止。当所有线程全部处于阻塞状态时,内核处于空闲模式(Idle mode),这时对CPU的电力供应将减小。
创建一个线程的API函数如下:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId );
Windows CE.NET 不支持安全性所以参数1必须设置为0。如果参数5为STACK_SIZE_PARAM_IS_A_RESERVATION,那么参数2可以指定栈的大小,内核将按照参数2的数值来为此线程拥有的栈保留地址空间。如果参数5不为STACK_SIZE_PARAM_IS_A_RESERVATION,那么参数2必须设置为0。参数3为执行路径的首地址,也就是函数的地址。参数4用来向线程中传递一个参数。参数5除了上面说明外,还可以为0、CREATE_SUSPENDED。CREATE_SUSPENDED表示这个线程在创建后一直处于挂起状态,直到用ResumeThread函数来恢复。最后一个参数保存函数返回的创建的线程ID。
退出一个线程同退出一个进程有类似的方法。最好是由函数返回,在线程中调用ExitThead函数也可以
。在当前线程中终止另一个线程使用TerminateThread函数。此函数在使一个线程退出时,会通知这个线
程加载的所有DLL。这样DLL就可以做结束工作了。
Windows CE.NET不像其他Windows操作系统将进程分为不同的优先级类,Windows CE.NET只将线程分
为256个优先级。0优先级最高,255最低,0到248优先级属于实时性优先级。0到247优先级一般分配给实
时性应用程序、驱动程序、系统程序。249到255优先级中,251优先级(THREAD_PRIORITY_NORMAL)是正
常优先级。255优先级(THREAD_PRIORITY_IDLE)为空闲优先级。249优先级(THREAD_PRIORITY_HIGHEST
)是高优先级。248到255优先级一般分配给普通应用程序线程使用。具体分段见下表:
优先级范围 分配对象
0-96 高于驱动程序的程序
97-152 基于Windows CE的驱动程序
153-247 低于驱动程序的程序
248-255 普通的应用程序
Windows CE.NET操作系统具有实时性,所以调度系统必须保证高优先级线程先运行,低优先级线程在
高优先级线程终止后或者阻塞时才能得到CPU时间片。而且一旦发生中断,内核会暂停低优先级线程的运
行,让高优先级线程继续运行,直到终止或者阻塞。具有相同优先级的线程平均占有CPU时间片,当一个
线程使用完了CPU时间片或在时间片内阻塞、睡眠,那么其他相同优先级的线程会占有时间片。这里提到
的CPU时间片是指内核限制线程占有CPU的时间,默认为100ms。OEM可以更改这个值,甚至设置为0。如果
为0,当前线程将一直占有CPU,直到更高优先级线程要求占有CPU。这个调度算法好像是很有效、很完美
,但却存在着一种情况,当这种情况发生时程序会死锁。举例来说:一个应用程序包含两个线程,线程1
是高优先级,线程2是低优先级,当线程1运行过程中处于阻塞时,线程2得到时间片,线程2这次进入了一
个临界区,我们都知道临界区内的资源是不会被其它线程访问的,当线程2正运行时,线程1已经从阻塞状
态转变为运行状态,而这次线程1却要访问线程2的资源,这个资源却被临界区锁定,那么线程1只能等待
,等待线程2从临界区中运行结束并释放资源的独占权。但是线程2却永远不会得到时间片,因为CE保证高
优先级线程会先运行。这时程序就会处于死锁状态。当然系统不会死锁,因为还有更高优先级的线程、驱
动程序在运行。对于这种情况,CE采取优先级转换的办法来解决。就是当发生这种情况时,内核将线程2
的优先级提高到线程1的优先级水平。这样线程2就可以执行完临界区代码了,线程1也就能够访问资源了
。然后内核再恢复线程2原来的优先级。
挂起一个线程使用SuspendThread函数。参数只有一个――线程的句柄。要说明的是如果要挂起的线
程正调用一个内核功能,这时执行此函数可能会失败。需要多次调用此函数直到函数返回值不为
0xFFFFFFFF,说明挂起成功。恢复线程使用ResumeThread函数。参数也只有一个――线程的句柄。
关于线程本地存储器和纤程,实际用到的时候非常少,这部分知识可以参考《Windows核心编程》。
来源:(http://blog.sina.com.cn/s/blog_537bca2a0100095i.html) - wince驱动开发15_猫头鹰_新浪博客
如果EnterCriticalSection将一个线程置于等待状态,那么该线程在很长时间内就不能再次被调度。实际上,在编写得不好的应用程序中,该线程永远不会再次被赋予CPU时间。TryEnterCriticalSection函数决不允许调用线程进入等待状态。它的返回值能够指明调用线程是否能够获得对资源的访问权。TryEnterCriticalSection发现该资源已经被另一个线程访问,它就返回FALSE。在其他所有情况下,它均返回TRUE。运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行某些其他操作,而不必进行等待。如果TryEnterCriticalSection函数确实返回了TRUE,那么CRITICAL_SECTION的成员变量已经更新。
8. BIB 文件格式
ROMIMAGE 使用二进制映像生成器 (BIB) 文件来对它应当如何配置 ROM 进行配置。BIB 文件只是以关键字定义四个不同节的纯文本文件。
模块节以在独立的一行上的关键字 MODULES 进行标识。在模块节中,列出了将要就地执行 (XIP) 的代码的可执行文件模块。文件节(关键字 FILES)列出了要放在映像中的其他文件(位图、数据文件、HTML 页等等)。它还可以指定不是专门用于 XIP 的可执行文件模块。很少使用的诊断应用程序很适合被指定在这里。默认情况下,文件节中的项目经过了压缩,以减小大小。
模块和文件节的条目的语法非常简单:
<Target Name> <Whitespace> <Workstation path> <memory Section> <flags>
<目标名>是出现在 ROM 中的该文件的名称。<工作站路径>是 ROMIMAGE 将用来查找实际文件的路径(通常基于 $(_FLATRELEASEDIR))。内存节将是几乎没有例外的“NK”。(启动加载程序是一个常见的例外)。
下表总结了这些标志:
标志 用途
C 压缩(默认用于文件节)
U 未压缩(默认用于模块节)
R 只压缩资源
H 隐藏文件
S 系统文件