8.2 数据包发送的幕后

在网络分析中,要灵活高效率地发送数据包,在设计与实现上存在着如下主要难点:

❑需要有非常高的网络流量,以及极高的网络带宽利用率。

❑对于同步发送方式,需要有精准的时间控制。

❑由于数据包的发送过程贯穿于用户空间与内核空间,所以还需要有效地提高交互效率。

❑需要给用户提供齐备的机制,才能使用户灵活使用所提供的功能。

接下来,我们通过分析WinPcap的具体实现来了解如何克服以上这些困难。WinPcap是通过内核空间与用户空间中各函数间的相互配合,来完成数据包的发送功能的。8.1节所提到的第一种与第二种发送方式的实现机制基本一致,第三种与第四种发送方式的实现机制基本一致。所以图8-8只给出了与发送单个数据包与发送一个数据包队列相关的重要函数。

8.2 数据包发送的幕后 - 图1

图 8-8 函数调用关系图

后续各节将依据从用户空间到内核空间的顺序,来详细描述各种发送方式的具体实现。

8.2.1 发送单个数据包的实现

WinPcap发送单个数据包的实现主要依赖于wpcap.dll库中的pcap_sendpacket函数,而该函数主要依赖于Packet.dll库所提供的PacketSendPacket函数,不过,最终的数据包发送是在内核空间的NPF_Write函数中调用NDIS库的NdisSend函数完成的。

这种发送方式,使用简单,较容易实现,理解起来也比较容易。图8-9给出了与发送单个数据包相关的重要函数的调用关系。下面将详细分析WinPcap中相应函数的具体设计与实现。

8.2 数据包发送的幕后 - 图2

图 8-9 发送单个数据包的重要函数调用关系图

1.wpcap.dll库中发送单个数据包的实现

(1)单个数据包发送函数

如果设置为只发送一次(默认情况,参见8.2.2节),那么每调用一次pcap_sendpacket函数,就会发送单个原始数据包一次。该函数的原型如下:


int pcap_sendpacket(pcap_tp,const u_charbuf,int size)


上述函数中,参数p用来发送数据包的一个pcap_t类型描述符(可通过pcap_open函数获得该描述符),参数buf包含所要发送数据包的内容(包含数据包的协议头);参数size是buf所指缓冲区的大小,也就是所要发送的数据包的大小。另外,无须在数据包中包含MAC的CRC校验字段,它是由网络接口驱动程序计算并添加的。

pcap_sendpacket函数如果执行成功则返回0,否则返回-1。

pcap_sendpacket函数的具体实现如下:


int pcap_sendpacket(pcap_tp,const u_charbuf,int size)

{

if(p->inject_op(p,buf,size)==-1)

return(-1);

return(0);

}


在pcap_create函数中设置p->activate_op为pcap_activate_win32函数,在pcap_activate_win32函数中设置p->inject_op为pcap_inject_win32函数,其过程如下:


pcap_tpcap_create(const chardevice,char*ebuf)

{

……

p->activate_op=pcap_activate_win32;

……

}

static int pcap_activate_win32(pcap_t*p)

{

……

p->inject_op=pcap_inject_win32;

……

}


所以可认为在pcap_sendpacket函数中是通过调用pcap_inject_win32函数来发送数据包的。下面将分析pcap_inject_win32函数的具体实现。

(2)pcap_inject_win32函数

pcap_inject_win32函数调用Packet.dll库提供的PacketSendPacket函数来发送数据包,具体实现如下:


static int pcap_inject_win32(pcap_tp,const voidbuf,

size_t size)

