DDK 的一些笔记,比较乱, 以后有时间再整理吧;
typedef struct _RWCONTEXT //读写的线程环境;
{
struct _URB urb; //共享的urb;
ULONG_PTR va; //mdl的开始地址;
ULONG length; //要传输的长度;
PMDL mdl; //指向当前的mdl;
ULONG numxfer; //已经传送的字节数;
} RWCONTEXT, *PRWCONTEXT;
//-----------------------
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT fdo; //当前的功能对象
PDEVICE_OBJECT NextDev; //下层驱动对象
PUSB_DEVICE_DESCRIPTOR pDevDp; //设备描述指针
PUSB_CONFIGURATION_DESCRIPTOR pCfgDp; //设备配制指针
USBD_CONFIGURATION_HANDLE hCfg; //配制句柄
USBD_INTERFACE_HANDLE hCurInf; //当前用的接口句柄
USBD_PIPE_INFORMATION PipeReadInfo; //读的管道句柄
USBD_PIPE_INFORMATION PipeWriteInfo;//写的管道句柄
int iInfNum; //接口数,为1
int iCurPipeNum; //管道数,为2
LANGID langid; //语言ID
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
2. 每个USB设备有一个或多个配置来控制其行为,每个配置中都含有一个或多个接口,接口指定软件访问硬件的方式。设备的接口具有一个或多个端点,每个USB设备在主机看来就是一些端点的结合。提取一个端点的地址、缓冲区长度、数据传输方向、类别等信息,就把一个端点封装成一个管道,利用这个管道句柄就可以方便的实现主机的一个内存缓冲区和设备多个端点之间的数据传输
USB通信模型,它表明了端点和管道所扮演的角色。整个模型分为3级结构:在最低一级,USB电缆把主控制器与设备的总线接口连接起来;在第二级,一个控制管道把系统软件与逻辑设备连接起来;在第三级,一捆数据管道把客户软件与一组接口连接起来,这些接口组成设备的功能
3. WDM驱动程序模型中的每个硬件设备都有两个驱动:功能驱动和总线驱动。功能驱动了解使硬件工作的所有细节,负责初始化I/O操作,处理I/O操作完成时所带来的中断事件,为用户提供一种设备适合的控制方式; 总线驱动程序(USB Driver,USBD)负责管理硬件与计算机的连接,实现繁琐的底层通信。总线驱动包括主控制器驱动程序(OPENHCI.SYS或者UHCD.SYS),hub驱动程序(USBHUB.SYS),一个由控制器驱动程序使用的类驱动程序(USBD.SYS)和发现USB主控制器并会装入相关功能驱动程序的PCI枚举器
4. USB设备驱动程序通过发送URB(USB Request Block)和IRP(I/O Request Packet)来使用总线驱动并完成对物理硬件的操作。设备驱动会创建一个URB并提交到总线驱动程序USBD.SYS,这样,向USBD的调用被转化为带有主功能代码为IRP_MJ_INTERNAL_DEVICE_CONTROL的IRP。然后USBD再调度总线时间,发出URB中指定的操作,完成收发数据包请求
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->
5. 有三个中断优先级(Interrupt Request Level,IRQL)对设备驱动程序有重要意义:PASSIVE_LEVEL、DISPATCH_LEVEL、DIRQL。一个普通线程运行在PASSIVE_LEVEL,它基本上和应用层运行在同一优先级下面,受系统调度的影响很大,数据包的接收效率不高。调用DDK中提供的函数调整它的运行优先级,并且直接在完成例程里发起下一次传输,发现在IRQL_ DISPATCH_LEVEL下速率较高。
提高了中断优先级后要特别注意函数的调用,而且在DISPATCH_LEVEL级以上必须使用非分页内存,否则会导致系统的崩溃。所以,不建议在此参数上做过多的修改。
6 USB基本知识:
1、端点:位于USB设备或主机上的一个数据缓冲区,用来存放和发送USB的各种数据,每一个端点都有惟一的确定地址,有不同的传输特性(如输入端点、输出端点、配置端点、批量传输端点)
2、帧:时间概念,在USB中,一帧就是1MS,它是一个独立的单元,包含了一系列总线动作,USB将1帧分为好几份,每一份中是一个USB的传输动作。
3、上行、下行:设备到主机为上行,主机到设备为下行
USB 驱动程序是绑定到USB 接口上,而不是绑定到整个USB设备上。
3. USB设备的加载过程
当USB设备接入hub或root hub后,主机控制器和主机软件(host controller & host software)能自动侦测到设备的接入。然后host software读取一系列的数据用于确认设备特征,如vendor ID, product ID, interface工作方式,电源消耗量等参数。之后主机分配给外设一个单独的地址。地址是动态分配的,各次可能不同。在分配完地址之后对设备进行初始化,初始化完成以后就可以对设备进行IO操作了
什么是OTG呢,这是On-The-Go的缩写,字面意思即为“直接传输”, 意思是USB设备既可以是主,又可以是从
<!--[if !vml]--><!--[endif]-->
USB主机是如何识别USB设备的?
答案六:当USB设备插上主机时,主机就通过一系列的动作来对设备进行枚举配置(配置是属于枚举的一个态,态表示暂时的状态),这这些态如下:
1、接入态(Attached):设备接入主机后,主机通过检测信号线上的电平变化来发现设备的接入;
2、供电态(Powered):就是给设备供电,分为设备接入时的默认供电值,配置阶段后的供电值(按数据中要求的最大值,可通过编程设置)
3、缺省态(Default):USB在被配置之前,通过缺省地址0与主机进行通信;
4、地址态(Address):经过了配置,USB设备被复位后,就可以按主机分配给它的唯一地址来与主机通信,这种状态就是地址态;
5、配置态(Configured):通过各种标准的USB请求命令来获取设备的各种信息,并对设备的某此信息进行改变或设置。
6、挂起态(Suspended):总线供电设备在3ms内没有总线***作,即USB总线处于空闲状态的话,该设备就要自动进入挂起状态,在进入挂起状态后,总的电流功耗不超过280UA。
8
Descriptor即描述符,是一个完整的数据结构,可以通过C语言等编程实现,并存储在USB设备中,用于描述一个USB设备的所有属性,USB主机是通过一系列命令来要求设备发送这些信息的。它的作用就是通过如问答节中的命令***作来给主机传递信息,从而让主机知道设备具有什么功能、属于哪一类设备、要占用多少带宽、使用哪类传输方式及数据量的大小,只有主机确定了这些信息之后,设备才能真正开始工作,所以描述符也是十分重要的部分,要好好掌握。标准的描述符有5种,USB为这些描述符定义了编号:
1——设备描述符
2——配置描述符
3——字符描述符
4——接口描述符
5——端点描述符
上面的描述符之间有一定的关系,一个设备只有一个设备描述符,而一个设备描述符可以包含多个配置描述符,而一个配置描述符可以包含多个接口描述符,一个接口使用了几个端点,就有几个端点描述符。这间描述符是用一定的字段构成的,分别如下说明:
1、设备描述符
struct _DEVICE_DEscriptOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x12
BYTE bDescriptorType; //描述符类型编号,为0x01
WORD bcdUSB; //USB版本号
BYTE bDeviceClass; //USB分配的设备类代码,0x01~0xfe为标准设备类,0xff为厂商自定义类型
//0x00不是在设备描述符中定义的,如HID
BYTE bDeviceSubClass; //usb分配的子类代码,同上,值由USB规定和分配的
BYTE bDeviceProtocl; //USB分配的设备协议代码,同上
BYTE bMaxPacketSize0; //端点0的最大包的大小
WORD idVendor; //厂商编号
WORD idProduct; //产品编号
WORD bcdDevice; //设备出厂编号
BYTE iManufacturer; //描述厂商字符串的索引
BYTE iProduct; //描述产品字符串的索引
BYTE iSerialNumber; //描述设备序列号字符串的索引
BYTE bNumConfiguration; //可能的配置数量
}
2、配置描述符
struct _CONFIGURATION_DEscriptOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x12
BYTE bDescriptorType; //描述符类型编号,为0x01
WORD wTotalLength; //配置所返回的所有数量的大小
BYTE bNumInterface; //此配置所支持的接口数量
BYTE bConfigurationVale; //Set_Configuration命令需要的参数值
BYTE iConfiguration; //描述该配置的字符串的索引值
BYTE bmAttribute; //供电模式的选择
BYTE MaxPower; //设备从总线提取的最大电流
}
3、字符描述符
struct _STRING_DEscriptOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x12
BYTE bDescriptorType; //描述符类型编号,为0x01
BYTE SomeDescriptor[36]; //UNICODE编码的字符串
}
4、接口描述符
struct _INTERFACE_DEscriptOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x12
BYTE bDescriptorType; //描述符类型编号,为0x01
BYTE bInterfaceNunber; //接口的编号
BYTE bAlternateSetting;//备用的接口描述符编号
BYTE bNumEndpoints; //该接口使用端点数,不包括端点0
BYTE bInterfaceClass; //接口类型
BYTE bInterfaceSubClass;//接口子类型
BYTE bInterfaceProtocol;//接口所遵循的协议
BYTE iInterface; //描述该接口的字符串索引值
}
5、端点描述符
struct _ENDPOIN_DEscriptOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x12
BYTE bDescriptorType; //描述符类型编号,为0x01
BYTE bEndpointAddress; //端点地址及输入输出属性
BYTE bmAttribute; //端点的传输类型属性
WORD wMaxPacketSize; //端点收、发的最大包的大小
BYTE bInterval; //主机查询端点的时间间隔
}
9. 在miniport驱动程序中规定在DriverEntry(这个函数相当于应用程序的main函数,是驱动程序的主要入口)中要用NdisMRegisterMiniport注册一组回调函数以便在ndis需要时使用,这组函数包括初始化、 halt、senddata、getdata、reset...等,如果要写一个普通的网卡驱动程序,只需要在这些状态下需要网卡做的事用中断、dma、或映射的内存,通知网卡即可,ndis支持这些操作。
但对于usb或1394设备则不行。你需要连接设备总线,并把你的要求提交给总线然M$提供了一个函数NdisMGetDeviceProperty(),在NdisMGetDeviceProperty()中会返回设备的 physicalDriverObject ,FunctionalDeviceObject , NextDeviceObject,等信息,个人感觉NdisMGetDeviceProperty函数只是在系统中注册了 physicalDriverObject ,FunctionalDeviceObject,NextDeviceObject等对象。这样就可以把USB设备链接到USB总线上了。具体过程只不过是将普通的USB驱动程序中的addDevice函数的代码移到MiniportInitialize而已。至于得到、发送数据,设备的电源管理等,向USB总线发URB就可以了
10.
支持 WDM 类设备的 NDIS 微型端口驱动程序被 NDIS 当作分层驱动程序,因为它不使用 NDIS 包装程序来访问硬件。 相反,这种 NDIS 微型端口驱动程序遵守对相应 WDM 总线驱动程序接口的要求。 对于 NDIS 规范,驱动程序必须:
1) 在其 .inf 文件中将设备类标识为 NET。
2) 在其 DriverEntry 中使用 NdisMRegisterMiniport。 该驱动程序必须不会阻塞其 MiniportInitialize 函数等待任何事件。
3) 使用 NdisSetAttributesEx,并设置属性标记的 NDIS_ATTRIBUTE_DESERIALIZE 位。 这样,就需要微型端口对入站和出站数据包执行串行化。
4) 对下层设备的 PDO 使用 NdisMGetDeviceProperty。
5) 如果需要 DeviceObject,则使用 NdisMRegisterDevice 而不使用 IoCreatDevice。
6) 对传入数据包进行排队,并使用 NdisTimerProc 清空该队列,并指示数据包到达较高网络层。 这样做是为了保证协议能够使用二进制兼容方法在 DPC 层接收这些数据包。
7) 允许 NDIS 安装下列 IRP 的处理程序:
IRP_MJ_PNP
IRP_MJ_POWER
IRP_MJ_SYSTEM_CONTROL
8) 将 NDIS 和 WDM 代码放在不同的源文件中。 在 Ndis.h 和 Wdm.h 之间有几个标记定义存在冲突。
9) 使用 HaltHandler 等待所有得到指示的数据包返回,并等待所有计时器完成计时。 应当使用 NdisWaitEvent 来执行等待操作。 在使用 USB/NDIS 驱动程序的情况下,应当等待所有读取操作被取消。
10) 在所有内存管理操作中使用 NDIS 包装程序调用;应当始终使用 NDIS 包装程序函数,而不是等价的 WDM 调用。
11) 指示媒体状态转换;连接到断开,或断开到连接。
NDIS 是微型端口的设备加载程序、类驱动程序、即插即用管理器、WMI 管理器和电源管理器。 例如,如果一个开发人员要为 USB 设备编写微型端口,则应当以 USB Bulk 示例为基础来编写 USB 客户端代码。不过,不要象在 USB BULK 示例中一样安装 IRP 处理程序。 NDIS 安装 IRP 处理程序,并确定合适的操作过程。 然后,NDIS 使用微型端口的 RequestHandler 向微型端口发送更新请求,并使用微型端口的 NDIS_MINIPORT_CHARACTERISTICS 中的如下回调函数:
12) NDIS 在 IRP_MN_START_DEVICE 期间调用微型端口 InitializeHandler。
13) NDIS 在 IRP_MN_STOP_DEVICE 和 IRP_MN_REMOVE_DEVICE 期间调用微型端口的 HaltHandler。
14) 当设备状态大于 D0 时,NDIS 在 IRP_MN_SET_POWER 期间调用微型端口的 SetInformationHandler 和 HaltHandler。
15) 当设备状态为 D0 时,NDIS 在 IRP_MN_SET_POWER 期间调用微型端口的 InitializeHandler 和 SetInformationHandler。
11.
1.USB 设备硬件部分
a.这个硬件的标识是用的 Vender ID 和 Product ID, 即“厂家标识”和“产品标识”
b.这个硬件规定了各个 End Point (端点) 的性质, 读/写 及 类型 (Control/Interrupt/Bulk/Isochronous)
c.这个硬件的固件里面有 DeviceIoControl 的实现部分, 规定了这个函数的具体参数和动作
2.USB 设备驱动
①硬件接口
a.需要识别 Vender ID 和 Product ID
b.对每个 EndPoint 的每个 I/O 分配一个 Pipe, 并且起一个名字作为软件接口
c.做 DeviceIoControl 的接口
②软件接口
a.GUID, 驱动程序的标识, 每个驱动程序使用不同的 GUID, GUID 是识别驱动的, 与硬件无关 (驱动程序升级版本 GUID 不能修改)
b.硬件接口里面的Pipe 名字是软件接口, 这个 Pipe 名字纯粹由驱动定义的, 和硬件无关, 升级驱动不能改 Pipe 的名字
c.硬件接口里面的 各个参数也是软件的接口, 这些参数是由硬件带来的, 不是驱动规定的, 当然也可以在驱动里面转义, 隐藏设备的真实情况
虚拟网卡的用户态协作程序(下):提高程序运行效率的方法
首先说事件触发。在用户态程序里,用CreateEvent就可以创建一个事件对象,并获取它的句柄。然后就可以用WaitForSingleObject的方法来等待这个事件(在这个事件上阻塞),而当条件符合时,另一个线程就可以用SetEvent来触发这个事件,使前面的线程继续执行。而在驱动程序里,也有KEVENT对象,可以用类似的方法运行驱动程序的多个函数之间协调运行。但是现在来看看我们的程序中的需求:我们要求的是驱动程序的MiniportSendPackets把数据包一准备好,就要通知用户态的协作程序开始动手发送数据了,也就是说我们有一个用户态的线程,它需要等待一个核心态的线程触发的事件。而实际上,这个是可以实现的,两种事件它们本质上是一样的。具体实现的时候要求事件由用户态的程序来创建,然后在某次DeviceIoControl的时候,将这个事件的句柄传递到驱动程序处,驱动程序使用ObReferenceObjectByHandle就可以把句柄转化成KEVENT对象了,以后驱动程序使用KeSetEvent来触发这个事件的时候,用户态的阻塞在这个事件的线程就会继续执行了。
下面说说内存映射。由于我们的程序需要传输大量的数据,如前所述,能尽量减少内存拷贝对于系统的性能都会有很大的提高。所以当有数据包要在用户态程序和驱动程序之间交互的时候,尽可能地避免直接拷贝大量数据。而应该使用内存映射的方法使用户态程序和驱动程序共用一段内存,这样就可以避免不少的内存拷贝。要进行内存映射,首先要申请一块不分页(NonPaged)的内存,这里由驱动程序来完成。然后为这块内存建立一个内存描述符。其实我们只要查看Ndis.h文件就可以知道,NDIS_BUFFER(缓冲区)和MDL(内存描述符)是同一个数据结构。当开始映射的时候,我们要调用MmBuildMdlForNonPagedPool对这个内存描述符进行处理,接下来,就可以调用函数MmMapLockedPagesSpecifyCache来实现内存映射了。传递给它的第二个参数是UserMode,表明需要映射到用户态的内存空间中。这个函数返回的是一个地址,这个地址在用户态程序中可以使用,把它传递给用户态的程序就可以了。内存映射时有一点要注意的就是进程上下文的问题,我在编写这段程序时就曾经犯过这个错误,在MiniportSendPackets处理完包裹后,顺便就进行了内存映射,并把这个用户态的地址保存起来,结果用户态的程序拿到这个地址后死活不能用,搞了好几天,才被提醒到进程上下文的问题。后来就在IOCTL的代码中实现这个,即当收到相应的IOCTL代码后,现场执行内存映射,然后把这个地址交给用户态程序,这才可以正常使用。当内存映射使用完毕时,在释放这段内存前,要调用MmUnmapLockedPages来解除内存映射。
12.
IoCompleteRequest函数来将IRP返回给I/O管理器﹒驱动程序必须调用这个函数﹐因为它标记设备对象空闲﹒如果没有这个调用﹐所有的新的IRP都被放置在悬挂队列﹐永远不会到达Start I/O例程
13. ExAllocatePoolWithTag函数代替ExAllocatePool函数调用﹒附加到这个函数的4个字节的标记参数用来标记驱动程序分配的存储空间块.
14.
微软为DDK提供一个命名规范,NTDDK.H定义所有的数据类型,结构,常数,宏。按照DDK规范,所有这些类型的名字都是大写的。甚至是C语言数据类型也提供一个相应的DDK名字。例如,C语言的数据类型void*在NTDDK.H中是PVOID。这些定义可以很容易的扩展到未来的64Bits的平台。、
15. 只能永久驻留在物理内存中而不能被换出来页的叫作“非分页内存”。
如果在DISPATCH_LEVEL或者更高的中断级中访问分页内存,就会引起缺页故障,内核会崩溃。如果在
PASSIVE_LEVEL中断级访问没有驻留在物理内存中的分页内存时,内核会阻塞我们的线程,直到内存管理器把此页
重新装回物理内存中。
不要随意使用非分页内存,因为系统的资源是有限的,如果永久驻留在物理内存中的页太多,将导致可分页内
存更加频繁地进行交换,降低系统性能。
在开发驱动程序时我们必须遵守这样几个原则:
决不(或几乎从不)直接引用用户模式的内存地址。
因为我们不能确切知道用户模式内存地址所指向的真实物理地址
16.
驱动程序对象(DRIVER_OBJECT)主要成员
DeviceObject: 指向一个设备对相链表,每个设备对象代表一个设备。
DriverExtension: 一个结构体, 该结构只有AddDevice成员可以直接访问。
DriverStartIo: 指向驱动程序中处理I/O请求的函数。
DriverUnload: 指向驱动程序中的清除函数。
MajorFunction: 为一个函数指针表, 指向存在于驱动程序中的各个IRP处理函数, 它定义了I/O请求如何进入驱动程序。
17.
4、设备对象(DEVICE_OBJECT)主要成员
DriverObject: 指向与该设备对象相关的驱动程序对象。过滤驱动程序有时需要用这个指针来寻找被过滤设备的驱动程序对象。
CurrentIrp: 指向最近发往驱动程序StartIo函数的I/O请求包。
Flags: 包含一组标志位
DO_BUFFERED_IO: 读写操作使用缓冲方式(系统复制缓冲区)访问用户模式数据
DO_EXCLUSIVE: 一次只允许一个线程打开设备句柄
DO_DIRECT_IO: 读写操作使用直接方式(内存描述符表)访问用户模式数据
DO_DEVICE_INITIALIZING: 设备对象正在初始化
DO_POWER_PAGABLE: 必须在PASSIVE_LEVEL级上处理IRP_MJ_PNP请求
DO_POWER_INRUSH: 设备上电期间需要大电流
Characteristics: 包含另一组标志位,描述设备的可选特征
FILE_REMOVABLE_MEDIA: 可更换媒介设备
FILE_READ_ONLY_DEVICE: 只读设备
FILE_FLOPPY_DISKETTE: 软盘驱动器设备
FILE_WRITE_ONCE_MDEIA: 只写一次设备
FILE_REMOTE_DEVICE: 通过网络连接访问的设备
FILE_DEVICE_IS_MOUNTED: 物理媒介已在设备中
FILE_DEVICE_SECURE_OPEN: 在打开操作中检查设备对象的安全属性
DeviceType: 一个枚举常量,描述设备类型。
FILE_DEVICE_PRINTER: 打印机
FILE_DEVICE_SCANNER: 扫描仪
...
FILE_DEVICE_UNKNOWN: 未知设备
驱动程序对象与设备对象的关系
一方面, 驱动程序对象通常有多个与它相关的设备对象,因此它利用DeviceObject指针指向一个设备对象列表,该列表表示驱动程序可以控制的物理设备。
另一方面, 设备对象反过来指向它自己的驱动程序对象, 这样I/O管理器就知道在接收一个I/O请求时应该调用哪个驱动程序。每个功能码都对应一个驱动程序的入口点。
I/O请求包(IRP)
IRP是I/O系统用来存储处理I/O请求所需信息的地方, I/O管理器在IRP中保存一个指向调用者文件对象的指针。从编程的角度看, IRP是I/O管理器在响应一个I/O请求时从非分页系统内存中分配的一块可变大小的数据结构内存, I/O管理器每收到一个来自用户的请求就创建一个该结构,并将其作为参数传给驱动程序的DispatchXxx、StartIo等例程。该结构中存放有请 求的类型、用户缓冲区的首地址、用户请求数据的长度等信息。驱动程序处理完这个请求后, 也在该结构中添加处理结果的有关信息, 然后调用IoCompleteRequest将其返回给I/O管理器, 用户程序的请求随即返回。
18.
驱动程序可用的内核态例程:
Ex... 执行支持
Hal... 硬件抽象层(仅NT/Windows 2000)
Io... I/O管理器(包括即插即用例程)
Ke... 内核
Mm... 内存管理器
Ob... 对象管理器
Po... 电源管理
Ps... 进程结构
Rtl... 运行时库
Se... 安全引用监视
Zw... 其他例程
总线驱动程序和类特定的例程:
BatteryClass... 小类驱动程序的电池类例程
Hid... 人工输入设备(HID)例程
Pc... 用于小端口驱动程序的SCSI Tape类例程
Usb... 用于USB客户驱动程序的通用串行总线驱动程序接口例程
IRP_MJ_CREATE 创建或打开设备文件
IRP_MJ_CLOSE 关闭句柄
IRP_MJ_READ 读
IRP_MJ_WRITE 写
IRP_MJ_CLEANUP 取消文件句柄上的任何等待的IRP
IRP_MJ_DEVICE_CONTROL 设备I/O控制
IRP_MJ_INTERNAL_DEVICE_CONTROL 来自高层驱动程序的I/O控制
IRP_MJ_SYSTEM_CONTROL WMI
IRP_MJ_POWER 电源管理请求
IRP_MJ_PNP 即插即用消息
IRP_MJ_SHUTDOWN 关闭通知
UNICODE_STRING结构定义:
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
Buffer域指向一个宽16位字符缓冲区,该字符串通常不是NULL终止的,而是由Length域指定字符串的当前大小(字符数)
IoCreateDevice函数:
NTSTATUS IoCreateDevice IRQL == PASSIVE_LEVELIN PDRIVER_OBJECT DriverObject 驱动程序对象
IN ULONG DeviceExtensionSize 要求的设备扩展的大小
IN PUNICODE_STRING DeviceName 设备名称,或者NULL
IN DEVICE_TYPE DeviceType 设备的类型,标准头文件WDM.H或NTDDK.H中列出的FILE_DEVICE_XXX值之一
IN ULONG DeviceCharacteristics 各种常量用OR组合在一起,指示可删除介质、只读等
IN BOOLEAN Exclusive 如果一次只有一个线程可以访问该设备,为TRUE
OUT PDEVICE_OBJECT* DeviceObject 返回的设备对象
IoRegisterDeviceInterface函数
NTSTATUS IoRegisterDeviceInterface IRQL == PASSIVE_LEVEL参数 描述
IN PDEVICE_OBJECT PhysicalDeviceObject 设备PDO
IN CONST GUID* InterfaceClassGuid 被注册的GUID
IN PUNICODE_STRING ReferenceString 通常是NULL,引用字符串或成为接口名称的一部分,所以可以用于区分同一设备的不同接口
OUT PUNICODE_STRING SymbolicLinkName 输出接口符号连接名,完成使用时,不要忘记使用RtlFreeUnicodeString释放这个Unicode字符串
IoRegisterDeviceInterface函数
NTSTATUS IoRegisterDeviceInterface
参数 描述
IN PDEVICE_OBJECT PhysicalDeviceObject 设备PDO
IN CONST GUID* InterfaceClassGuid 被注册的GUID
IN PUNICODE_STRING ReferenceString 通常是NULL,引用字符串成为接口名称的一部分,所以可以用于区分统一设备的不同接口
OUT PUNICODE_STRING SymbolicLinkName 输出接口符号链接名。完成使用时,不要忘记使用RtlFreeUnicodeString释放这个Unicode字符串
设备名:
符号链接:这个名字向内核标识设备,而不是向Win32标识设备。根据约定,内核设备名从0开始编号,而符号链接名从1开始编号。用IoCreateSymbolicLink创建。
设备接口:驱动程序使用设备接口使它的设备对Win32程序可见,主要思想是:每个设备使一个定义的应用程序编程接口(API)可用,全局唯一标识符(GUID)用于标识这个接口。
20.
可重入性:就是说它可以被"同时"调用处理连个不同的IRP。在多处理器的Windows 2000系统中,一个处理器可能是用一个IRP调用Wdm1Read,另一个CPU上的第二个进程也同时使用另一个IRP调用Wdm1Read。
使一个例程是可重入的第一个技术是使用局部变量,
缓冲I/O:
驱动程序可以使用两个主要的方法访问用户缓冲区,这两个方法是缓冲I/O和直接I/O。在创建设备时,必须设置新设备对象的Flags域中的DO_BUFFERED_IO位来使用缓冲I/O,或设置DO_DIRECT_IO位来使用直接I/O。
如果使用缓冲I/O,内核使用户的缓冲区在某个非分页内存中可用,并在IRP首部的AssociatedIrp.SystemBuffer域中存储合适的指针。在驱动程序中简单地读或写这个内存。它总体上速度要慢些,因为操作系统通常必须要把用户缓冲区复制到非分页内存或者从非分页内存复制出来。
直接I/O:
使用内存描述符列表(MDL)速度要快些,但这仅可用于执行直接内存访问(DMA)的硬件。用户缓冲区的MDL放在IRP首部的MdlAddress域中
DeviceIoControl缓冲区
自旋锁:内核自旋锁在BufferLock提供这个保护。自旋锁可以用在代码需要短时访问某种资源的地方。
KSPIN_LOCK BufferLock; // 声明一个自旋锁对象
KeInitializeSpinLock( &BufferLock ); // 初始化
KeAcquireSpinLock( &BufferLock, XXX ); // 获得自旋锁
KeReleaseSpinLock( &BufferLock, XXX ); // 释放自旋锁
"自旋(spin)"是指KeAcquireSpinLock不断进行监视。由于这个原因,我们只能持有一个自旋锁很短的时间。DDK建议不要持有自旋锁超过25微秒。
我 们在KeAcquireSpinLock的调用中,必须提供一个指向KIRQL变量的指针,这个变量存储在提升到DISPATCH_LEVEL之前的原始 IRQL级。在必要时,KeReleaseSpinLock的调用降低IRQL。如果肯定代码在DISPATCH_LEVEL IRQL运行,可以使用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel例程得到 更好的性能。
在持有一个自旋锁时,不要访问分页代码或数据,因为系统几乎肯定会崩溃。在持有一个自旋锁时,千万不要退出主分发例程。
有两类电路可以用于PC到物理USB总线的接口:开放主机控制接口(OpenHCI)和通用主机控制器接口(UHCI)。Windows选择OpenHCI.sys驱动程序或者UHCI.sys驱动程序作为到这些电路接口的驱动程序层
22.
记录有多少I/O请求正在处理过程中的最好方法是什么呢?答案是在设备扩展中的UsageCount域中记录打开的I/O请求的数目。在每个IRP请求的 开始时,进行对LockDevice的调用,对UsageCount计数器增1。在每个IRP完成时,调用UnlockDevice,对 UsageCount计数器减1。
23,
自旋锁其实和Mutex互斥量的作用是类似的,防止在多线程或多 CPU下数据共享冲突,两者的不同之处在于自旋锁的持有时间不能太长(一般在毫秒级),否则会造成系统死锁。而Mutex则无时间限制。另外自旋锁切换时 间比Mutex要短很多。所以自旋锁一般用于保护一个变量或内存拷贝,而Mutex用于保护I/O操作或较耗费时间的代码段。
24,
AddDevice为设备对象注册一个接口,以便应用程序能通过注册接口来访问该设备。一个设备接口被一个128位的GUID唯一标识。
创建一个URB是USB驱动最基本的工作,首先应该为URB分配内存,然后调用初始化例程把URB结构中的各个域填入请求要求的内容。最后通过创建并发送一个内部I/O控制(IOCTL)请求到USBD驱动程序来发送这个URB包,从而完成USB请求。
2)创建并发送IOCTL请求
创建完URB后,我们创建并发送一个IOCTL(内部I/O控制)请求到USBD,然后等待设备回应,相应的函数为SendAwaitUrb
3) 返回设备完成状态
25.
Dispatch例程基本功能
完成一个IRP
如果一个输入IRP能被迅速完成,Dispatch例程要做以下工作:
1.通常将IRP的I/O状态块中的Status与Information成员设置为恰当的值:
Dispatch§例程将Status设置为STATUS_SUCCESS或者一个适当错误STATUS_XXX,可以是调用一个支持例程所返回的值,或者对于某些的同步请求来说,是一个较低层驱动程序返回的值。
如果较低层驱动程序返回STATUS_PENDING,较高层驱动程序不应当用IRP调用IoCompleteRequest,除非它首先用该IRP调用了IoMarkIrpPending。当然,较高层驱动程序的Dispatch例程未必为返回STATUS_PENDING的较低层驱动程序完成任何IRP。
§ 如果满足了一个传输数据的请求,例如一个读或写请求,则将Information设置为成功传输的比特数。
§ 对其他以STATUS_SUCCESS完成的IRP的不同特定请求而将Information设置为不同的值。
§ 对以警告性STATUS_XXX完成的IRP的不同特定请求而将Information设置为不同的值。例如,对于象STATUS_BUFFER_OVERFLOW这样的警告,将Information设置为传输的字节数。
§ 通常,对以一个错误STATUS_XXX完成的请求,将Information置为零。
2.用IRP和PriorityBoost IO_NO_INCREMENT调用IoCompleteRequest。
3.返回已在I/O状态块中设置的适当的STATUS_XXX。注意,对IoCompleteRequest的调用使给定的IRP不能被调用者访问,因此来自一个Dispatch例程的返回值不能从一个已完成的IRP的I/O状态块中设置。 调用IoCompleteRequest之前,总是释放驱动程序持有的自旋锁。
传送带有有效参数的IRP
1.用输入IRP调用IoGetCurrentIrpStackLocation,如果Dispatch例程还没有这样做,这样就为IRP中它自己的I/O栈位置获得一个指针。在大多数情况下,一个较高层驱动程序的Dispatch例程已经进行了该调用。然而,一个Dispatch例程可能只是处理特定的IRP_MJ_XXX,它或者带有驱动程序无法验证有效性的参数或者不带参数,这种Dispatch例程可能会调用IoGetCurrentIrpStackLocation。
2.如果驱动程序传送输入IRP到相邻较低层驱动程序,调用IoGetNextIrpStackLocation。如果它为一个或多个较低层驱动程序分配了附加IRP,Dispatch例程用每个它分配的IRP进行这两组调用:
§ 如果驱动程序没有在新IRP中分配栈位置,调用IoGetNextIrpStackLocation获得一个指向相邻较低层驱动程序I/O栈位置的指针
§调用IoSetNextIrpStackLocation(在它之后是IoGetCurrentIrpStackLocation)获得一个指向新IRP中它自己的栈位置的指针,在那里它可以设置任何IoCompletion例程需要的环境;然后调用IoGetNextIrpStackLocation获得一个指向相邻较低层驱动程序I/O栈位置的指针
3.设置相邻较低层驱动程序的I/O栈位置,这通常是通过拷贝它自己在初始IRP中的I/O栈位置到相邻较低层驱动程序的I/O栈位置来实现的。不过,Dispatch例程可以为某些请求修改相邻较低层驱动程序的I/O栈位置中的参数。
例如,当下层的设备已经知道传输能力限制,并重新使用IRP发送部分传输请求到下层的设备驱动程序时,一个较高层驱动程序可以为一个大的传输请求修改参数
4.用每个Dispatch例程分配的IRP调用IoSetCompletionRoutine,以便驱动程序的IoCompletion例程在较低层驱动程序完成它时释放每个这样的IRP
即使最高层驱动程序也不能在它的Dispatch例程中为较低层驱动程序等待,以完成处理一个异步的读或写请求。它必须传送这样的请求给较低层驱动程序并从它的DispatchRead或DispatchWrite例程返回STATUS_PENDING
26.
对I/O管理器作进一步讨论之前,最后复习一下以下这些步骤:
1.应用程序向I/O管理器发出I/O操作请求(读、写等操作)。
2.I/O管理器通过建立一个IRP来分发I/O请求并把它传递给调用链中第一个驱动程序。
3.第一个驱动程序通过向第二个驱动程序发出I/O请求以便继续处理。
4.第二个驱动程序通过向第三个驱动程序发出I/O请求以便继续处理。
5.第三个驱动程序完成I/O操作。它调用IoCompleteRequest()函数并开始执行I/O完成的第一阶段。
6.在第一阶段执行期间,第二个驱动程序的完成例程(如果有的话)会被回调。
7.在第一阶段执行期间,第一个驱动程序的完成例程(如果有的话)会被回调。
8.接下来,IoCompleteRequest()函数会把控制权返回给第三个驱动程序。I/O完成的第一阶段处理结束。
9.第三个驱动程序把一个状态值(例如,SUCCESS)返回给第二个驱动程序。
10.第二个驱动程序把一个状态值返回给第一个驱动程序。
11.第一个驱动程序把一个状态值返回给I/O管理器。
12.I/O管理器注意到必须执行I/O完成的第二阶段,并开始执行。
13.第二阶段I/O完成例程返回I/O操作的状态信息并把数据拷贝到应用程序的地址空间中。一旦完成了这些操作,I/O管理器拆除IRP并将其丢弃。
14.第二阶段I/O完成例程把控制权返回给I/O管理器。
15.现在,I/O操作已经完成,I/O管理器把控制权返回给调用者。
28.
栈单元是从上到下使用的(例如,I/O栈在内存中是向下增长的)。你可以在I/O管理器的实现函数IoSetNextIrpStackLocation(在ntddk.h中的一个宏)中看到这一点:
#define IoSetNextIrpStackLocation( Irp ) { "
(Irp)->CurrentLocation--; "
(Irp)->Tail.Overlay.CurrentStackLocation--; }
当IRP首次被创建时,当前IRP栈单元是无效的,调用 IoGetCurrentIrpStackLocation()函数返回一个指向IRP结构内部的指针。但是,通过使用 IoGetNextIrpStackLocation()函数获取的下一个IRP栈单元则是有效的,事实上第二个IRP栈单元在为第一个被调用的驱动程序 设置参数时用到(设置第一个I/O栈单元的机制在"Building IRPs to Perform File I/O" , The NT Insider V4N1中已经讨论过了)
29.
IRP整体:
名称 描述 调用者
IoStartPacket 发送IRP到Start I/O例程  Dispatch
IoCompleteRequest 表示所有的处理完成  DpcForIsr
IoStartNextPacket 发送下一个IRP到Start I/O例程  DpcForIsr
IoCallDriver 发送IRP请求  Dispatch
IoAllocateIrp 请求另外的IRP  Dispatch
IoFreeIrp 释放驱动程序分配的IRP  I/O Completion
IRP堆栈:
名称 描述 调用者
IoGetCurrentIrpStackLocation 得到调用者堆栈的指针  Dispatch
IoMarkIrpPending 为进一步的处理标记调用者I/O堆栈  Dispatch
IoGetNextIrpStackLocation 得到下一个驱动程序的I/O堆栈的指针 Dispatch
IoSetNextIrpStackLocation 将I/O堆栈指针压入堆栈  Dispatc
在驱动程序,IRP派遣例程起着很重要的作用,每个IRP派遣例程,几乎都有对应的Win32函数,下面是几个常用的:
IRP派遣例程:
名称 描述 调用者
IRP_MJ_CREATE 请求一个句柄  CreateFile
IRP_MJ_CLEANUP 在关闭句柄时取消悬挂的IRP  CloseHandle
IRP_MJ_CLOSE 关闭句柄  CloseHandle
IRP_MJ_READ 从设备得到数据  ReadFile
IRP_MJ_WRITE 传送数据到设备  WriteFile
IRP_MJ_DEVICE_CONTROL 控制操作(利用IOCTL宏)  DeviceIoControl
IRP_MJ_INTERNAL_DEVICE_CONTROL 控制操作(只能被内核调用)  N/A
IRP_MJ_QUERY_INFORMATION 得到文件的长度  GetFileSize
IRP_MJ_SET_INFORMATION 设置文件的长度  SetFileSize
IRP_MJ_FLUSH_BUFFERS 写输出缓冲区或者丢弃输入缓冲区  FlushFileBuffers FlushConsoleInputBuffer PurgeComm
IRP_MJ_SHUTDOWN 系统关闭  InitiateSystemShutdown
电源管理员状态 NDIS 状态
D0 NdisDeviceStateD0
D1 NdisDeviceStateD0
D2 NdisDeviceStateD0
D3 请参阅下表。
D4 NdisDeviceStateD3
27.
NdisQueryPacket?返回一组描述了包和链的大指针的信息
28. 创建IRP
- IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
- IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
- IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
- IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。
前两个函数中的Fsd表明这些函数专用于文件系统驱动程序(FSD)。虽然FSD是这两个函数的主要使用者,但其它驱动程序也可以调用这些函数。DDK还公开了一个IoMakeAssociatedIrp函数,该函数用于创建某些IRP的从属IRP。WDM驱动程序不应该使用这个函数。
发往派遣例程
创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序:
PDEVICE_OBJECT DeviceObject; //something gives you thisPIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);stack->MajorFunction = IRP_MJ_Xxx;<other initialization of "stack">NTSTATUS status = IoCallDriver(DeviceObject, Irp); |
IoCallDriver函数的第一个参数是你在某处获得的设备对象的地址。
IRP中的第一个堆栈单元指针被初始化成指向该堆栈单元之前的堆栈单元,因为I/O堆栈实际上是IO_STACK_LOCATION结构数组,你可以认为这 个指针被初始化为指向一个不存在的“-1”元素,因此当我们要初始化第一个堆栈单元时我们实际需要的是“下一个”堆栈单元。IoCallDriver将沿 着这个堆栈指针找到第0个表项,并提取我们放在那里的主功能代码
派遣例程的职责
NTSTATUS DispatchXxx(PDEVICE_OBJECT device, PIRP Irp){ PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); <--1 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension; <--2 ... return STATUS_Xxx; <--3} |
- 你通常需要访问当前堆栈单元以确定参数或副功能码。
- 你可能还需要访问你创建的设备扩展。
- 你将向IoCallDriver函数返回某个NTSTATUS代码,而IoCallDriver函数将把这个状态码返回给它的调用者
派遣函数必须做出决定的地方,有三种选择:
- 派遣函数立即完成该IRP。
- 把该IRP传递到处于同一堆栈的下层驱动程序。
- 排队该IRP以便由这个驱动程序中的其它例程来处理。
有大量读写请求进入设备时,通常需要把这些请求放入一个队列中,以便使硬件访问串行化。
每个设备对象都自带一个请求队列对象,下面是使用这个队列的标准方法:
NTSTATUS DispatchXxx(...){ ... IoMarkIrpPending(Irp); <--1 IoStartPacket(device, Irp, NULL, NULL); <--2 return STATUS_PENDING; <--3} |
- 无论何时,当你的派遣例程返回STATUS_PENDING状态代码时,你应该先调用这个IoMarkIrpPending函数,以帮助I/O管理器避免内部竞争。我们必须在放弃IRP所有权之前做这一点。
- 如果设备正忙,IoStartPacket就把请求放到队列中。如果设备空闲,IoStartPacket将把设备置成忙并调用StartIo例 程。IoStartPacket的第三个参数是用于排序队列的键(ULONG)的地址,例如磁盘驱动程序将在这里指定一个柱面地址以提供顺序搜索的排队。 如过你在这里指定一个NULL,则该请求被加到队列的尾部。最后一个参数是取消例程的地址。我将在本章的后面讨论取消例程,这种例程比较复杂。
- 返回STATUS_PENDING以通知调用者我们没有完成这个IRP。
注意,一旦我们调用了IoStartPacket函数,就不要再碰IRP。因为在该函数返回之前,IRP可能已经被完成并且其占用的内存可能被释放,而我们拥有的该IRP的指针也许是无效的。
ISR(中断服务例程)DPC(推迟过程调用)例程
IRQL是处理器中断请求级,级别低的代码可以被级别高的代码中断,在驱动程序里 面一般容易混淆的是PASSIVE_LEVEL和DISPATCH_LEVEL,DISPATCH_LEVEL级要比PASSIVE_LEVEL级高,一 般的用户模式程序执行在PASSIVE_LEVEL上,驱动程序有一些例程需要在PASSIVE_LEVEL上(比如DriverEntry、 AddDevice等),有一些例程则是需要在DISPATCH_LEVEL上执行的,包括StartIo例程,DPC(推迟过程调用)例程等
32. INF 文件目录代号;
-01,0xffff 目录
01 SourceDrive:path.
10 Windows directory.
11 System directory. (%windir%system on Windows 95, %windir%system32 on Windows NT)
12 Drivers directory.(%windir%system32drivers on Windows NT)
17 INF 文件目录
18 help目录
20 Fonts directory.
21 察看器目录
24 应用程序目录
25 共享目录Shared directory.
30 启动驱动器的根目录Root directory of the boot drive.
50 %windir%system
51 假脱机目录Spool directory.
52 Spool drivers directory.
53 用户 Profile 目录
54 ntldr or OSLOADER.EXE 所在目录
33.
minimum system required macros to define
windows 95 and
windows nt 4.0 winver=0x0400
windows 98 and
windows nt 4.0 _win32_windows=0x0410 and winver=0x0400
windows nt 4.0 _win32_winnt=0x0400 and winver=0x0400
windows 98 _win32_windows=0x0410
windows 2000 _win32_winnt=0x0500 and winver=0x0500
windows me _win32_windows=0x0490
windows xp and
windows .net server _win32_winnt=0x0501 and winver=0x0501
internet explorer 3.0, 3.01, 3.02 _win32_ie=0x0300
internet explorer 4.0 _win32_ie=0x0400
internet explorer 4.01 _win32_ie=0x0401
internet explorer 5.0, 5.0a, 5.0b _win32_ie=0x0500
internet explorer 5.01, 5.5 _win32_ie=0x0501
internet explorer 6.0 _win32_ie=0x0560 or
_win32_ie=0x0600
USB枚举详细过程剖析
<!--[if !supportLists]-->(1) <!--[endif]-->集线器检测新设备
主机集线器监视着每个端口的信号电压,当有新设备接入时便可觉察。(集线器端口的两根信号线的每一根都有 15kΩ的下拉电阻,而每一个设备在D+都有一个1.5kΩ的上拉电阻。当用USB线将PC和设备接通后,设备的上拉电阻使信号线的电位升高,因此被主机 集线器检测到。)
(2)主机知道了新设备连接后
每个集线器用中断传输来报告在集线器上的事件。当主机知道了这个事件,它给集线器发送一个Get_Status请求来了解更多的消息。返回的消息告诉主机一个设备是什么时候连接的。
(3)集线器重新设置这个新设备
当主机知道有一个新的设备时,主机给集线器发送一个Set_Feature请求,请求集线器来重新设置端口。集线器使得设备的USB数据线处于重启(RESET)状态至少10ms。
(4)集线器在设备和主机之间建立一个信号通路
主 机发送一个Get_Status请求来验证设备是否激起重启状态。返回的数据有一位表示设备仍然处于重启状态。当集线器释放了重启状态,设备就处于默认状 态了,即设备已经准备好通过Endpoint 0 的默认流程响应控制传输。即设备现在使用默认地址0x0与主机通信。
(5)集线器检测设备速度
集线器通过测定那根信号线(D+或D-)在空闲时有更高的电压来检测设备是低速设备还是全速设备。(全速和高速设备D+有上拉电阻,低速设备D-有上拉电阻)。
以下,需要USB的firmware进行干预
(6)获取最大数据包长度
PC 向address 0发送USB协议规定的Get_Device_Descriptor命令,以取得却缺省控制管道所支持的最大数据包长度,并在有限的时间内等待USB设备 的响应,该长度包含在设备描述符的bMaxPacketSize0字段中,其地址偏移量为7,所以这时主机只需读取该描述符的前8个字节。注意,主机一次 只能列举一个USB设备,所以同一时刻只能有一个USB设备使用缺省地址0。
以下操作雷同,不同操作系统设定时延是不一样的,比如说win2k大概是几毫秒,如果没有反应就再发送一次命令,重复三次。
(7)主机分配一个新的地址给设备
主机通过发送一个Set_Address请求来分配一个唯一的地址给设备。设备读取这个请求,返回一个确认,并保存新的地址。从此开始所有通信都使用这个新地址。
(8)主机向新地址重新发送Get_Device_Descriptor命令,此次读取其设备描述符的全部字段,以了解该设备的总体信息,如VID,PID。
(9)主机向设备循环发送Get_Device_Configuration命令,要求USB设备回答,以读取全部配置信息。
(10)主机发送Get_Device_String命令,获得字符集描述(unicode),比如产商、产品描述、型号等等。
(11)此时主机将会弹出窗口,展示发现新设备的信息,产商、产品描述、型号等。
(12) 根据Device_Descriptor和Device_Configuration应答,PC判断是否能够提供USB的Driver,一般win2k能 提供几大类的设备,如游戏操作杆、存储、打印机、扫描仪等,操作就在后台运行。但是Win98却不可以,所以在此时将会弹出对话框,索要USB的 Driver。
(13)加载了USB设备驱动以后,主机发送Set_Configuration(x)命令请求为该设备选择一个合适的配置(x代表非0的配置值)。如果配置成功,USB设备进入“配置”状态,并可以和客户软件进行数据传输。
此时,常规的USB完成了其必须进行的配置和连接工作。查看注册表,能够发现相应的项目已经添加完毕,至此设备应当可以开始使用。不过,USB协议还提供了一些用户可选的协议,设备如果不应答,也不会出错,但是会影响到系统的功能。
NDIS: <!--[if !vml]--><!--[endif]-->
微端口驱动程序工作在数据链路层,是与网络接口卡(Network Interface Card(NIC),基于网卡编程的主要控制对象)结合最紧密的一层驱动程序。它调用NDIS库提供的接口函数来完成NIC与上层驱动程序之间的相互通讯。NDIS库导出一组函数集合(NdisXxx函数)来封装所有微端口需要调用的操作系统函数,而微端口也输出一组MiniportXxx函数供NDIS和上层驱动程序调用。
发送过程可分为两种情况:
1) 当协议驱动程序有数据要发送时,启动传输操作,通过NIDS库调用微端口驱动程序的MPSend函数。该函数调用的参数是一个指向NDIS_PACKET包(描述将要发送的信息)的指针。驱动调用NdisQueryPacket函数得到包的长度和存放待发送包缓冲区的逻辑地址。然后设置NIC上的寄存器将包发送出去,并返回一个发送成功的状态。
2) 如果驱动程序不能立即发送包,则将它送到“待传输”队列中,然后由中断处理函数MYNE2000HandleInterrupt来完成发送。完成发送以后,调用NdisMSendComplete函数通知上层发送已经完成。
由于从上层传下来的数据包到微端口层被放在预先分配的缓冲区中。用NDIS提供的相应函数,可以得到该缓冲区的首地址和数据长度。利用NDIS提供的函数得到存放数据缓冲区的相应参数,
这里需要注意的是当微端口驱动程序准备发送数据包时,总是认为上层的协议驱动程序不会发送一个太大的数据包到微端口层。因为根据NDIS,协议驱动程序会在初始化时查询微端口并决定包的最大长度,协议驱动只能传送大小在微端口层所支持范围内的数据包