第三章 网络驱动器编程注意事项
这一章主要描述写一个Windows2000系统下的网络驱动器一般要注意的事项。在Windows2000写网络驱动器程序需要考虑许多一般驱动程序的相同设计目的:
简捷的跨平台性
可升级到多处理器系统
软硬件的简单配置
基于对象的接口
异步I/O的支持
下面各节为windows2000网络驱动器写作者解释这些设计目标的含义:
跨平台的简捷性在3.1节中解释
多处理器支持在3.2节中
IRQL在3.3节中
同步和通知在3.4节
包结构在3.5节
使用共享存储在3.6节
异步I/O和完成函数在3.7节
3.1 简捷的跨平台性
NDIS驱动器应该很容易写成所有支持windows2000平台下的简捷式驱动程序。一般而言,从一个平台到另一个平台,只需要用兼容的系统编译器重新编一下就可以了。驱动器开发人员应该尽量避免调用操作系统的特殊函数,因为这可能导致驱动器不支持跨平台性。相反应该使用NDIS等价的函数。使用NDIS函数调用使得代码可以在所有微软支持NDIS的操作系统上运行。NDIS输出了支持编写驱动程序的丰富的函数集,当然也就没有表直接调用操作系统函数了。
应该用C编写驱动程序,进一步,驱动器代码应该被限制在ANSI C的标准,并且应该避免使用任何不被其他系统兼容编译器支持的语言特征。驱动器代码也不应该包含任何ANSI C标准指明为“实现定义”的语言特征。
驱动器代码应该避免平台相关的数据类型,因为它们的尺寸和布局是各平台变化的。驱动器代码不应该调用任何C运行时库函数,应该以NDIS提供的函数代替。
在核模式下不允许浮点操作,致力于这样的操作将引起重大错误。
如果驱动器包含平台相关的代码,这些代码应该被封装在#ifdef 和 #endif语句之间。
3.2 对多处理器的支持
编写代码能在具有一个或多个处理器并发执行的机器上安全执行是开发windows2000驱动器程序最基本的要求。网络驱动器必须是多处理器安全的,并且,必须使用NDIS库函数。
在单处理器环境下,处理器一次只执行一个机器指令,即使NIC或其它设备在数据包到达时产生中断,或定时器中断,也是这样。典型地,在处理数据结构如包队列时,驱动器禁止NIC上的中断,执行数据处理,然后再允许中断。在单处理器环境下,许多线程表现为同时运行但是实际上是在不同的时间片上交叉运行。
在多处理器环境那个中,处理器同时运行几个机器指令。因此,驱动器程序必须同步,以使当一个驱动器函数处理公共数据结构时,在另一个处理器上执行的相同的或其它的驱动器函数不要致力于在同一时刻修改共享的数据。所有驱动器代码在SMP机器上都是可重入的。为了xx这种资源保护问题,windows2000设备驱动使用自旋锁(spin locks)。
对于NDIS小口驱动,NDIS库函数处理了许多这类多处理问题。NDIS库队列请求和串行化调用标准的小口函数,除非有下面情况发生:
NIC是为无连接网络媒体而设计的,并且小口驱动是非串行化设计和实现的,无论是因为NIC有一些板级包队列的支持和同步,还是因为驱动器作者原意在小口驱动中管理包,队列和同步收发。
NIC的设计是面向连接媒体的。NDIS假设任何面向连接的NIC都是非串行的小口驱动。
无论如何,绝大多数无连接NIC驱动都是串行的小口驱动,因此,它们依靠NDIS来管理包队列,同步和串行收发。
3.3 IRQL层
有NDIS调用的每一个驱动器函数都运行在一个系统确定的IRQL层,是PASSIVE_LEVEL < DISPATCH_LEVEL < DIRQL层之一。例如,小口的初始化函数,停止函数,重置函数有时关机函数运行在PASSIVE_LEVEL。中断代码运行在DIRQL。所以,一个NDIS中间驱动和协议驱动不可能运行在DIRQL。所有其他NDIS驱动器函数运行在IRQL <= DISPATCH_LEVEL.
驱动程序函数运行的IRQL层影响它所调用的NDIS函数。某些函数仅可以在IRQL,PASSIVE_LEVEL层上调用,另外一些可以在DISPATCH_LEVEL或更低层上调用。驱动器程序开发者应该检查每一个NDIS函数的IRQL限制。
任何与驱动器的ISR共享资源的函数都必须能够提升它的IRQL层到DIRQL层以防止环境的异化。NDIS提供这样的机理。
3.4 同步与通知
无论是在单处理器的机器上函数SMP机器上,任何两个线程执行共享资源访问时,同步都是必须的。例如在单处理器机器上,如果一个驱动器函数正在访问共享资源,并且被另一个运行在更高IRQL层的函数所中断,就像一个ISR,这个共享资源就必须被保护以防止环境异化所引起的资源状态的模糊。在SMP机器上,两个线程能够在不同处理器上同时运行,并且都想修改同一个数据,这样的访问必须被同步。
NDIS提供的自旋锁可用于在相同的IRQL层执行的线程间访问共享资源的同步化操作。当两个共享资源的线程执行在不同的IRQL层时,NDIS提供一种机理,暂时提升低层IRQL代码的IRQL层,以使访问共享资源可以被串行化。
当一个线程依赖于线程外事件发生时,通知就是必须的。例如,驱动器可能需要被告知过了多少个时间周期以检查其设备状态。或NIC驱动器可能需要执行周期操作,比如表决信号,定时器提供这样的机理。
事件提供了两个执行线程用于同步操作的机理,例如,一个小口NICe驱动可能想要通过写入设备来检测它的NIC中断。它必须等待一个中断来通知驱动器这个操作是否成功。事件也可用于在等待中断完成的线程和处理中断线程之间进行同步化操作。下面是对这些NDIS机理的描述:
自旋锁
自旋锁是为核模式线程运行在IRQL > PASSIVE_LEVEL层上保护共享资源提供的同步化机理,无论是单处理器系统还是多处理器系统。自旋锁在运行于SMP机器上的各个并发运行的线程之间处理同步化操作。线程在访问保护资源之前获得自旋锁,自旋锁维持所有线程,但是只有一个能握有可以使用这个资源的自旋锁。一个等待自旋锁的线程循环查询自旋锁直到它被握有锁的线程释放。
自旋锁的另一个特征是关联IRQL。试图占有自旋锁的线程临时提升它的IRQL到与自旋锁关联的IRQL层,这就防止了同一个处理器上的所有低层IRQL线程被高层线程占先执行。
运行在较高IRQL上的线程可以占先执行,但是这些线程不能占有自旋锁,因为已经有一个底层的线程占有了它。因而,在同一个处理器上不能有其他线程企图占有自旋锁,除非他已经由占有它的执行线程释放。一个好的网络驱动,应该最小化握有自旋锁的时间。
自旋锁的典型应用是保护队列。例如,小口驱动发送函数MiniPortSend,可能由协议驱动器传过来一个包到队列。由于其他驱动器函数也使用这个队列,MiniPortSend就必须用自旋锁保护这个队列,以使每次只能有一个线程处理链或内容。MiniPortSend占有自旋锁,添加包到队列,然后释放自旋锁。使用自旋锁来保证当有包要安全地添加到队列时,握有锁的线程是{wy}修改队列链的线程。在NIC驱动器从队列中取出包的时候,这样的访问由同样的自旋锁保护。在执行修改队列头或任何组成队列的链字段的指令时,驱动器必须用自旋锁保护队列。
驱动器还要特别仔细,不要过分保护队列,例如驱动器可以在包放入队列之前执行某些操作,(如,在网络驱动器保留的字段上填写包含长度的字段)。驱动器也可以做一些在自旋锁保护范围以外的操作,但是做这些操作必须在包被放入队列之前进行。一旦包放入到队列,并且执行程序释放了自旋锁,驱动器就应该认为其它线程立刻能够取出数据包。
避免死锁问题
Windows2000并不限制网络驱动器同时握有多于一个自旋锁。然而,如果一段驱动器企图占有自旋锁A而已经拥有自旋锁B,而另一节企图占有自旋锁B,而已经拥有自旋锁A,这就导致了死锁。如果一个驱动器占有多个自旋锁,它就应该通过强迫占有命令来避免死锁。即,如果驱动器在握有自旋锁B之前强迫占有自旋锁A,则上面描述的情况据不能发生。
就是用自旋锁冲突而言,一般,驱动器不应该使用多个自旋锁。偶尔,有函数明显使用两个自旋锁(例如发送和接受函数),它们基本没有重叠。对于两个操作无关函数在不同处理器上使用多个自旋锁可能是值得的。
定时器
定时器用于xx表决或到时提醒操作。驱动器建立定时器和相关函数,这个相关函数在定时器指定周期到时后被调用。定时器可以试一次性的或周期的。一旦设定了周期性定时器,它将在到时后,连续触发,直到明显操作xx为止。一次性定时器每次触发后必须被重置。
定时器可以通过调用NdisMInitializeTimer来建立和初始化,调用NdisMSetTimer来设置,对于周期性定时器调用NdisMSetPeriodicTimer设置。如果使用非周期性定时器,必须NdisMSetTimer进行重置。定时器由NdisMCancelTimerxx。
事件
事件用于两个执行线程之间的同步操作。一个事件有驱动器所分配,并通过调用NdisInitilizeEvent初始化。一个运行在IRQL= passive_level的线程调用NdisWaitEvent使其进入等待状态。驱动器线程处于等待事件状态时,它指定等待的{zd0}时间以及等待的事件。当有NdisSetEvent调用引起事件发信号时,或指定的{zd0}时间到达,无论哪一个发生,等待线程的条件都被满足而退出等待。
典型地,事件通过协操作线程调用NdisSetEvent被设置。事件在建立时是无信号的必须被设置来发信号给等待线程。事件一直都处于信号状态,直到调用NdisResetEvents。
3.5 包结构
NDIS包是由协议驱动器所分配的,充填有数据,并传送到下一层NDIS驱动器。所以,数据可以被发送到网络上。有些低层NIC驱动器分配包来放置接收数据,并传送包到上层感兴趣的驱动器。有时协议驱动器分配包并传送它到NIC驱动来请求NIC驱动拷贝接收数据到提供的包。NDIS提供一些函数来分配和处理这些组成包的子结构。
图3.1
包由下列部分组成:
包描述符,它包含有小口NIC驱动和协议驱动的私有部分,一个包的关联标志集,其意义有协操作的小口驱动器和协议驱动器所定义,这个包所包含的一定数量的物理页面,包的总长度,以及头一个缓冲描述符的指针,它映射包中的{dy}个缓冲。
缓冲描述符集。一个缓冲描述符描述了每一个缓冲开始的虚地址,这个缓冲在虚地址也中的字节偏移,缓冲的总字节数和指向下一个缓冲描述符的指针。
虚地址范围,可能生成多于一页,它们组成了由缓冲描述符描述的缓冲。这些虚页映射到物理内存。
总线控制NIC驱动器分配共享存储来接收输入包,而分配发送包存储的协议驱动器必须保证任何包含输入或外出数据的缓冲是Cache对齐的。这是必要的,因为在包发送前小口驱动能快速处理缓冲,和在向上层表达接收数据之前,快速处理接收缓冲。
3.6使用共享存储
对于总线仲裁DMA设备的小口NIC驱动器必须分配共享存储由NIC和NIC驱动使用。特别注意的是,在那种驱动器和它的NIC之间使用共享的缓存存储器时,在一定的体系结构上,必须执行特殊的步骤来保证存储器是连贯的,因为NIC驱动通过缓存来访问存储器。这可能在NIC和驱动器探测存储时引起误差,即使看上去它们在同一位置。
使用非缓存共享存储避免了大多数缓存共享存储带来的问题。然,非缓存存储器是稀有的系统资源,而且这样的存储分配能够被限制。当写到物理存储的数据必须使用小的写队列时,甚至非缓存数据可能不会立即出现在物理存储中。此外,有一些情况是既不推荐使用非缓存存储,也不可能使用非缓存存储。例如,NIC驱动重复读数据,为改进性能总是缓存操作的,另一个例子,NIC驱动可能被要求传输接收数据包到一定数量不同的协议驱动器。在这些情况下,缓存存储就会被更强烈地推荐使用。NIC驱动也应该使用缓存存储与协议驱动进行任何数据传输。
NdisMAllocatedSharedMemory可以由总线仲裁的NIC驱动器调用来分配网络适配器和NIC启动器之间{yj}共享的存储。这个函数返回共享存储的虚地址和物理地址。这两个地址一直可用,直到调用NdisMFreeSharedMemory释放这块存储。
在使用共享缓存存储时,NDIS提供提供必须由NIC驱动调用的函数来保证NIC所看到的和NIC驱动所看到的内容的一致性。在发送和接收时访问共享存储之前,NICe驱动必须调用NdisFlushBuffer和NdisMUpdateSharedMemory来保证缓存的一致性。
3.7 异步I/O和完成函数
由于某些网络操作内在的反应时间,很多有NIC驱动提供的上端函数和协议驱动提供的下端函数被设计成支持异步操作模式。与某些耗时的任务使用循环等待完成或硬件信号来浪费CPU周期不同,网络驱动器依靠异步处理绝大多数操作的能力来提高性能。
异步网络I/O由完成函数支持。下面的例子说明怎样使用完成函数进行网络发送操作,同样的机理也可以用于许多其他的由协议和NIC驱动执行的操作。
在协议驱动调用NDIS发送一个包时,导致调用NIC驱动的MiniPortSend函数,NIC驱动可以试着立即完成这个请求,并返回适当的状态值作为结果。对于同步操作,可能的响应是NDIS_STATUS_SUCCESS说明发送成功地完成,NDIS_STATUS_RESOURCES和NDIS_STATUS_FAILURE表示某种失败。
但是,当NIC驱动(或NDIS)排队包并等待NIC表述发送操作结果时,发送操作可能花费一些时间来完成。NIC驱动的MiniportSend函数可以通过返回NDIS_STATUS_PENDING状态值,异步处理这个操作。在NIC驱动完成这个发送操作后,它调用完成函数NdisMSendComplete,传送一个指针到被发送的包描述符。这个信息就被传送到协议驱动,打出完成信号。
绝大多数驱动器操作都可以请求一个延长的时间来xx支持类似于完成函数一样的异步操作。这些函数有相同的命名形式,NdisMXxxComplete。连同显然的发送和接收函数一起,完成函数对于设置,配置请求硬件重置,状态表述,接收数据表述和传输接收数据都是可用的。
发表于 @ 2009年05月30日 09:41:00 | |
|