{

LPPACKET PacketToSend;

/分配一个_PACKET结构体的内存空间,用来存储数据包/

PacketToSend=PacketAllocatePacket();

if(PacketToSend==NULL)

{//分配失败

……

}

/初始化_PACKET结构体/

PacketInitPacket(PacketToSend,(PVOID)buf,size);

/发送数据包/

if(PacketSendPacket(p->adapter,PacketToSend,TRUE)==FALSE)

{//发送失败,处理错误

……

}

/释放_PACKET结构体的内存空间/

PacketFreePacket(PacketToSend);

/*

*若PacketSendPacket函数成功,就认为数据包全部被发送

*本函数期望返回所发送的字节数

*/

return size;

}


在pcap_inject_win32函数中,首先分配一个_PACKET结构体的内存空间PacketToSend;接着,PacketInitPacket函数使用应用程序所传递的数据包数据参数buf与数据包大小参数size来初始化PacketToSend所指的内存空间;然后调用PacketSendPacket函数发送数据包;最后调用PacketFreePacket函数释放PacketToSend所指的内存空间。

其中_PACKET结构体及PacketAllocatePacket、PacketInitPacket、PacketSendPacket、PacketFreePacket函数都由Packet.dll库提供。

2.Packet.dll库中发送单个数据包的实现

我们首先需要了解Packet.dll库中_PACKET结构体的定义,它是单个数据包发送的基本数据结构。结构体_PACKET是在Common\Packet32.h文件中定义的,其具体定义如下:


/*

*该结构体包含从驱动程序传递来的一组数据包

*该结构体定义了每个发送给应用程序的数据包的包头信息

*/

typedef struct_PACKET

{

//包含数据包的缓冲区

//查看PacketReceivePacket函数,了解该缓冲区数据组织的细节

PVOID Buffer;

//缓冲区长度

UINT Length;

//缓冲区中的有效数据长度

//比如最后调用PacketReceivePacket函数时所接收的数据大小

DWORD ulBytesReceived;

……

//省略掉为了与老版本兼容的一些字段

}PACKET,*LPPACKET;


该结构体的主要用途是接收数据包,不过Packet.dll库中也在数据包的发送中使用。所以此处对各字段含义的解释以源代码注释为准,用于发送时的含义可作相应的理解。

(1)分配与释放数据包内存空间的函数

可在Packet.dll库中使用PacketAllocatePacket函数分配一个_PACKET结构体的内存空间,该函数在packetNtx\Dll\Packet32.c中实现,其源代码如下:


LPPACKET PacketAllocatePacket(void)

{

LPPACKET lpPacket;

/调用GlobalAllocPtr系统函数分配内存空间/

lpPacket=(LPPACKET)GlobalAllocPtr(

GMEM_MOVEABLE|GMEM_ZEROINIT,sizeof(PACKET));

if(lpPacket==NULL)

{//分配失败

……

}

return lpPacket;

}


从上面的代码中可以看出,PacketAllocatePacket函数主要是使用GlobalAllocPtr系统函数来分配_PACKET结构体大小的内存空间的,同时它会对所分配的内存进行清零处理。

与PacketAllocatePacket函数相对应的函数为PacketFreePacket,它所实现的功能与PacketAllocatePacket函数完全相反,即释放所分配的资源,其实现代码如下:


VOID PacketFreePacket(LPPACKET lpPacket)

{

CloseHandle(lpPacket->OverLapped.hEvent);

GlobalFreePtr(lpPacket);

}


从上可以看出,函数PacketFreePacket主要使用GlobalFreePtr系统函数来释放_PACKET结构体。

(2)初始化数据包内存的函数

在PacketInitPacket函数中,会对待发数据包进行封装,其中ulBytesReceived字段为0,PacketInitPacket函数的具体实现代码如下:


VOID PacketInitPacket(LPPACKET lpPacket,

PVOID Buffer,UINT Length)

{

lpPacket->Buffer=Buffer;

lpPacket->Length=Length;

lpPacket->ulBytesReceived=0;

lpPacket->bIoComplete=FALSE;

}


上述代码中,参数Buffer保存待发数据包的数据,Length为数据包数据长度。

(3)发送数据包的函数

