开发驱动程序的过程二

Unicode 字符串

所有的WIN2000操作系统的字符串是作为Unicode 字符串存储的。使用16 bits来记录每个字符,这样使它容易的转移应用程序和操作系统到世界上大多数的语言环境中。Unicode 字符串是工业标准,除非特别说明,任何驱动程序发送到win2000或者受到的字符串都是Unicode 字符串。在用户缓冲区和驱动程序之间的数据传输不是必须使用Unicode 字符串,对于Win2000I/O子系统,数据传输被认为是二进制和透明的。

Unicode 字符串数据类型

  现在,Unicode 字符串数据类型是C语言规范的一部分,使用下列方法来使用Unicode 字符串:

1. 将字符串常量加上前缀L例如,L”Some text”是一个Unicode 字符串,而”Some text”8-bit ANSI

2. 使用wchar_t数据类型。DDK头文件定义了WCHARwchar_tPWSTRwchar_t* 

3. 使用常数UNICODE_NULL去终止Unicode 字符串,UNICODE_NULL被定义为16 bits的零。

Win2000系统例程使用Unicode结构(UNICODE_STRING)工作,如表5.2所示,这个结构的目的是使操作系统容易的传递和管理Unicode 字符串。虽然C语言的标准库提供了执行Unicode 字符串公共的操作的函数(例如,wcscpy是像strcpy一样的操作,但是数据是Unicode 字符串),但是这个环境不能被内核模式的驱动程序代码利用。

部分

内容

USHORT

当前字符串的长度,单位为Byte

USHORT MaximumLength

最大字符串的长度,单位为Byte

PWSTR Buffer

指向驱动程序分配的字符串数据的指针

5.2 UNICODE_STRING结构

顺便提一下,DDK也定义了ANSI_STRING结构,它和UNICODE_STRING一样,只是缓冲区的偏移量是char*类型,一些Rtl转换例程需要ANSI_STRING数据类型。

Unicode函数

意义

RtlInitUnicodeString

使用一个NULL为终止符初始化UNICODE_STRING结构

RtlAnsiStringToUnicodeSize

计算将一个ANSI字符串转换为Uncode字符串需要的字节数

RtlAnsiStringToUnicodeString

转换ANSI字符串为Unicode字符串

RtlIntegerToUnicodeString

将一个整形数转换为Unicode字符串

RtlAppendUnicodeStringToString

连接两个Unicode字符串

RtlCopyUnicodeString

复制源字符串到目的字符串

RtlUpcaseUnicodeString

转换Unicode字符串为大写

RtlCompareUnicodeString

对比两个Unicode字符串

RtlEqualUnicodeString

测试两个Unicode字符串是否相等

5.3  部分的Unicode函数

使用Unicode

    kernel提供了一些使用ANSIUnicode字符串的函数。这些函数代替标准的C语言关于Uncode

的函数库,表5.3中列出了一些。Win2000DDK提供了权威的函数的目录和这些函数的用法。一些函数的调用被约束在一定的IRQL,所以必须注意它们的使用方法。最安全的做法是调用Rtl Unicode函数的IRQLPASSIVE_LEVEL上。

起初,使用Unicode字符会感到比较难受,因为Unicode字符的长度是两倍的Unicode字符串内容的长度,C语言的程序员的一个字符是一个byte的思想根深蒂固,但是Unicode字符的将这个规则改变了。使用Unicode字符的时候,应该考虑以下几点:

1. 记得Unicode字符串的字符数与它的字符串长度是不同的。要小心Unicode字符串长度的计算。

2. 不要假设字符排列顺序或者大写字符和小写字符的关系。

3.  不要假设256个字符的字符串长度已经足够了。

                          中断的同步

编写执行在多IRQL可重入的代码需要注意同步问题。这个部分将介绍这个问题的解决。

问题的产生

  如果执行在两个不同的IRQLs代码尝试同时访问同一个数据结构,这个数据结构可能被破坏。图5.1说明了这个问题。

  请考虑如下问题:

    1. 假设一段低IRQL的代码决定去修改foo数据结构中的几个数。如设置foo.x1

2. 这时一个中断产生,一段高优先级的代码得到处理器的控制权,这段代码将设置foo.x10 foo.y20

3. 当这段高优先级的代码离开,控制权又回到了那段低IRQL的代码,它设置foo.y2

4. 这时foo数据结构产生了矛盾,x10y2,不是我们想得到的结果。

5.1中断的同步问题

当然,当多个线程使用同一个的共享数据也会产生相似的结果。当一个代码尝试增加这个数据,这个改变的数据将占用处理器的寄存器一段时间。如果这个线程在传送这个寄存器的数据回到变量之前被中断,同样的问题产生。

下面介绍一些解决这个问题的方法:

中断阻塞

  低的IRQL的例程可以通过防止被中断来避免同步的问题,它可以暂时的提高CPUIRQL,完成操作之后使IRQL回到原来的水平。这个技术称作中断阻塞。表5.4列出了驱动程序更改CPU IRQL内核函数。

    函数名

     意义

KeRaiseIrql

