8.2.3 发送队列方式的实现
可以通过pcap_sendpacket函数设置每次发送一个数据包一次,也可以根据预先设定的次数来重复发送相应的数据包。到此,WinPcap已经为数据包的发送提供了两种发送方式,但是,这两种方式只是对每个数据包进行了简单的重复。如果我们需要发送端发送的数据包具备序号标识,以利于接收端软件判断数据包是否掉包,同时还要生成大的网络流量,那么单个数据包重复发送多次的方式就无法实现了。再如,我们需要严格控制每个数据包发送的时间间隔,这也是前面两种发送方式无法实现的。也就是说,在某些应用中这两种发送方式都不适用。鉴于此,WinPcap还提供了基于发送队列来发送数据包的方式。这是一种高级的、强大的、结构更优的数据包发送方式,可用来发送一组数据包。
队列数据包的发送方式可细分为两种方式:同步发送方式与异步发送方式。如果为同步发送方式,那么将严格遵循每个数据包的时间戳来发送每个数据包。而异步发送方式则不根据时间戳发送,它会尽所能快速地发送各数据包。
关于数据包队列的发送,WinPcap库是采用pcap_sendqueue_transmit函数来实现的。使用pcap_sendqueue_transmit函数要比直接使用pcap_sendpacket函数(它通过循环多次来发送一系列数据)更加有效率,因为这种方式的发送队列保存在内核空间驱动程序提供的缓冲区中,这就减少了上下文交换的次数,将会获得更好的吞吐量。
通过设置pcap_sendqueue_transmit函数的第三个参数sync,可选择是采用同步发送方式还是异步发送方式。如果sync为非零值,那么发送过程将是同步进行的,也就是说,只有与时间戳相符的数据包才会被处理。该同步操作由内核驱动程序“忙等待”来实现,所以此操作会消耗大量的CPU资源。尽管这个操作对CPU的要求很高,但它对数据包发送的处理结果通常是很精确的(通常可达数微秒,或更小),如此的时间精度采用pcap_sendpacket函数是不可能达到的。
图8-12给出了与发送数据包队列相关的重要函数的调用关系。下面将详细分析WinPcap中相应函数的具体设计与实现。
图 8-12 发送队列方式实现的重要函数调用关系图
1.wpcap.dll库中发送队列方式的实现
在wpcap.dll库中,与发送队列相关的函数所使用的pcap_send_queue结构体是存储原始数据包(即将被pcap_sendqueue_transmit函数发送到网络上的数据包)的数据结构,其具体定义如下:
struct pcap_send_queue
{
u_int maxlen;//队列最大长度(字节数),描述了buffer成员的容量
u_int len;//队列当前的字节数
char*buffer;//储存待发数据包的缓冲区
};
typedef struct pcap_send_queue pcap_send_queue;
(1)发送队列的创建与销毁
pcap_sendqueue_alloc函数用于创建发送队列,为存储发送队列分配内存空间,其原型如下:
pcap_send_queue*pcap_sendqueue_alloc(u_int memsize);
上述函数中,参数memsize是队列的大小(以字节为单位),它决定了发送队列能存储的数据包的最大容量。
pcap_sendqueue_alloc函数执行成功则返回所分配发送队列的内存指针,否则返回NULL。该函数的具体实现如下:
pcap_send_queue*pcap_sendqueue_alloc(u_int memsize)
{
pcap_send_queue*tqueue;
/分配发送队列结构体pcap_send_queue的内存空间/
tqueue=(pcap_send_queue*)malloc(sizeof(pcap_send_queue));
if(tqueue==NULL){
return NULL;
}
/分配发送队列buffer成员的内存空间/
tqueue->buffer=(char*)malloc(memsize);
if(tqueue->buffer==NULL){
free(tqueue);
return NULL;
}
/设置发送队列的最大存储空间/
tqueue->maxlen=memsize;
/初始化发送队列的已用存储空间/
tqueue->len=0;
//返回发送队列的指针
return tqueue;
}
在设置内存空间大小的memsize参数时,应注意它包括每个数据包头信息结构体(struct pcap_pkthdr)所占用的空间。实际使用的实例如下所示:
squeue=pcap_sendqueue_alloc(
(unsigned int)((packetlen+sizeof(struct pcap_pkthdr))*npacks));
上述代码中,packetlen为数据包长度,sizeof(struct pcap_pkthdr)为每个数据包头信息结构体的长度,npacks为数据包个数。此处假定每个数据包的长度一样。
当不再使用发送队列时,需使用pcap_sendqueue_destroy函数来释放它所占用的内存,该函数原型如下:
void pcap_sendqueue_destroy(pcap_send_queue*queue);
该函数会销毁一个发送队列,释放与发送队列相关的所有内存资源,pcap_sendqueue_destroy函数的具体实现如下:
void pcap_sendqueue_destroy(pcap_send_queue*queue)
{
free(queue->buffer);//释放发送队列buffer成员的内存空间
free(queue);//释放发送队列结构体的空间
}
(2)给发送队列添加数据包
发送队列被创建后,即可以通过pcap_sendqueue_queue函数将待发数据包添加到发送队列中。该函数会把数据包添加到queue参数所指的发送队列尾部,其原型如下:
int pcap_sendqueue_queue(pcap_send_queue*queue,
const struct pcap_pkthdr*pkt_header,
const u_char*pkt_data)
上述函数中,参数queue指向pcap_sendqueue_alloc函数所分配的发送队列;参数pkt_header是WinPcap为每个待发数据包所附加的数据头信息,它用来说明数据包的长度与发送时间戳;参数pkt_data为待发数据包。
如果pcap_sendqueue_queue函数执行成功则返回0,否则返回-1。
结构体pcap_pkthdr包含数据包的时间戳和长度信息,其具体定义如下:
struct pcap_pkthdr{
struct timeval ts;//时间戳
bpf_u_int32 caplen;//所捕获数据包的长度
bpf_u_int32 len;//数据包原始长度
}
上述代码中,结构体timeval的具体定义如下:
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
};
pcap_sendqueue_queue函数的具体实现如下:
int pcap_sendqueue_queue(pcap_send_queue*queue,
const struct pcap_pkthdr*pkt_header,
const u_char*pkt_data)
{
/检查发送队列的容量,如果不够则函数返回-1/
if(queue->len+sizeof(struct pcap_pkthdr)+
pkt_header->caplen>queue->maxlen)
{
return-1;
}
/复制数据包的头信息pcap_pkthdr/
memcpy(queue->buffer+queue->len,pkt_header,sizeof(struct pcap_pkthdr));
queue->len+=sizeof(struct pcap_pkthdr);
/复制数据包/
memcpy(queue->buffer+queue->len,pkt_data,pkt_header->caplen);
queue->len+=pkt_header->caplen;
return 0;
}
(3)发送发送队列
一旦数据包添加到发送队列完毕,则可以通过pcap_sendqueue_transmit函数将该发送队列中的数据包发送到网络中,其原型如下:
u_int pcap_sendqueue_transmit(pcap_t*p,
pcap_send_queue*queue,int sync)
上述函数中,参数p指向发送数据包的适配器;参数queue指向一个容纳待发送数据包的pcap_send_queue结构体;参数sync决定了是否以同步方式发送,如果sync不为0,则根据时间戳发送数据包,否则不根据时间戳,而是尽快发送各数据包。
函数返回值为实际所发送的字节数,如果该值小于希望发送的大小,表示发送过程中出现了错误。错误可能是由驱动程序/适配器的问题或冲突的/假的发送队列导致的。
pcap_sendqueue_transmit函数的主要实现代码如下:
u_int pcap_sendqueue_transmit(pcap_t*p,
pcap_send_queue*queue,int sync)
{
u_int res;
……
if(p->adapter==NULL)
{//无合适的适配器,函数返回
……
}
/发送数据包队列/
res=PacketSendPackets(
p->adapter,
queue->buffer,
queue->len,
(BOOLEAN)sync);
if(res!=queue->len)
{
//错误,记录错误信息
……
}
return res;
}
2.Packet.dll库中发送队列方式的实现
wpcap.dll库主要依赖于Packet.dll库的PacketSendPackets函数实现队列的发送,而在Packet.dll库中又主要是依赖操作系统的系统调用DeviceIoControl来实现数据包发送的。
下面将详细分析Packet.dll库中发送队列方式的具体实现。
(1)PacketSendPackets函数
该函数用来发送队列的待发数据包到网络上,队列中包含大量数目的待发数据包,其原型如下:
INT PacketSendPackets(LPADAPTER AdapterObject,
PVOID PacketBuff,ULONG Size,BOOLEAN Sync)
上述函数中,参数AdapterObject指向_ADAPTER结构体,该结构体表示将发送数据包的网络适配器;参数PacketBuff指向待发送数据包的缓冲区;参数Size为参数PacketBuff所指缓冲区的大小;如果参数Sync为TRUE,则根据时间戳发送数据包,如果为FALSE,则不根据时间戳发送,而是尽可能快速地发送各数据包。
PacketSendPackets函数的返回值是实际所发送的字节数,如果该值小于Size参数规定的大小,则意味着发送过程中出现了错误。错误可能是由驱动程序/适配器的问题或冲突的/假的数据包缓冲区所导致的。
该函数的具体实现代码如下:
INT PacketSendPackets(LPADAPTER AdapterObject,
PVOID PacketBuff,ULONG Size,BOOLEAN Sync)
{
BOOLEAN Res;
DWORD BytesTransfered,TotBytesTransfered=0;
struct timeval BufStartTime;
LARGE_INTEGER StartTicks,CurTicks,TargetTicks,TimeFreq;
if(AdapterObject->Flags==INFO_FLAG_NDIS_ADAPTER)
{
/获得发送队列的起始时间戳/
BufStartTime.tv_sec=((struct timeval*)(PacketBuff))->tv_sec;
BufStartTime.tv_usec=((struct timeval*)(PacketBuff))->tv_usec;
/获得参考的时间计数器/
QueryPerformanceCounter(&StartTicks);
QueryPerformanceFrequency(&TimeFreq);
CurTicks.QuadPart=StartTicks.QuadPart;
do{
//把数据发送给驱动程序
Res=(BOOLEAN)DeviceIoControl(
AdapterObject->hFile,
(Sync)?BIOCSENDPACKETSSYNC:BIOCSENDPACKETSNOSYNC,
(PCHAR)PacketBuff+TotBytesTransfered,
Size-TotBytesTransfered,
NULL,
0,
&BytesTransfered,
NULL);
//更新已传输字节数
TotBytesTransfered+=BytesTransfered;
//从循环中退出,发送结束或出错
if(TotBytesTransfered>=Size||Res!=TRUE)
break;
//计算发送下一块数据的时间间隔
TargetTicks.QuadPart=
StartTicks.QuadPart+(LONGLONG)((((struct timeval*)
((PCHAR)PacketBuff+TotBytesTransfered))->tv_sec-
BufStartTime.tv_sec)*1000000+
(((struct timeval*)((PCHAR)PacketBuff+
TotBytesTransfered))->tv_usec-BufStartTime.tv_usec))*(TimeFreq.QuadPart
)/1000000;
//等待时间间隔的逝去
while(CurTicks.QuadPart<=TargetTicks.QuadPart)
QueryPerformanceCounter(&CurTicks);
}
while(TRUE);
}
else
{//错误,未知设备类型
TotBytesTransfered=0;
}
return TotBytesTransfered;
}
此函数最终会调用DeviceIoControl系统函数来实现数据包的发送功能。注意do…while循环的作用,表明Packet.dll库考虑了可能一次DeviceIoControl调用不能把发送队列的数据全部提交给内核缓冲区的可能。关于时间同步操作的处理可参见8.2.3节。
(2)高分辨率的执行计数器
计数器是一个通用的术语,在程序设计中用来指一个递增的变量。一些系统中会包含一个高分辨率的执行计数器,也就表示它会以高的分辨率来度量所逝去的时间。
如果系统上存在硬件形式的高分辨率的执行计数器且可用,那可使用函数QueryPer-formanceCounter来获得高分辨率执行计数器的当前值(采用个数的形式表示),使用QueryPerformanceFrequency函数来获得高分辨率执行计数器的频率(采用每秒多少个的形式表示)。通过在一个代码块的开始处与结束处调用QueryPerformanceCounter函数,获得计数器的差值,结合高分辨率执行计数器的频率,可把该计数器作为一个高分辨率的计时器使用以获得代码块执行的时间间隔。
QueryPerformanceCounter函数用于获得高分辨率执行计数器的当前值,其原型如下:
BOOL QueryPerformanceCounter(LARGE_INTEGER*lpPerformanceCount);
上述函数中,参数lpPerformanceCount指向一个接收执行计数器当前值的变量,用个数表示。
如果QueryPerformanceCounter函数执行成功则返回非0值,否则返回0值。
函数QueryPerformanceFrequency返回执行计数器的频率,其原型如下:
BOOL QueryPerformanceFrequency(LARGE_INTEGER*lpFrequency);
上述函数中,参数lpFrequency指向一个接收当前执行计数器频率的变量,用个数/秒的形式表示。
如果硬件支持分辨率执行计数器,参数lpFrequency则会获得一个非0值,否则获得0值。
如果函数QueryPerformanceFrequency执行失败,则返回0值,否则返回非0值。
注意 通过QueryPerformanceFrequency函数获得的频率值依赖于处理器,例如,在一些处理器上,这个值可能就是处理器的时钟频率。另外,当系统正在运行时该频率值是不变的。
下面介绍如何通过配合使用QueryPerformanceFrequency和QueryPerformanceCounter这两个函数来获得一个代码块的实际执行时间。假设通过QueryPerformanceFrequency函数获得的高分辨率执行计数器的频率是50 000个/s,且在需要计时的程序代码块的开始处与结束处直接调用QueryPerformanceCounter函数所获得的计数值分别为1500个与3500个。此时即可通过这些值计算出这个代码块的执行时间,即0.04s(3500个-1500个=2000个,2000个/50000个/s=0.04s)。
3.内核空间中发送队列方式的实现
WinPcap的内核驱动程序NPF为了实现发送队列相关功能,主要提供了NPF_BufferedWrite函数,同时还在NPF_IoControl函数中实现了一部分辅助功能,以便将把数据包传递给NDIS层,最终它会调用NdisSend函数把数据包发送出去。
下面将分析各函数的具体实现。
(1)NPF_IoControl函数
Packet.dll库中的PacketSendPackets函数通过BIOCSENDPACKETSSYNC或BIOCSEND-PACKETSNOSYNC的IOCTL命令码来进行DeviceIoControl系统调用,最终在NPF中通过NPF_IoControl函数的部分代码完成该请求,从而实现数据包队列的发送功能。
下面为NPF_IoControl函数相应代码的具体实现:
NTSTATUS NPF_IoControl(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{……
BOOLEAN SyncWrite=FALSE;//默认为异步方式发送
……
case BIOCSENDPACKETSSYNC://同步方式发送
SyncWrite=TRUE;
case BIOCSENDPACKETSNOSYNC://异步方式发送
/首先检查是否可设置为写状态/
NdisAcquireSpinLock(&Open->WriteLock);
if(Open->WriteInProgress)
{
//另一个写操作当前正在处理,设置失败,函数返回
NdisReleaseSpinLock(&Open->WriteLock);
SET_FAILURE_UNSUCCESSFUL();
break;
}
else
{
Open->WriteInProgress=TRUE;
}
NdisReleaseSpinLock(&Open->WriteLock);
/执行写操作/
WriteRes=NPF_BufferedWrite(
Irp,
(PUCHAR)Irp->AssociatedIrp.SystemBuffer,
IrpSp->Parameters.DeviceIoControl.InputBufferLength,
SyncWrite
);
/写操作结束,设置为非写状态/
NdisAcquireSpinLock(&Open->WriteLock);
Open->WriteInProgress=FALSE;
NdisReleaseSpinLock(&Open->WriteLock);
/函数返回/
if(WriteRes!=-1)
{//成功
SET_RESULT_SUCCESS(WriteRes);//设置成功写入的字节数
}
else
{//失败
SET_FAILURE_UNSUCCESSFUL();
}
break;
……
Irp->IoStatus.Information=Information;
Irp->IoStatus.Status=Status;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return Status;
}
defne SETRESULTSUCCESS(__a)do{\
Information=a;\
Status=STATUS_SUCCESS;\
}while(FALSE)
默认方式为异步方式发送,通过对IOCTL命令码的判断来设置SyncWrite变量,然后把该变量作为NPF_BufferedWrite函数的第四个参数输入,从而决定实际的发送方式。
此处还有检测与设置可写状态Open->WriteInProgress的操作,注意其中自旋锁的使用。
(2)NPF_BufferedWrite函数
在NPF_IoControl函数中调用NPF_BufferedWrite函数,该函数用于把发送队列中的原始数据包发送到网络中,其原型如下:
INT NPF_BufferedWrite(IN PIRP Irp,IN PCHAR UserBuff,
IN ULONG UserBuffSize,BOOLEAN Sync);
上述函数中,参数UserBuff指向待发队列的缓冲区;参数UserBuffSize为缓冲区的大小。
如果NPF_BufferedWrite函数执行成功,则返回实际所发送的字节数。如果该值小于UserBuffSize参数规定的大小,则意味着在发送过程中出现了错误,错误可能是由网络适配器的问题或冲突的/假的数据包缓冲区导致的。
输入参数UserBuff指向包含大量数据包的缓冲区,每个数据包均带有一个sf_pkthdr结构体,它用于描述数据包的头信息。函数NPF_BufferedWrite会扫描并分析该缓冲区,以获得每个数据包的信息,最后通过NdisSend库函数发送所有数据包。如果参数Sync为TRUE,数据包将以同步方式发送,否则就以异步方式发送。
NPF_BufferedWrite函数的主要实现代码如下:
INT NPF_BufferedWrite(IN PIRP Irp,IN PCHAR UserBuff,IN ULONG UserBuffSize,
BOOLEAN Sync)
{
……
/检查各参数的合法性/
……
/执行数据包的发送/
//复位WriteEvent事件,用于数据包空间分配的同步
NdisResetEvent(&Open->WriteEvent);
//复位挂起的数据包个数
Open->Multiple_Write_Counter=0;
//获得时间参考
CurTicks=KeQueryPerformanceCounter(&TimeFreq);
//主循环,发送缓冲区的数据到网络
while(TRUE)
{
if(Pos==UserBuffSize)
{//到达缓冲区结尾,退出循环
result=Pos;
break;
}
//检查数据包头位置是否正确
if(UserBuffSize-Pos<sizeof(*pWinpcapHdr))
{//错误,退出循环
result=-1;
break;
}
//获得下一个数据包的数据包头
pWinpcapHdr=(struct sf_pkthdr*)(UserBuff+Pos);
//检查数据包头的数值是否正确
if(pWinpcapHdr->caplen==0||pWinpcapHdr->caplen>
Open->MaxFrameSize)
{
result=-1;
break;
}
if(Pos==0)
{//重新获得时间参考
StartTicks=KeQueryPerformanceCounter(&TimeFreq);
BufStartTime.tv_sec=pWinpcapHdr->ts.tv_sec;
BufStartTime.tv_usec=pWinpcapHdr->ts.tv_usec;
}
Pos+=sizeof(*pWinpcapHdr);//偏移数据包头长度,得到数据包位置
//检查数据包长度是否正确
if(pWinpcapHdr->caplen>UserBuffSize-Pos)
{
result=-1;
break;
}
//分配一个MDL来映射待发数据包的数据,并设置为不可交换的内存
TmpMdl=IoAllocateMdl(UserBuff+Pos,
pWinpcapHdr->caplen,FALSE,FALSE,NULL);
if(TmpMdl==NULL)
{
result=-1;
break;
}
MmBuildMdlForNonPagedPool(TmpMdl);
Pos+=pWinpcapHdr->caplen;//偏移数据包长度
//从发送数据包缓冲池中分配并初始化一个数据包描述符
//由pPacket返回所分配的数据包描述符
NdisAllocatePacket(&Status,&pPacket,Open->PacketPool);
if(Status!=NDIS_STATUS_SUCCESS)
{
//没有足够的空闲空间,等待1000ms,试图再分配
NdisResetEvent(&Open->WriteEvent);
NdisWaitEvent(&Open->WriteEvent,1000);
NdisAllocatePacket(&Status,&pPacket,Open->PacketPool);
if(Status!=NDIS_STATUS_SUCCESS)
{//第二次分配失败
IoFreeMdl(TmpMdl);//释放映射
result=-1;
break;
}
}
//如果有要求,为该数据包设置SkipSentPackets标识
//目前,只在禁止接收环回数据包时设置该标识
//比如,拒收由自己发送的数据包
if(Open->SkipSentPackets)
{
NdisSetPacketFlags(pPacket,g_SendPacketFlags);
}
//设置FreeBufAfterWrite为TRUE
//以便在NPF_SendComplete函数中区别不同的处理方式
RESERVED(pPacket)->FreeBufAfterWrite=TRUE;
TmpMdl->Next=NULL;
//给pPacket附加MDL
NdisChainBufferAtFront(pPacket,TmpMdl);
//对挂起并等待发送的数据包个数执行递增操作
InterlockedIncrement(&Open->Multiple_Write_Counter);
//执行数据的MAC层发送
NdisSend(&Status,Open->AdapterHandle,pPacket);
if(Status!=NDIS_STATUS_PENDING)
{//发送没有被挂起,直接调用完成函数
NPF_SendComplete(Open,pPacket,Status);
}
//处理同步发送
if(Sync)
{
if(Pos==UserBuffSize)
{//到达缓冲区结尾,退出循环
result=Pos;
break;
}
//检查数据包头位置是否正确
if((UserBuffSize-Pos)<sizeof(*pWinpcapHdr))
{//错误,退出循环
result=-1;
break;
}
/获得下一个数据包的数据包头/
pWinpcapHdr=(struct sf_pkthdr*)(UserBuff+Pos);
//检查数据包头的数值是否正确
if(pWinpcapHdr->caplen==0
||pWinpcapHdr->caplen>Open->MaxFrameSize
||pWinpcapHdr->caplen>
(UserBuffSize-Pos-sizeof(*pWinpcapHdr))
)
{
result=-1;
break;
}
//如果需要阻塞超过1s,则函数返回
if(pWinpcapHdr->ts.tv_sec-BufStartTime.tv_sec>1)
{
result=Pos;
break;
}
//计算在发送下一个数据包之前等待的时间间隔
TargetTicks.QuadPart=StartTicks.QuadPart+(LONGLONG)
((pWinpcapHdr->ts.tv_sec-BufStartTime.tv_sec)*1000000
+pWinpcapHdr->ts.tv_usec-BufStartTime.tv_usec)
*(TimeFreq.QuadPart)/1000000;
//等待直到时间间隔逝去
while(CurTicks.QuadPart<=TargetTicks.QuadPart)
CurTicks=KeQueryPerformanceCounter(NULL);
}
}
//等待挂起的发送操作完成
NPF_WaitEndOfBufferedWrite(Open);
//释放对NdisAdapter的绑定
NPF_StopUsingBinding(Open);
//返回已发送的字节数
return result;
}
NPF_BufferedWrite函数的处理过程如图8-13所示。
图 8-13 NPF_BufferedWrite函数的处理过程
Packet.dll库中的PacketSendPackets函数调用NPF_IoControl函数,然后NPF_IoControl函数调用NPF_BufferedWrite函数,以发送数据包队列。在这个过程中需要考虑下面两个问题:
❑数据包队列并不一定是一次全部传递给内核,可能会分几次,所以NPF_BufferedWrite函数需要返回成功发送的字节数,并需要进行数据包头部对齐的处理。
❑同步发送的时间间隔处理。
发送队列同步发送方式的时间间隔处理如图8-14所示。
图 8-14 等待时间间隔示意图
在PacketSendPackets与NPF_BufferedWrite两个函数中,都能见到下面的忙等待代码:
INT PacketSendPackets(LPADAPTER AdapterObject,
PVOID PacketBuff,ULONG Size,BOOLEAN Sync)
{
……
//等待时间间隔的逝去
while(CurTicks.QuadPart<=TargetTicks.QuadPart)
QueryPerformanceCounter(&CurTicks);
……
}
INT NPF_BufferedWrite(IN PIRP Irp,IN PCHAR UserBuff,
IN ULONG UserBuffSize,BOOLEAN Sync)
{
……
//等待时间间隔逝去
while(CurTicks.QuadPart<=TargetTicks.QuadPart)
CurTicks=KeQueryPerformanceCounter(NULL);
……
}
NPF_BufferedWrite函数与PacketSendPackets函数中各忙等待代码的区别在于,NPF_BufferedWrite函数处理每个数据包之间的时间间隔,而在PacketSendPackets函数处理每块数据包之间的时间间隔。这与上述第一个问题也有关,因为在PacketSendPackets函数中通过DeviceIoControl函数向内核空间传递数据包数据时不一定一次把数据传输完毕,可能分成多个数据块传递,所以才存在数据块间的时间间隔处理问题。这也就是在PacketSendPackets与NPF_BufferedWrite两个函数中都能看见类似的忙等待代码的原因。
(3)NPF_WaitEndOfBufferedWrite函数
在NPF_BufferedWrite函数正常执行完毕时,会调用NPF_WaitEndOfBufferedWrite函数来等待NPF_BufferedWrite函数发送的所有数据包均执行完成,然后成功返回已消耗的内存字节数。NPF_BufferedWrite函数中的相关实现代码如下:
if((PCHAR)winpcap_hdr>=EndOfUserBuff)
{
/已达缓冲区的尾部,说明本次发送完成,函数返回/
//等待挂起的发送完成,返回
NPF_WaitEndOfBufferedWrite(Open);
NPF_StopUsingBinding(Open);
return(INT)((PCHAR)winpcap_hdr-UserBuff);
}
……
if(winpcap_hdr->ts.tv_sec-BufStartTime.tv_sec>1)
{
/如果下一个数据包的发送需要阻塞超过1s,则函数返回/
//等待挂起的发送完成,返回
NPF_WaitEndOfBufferedWrite(Open);
NPF_StopUsingBinding(Open);
return(INT)((PCHAR)winpcap_hdr-UserBuff);
}
NPF_WaitEndOfBufferedWrite函数的实现代码如下:
VOID NPF_WaitEndOfBufferedWrite(POPEN_INSTANCE Open)
{
UINT i;
NdisResetEvent(&Open->WriteEvent);
for(i=0;Open->Multiple_Write_Counter>0&&i<TRANSMIT_PACKETS;i++)
{
NdisWaitEvent(&Open->WriteEvent,100);
NdisResetEvent(&Open->WriteEvent);
}
return;
}
上述代码中,参数TRANSMIT_PACKETS为发送缓冲池中最大的数据包数目,定义为256个。参数Open->Multiple_Write_Counter表示挂起并等待发送的数据包个数,每发送一个数据包其值将在NPF_BufferedWrite函数中递增,而在NPF_SendComplete函数中则会递减,相关代码如下:
INT NPF_BufferedWrite(IN PIRP Irp,IN PCHAR UserBuff,
IN ULONG UserBuffSize,BOOLEAN Sync)
{
……
Open->Multiple_Write_Counter=0;
……
InterlockedIncrement(&Open->Multiple_Write_Counter);
……
}
VOID NPF_SendComplete(IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET pPacket,IN NDIS_STATUS Status)
{
……
InterlockedDecrement(&Open->Multiple_Write_Counter);
……
}
在NPF_WaitEndOfBufferedWrite函数中,只有当Open->Multiple_Write_Counter为0时,才会结束for循环,然后退出函数。而每次执行for循环都需要等待Open->WriteEvent事件。该事件由NPF_SendComplete函数产生,它意味着此次数据包发送已完毕。在NPF_SendComplete函数中针对NPF_BufferedWrite函数的处理如下:
/数据包由NPF_BufferedWrite函数发送/
//释放与数据包关联的MDL
NdisUnchainBufferAtFront(pPacket,&TmpMdl);
IoFreeMdl(TmpMdl);
//把该数据包空间放回发送数据包缓冲池中
NdisFreePacket(pPacket);
//递减挂起待发数据包的数目
InterlockedDecrement(&Open->Multiple_Write_Counter);
//给出数据包可写入的事件通知
NdisSetEvent(&Open->WriteEvent);
return;
NPF_SendComplete函数给出数据包可写入的事件通知WriteEvent,并释放各种必要的资源、递减挂起等待发送数据包个数的计数。