封装后的数据包,在Packet.dll库中通过PacketSendPacket函数发送,该函数主要调用WriteFile系统函数实现,其具体实现代码如下:


BOOLEAN PacketSendPacket(LPADAPTER AdapterObject,

LPPACKET lpPacket,BOOLEAN Sync)

{

DWORD BytesTransfered;

BOOLEAN Result;

UNUSED(Sync);

……

if(AdapterObject->Flags==INFO_FLAG_NDIS_ADAPTER)

{//调用WriteFile系统函数发送数据包

Result=(BOOLEAN)WriteFile(AdapterObject->hFile,

lpPacket->Buffer,lpPacket->Length,

&BytesTransfered,NULL);

}

else

{//未明设备类型

AdapterObject->Flags);

Result=FALSE;

}

return Result;

}


3.内核空间中发送单个数据包的实现

Packet.dll库的PacketSendPacket函数执行WriteFile系统调用时,内核空间的NPF_Write函数被调用(响应IRP_MJ_WRITE)。


DriverObject->MajorFunction[IRP_MJ_WRITE]=NPF_Write;


由NPF_Write函数在内核空间执行数据包的发送操作,并根据Open->Nwrites成员的值完成相应的发送次数。该数值在NPF_Open函数中默认设置为1,但可通过NPF_IoControl函数修改该值。

NPF_Write函数的原型如下:


NTSTATUS NPF_Write(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)


上述函数中,参数DeviceObject指向用户所使用的设备驱动对象,参数Irp指向包含用户请求的IRP。

函数NPF_Write返回结果状态。该函数主要由下面五部分组成:


NTSTATUS NPF_Write(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)

{

POPEN_INSTANCE Open;

PIO_STACK_LOCATION IrpSp;

PNDIS_PACKET pPacket;

NDIS_STATUS Status;

ULONG NumSends;

ULONG numSentPackets;

/获得调用者在给定IRP中的堆栈位置/

IrpSp=IoGetCurrentIrpStackLocation(Irp);

/获得POPEN_INSTANCE的Open实例/

Open=IrpSp->FileObject->FsContext;

if(NPF_StartUsingOpenInstance(Open)==FALSE)

{

//收到IRP_MJ_CLEANUP IRP,中止该操作

Irp->IoStatus.Information=0;

Irp->IoStatus.Status=STATUS_CANCELLED;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_CANCELLED;

}

/———————-函数的五个重要组成部分——————————/

……

获得数据包重复发送的次数

进行参数有效性检查

判断该实例是否可执行发送操作

循环发送数据包

发送结束的后处理工作

……

/——————————————————————————-/

//完成该IRP,并返回成功状态

Irp->IoStatus.Status=STATUS_SUCCESS;

Irp->IoStatus.Information=IrpSp->Parameters.Write.Length;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;

}


接下来将详细介绍这五部分的具体实现。

(1)获得数据包重复发送的次数

该部分主要是获得Open->Nwrites值,并检查该值的有效性。下面为具体的实现代码:


/获得重复发送的次数/

NumSends=Open->Nwrites;

/验证重复发送次数的有效性,必须大于0,否则函数返回/

if(NumSends==0)

{

NPF_StopUsingOpenInstance(Open);

Irp->IoStatus.Information=0;

Irp->IoStatus.Status=STATUS_SUCCESS;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;

}


(2)进行参数有效性检查

该部分主要进行参数的有效性检查。具体检查项如下所示:

❑数据包的长度(IrpSp->Parameters.Write.Length)应该大于0。

❑数据包的长度应该小于或等于数据链路层帧的最大长度(Open->MaxFrameSize)。

❑帧的最大长度不应该为0。

❑检查用户提供的缓冲区地址(Irp->MdlAddress),应不为空。

下面为具体的实现代码:


if(IrpSp->Parameters.Write.Length==0||

Open->MaxFrameSize==0||

Irp->MdlAddress==NULL||

IrpSp->Parameters.Write.Length>Open->MaxFrameSize)