更改CPU IRQL到指定的值

KeLowerIrql

降低CPU IRQL

KeGetCurrentIrql

返回这个调用的CPU IRQL

5.4 更改CPU IRQL内核函数

阻塞中断的规则

  使用中断阻塞必须遵循的规则如下:

1. 接触的每一个共享数据结构数据的代码必须使用同一个IRQL,除非IRQL在选择的级别上否则不能访问共享的数据结构。

2. 如果低的IRQL的代码提升到允许的IRQL,它必须尽可能快的恢复原来的IRQL。如果不遵循这个规则,根据不同的阻塞级别,其它的硬件中断可能被阻塞很长时间。

3. 如果一个驱动程序代码的IRQL被提升了,一定不能将它的IRQL降到原来的IRQL以下。违反这个规则将破坏整个系统的优先权机制。

使用DPC同步

  DPC是另外一个避免数据冲突的方法。如果所有的内核模式组件仅仅使用DPC例程去访问共享的数据,就不会产生冲突的问题,因为DPC例程总是串行的执行。使用DPC来进行同步的主要原因是它们运行较低的IRQL

  DPC例程的另一个优点是内核模式的DPC派遣器自动的处理多处理器的同步问题。

多处理器同步

  在多处理器系统中,改变一个处理器的IRQL并不影响其它处理器的IRQL,因此IRQL的提高仅仅保护本地数据。为了在多处理器的环境中保护数据,WIN2000使用了叫做自旋锁的同步对象。

自旋锁如何工作

自旋锁是一个与一定数据结构相关联的互斥对象。当内核模式的代码想要去访问被保护的数据结构,必须先请求相关联的自旋锁的拥有权,因为同一时刻只有一个处理器可以得到自旋锁的拥有权,所以这个数据是安全的。任何处理器请求一个已经被拥有的自旋锁的操作将被悬挂直到自旋锁变的可用。图5.2说明了这个过程。

自旋锁总是在特定的IRQL下工作。这样有在本地处理器阻塞危险中断和防止同步问题的作用。当一个处理器等待自旋锁的时候,所有相同或者更低IRQL的代码在这个处理器上被阻塞,提高IRQL水平和请求自旋锁是非常重要的。

使用自旋锁

内核提供两种重要的自旋锁,它们使用的IRQL不同:

1. 中断自旋锁。同步和提供访问驱动程序的被多个设备共享的数据结构的例程,它在与设备相关联的DIRQL上获得。

2.  Executive自旋锁。它保护操作系统的数据结构,它们相关联的IRQLDISPATCH_LEVEL

当一个设备使用中断自旋锁的时候,操作是相当简单的。函数KeSynchronizeExecution将在介绍中断I/O的时候讨论。

5.2 自旋锁的工作过程

使用Executive自旋锁的过程相当复杂,必须按照以下的步骤来使用自旋锁:

1. 决定什幺数据必须被保护和需要多少自旋锁。较多的自旋锁允许程序更大程度的同步执行,但是同时获得多于一个的自旋锁可能引起死锁。

2. 为每一个自旋锁准备一个KSPIN_LOCK数据结构。自旋锁必须存储在非分页池。通常,自旋锁在设备或者控制器的Extension中声明。

3. 通过KeInitializeSpinLock函数初始化自旋锁一次,这个函数可以在任何IRQL上调用。大多数情况下在DriverEntry例程中调用。

4. 在访问任何自旋锁保护的数据之前调用KeAcquireSpinLock,这个函数提高IRQLDISPATCH_LEVEL,获得自旋锁后回到之前的IRQL。它必须被低于或者等于DISPATCH_LEVEL IRQL的代码调用。如果代码的IRQL已经在DISPATCH_LEVEL,这时调用KeAcquireSpinLockFromDpcLevel会更加有效。

5. 当访问资源完成后,使用KeReleaseSpinLock函数释放自旋锁。调用这个函数的代码的IRQLDISPATCH_LEVEL 它将把IRQL恢复到原来的值。如果原来的值是DISPATCH_LEVEL,这时调用KeReleaseSpinLockFromDpcLevel会更加有效一些,它释放自旋锁而不改变IRQL

一些驱动程序支持例程(像互锁列表和队列)使用Executive自旋锁来保护数据。在这种情况下,只需要初始化自旋锁就可以了,管理互锁对象的例程自己请求和释放自旋锁。

使用自旋锁的规则

  使用自旋锁不是十分困难,但是必须遵循以下几点:

1. 尽快释放自旋锁,因为当一个处理器拥有它的时候,其它的处理器的执行可能被阻塞。官方的DDK推荐不要拥有一个自旋锁超过25ms

2. 当拥有自旋锁的时候,不要引起任何软件或者硬件的异常。这样会引起系统崩溃。

3. 当拥有自旋锁的时候,不要访问任何分页的代码或者数据。这样可能导致缺页故障。

4. 当拥有自旋锁的时候,不要尝试请求处理器已经拥有的自旋锁。这将导致死锁,因为处理器等待它自己释放自旋锁。

