8.2 数据包发送的幕后
在网络分析中,要灵活高效率地发送数据包,在设计与实现上存在着如下主要难点:
❑需要有非常高的网络流量,以及极高的网络带宽利用率。
❑对于同步发送方式,需要有精准的时间控制。
❑由于数据包的发送过程贯穿于用户空间与内核空间,所以还需要有效地提高交互效率。
❑需要给用户提供齐备的机制,才能使用户灵活使用所提供的功能。
接下来,我们通过分析WinPcap的具体实现来了解如何克服以上这些困难。WinPcap是通过内核空间与用户空间中各函数间的相互配合,来完成数据包的发送功能的。8.1节所提到的第一种与第二种发送方式的实现机制基本一致,第三种与第四种发送方式的实现机制基本一致。所以图8-8只给出了与发送单个数据包与发送一个数据包队列相关的重要函数。
图 8-8 函数调用关系图
后续各节将依据从用户空间到内核空间的顺序,来详细描述各种发送方式的具体实现。
8.2.1 发送单个数据包的实现
WinPcap发送单个数据包的实现主要依赖于wpcap.dll库中的pcap_sendpacket函数,而该函数主要依赖于Packet.dll库所提供的PacketSendPacket函数,不过,最终的数据包发送是在内核空间的NPF_Write函数中调用NDIS库的NdisSend函数完成的。
这种发送方式,使用简单,较容易实现,理解起来也比较容易。图8-9给出了与发送单个数据包相关的重要函数的调用关系。下面将详细分析WinPcap中相应函数的具体设计与实现。
图 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-10 重要变量、事件的变化关系图
图 8-10 (续)
图 8-10 (续)
图 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);
}