{//输入参数的有效性检查失败,函数返回

NPF_StopUsingOpenInstance(Open);

Irp->IoStatus.Information=0;

Irp->IoStatus.Status=STATUS_UNSUCCESSFUL;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_UNSUCCESSFUL;

}


(3)判断该实例是否可执行发送操作

在该部分,函数首先调用如下代码增加适配器的绑定引用计数。


/如果可能,增加适配器的绑定引用计数/

if(NPF_StartUsingBinding(Open)==FALSE)

{

//适配器没有被绑定,不能发送数据包,函数返回

NPF_StopUsingOpenInstance(Open);

Irp->IoStatus.Information=0;

Irp->IoStatus.Status=STATUS_INVALID_DEVICE_REQUEST;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_INVALID_DEVICE_REQUEST;

}


接下来判断该实例是否可执行发送操作。此处使用自旋锁Open->WriteLock来保护Open->WriteInProgress的状态,防止竞态的产生。代码如下:


/获取保护WriteInProgress变量的自旋锁/

NdisAcquireSpinLock(&Open->WriteLock);

/检查实例的写操作状态/

if(Open->WriteInProgress)

{

//另一个写操作当前正在处理中,本实例无法执行写操作

//释放自旋锁,函数返回

NdisReleaseSpinLock(&Open->WriteLock);

//解除绑定

NPF_StopUsingBinding(Open);

NPF_StopUsingOpenInstance(Open);

Irp->IoStatus.Information=0;

Irp->IoStatus.Status=STATUS_UNSUCCESSFUL;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_UNSUCCESSFUL;

}

else

{

//无正在处理中的写操作,本实例可开始写操作

//并把当前实例设置为正在执行写操作的状态

Open->WriteInProgress=TRUE;

//复位NdisWriteCompleteEvent事件

NdisResetEvent(&Open->NdisWriteCompleteEvent);

}

/释放保护WriteInProgress变量的自旋锁/

NdisReleaseSpinLock(&Open->WriteLock);


(4)循环发送数据包

如果可以执行数据包发送操作,则开始执行数据包的循环发送指令,代码如下:


/发送前的准备工作/

//复位挂起并设置等待SendComplete事件的数据包个数为0

Open->TransmitPendingPackets=0;

//复位同步多个写进程的事件WriteEvent

NdisResetEvent(&Open->WriteEvent);

//已发数据包次数初始化为0

numSentPackets=0;

/进入重复发送数据包的while循环中/

While(numSentPackets<NumSends)

{

//从发送数据包缓冲池中分配并初始化一个数据包描述符

//由pPacket返回所分配的数据包描述符

NdisAllocatePacket(

&Status,

&pPacket,

Open->PacketPool);

if(Status==NDIS_STATUS_SUCCESS)

{

//缓冲池中有空闲数据包可用,准备用NdisSend发送该数据包

//如果有要求,可为该数据包设置SkipSentPackets标识

//目前,只在禁止接收环回数据包时设置该标识

//也就是拒收由自己发送的数据包

if(Open->SkipSentPackets)

{

NdisSetPacketFlags(pPacket,g_SendPacketFlags);

}

//数据包没有一个缓冲区,不需要在每次执行单个写操作后

//执行内存释放

//从而在NPF_SendComplete函数中区别不同的处理方式

RESERVED(pPacket)->FreeBufAfterWrite=FALSE;

//把写缓冲区附加给该数据包

NdisChainBufferAtFront(pPacket,Irp->MdlAddress);

//递增挂起待发数据包的数目

InterlockedIncrement(

&Open->TransmitPendingPackets);

//复位NdisWriteCompleteEvent事件

NdisResetEvent(&Open->NdisWriteCompleteEvent);

//向底层的MAC层请求数据包发送

NdisSend(&Status,Open->AdapterHandle,pPacket);

if(Status!=NDIS_STATUS_PENDING)

{

//数据包的发送没有被挂起,立即调用完成函数

NPF_SendComplete(Open,pPacket,Status);

}

numSentPackets++;//已发数据包增加1个

}

else

{

//传输池中没有空闲数据包可用,需要等待一段时间

//当至少发送数据包缓冲池有一半是可用的时候

//获得Open->WriteEvent事件通知,循环继续执行

//由NPF_SendComplete函数发送该事件通知

NdisWaitEvent(&Open->WriteEvent,1);

}

}