5. 避免设计同一个时刻需要多个自旋锁的程序。除非特别小心,否则会死锁。如果必须使用这种情况,确定请求它们必须要一个固定的顺序,释放它们的顺序则于这个顺序相反。

链表

驱动程序有时需要维持一个链表数据结构。

单链表

  在使用单链表之前,先声明一个SINGLE_ LIST_ENTRY类型的链表头。它也是链表指针的数据结构。确实,SINGLE_LIST_ENTRY结构有且仅有一个成员: Next,链表头必须被手动初始化,示范如下:

typedef struct _DEVICE_EXTENSION

{     :    

SINGLE_LIST_ENTRY listHead;           // 声明链表头

}   DEVICE_EXTENSION, *PDEVICE_EXTENSION;

     :   

pDevExt->listHead.Next = NULL;            // 初始化链表

调用PushEntryList或者PopEntryList来添加或者删除链表项,操作从链表的前端进行。链表可以存储在分页或者非分页存储器中。记得这些函数的使用可能需要同步。

WIN2000的内核对被自旋锁保护的单链表也提供方便的支持,当运行在低于或者等于DISPATCH_LEVEL IRQL的例程共享一个单链表的时候,这种保护是重要的。使用这种单链表时,除了正常的建立链表头之外,还要初始化一个Executive自旋锁。

typedef struct _DEVICE_EXTENSION

{     :   

 SINGLE_LIST_ENTRY listHead;  // head pointer

     KSPIN_LOCK listLock;   // declare list lock

} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

     :  

KeInitializeSpinLock(&pDevExt->listLock);

pDevExt->listHead.Next = NULL;

一旦链表头和自旋锁被声明和初始化之后,ExInterlockedPushEntryList ExInterlockedPopEntryList可以方便的访问保护的链表。使用这两个函数的代码IRQL必须等于或者低于DISPATCH_LEVEL,整个连表必须在非分页的存储器中,因为系统将连接和断开它们在DISPATCH_LEVEL IRQL

双向链表

  在使用单链表之前,先声明一个LIST_ENTRY类型的链表头。它也是链表指针的数据结构。LIST_ENTRY结构有两个成员: FlinkBlink,表示前向和后向的指针。链表头使用一个InitializeListHead例程初始化。

typedef struct _DEVICE_EXTENSION

{     : 

 LIST_ENTRY listHead;   // head pointer

} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

     :  

InitializeListHead( &pDevExt->listHead );

使用InsertHeadList或者InsertTailList给链表中添加项目,使用RemoveHeadList或者RemoveTailList删除项目,使用IsListEmpty来判断链表是否为空。它可以被存储在分页或者非分页的存储器中,但是这些函数不支持同步。

WIN2000也支持互锁的双连表,使用这种单链表时,除了正常的建立链表头之外,还要初始化一个Executive自旋锁。

typedef struct _DEVICE_EXTENSION

{     :   

LIST_ENTRY listHead;   // head pointer

KSPIN_LOCK listLock;   // the list's lock

} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

     : 

 KeInitializeSpinLock( &pDevExt->listLock );

InitializeListHead( &pDevExt->listHead );

在调用ExInterlockedInsertTailListExInterlockedInsertHeadList 或者ExInterlockedRemoveHeadList时,传递了自旋锁。使用这几个函数的代码IRQL必须等于或者低于DISPATCH_LEVEL,整个连表必须在非分页的存储器中。

删除链表块

  当从链表上删除一个块时,函数返回一个指向LIST_ENTRY或者SINGLE_LIST_ENTRY结构的指针。因为这个结构仅仅是数据结构的一部分,而我们想要的是整个数据结构的指针。一个简单的技术可以确定LIST_ENTRY结构是否是整个数据结构的第一个部分,是的话,指向LIST_ENTRY结构的指针同样是整个数据结构的指针。否则,必须通过计算来得到。幸运的是,WIN2000提供了一个CONTAINING_RECORD宏是这个操作变的容易。

参数

意义

Address

删除函数返回的地址

Type

整个结构的数据类型

Field

整个结构中域

Return value

指向整个数据结构的指针

5.5 CONTAINING_RECORD 宏的各个参数 

  以下的代码片段说明了CONTAINING_RECORD宏的用法:

typedef struct _MYBLOCK

{    ULONG ulSomeThingAtTopOfBlock;

     :    

LIST_ENTRY listEntry;   

 :   

 } MYBLOCK, *PMYBLOCK;

     : 

PMYBLOCK pMyBlock;   

PLIST_ENTRY pEntry;       

 :

pEntry = RemoveHeadList( &pDevExt->listHead );

pMyBlock = CONTAINING_RECORD( pEntry, MYBLOCK, listEntry);

无论什幺原因,要注意LIST_ENTRY域不能放在MYBLOCK结构的第一个偏移地址处。因此,被RemoveHeadList函数返回的地址需要转换成整个结构的地址。

                 小结

这一章覆盖了在WIN2000环境下开发设备驱动程序的一般的指导方针,它是编写驱动程序的基础。在下一章中,将完成一个真正的驱动程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值