网络驱动程序编程要点
编写 Windows 2000 的任何网络驱动程序时通常都需要考虑的几点问题
1 可移植性
2 多处理器支持
3 IRQLs
4 同步和指示
5 包结构
6 使用共享内存
7 异步I/O和完成函数
1. 可移植性
NDIS 驱动程序应很容易在支持 Windows 2000 的平台间移植。一般说来,从一个硬件平台移植到另一个平台只需要将它在兼容系统的编译中重新编译即何。
驱动程序开发者应当避免调用与操作系统相关的函数,因为这将使他们的驱动程序不可移植。应用 NDIS 函数替换这些调用,只调用 NDIS 函数将使代码在支持 NDIS 的微软操作系统中可移植, NDIS 为编写驱动程序提供了很多支持函数,并且直接调用操作系统是不需要的。
驱动程序应用 C 来编写。更近一步,驱动程序代码应当限制在 ANSI C 标准并且应当避免使用不被其他兼容系统编译器支持的语言特性。驱动程序代码编写不应当使用任何 ANSI C 标准指明为“ implementation defined” 的特性来编写。
驱动程序应当避免使用任何在不同平台上大小和结构变化的数据类型,驱动程序代码不应该调用任何 C 的运行时库函数,而是限于调用 NDIS 提供的函数。
在内核模式下漂移指针是不允许的。试图运行这样的操作将是一个致命的错误。
如果驱动程序代码要支持特殊平台的特性,那么这些代码应当包含在 #ifdef 和 #endif 声明之间。
2 . 多处理器支持
编写的代码应当可以在多处理器系统中安全运行。这对于编写可移植的 Windows 2000 驱动程序是很重要的。一个网络驱动程序必须使用 NDIS 函数库提供的多处理器安全保证。
在单处理器环境下,在一个时刻单处理器只运行一条机器指令,既使这样,当包到达或时间中断发生时,对于NIC或其他设备执行的中断也可能发生。典型的,当正在操纵数据结构时,例如它的队列时,驱动程序对NIC发出停用中断来操纵数据,随后再发生可用中断。许多线程在单处理器环境下表现的好像是在同一时刻运行的,但是实际上它们却是在独立的时间片上运行的。
在多处理器环境下,处理器同时可运行多条机器指令,一个驱动程序必须异步化,这使得当一个驱动程序函数正在操纵一个数据结构时,同样的在其他处理器运行的驱动程序函数不能修改共享的数据。在一个SMP机器中,所有的驱动程序代码都要重新装入,为了消除这种资源保护问题,Windows 2000驱动程序使用自旋锁。
3. IRQL
所有 NDIS 调用的驱动程序函数都运行在系统决定的 IRQL 下, PASSIVE_LEVEL<DLSPATCH_LEVEL<DIRQL 中的一个。例如,一个微端口初始化函数、挂起函数、重启函数和有时的关闭函数通常都运行在 PASSIVE_LEVEL 下。中断代码运行在 DIRQL 下,所以 NDIS 中层或协议驱动程序从不运行在 DIRQL 下。所有其他的 NDIS 驱动程序函数运行在 IRQL<=DISPATCH_LEVEL 下。
驱动程序函数运行于的 IRQL 将影响调用什么样的 NDIS 函数。特定的函数只可在 IRQL PASSIVE_LEVEL 下调用,其他的函数可在 DISPATCH_LEVEL 或更低层调用。一个驱动程序的编写者应当检查每一个 NDIS 函数的 IRQL 限制。
任何与驱动程序的 ISR 共享资源的驱动程序函数必须能将它的 IRQL 升级到 DTRQL 来防止争用情况的发生, NDIS 提供了这种机质。
4. 同步和指示
当两个线程共享可被同时访问的资源时,无论是单处理机还是 SMP ,同步是必须的。例如,对于一个单处理机,如果一个驱动程序正在访问一个共享资源时,被一个运行在更高 IRQL (例如 ISR )的函数中断时,必须保护共享资源以阻止这种争用的发生而使资源处于不确定状态。在一个 SMP 中,两个线程可以在同一时刻运行,在不同处理器上并且试图来修改同一数据的访问必须同步。
NDIS 提供了自旋锁可以用来对在同一 IRQL 下运行的线程间访问共享资源实现同步。当两个线程在不同 IRQL 下访问共享资源时, NDIS 提供了一种机制来临时 提 高低 IRQL 代码的 IRQL ,以使得访问共享资源串行化。
NDIS提供下面四种机制来保证同步:
自旋锁
自旋锁提供了一个用来保护共享资源的同步机制,这种资源是单处理器或一个多处理机下的、运行在 IRQL>PASSIVE_LEVEL 下的、内核模式中的线程所共享使用的。一个自旋锁在同时运行在一个 SMP 机上不同的执行线程之间提供同步。一个线程在访问保护资源前获得一个自旋锁。自旋锁使得任务线程中只有持有自旋锁的线程可使用资源。一个等待自旋锁的线程将在试图获得锁时间内循环,直到持有锁的线程释放为止。
自旋锁还存在着一个不太明显但很重要的事实:你仅能在低于或等于 DISPATCH_LEVEL 级上请求自旋锁,在你拥有自旋锁期间,内核将把你的代码提升到 DISPATCH_LEVEL 级上运行。在内部,内核能在高于 DISPATCH_LEVEL 的级上获取自旋锁,但你和我都做不到这一点。 当 KeAcquireSpinLock 获取自旋锁时,它会把 IRQL 提升到 DISPATCH_LEVEL 级上。当 KeReleaseSpinLock 释放自旋锁时,它会把 IRQL 降低到原来的 IRQL 级上。如果你知道代码已经处在 DISPATCH_LEVEL 级上,你可以调用两个专用函数来获取自旋锁。 KeAcquireSpinLockAtDpcLevel 及 KeReleaseSpinLockFromDpcLevel 。一个编写很好的网络驱动程序应该会减少自旋锁持有的时间。
一个典型的使用自旋锁的例子是保护一个队列。例如,微端口发送函数MiniportSend将协议驱动程序传来的包进行排队。因为其他驱动程序函数也使用这个队列,MiniportSend必须用一个自旋锁保护这个队列使得在一个时刻只有一个线程可操纵这个队列。Miniport Send获得自旋锁,添加包到队列后释放自旋锁。使用自旋锁保证持锁线程是唯一修改队列的线程,同时使得包被安全地添加到队列中。当NIC驱动程序从队列中取走包时,通过同样的自旋锁保护这个访问。当执行指令修改队列头或任何队列组成域时,驱动程序必须用自旋锁保护队列。
避免死锁问题
Windows 2000 并不限制网络驱动程序同时持有多于一个的自旋锁。但是,驱动程序的某部分在持有自旋锁 B 时,试图获得自旋锁 A ,并且其他部分在持有锁 A 时,试图获得自旋锁 B 时,死锁就会发生。如果要获得多于一个的自旋锁,驱动程序应当通过强制以某一顺序获得锁来避免死锁,这就是说,如果一个驱动程序强制在获得自旋锁 A 之后才可获得锁 B ,那么上述情况就不会发生。
总得来说,使用自旋锁将对系统性能带来负面效应,所以驱动程序不应当使用许多锁。
时钟
时钟被用来轮询或进行超时操作的。一个驱动程序可以产生一个时钟并与一个函数关联上。当一个特定周期时钟期满时,调用相关函数。时钟可以是一次的或周期性的,一但设置了一个周期时钟,当每个周期结束时都会触发,直到它被完全清除掉为止。一次性时钟在触发后必须重新设置。
时钟通过调用 NdisMInitializeTimer 来产生和初始化,并且通过调用 NdisMsetTimer 来设置,也可调用 NdisMsetPeriodicTimer 设置周期时钟。如果使用了一个非周期时钟,那么通过调用 NdisMSetPeriodicTimer 重新设置时钟。通过调用 NdisMCancelTimer 可以清除时钟。
事件
事件在两个执行线程之间实现同步操作。一个事件通过一个驱动程序装入并且通过调用 NdisInitializeEvent 初始化。一个运行在 IRQL PASSIVE_LEVEL 下的线程调用 NdisWaitEvent 来将自身转入等侯状态。当一个驱动程序线程等待一个事件时,它指定了最大等待时间即等待事件的时间。当调用 NdisSetEvent 使时间得到信号量,或最大等待时间段结束时,它们两个无论是谁先发生时都将结束线程等待状态。
典型的,事件是通过相互协调的线程调用 NdisSetEvent 来设置的。事件被创建时是没有信号量的,但为了指示等待线程,它必须要设置信号量,事件将一直处于保持有信号状态,直到 NdiResetEvent 调用后为止。
5. 包结构
通过一个协议驱动程序可以分配NDIS包、填充数据,并且将它传递到下层的NDIS驱动程序,以便将数据发送到网络上。一些最底层的NIC驱动程序分配包用来保存接收到的数据,并将包传递到对应的高层驱动程序。有时,一个协议驱动程序分配一个包,并且通过一个请求将它传给NIC驱动程序,以使NIC驱动程序将接收到的数据拷贝到提供的包中。NDIS提供函数用来分配和操纵构成包的子结构。
包描述符 Flags : physicallPageCount : ToralLenth : FirstBuffer : P … |
缓存描述符: StartVirtualAddress : P ByteOffset ByteCount PhysicalPage [] Next P....
|
缓存
|
缓存描述符: StartVirtualAddress : P ByteOffset ByteCount PhysicalPage [] Next P....
|
虚拟内存 |
虚拟页 |
虚拟页 |
虚拟页 |
虚拟页 |
虚拟页 |
物理内存 |
物理页 |
物理页 |
物理页 |
物理页 |
物理页 |
图 1: NDIS包结构
具体详情可以参考:http://www.ndis.com/papers/ndispacket/ndispacket1.htm
6 . 使用共享内存
用作总线管理DMA设备的微端口驱动程序必须为NIC和NIC驱动程序分配共享内存。当在一个驱动程序和它的NIC之间共享cache时,特别的预防是必须的。在某种结构下,必须采取特别步骤来保证内存一致,因为NIC可以直接访问共享的物理内存,而NIC驱动程序却要通过cache访问内存。这就引起驱动程序和NIC访问内存的不同,即使它们看起来在同一位置。
7. 异步I/O和完成函数
因为在一些网络操作中有继承的因素,许多由 NIC 驱动程序提供的上层函数和协议驱动程序提供的下层函数被设计成支持异步操作,而不是用 CPU 消耗一定时间的循环来等待一个任务的完成或硬件事件的指示,网络驱动程序依赖处理许多异步操作的能力。
通过使用完成函数来支持异步网络 I/O 。以下的例子将说明网络的 send 操作如何使用一个完成函数,同样的机制也存在一个协议或 NIC 驱动程序的其他操作中。
当协议驱动程序调用 NDIS 发送一个包时, NDIS 调用 NIC 驱动程序的 MiniportSend 函数发送请求, NIC 驱动程序试图立即完成这个请求并且返回一个恰当的状态值。对于 同 步操作,可能返回 NDIS_STATUS_SUCCESS 作为发送成功的标志, NDIS_STATUS_RESOURCES 和 NDIS_STATUS_FAILURE 表明有某些失败。
但是一个发送操作要花费一些时间来完成,此时 NIC 驱动程序 ( 或 NDIS) 可将包排队并且等侯 NIC 指示发送操作的结果。 NIC 驱动程序的 MiniportSend 函数可以通过返回一个 NDIS_STATUS_PENDING 的状态值来异步处理这个操作,当 NIC 驱动程序完成了发送操作后,包调用完成函数 NdisMSendComplete 在调用中传递指向一个已被发送的包的描述符的指针。这个信息会传给协议驱动程序,指示完成了操作。
许多需要一定时间来完成的驱动程序操作用完成数来完成支持异步的操作。这种函数有同一形式的名字 NidisMXxxComplete 。不仅可用于发送和接收函数,完成函数也可用于查询、配置、重新设置硬件、状态指示、指示收到数据和传送收到数据。