在NPF_Write函数中,数据包的发送主要通过调用NdisSend函数来完成。NdisSend为NDIS库函数,执行数据包的底层发送,其原型如下:


VOID NdisSend(

OUT PNDIS_STATUS Status,

IN NDIS_HANDLE NdisBindingHandle,

IN PNDIS_PACKET Packet

);


上述函数中,参数Status指向一个调用者提供的变量,并存储函数返回的状态。底层驱动决定所返回的NDIS_STATUS_XXX的状态,NDIS_STATUS_XXX通常为下列值:

❑NDIS_STATUS_SUCCESS:给定的数据包已在网络上传输。

❑NDIS_STATUS_PENDING:数据包的请求被异步操作,传输结束后调用者的ProtocolSendComplete函数将被调用。

❑NDIS_STATUS_INVALID_PACKET:请求传输的大小对于NIC来说太大,或者NIC可能指出了一个错误的数据包传输给了驱动程序。

❑NDIS_STATUS_CLOSING:底层驱动程序已关闭。

❑NDIS_STATUS_RESET_IN_PROGRESS:底层驱动当前正在复位NIC。

❑NDIS_STATUS_FAILURE:返回一个不是特定描述的失败。如果不是上述的NDIS_STATUS_XXX状态,就返回该状态。

一般情况下,特定的NDIS_STATUS_XXX会返回在一个传输操作中所存在的设备I/O错误,这取决于NIC的特性与NIC驱动程序写函数的判断力。例如,一个微端口驱动程序可能会返回NDIS_STATUS_NO_CABLE,如果它的NIC为驱动程序指明了这种错误情况。

在NdisSend函数中,参数NdisBindingHandle描述了NdisOpenAdapter库函数返回的句柄,同时还描述了目标NIC或与调用者所绑定的下一底层驱动程序的虚拟适配器。

参数Packet指向调用者所提供的数据包描述,由NdisAllocatePacket分配,它会把底层NIC驱动应该传输到网线上的数据进行封装。

只要NdisSend函数返回的是NDIS_STATUS_PENDING状态,驱动程序的ProtocolSendComplete函数(NPF中对应的是NPF_SendComplete函数)在数据包被NIC发送完成后就会被调用来处理传输操作结束后任何必要的收尾工作,诸如提示最初的请求发送已经完成等。

NPF_SendComplete函数的原型如下:


VOID NPF_SendComplete(

IN NDIS_HANDLE ProtocolBindingContext,

IN PNDIS_PACKET pPacket,

IN NDIS_STATUS Status

)


上述函数中,参数ProtocolBindingContext描述一个协议驱动分配的上下文句柄,而驱动程序则调用NdisOpenAdapter函数获得该句柄;参数pPacket指向协议驱动提供的已完成发送的数据包的描述符;参数Status描述了发送操作的最终状态。

NPF_SendComplete函数会针对NPF_Write与NPF_BufferedWrite函数(见后续发送队列的内容)做不同的处理。比如,针对NPF_Write操作,如果发送数据包缓冲池中空闲的数据包个数不少于一半,那么该函数就会给出数据包可写入的事件(Open->WriteEvent)通知。同时如果检测到待发数据包个数(stillPendingPackets)为0,那么就产生数据包发送完毕的事件(Open->NdisWriteCompleteEvent)通知,并释放各种必要的资源,递减挂起待发数据包的数目。

NPF_SendComplete函数的具体实现如下:


VOID NPF_SendComplete(

IN NDIS_HANDLE ProtocolBindingContext,

IN PNDIS_PACKET pPacket,

IN NDIS_STATUS Status

)

{

POPEN_INSTANCE Open;

PMDL TmpMdl;

Open=(POPEN_INSTANCE)ProtocolBindingContext;

if(RESERVED(pPacket)->FreeBufAfterWrite)

{

/数据包由NPF_BufferedWrite函数发送/

……

}

else

{

/数据包由NPF_Write函数发送/

//递减挂起待发数据包的数目

ULONG stillPendingPackets=

InterlockedDecrement(&Open->TransmitPendingPackets);

//把该数据包空间放回到发送数据包缓冲池中

NdisFreePacket(pPacket);

if(stillPendingPackets<TRANSMIT_PACKETS/2)

{

//如果通过NdisSend函数提交,并且未得到确认发送的数据包个数

//低于发送数据包缓冲池中数据包个数的一半

//将唤醒正在等待发送数据包缓冲池中具有可用数据包空间的发送者

//(如NPF_Write函数)

NdisSetEvent(&Open->WriteEvent);

}

else

{

//否则,复位该事件,因此我们可确认NPF_Write最终将

//阻塞,等待发送数据包缓冲池中的数据包可用

NdisResetEvent(&Open->WriteEvent);

}

if(stillPendingPackets==0)

{

//当所有数据包被成功发送后

//产生NdisWriteCompleteEvent事件通知

NdisSetEvent(&Open->NdisWriteCompleteEvent);

}

return;

}

}


另外,在NPF_Write与NPF_SendComplete这两个函数中,我们需要特别注意的是Open->TransmitPendingPackets变量、Open->WriteEvent事件与Open->NdisWriteCompleteEvent事件的变化关系,这些变量或事件的变化关系如图8-10所示。

8.2 数据包发送的幕后 - 图3

图 8-10 重要变量、事件的变化关系图

8.2 数据包发送的幕后 - 图4

图 8-10 (续)

8.2 数据包发送的幕后 - 图5

图 8-10 (续)

8.2 数据包发送的幕后 - 图6

图 8-10 (续)

NPF_Write函数在执行到while循环之前,代码行Open->TransmitPendingPackets=0设置挂起并等待发送完成的数据包个数为0。待执行到循环后,在调用NdisSend函数发送数据包前,代码行InterlockedIncrement(&Open->TransmitPendingPackets)原子递增挂起并等待发送完成的数据包个数;NPF_SendComplete函数中代码行ULONG stillPendingPackets=InterlockedDecrement(&Open->TransmitPendingPackets)原子递减挂起并等待发送完成的数据包个数。

NPF_Write函数在执行到while循环之前,代码行NdisResetEvent(&Open->WriteEvent)复位WriteEvent事件。待执行到循环后,调用NdisAllocatePacket函数,如果发送数据包缓冲池中没有空闲数据包可用,则需要等待一段时间。通过NdisWaitEvent(&Open->WriteEvent,1)代码行等待NPF_SendComplete函数发送的事件通知。在NPF_SendComplete中,如果发送数据包缓冲池中空闲数据包不少于一半,将通过NdisSetEvent(&Open->WriteEvent)代码行发送事件通知,否则代码行NdisResetEvent(&Open->WriteEvent)复位该事件。

NPF_Write函数若通过检查发现可以进行写操作,那么NdisResetEvent(&Open->NdisWrite CompleteEvent)代码行将复位NdisWriteCompleteEvent事件。接着进入循环体,并在每次调用NdisSend函数发送数据包前,代码行NdisResetEvent(&Open->NdisWriteCompleteEvent)复位该事件;循环体结束后使用NdisWaitEvent(&Open->NdisWriteCompleteEvent,0)代码行等待数据包发送完毕。

另外,由NPF_SendComplete函数中的下列代码检测待发数据包的个数是否为0。如果为0,则产生NdisWriteCompleteEvent事件,通知NPF_Write函数数据包发送已经完毕,NPF_Write函数将结束该事件的等待。


if(stillPendingPackets==0)

{

NdisSetEvent(&Open->NdisWriteCompleteEvent);

}


在这个过程中,应注意自旋锁的使用。在NPF_Write函数中,自旋锁Open->WriteLock会保护Open->WriteInProgress变量访问的唯一性。不过,自旋锁必须配对使用,否则将导致死锁。

NPF_Write函数中自旋锁简单的使用情况如下:


NdisAcquireSpinLock(&Open->WriteLock);

Open->WriteInProgress=FALSE;

NdisReleaseSpinLock(&Open->WriteLock);


NPF_Write函数中自旋锁较复杂的使用情况如下:


NdisAcquireSpinLock(&Open->WriteLock);

if(Open->WriteInProgress)

{

NdisReleaseSpinLock(&Open->WriteLock);

……

return STATUS_UNSUCCESSFUL;

}

else

{

Open->WriteInProgress=TRUE;

NdisResetEvent(&Open->NdisWriteCompleteEvent);

}

NdisReleaseSpinLock(&Open->WriteLock);


(5)发送结束的后处理工作

NPF_Write函数结束发送循环,并不等于数据包已经发送结束了。因为NdisSend函数的发送方式为异步方式。NPF_Write函数需要等待Open->NdisWriteCompleteEvent事件的通知,才能确定数据包已发送完毕。当NPF_SendComplete函数检测到待发数据包个数为0时,该事件由NdisSetEvent(&Open->NdisWriteCompleteEvent);语句产生。

下面为相应的代码实现:


/*

*当程序运行到此位置时,

*所有的数据包已通过NdisSend函数在排队等待发送了

*/

//现在我们仅需等待SendComplete函数产生

//Open->NdisWriteCompleteEvent事件,来确定所有数据包已发送完毕

//(当然至少需要存在一个NdisSend请求返回的是STATUS_PENDING状态)

NdisWaitEvent(&Open->NdisWriteCompleteEvent,0);

/所有的数据包被发送,释放适配器的绑定/

NPF_StopUsingBinding(Open);

/已没有写操作正在处理,更新写操作状态/

NdisAcquireSpinLock(&Open->WriteLock);

Open->WriteInProgress=FALSE;

NdisReleaseSpinLock(&Open->WriteLock);

NPF_StopUsingOpenInstance(Open);


当确认发送完毕时,NPF_Write函数释放适配器的绑定,并把该实例的写操作状态更新为非处理中的状态,这样,其他实例就可执行写操作来发送数据包了。

NPF_StartUsingOpenInstance与NPF_StopUsingOpenInstance函数。主要通过对NumPendingIrps字段的操作,来维护在pOpen实例上挂起的IRP个数,并使用OpenInUseLock自旋锁来保护该字段的同步操作。具体的实现代码如下:


BOOLEAN NPF_StartUsingOpenInstance(IN POPEN_INSTANCE pOpen)

{

BOOLEAN returnStatus;

NdisAcquireSpinLock(&pOpen->OpenInUseLock);

if(pOpen->ClosePending)

{//不能申请IRP操作

returnStatus=FALSE;

}

else

{//可以申请IRP操作

returnStatus=TRUE;

pOpen->NumPendingIrps++;//挂起的IRP操作增加一个

}

NdisReleaseSpinLock(&pOpen->OpenInUseLock);

return returnStatus;

}

VOID NPF_StopUsingOpenInstance(IN POPEN_INSTANCE pOpen)

{

NdisAcquireSpinLock(&pOpen->OpenInUseLock);

pOpen->NumPendingIrps—;//挂起的IRP操作减少一个

NdisReleaseSpinLock(&pOpen->OpenInUseLock);

}