10.2.3 内核空间部分的实现
WinPcap在用户空间与内核空间之间主要通过环形缓冲区进行数据传输,其中NPF_tap函数向缓冲区中写入数据,而NPF_Read函数从缓冲区中读取数据。
1.环形缓冲区的读写
下面将结合NPF_tap函数与NPF_Read函数的具体实现来分析NPF对内核环形缓冲区的操作,具体可分为以下三种情况。
在图10-6至图10-10中LocalData->P为缓冲区的写索引(生产者),LocalData->C为缓冲区的读索引(消费者)。0表示缓冲区的起始位置,Open->Size表示缓冲区的大小。
(1)读写无回卷
如果写入或读取数据后的索引不超过Open->Size位置,就不会发生回卷现象,只需一次数据复制就可以了,图10-6所示的就是这种情况。
图 10-6 读写无回卷
在这种情况下,NPF_tap函数中更新LocalData->P的操作如下:
LocalData->P+=sizeof(struct PacketHeader);//增加数据包头部的长度
LocalData->P+=fres;//fres为需要接收的数据包的长度
而在NPF_Read函数中,更新LocalData->C的操作如下:
LocalData->C+=sizeof(struct PacketHeader);
LocalData->C+=plen;//plen为数据包的长度
(2)数据包数据的读写出现回卷
如果读取数据包数据后,索引会超过Open->Size位置,则会发生读回卷现象,这时就需要两次数据复制操作,如图10-7所示。
图 10-7 数据包数据读出现回卷
在这种情况下,NPF_Read函数中更新LocalData->C的操作如下所示:
ToCopy=Open->Size-LocalData->C;//读入缓冲区尾部的数据长度
LocalData->C=Plen-ToCopy;//读入缓冲区头部的数据长度
另外,如果写入数据后的索引会超过Open->Size位置,同样会发生写回卷现象,这时仍需要两次数据复制操作,如图10-8所示。
图 10-8 数据包数据写出现回卷
在这种情况下,NPF_tap函数中更新LocalData->P的操作如下所示:
ToCopy=Open->Size-LocalData->P;//写取缓冲区尾部的数据长度
LocalData->P=fres-ToCopy;//写取缓冲区头部的数据长度
(3)数据包头信息的读写出现回卷
对于数据包头信息的写入与读取,NPF中做了特殊处理,以防止把数据包的头信息分割为两部分存储到两块不连续的内存中。
在写入数据包头信息时,如果Open->Size减去LocalData->P小于sizeof(struct PacketHeader)(其中,sizeof(struct PacketHeader)为数据包头信息的长度),则NPF执行写回卷,LocalData->P直接从缓冲区起始(0)位置开始存储头信息数据,只需一次数据复制即可,如图10-9所示。
图 10-9 数据包头信息写出现回卷
在这种情况下,NPF_tap函数中更新LocalData->P的操作如下所示:
if(Open->Size-LocalData->P<sizeof(struct PacketHeader))
{
LocalData->P=0;
}
而在读取数据包头信息时,如果Open->Size减去LocalData->C小于sizeof(struct PacketHeader),则NPF执行读回卷,LocalData->C直接从缓冲区起始(0)位置开始存储头信息数据,也只需一次数据复制即可,如图10-10所示。
图 10-10 数据包头信息读出现回卷
在这种情况下,NPF_Read函数中更新LocalData->C的操作如下所示:
if(Open->Size-LocalData->C<sizeof(struct PacketHeader))
{
LocalData->C=0;
}
下面我们来了解一下NPF驱动程序中与数据包捕获相关的代码实现。
2.NPF_Read函数
Packet.dll库中的PacketReceivePacket函数最终将进行ReadFile系统调用。在调用了ReadFile系统函数后,把内核缓冲区中的数据搬移到用户缓冲区中的具体实现将由NPF_Read函数完成。在驱动程序DriverEntry函数中,NPF_Read函数的调用由下面这行代码来指定:
DriverObject->MajorFunction[IRP_MJ_READ]=NPF_Read;
NPF_Read函数服务于用户的读操作(ReadFile系统调用),其原型如下:
NTSTATUS NPF_Read(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
);
上述函数中,参数DeviceObject指向用户使用的设备对象;参数Irp指向包含用户请求的IRP。
NPF_Read函数将返回操作的状态,可通过WDK的ntstatus.h文件了解状态的详细定义。
NPF_Read函数的主要执行步骤如图10-11所示。
图 10-11 NPF_Read函数的主要步骤
NPF_Read函数的主要实现代码如下:
NTSTATUS NPF_Read(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{
POPEN_INSTANCE Open;
PIO_STACK_LOCATION IrpSp;
PUCHAR packp;
ULONG Input_Buffer_Length;
UINT Thead;
UINT Ttail;
UINT TLastByte;
PUCHAR CurrBuff;
LARGE_INTEGER CapTime;
LARGE_INTEGER TimeFreq;
struct bpf_hdr*header;
KIRQL Irql;
PUCHAR UserPointer;
ULONG bytecopy;
UINT SizeToCopy;
UINT PktLen;
ULONG
copied,count,current_cpu,av,plen,increment,ToCopy,available;
CpuPrivateData*LocalData;
ULONG i;
ULONG Occupation;
IrpSp=IoGetCurrentIrpStackLocation(Irp);
Open=IrpSp->FileObject->FsContext;
/检测是否可以申请该IRP操作/
if(NPF_StartUsingOpenInstance(Open)==FALSE)
{
//收到一个IRP_MJ_CLEANUP请求,该请求失败,函数返回
Irp->IoStatus.Information=0;
Irp->IoStatus.Status=STATUS_CANCELLED;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_CANCELLED;
}
/需要测试该设备是否仍然绑定到网络适配器上/
if(NPF_StartUsingBinding(Open)==FALSE)
{
//网络适配器被移出或禁止,在没有绑定的情况下,函数立即返回错误
NPF_StopUsingOpenInstance(Open);
EXIT_FAILURE(0);
}
NPF_StopUsingBinding(Open);
/检查内核缓冲区的大小/
if(Open->Size==0)
{
NPF_StopUsingOpenInstance(Open);
EXIT_FAILURE(0);
}
/检查是否处于内核转储模式,并且转储文件有效,在该模式下不能执行读操作/
if(Open->mode&MODE_DUMP&&Open->DumpFileHandle==NULL)
{//如果处于内核转储模式,并且转储文件为空,函数返回
NPF_StopUsingOpenInstance(Open);
EXIT_FAILURE(0);
}
/计算每个内核缓冲区内数据的数量/
Occupation=0;
for(i=0;i<g_NCpu;i++)
Occupation+=(Open->Size-Open->CpuData[i].Free);
/查看内核缓冲区中的数据量是否足够进行复制/
if(Occupation<=Open->MinToCopy*g_NCpu||Open->mode&MODE_DUMP)
{//数据不够
if(Open->ReadEvent!=NULL)
{
//如果不是立即返回,则等待一些数据包被接收直到缓冲区的
//数据量足够多,或者等待超时时间到
if(Open->TimeOut.QuadPart!=(LONGLONG)IMMEDIATE)
{
KeWaitForSingleObject(
Open->ReadEvent,
UserRequest,
KernelMode,
TRUE,
(Open->TimeOut.QuadPart==(LONGLONG)0)?
NULL:&(Open->TimeOut)
);
}
KeClearEvent(Open->ReadEvent);
}
//统计模式的处理
if(Open->mode&MODE_STAT)
{//该捕获实例处于网络统计模式
……
}
}
/数据捕获模式,并且不为内核转储方式/
/从内核缓冲区中复制数据到用户缓冲区中/
……
/成功返回所复制的字节数/
{
NPF_StopUsingOpenInstance(Open);
EXIT_SUCCESS(copied);
}
}
首先,NPF_Read函数会检查与当前NPF实例相关联的内核环形缓冲区中的数据量。如果实例处于捕获模式,并且缓冲区包含多于OPEN_INSTANCE::MinToCopy值的数据量,NPF_Read函数将会把数据搬移到用户缓冲区内,并立即返回。在该方式中,用户的读操作不会被阻塞。如果内核缓冲区包含少于OPEN_INSTANCE::MinToCopy值的数据量,应用程序的请求不会立即满足,那么它将一直被阻塞,直到达到MinToCopy个字节或者读操作超时时间到。其中,读超时时间设置在OPEN_INSTANCE::TimeOut成员中。
如果实例是在统计模式或转储模式下,应用程序的请求将会被阻塞,直到OPEN_INSTANCE::TimeOut的超时时间达到。
注意区分struct PacketHeader、struct bpf_hdr与数据包协议头三种头信息的区别。
结构体PacketHeader会被附加给内核缓冲区中的每个数据包。它封装了bpf_hdr结构体,其会被传递给用户空间的程序,而数据包的序列号(SN)由生产者(NPF_tap函数)设置,并且消费者(NPF_Read函数)会用它来对各内核缓冲区中的数据包执行“排序”操作。
结构体PacketHeader的具体定义如下:
struct PacketHeader
{
ULONG SN;//数据包的序列号
struct bpf_hdr header;//数据包的头信息
};
上述代码中,结构体bpf_hdr描述了所捕获数据包的信息,包括捕获时间、捕获长度、数据包原始长度等信息。而数据包协议头是所捕获数据包本身的网络协议头信息。其中从内核缓冲区中复制数据到用户缓冲区中的过程如图10-12所示。
图 10-12 从内核缓冲区复制数据到用户缓冲区的主要步骤
从内核缓冲区复制数据到用户缓冲区的主要实现代码如下:
/初始化各计数,获得用户空间缓冲区可用空间与用户空间缓冲区地址/
copied=0;
count=0;
current_cpu=0;
available=IrpSp->Parameters.Read.Length;//用户空间缓冲区
//可用空间
packp=(PUCHAR)MmGetSystemAddressForMdlSafe(
Irp->MdlAddress,NormalPagePriority);//获得用户空间缓冲区地址
if(packp==NULL)
{
NPF_StopUsingOpenInstance(Open);
EXIT_FAILURE(0);
}
/清除读取事件/
if(Open->ReadEvent!=NULL)
KeClearEvent(Open->ReadEvent);
/依次循环从每个CPU的内核缓冲区中读取数据包内容到用户空间/
……
/依次循环从每个CPU的内核缓冲区中读取数据包内容到用户空间/
while(count<g_NCpu)
{
//检查用户空间缓冲区有无可用空闲空间
if(available==copied)
{//无可用空闲空间,函数成功返回
NPF_StopUsingOpenInstance(Open);
EXIT_SUCCESS(copied);
}
//获得待读取的内核缓冲区
LocalData=&Open->CpuData[current_cpu];
//在所选的缓冲区中是否有一些数据包可读取
if(LocalData->Free<Open->Size)//有数据包可读取
{
//获得数据包的头结构体,LocalData->C为缓冲区开始读取的位置
struct PacketHeaderHeader=(struct PacketHeader)
(LocalData->Buffer+LocalData->C);
//检测是否为下一个待复制的数据包
if(Header->SN==Open->ReaderSN)
{
//获得该次所捕获数据包的数据长度
plen=Header->header.bh_caplen;
if(plen+sizeof(struct bpf_hdr)>available-copied)
{//如果用户空间缓冲区没有适合的空间放置数
//据包,结束数据包的复制,函数成功返回
EXIT_SUCCESS(copied);
}
//给用户空间缓冲区赋数据包头结构体bpf_hdr,
//更新已复制数据的量,更新LocalData->C
((struct bpf_hdr)(&packp[copied]))=Header->header;
copied+=sizeof(struct bpf_hdr);
LocalData->C+=sizeof(struct PacketHeader);
if(LocalData->C==Open->Size)
{//处理回卷
LocalData->C=0;
}
//检查数据在内核缓冲区中是否分成两块存储
//(环形缓冲区回卷导致)
if(Open->Size-LocalData->C<plen)
{
//分成两块,需要两次复制
ToCopy=Open->Size-LocalData->C;
RtlCopyMemory(packp+copied,
LocalData->Buffer+LocalData->C,ToCopy);
RtlCopyMemory(packp+copied+ToCopy,
LocalData->Buffer,plen-ToCopy);
//更新LocalData->C的位置
LocalData->C=plen-ToCopy;
}
else
{
//未分成两块,只需一次复制
RtlCopyMemory(packp+copied,
LocalData->Buffer+LocalData->C,plen);
//更新LocalData->C的位置
LocalData->C+=plen;
}
//递增已读序号
Open->ReaderSN++;
//处理缓冲区存储内容的字对齐
copied+=Packet_WORDALIGN(plen);
//更新内核缓冲区空闲空间数量的计数
increment=plen+sizeof(struct PacketHeader);
if(Open->Size-LocalData->C<
sizeof(struct PacketHeader))
{//如果下一个数据包将会存储在内核缓冲区的
//尾部,那么新的PacketHeader结构体将被分成两块存储,为了避免该情况,
//生产者(LocalData->P)直接跳到内核缓冲区的开始位置进行存储,
//与之对应的消费者(LocalData->C)也将直接跳到内核缓冲区的开始位置进行读取
increment+=Open->Size-LocalData->C;
LocalData->C=0;
}
InterlockedExchangeAdd(&Open->CpuData[current_cpu].Free,increment)
//对已处理的CPU个数清零,
//可实现直到所有内核缓冲区中都无数据包了,循环才会退出,
//并保证数据包序号的顺序不乱
count=0;
}
else//不是下一个待复制的数据包
{//获得下一个CPU的内核缓冲,计数加一
current_cpu=(current_cpu+1)%g_NCpu;
count++;
}
}
else//无数据包需要复制
{//获得下一个CPU的内核缓冲,计数加一
current_cpu=(current_cpu+1)%g_NCpu;
count++;
}
}
在上面的代码中注意处理内核缓冲区的回卷问题,它决定了是否需要复制两次数据。同时要考虑数据包的顺序问题,也就是说多个CPU缓冲区中的数据包不一定是按顺序排列的,在复制到用户缓冲区时需要重新排序。
3.NPF_tap函数
NPF_tap函数主要实现从NIC驱动缓冲区中把数据复制到内核环形缓冲区中,该部分的实现代码如下:
NDIS_STATUS NPF_tap(
IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_HANDLE MacReceiveContext,
IN PVOID HeaderBuffer,
IN UINT HeaderBufferSize,
IN PVOID LookaheadBuffer,
IN UINT LookaheadBufferSize,
IN UINT PacketSize)
{
POPEN_INSTANCE Open;
PNDIS_PACKET pPacket;
ULONG SizeToTransfer;
NDIS_STATUS Status;
UINT BytesTransfered;
ULONG BufferLength;
PMDL pMdl1,pMdl2;
LARGE_INTEGER CapTime;
LARGE_INTEGER TimeFreq;
UINT fres;
USHORT NPFHdrSize;
CpuPrivateData*LocalData;
ULONG Cpu;
struct PacketHeader*Header;
ULONG ToCopy;
ULONG increment;
ULONG i;
BOOLEAN ShouldReleaseBufferLock;
Open=(POPEN_INSTANCE)ProtocolBindingContext;
/获得系统给当前CPU分配的编号/
Cpu=KeGetCurrentProcessorNumber();
/获得当前CPU的内核缓冲区/
LocalData=&Open->CpuData[Cpu];
/当前CPU接收数据包的计数值递增/
LocalData->Received++;
/执行数据包过滤,分析参见第9章/
……
/处理过滤结果,fres表示过滤结果/
//如果过滤器返回0,数据包不被过滤器接收,则忽略该数据包,函数返回
if(fres==0)
{
return NDIS_STATUS_NOT_ACCEPTED;
}
//如果过滤器返回-1,则接收整个数据包
if(fres==-1||fres>PacketSize+HeaderBufferSize)
fres=PacketSize+HeaderBufferSize;
/处于统计模式的处理,分析参见第11章/
……
/如果内核缓冲区大小为0,则丢弃该数据包,函数返回/
if(Open->Size==0)
{
LocalData->Dropped++;
return NDIS_STATUS_NOT_ACCEPTED;
}
/处于内核转储模式/
……
/处于数据捕获模式/
//开始数据更新,获取内核缓冲区的自旋锁
ShouldReleaseBufferLock=TRUE;
NdisDprAcquireSpinLock(&LocalData->BufferLock);
//复制数据
do
{//检查内核缓冲区的空闲空间够不够
if(fres+sizeof(struct PacketHeader)>LocalData->Free)
{//空间不够,数据包被丢弃
LocalData->Dropped++;
break;
}
//是否存在TransferData被挂起
if(LocalData->TransferMdl1!=NULL)
{//如果TransferMdll不为NULL,表示存在
//TransferData被挂起
//(如,还没有调用完成函数TransferDataComplete),
//为了防止缓冲区的错误,该数据包被丢弃
LocalData->Dropped++;
break;
}
//HeaderBuffer与LookaheadBuffer中的数据够不够
if(LookaheadBufferSize+HeaderBufferSize>=fres)
{//第一种情况:HeaderBuffer与LookaheadBuffer中的数据足够
……
}
else
{//第二种情况:HeaderBuffer与LookaheadBuffer中的数据不够,
//需要调用NdisTransferData函数获得数据,传输
//fres-HeaderBufferSize-LookaheadBufferSize个字节的数据
……
}
}
while(FALSE);
//数据更新结束,释放内核缓冲区的自旋锁
if(ShouldReleaseBufferLock)
{
NdisDprReleaseSpinLock(&LocalData->BufferLock);
}
/函数返回/
return NDIS_STATUS_NOT_ACCEPTED;
}
如果处于数据捕获模式,NPF_tap函数主要对下面两种情况进行处理。而对每种情况都需要处理数据包头信息的填充问题,同时还得考虑缓冲区的回卷问题。
(1)HeaderBuffer与LookaheadBuffer中的数据足够
在该情况下,NIC的HeaderBuffer与LookAheadBuffer缓冲区中的可用数据(LookaheadBufferSize+HeaderBufferSize)会大于或等于所需的数据量(fres),所以只需要直接读取所需的数据。
这种情况处理的过程如图10-13所示,这种情况的主要实现代码如下:
/HeaderBuffer与LookAheadBuffer缓冲区中的可用数据够/
/在内核缓冲区中为每个数据包设置对应的数据包头部信息/
//Header指向内核缓冲区的可写位置的起点
Header=(struct PacketHeader*)(LocalData->Buffer+LocalData->P);
//接收计数加1
LocalData->Accepted++;
//设置数据包头的信息
GET_TIME(&Header->header.bh_tstamp,&G_Start_Time);//设置时间戳
Header->SN=InterlockedIncrement(&Open->WriterSN)-1;//设置
//序号
Header->header.bh_caplen=fres;//设置需要捕获的长度
Header->header.bh_datalen=
PacketSize+HeaderBufferSize;//设置所接收数据包的原始长度
Header->header.bh_hdrlen=sizeof(struct bpf_hdr);//设置结构体长度
//更新内核缓冲区的写位置LocalData->P,因为Header的值已经设置
LocalData->P+=sizeof(struct PacketHeader);
if(LocalData->P==Open->Size)//环形缓冲区的回卷处理
LocalData->P=0;
/接下来复制从网络上所接收的数据包数据/
//所需的数据是否存储在连续的缓冲区中
if(fres<=HeaderBufferSize||
(UINT)((PUCHAR)LookaheadBuffer-(PUCHAR)HeaderBuffer)
==HeaderBufferSize)
{//连续
//因为所需数据只是HeaderBuffer中的数据(fres<=HeaderBufferSize),
//或者HeaderBuffer与LookaheadBuffer是连续的,因为((UINT)((PUCHAR)LookaheadBuffer-
//(PUCHAR)HeaderBuffer)==HeaderBufferSize)
//需要复制的数据包是否要被分成两块在内核缓冲区中存储
if(Open->Size-LocalData->P<fres)
{//需要分成两块存储(因为对环形缓冲区的写操作越界,需要回卷)
//计算第一次需要复制的数据长度
ToCopy=Open->Size-LocalData->P;
//复制数据到环形缓冲区尾部
NdisMoveMappedMemory(LocalData->Buffer+
LocalData->P,HeaderBuffer,ToCopy);
//复制数据到环形缓冲区头部
NdisMoveMappedMemory(LocalData->Buffer+0,
(PUCHAR)HeaderBuffer+ToCopy,fres-ToCopy);
//更新内核缓冲区可写位置的起点
LocalData->P=fres-ToCopy;
}
else
{//只需一块存储(因为对环形缓冲区的写操作没有越界)
//复制数据到环形缓冲区
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,HeaderBuffer,fres);
//更新内核缓冲区可写位置的起点
LocalData->P+=fres;
}
}
else
{//不连续
//HeaderBuffer与LookAheadBuffer不是连续的
//并且我们需要从LookaheadBuffer复制一些数据
//需要复制的数据包是否要被分成两块在内核缓冲区中存储
if(Open->Size-LocalData->P<fres)
{//需要分成两块存储(因为对环形缓冲区的写操作越界,需要回卷)
//HeaderBuffer的内容是否需要分成两次复制
if(Open->Size-LocalData->P>=HeaderBufferSize)
{//只需一次复制
//复制HeaderBuffer
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,
HeaderBuffer,HeaderBufferSize);
LocalData->P+=HeaderBufferSize;
//LookaheadBuffer的内容是否需要分成两块存储
if(LocalData->P==Open->Size)
{//一块存储
LocalData->P=0;
NdisMoveMappedMemory(
LocalData->Buffer+0,
LookaheadBuffer,
fres-HeaderBufferSize);
LocalData->P+=(fres-HeaderBufferSize);
}
else
{//需要两块存储
ToCopy=Open->Size-LocalData->P;
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,
LookaheadBuffer,ToCopy);
LocalData->P=0;
NdisMoveMappedMemory(
LocalData->Buffer+0,
(PUCHAR)LookaheadBuffer+ToCopy,
fres-HeaderBufferSize-ToCopy);
LocalData->P=fres-HeaderBufferSize-ToCopy;
}
}
else
{//HeaderBuffer的内容需要被分成两块存储,
ToCopy=Open->Size-LocalData->P;
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,HeaderBuffer,ToCopy);
LocalData->P=0;
NdisMoveMappedMemory(
LocalData->Buffer+0,
(PUCHAR)HeaderBuffer+ToCopy,
HeaderBufferSize-ToCopy);
LocalData->P=HeaderBufferSize-ToCopy;
//LookaheadBuffer就只需要一次复制即可
//(内核缓冲区足够大,每个数据包最多只能导致一次环形缓冲区的回卷)
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,
LookaheadBuffer,fres-HeaderBufferSize);
LocalData->P+=(fres-HeaderBufferSize);
}
}
else
{//需要复制的数据包不会被分成两块在内核缓冲区中存储,
//(因为对环形缓冲区的写操作没有越过边界)
//只需要两次复制
//前一次复制HeaderBuffer中的数据
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,HeaderBuffer,HeaderBufferSize);
LocalData->P+=HeaderBufferSize;
//后一次复制LookaheadBuffer中的数据
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->P,
LookaheadBuffer,fres-HeaderBufferSize);
LocalData->P+=(fres-HeaderBufferSize);
}
}
//更新内核缓冲区的空闲内存的数量LocalData->Free
//修改内核缓冲区的空闲内存的数量,increment为缓冲区增加的空闲数据量
increment=fres+sizeof(struct PacketHeader);
//检测内核缓冲区空间的有效性与连续性,
//是否适合新的数据包的头结构体存储,
//不希望头结构体存储在缓冲区尾部的边界处。
if(Open->Size-LocalData->P<sizeof(struct PacketHeader))
{
//如果下一个头结构的存储是处于尾部的边界处,
//修改内核缓冲区的空闲内存的数量
//那么把LocalData->P(写入数据的起始位置)
//直接设置为缓冲区的起始位置
increment+=Open->Size-LocalData->P;
LocalData->P=0;
}
//更新内核缓冲区的空闲内存的数量,直接减去增加的数据量
InterlockedExchangeAdd(&LocalData->Free,(ULONG)(-(LONG)increment));
//如果缓冲区数据大于最小复制数据的要求,触发数据转储或读取事件
if(Open->Size-LocalData->Free>=Open->MinToCopy)
{
if(Open->mode&MODE_DUMP)//触发数据转储事件
NdisSetEvent(&Open->DumpEvent);
else
{
if(Open->ReadEvent!=NULL)
{//触发数据读取事件
KeSetEvent(Open->ReadEvent,0,FALSE);
}
}
}
break;
图 10-13 从内核缓冲区复制数据到用户缓冲区的主要步骤(一)
(2)HeaderBuffer与LookaheadBuffer中的数据不够
在这种情况下,NIC的HeaderBuffer与LookaheadBuffer缓冲区中可用的数据(LookaheadBufferSize+HeaderBufferSize)会少于所需的数据量(fres),所以除了要直接读取所需的数据外,还需调用NDIS库的NdisTransferData函数来获取不够的数据。
这种情况的处理过程如图10-14所示。
图 10-14 从内核缓冲区复制数据到用户缓冲区的主要步骤(二)
这种情况下的主要实现代码如下:
/*
*HeaderBuffer与LookaheadBuffer中的数据不够,需要调用NdisTransferData函数
*获得数据,传输fres-HeaderBufferSize-LookaheadBufferSize字节的数据
*/
/*
*在内核缓冲区中为每个数据包预留对应的数据包头部信息(PacketHeader)位置,
*在后面设置对应的信息
*/
LocalData->NewP=LocalData->P;
LocalData->NewP+=sizeof(struct PacketHeader);
if(LocalData->NewP==Open->Size)
LocalData->NewP=0;
/首先复制协议头信息(HeaderBuffer的内容)/
if(Open->Size-LocalData->NewP>=HeaderBufferSize)
{//只要一次复制,因为无回卷的情况
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->NewP,HeaderBuffer,HeaderBufferSize);
LocalData->NewP+=HeaderBufferSize;
//处理Open->Size-LocalData->NewP=HeaderBufferSize的情况
if(LocalData->NewP==Open->Size)
LocalData->NewP=0;//回卷到开始
}
else
{//需要两次复制,因为有回卷的情况
ToCopy=Open->Size-LocalData->NewP;
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->NewP,HeaderBuffer,ToCopy);
NdisMoveMappedMemory(LocalData->Buffer+0,
(PUCHAR)HeaderBuffer+ToCopy,HeaderBufferSize-ToCopy);
LocalData->NewP=HeaderBufferSize-ToCopy;
}
/接着复制LookaheadBuffer的内容/
if(Open->Size-LocalData->NewP>=LookaheadBufferSize)
{//只要一次复制,因为无回卷的情况
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->NewP,
LookaheadBuffer,LookaheadBufferSize);
LocalData->NewP+=LookaheadBufferSize;
if(LocalData->NewP==Open->Size)
LocalData->NewP=0;
}
else
{//需要两次复制,因为有回卷的情况
ToCopy=Open->Size-LocalData->NewP;
NdisMoveMappedMemory(
LocalData->Buffer+LocalData->NewP,LookaheadBuffer,ToCopy);
NdisMoveMappedMemory(LocalData->Buffer+0,
(PUCHAR)LookaheadBuffer+ToCopy,LookaheadBufferSize-ToCopy);
LocalData->NewP=LookaheadBufferSize-ToCopy;
}
/调用NdisTransferData函数,从底层NIC复制数据到内核缓冲区/
/准备NdisTransferData使用的缓冲区,要注意内核缓冲区回卷的问题/
if((Open->Size-LocalData->NewP)>=
(fres-HeaderBufferSize-LookaheadBufferSize))
{//无回卷,只需要一个缓冲区
pMdl1=IoAllocateMdl(LocalData->Buffer+LocalData->NewP,
fres-HeaderBufferSize-LookaheadBufferSize,
FALSE,FALSE,NULL);
if(pMdl1==NULL)
{//分配失败,数据包丢失
LocalData->Dropped++;
break;
}
//把pMdll参数描述的虚拟内存中的内容存在非分页内存池中
MmBuildMdlForNonPagedPool(pMdl1);
pMdl2=NULL;
//更新LocalData->NewP的位置
LocalData->NewP+=fres-HeaderBufferSize-LookaheadBufferSize;
}
else
{//存在回卷,需要两个缓冲区
pMdl1=IoAllocateMdl(
LocalData->Buffer+LocalData->NewP,//起始地址
Open->Size-LocalData->NewP,//长度
FALSE,FALSE,NULL);//分配一个足够大的MDL来映射一个缓冲区
if(pMdl1==NULL)
{//分配失败,数据包丢弃
LocalData->Dropped++;
break;
}
pMdl2=IoAllocateMdl(
LocalData->Buffer+0,
fres-HeaderBufferSize-LookaheadBufferSize
-(Open->Size-LocalData->NewP),
FALSE,FALSE,NULL);
if(pMdl2==NULL)
{//分配失败,释放pMdll,数据包丢弃
IoFreeMdl(pMdl1);
LocalData->Dropped++;
break;
}
//更新LocalData->NewP的位置
LocalData->NewP=fres-HeaderBufferSize-
LookaheadBufferSize-(Open->Size-LocalData->NewP);
MmBuildMdlForNonPagedPool(pMdl1);
MmBuildMdlForNonPagedPool(pMdl2);
}
/从Open->PacketPool中分配一个数据包描述符pPacket,并初始化/
NdisAllocatePacket(&Status,&pPacket,Open->PacketPool);
if(Status!=NDIS_STATUS_SUCCESS)
{//失败,无空闲的数据包内存分配,该数据包丢弃
IoFreeMdl(pMdl1);
if(pMdl2!=NULL)
IoFreeMdl(pMdl2);
LocalData->Dropped++;
break;
}
/*
*把一个给定的缓冲描述符(pMdll2)
*连接到一个数据包描述符(pPacket)上附加的缓冲描述符链表的头部
*/
if(pMdl2!=NULL)
NdisChainBufferAtFront(pPacket,pMdl2);
NdisChainBufferAtFront(pPacket,pMdl1);
RESERVED(pPacket)->Cpu=Cpu;
LocalData->TransferMdl1=pMdl1;
LocalData->TransferMdl2=pMdl2;
/设置数据包头信息,不过时间戳不在此设置,而是等到数据传递结束后设置/
Header=(struct PacketHeader*)(LocalData->Buffer+LocalData->P);
Header->header.bh_caplen=fres;
Header->header.bh_datalen=PacketSize+HeaderBufferSize;
Header->header.bh_hdrlen=sizeof(struct bpf_hdr);
/发起从底层NIC复制数据到协议驱动程序提供的数据包内存空间的请求/
NdisTransferData(
&Status,
Open->AdapterHandle,
MacReceiveContext,
LookaheadBufferSize,
fres-HeaderBufferSize-LookaheadBufferSize,
pPacket,
&BytesTransfered);
if(Status!=NDIS_STATUS_PENDING)
{//数据传输没有挂起,而是立即返回
//释放为数据传输所分配的资源
LocalData->TransferMdl1=NULL;
LocalData->TransferMdl2=NULL;
IoFreeMdl(pMdl1);
if(pMdl2!=NULL)
IoFreeMdl(pMdl2);
NdisReinitializePacket(pPacket);
NdisFreePacket(pPacket);//把packet放置到空闲队列中
//更新LocalData->P的位置
LocalData->P=LocalData->NewP;
//该数据包被正确接收,增加计数
LocalData->Accepted++;
//设置数据包头的时间戳与序号
GET_TIME(&Header->header.bh_tstamp,&G_Start_Time);
Header->SN=InterlockedIncrement(&Open->WriterSN)-1;
/更新内核缓冲区的LocalData->Free与LocalData->P/
//修改内核缓冲区的空闲内存的数量,increment为缓冲区增加的空闲数据量
increment=fres+sizeof(struct PacketHeader);
//检测内核缓冲区空间的有效性与连续性,
//确定其是否适合新的数据包的头结构体储存,
//不希望头结构体存储在缓冲区尾部的边界处。
if(Open->Size-LocalData->P<sizeof(struct PacketHeader))
{//如果下一个头结构的存储是处于尾部的边界处,修改内核缓冲区中空闲内存
//的数量,把LocalData->P(写入数据的起始位置)直接设置为缓冲区的起始位置
increment+=Open->Size-LocalData->P;
LocalData->P=0;
}
//更新内核缓冲区的空闲内存的数量,直接减去增加的数据量
InterlockedExchangeAdd(&LocalData->Free,(ULONG)(-(LONG)increment));
//如果缓冲区数据大于最小复制数据的要求,触发数据转储或读取事件
if(Open->Size-LocalData->Free>=Open->MinToCopy)
{
if(Open->mode&MODE_DUMP)//触发数据转储事件
NdisSetEvent(&Open->DumpEvent);
else
{
if(Open->ReadEvent!=NULL)
{//触发数据读取事件
KeSetEvent(Open->ReadEvent,0,FALSE);
}
}
}
break;
}
else
{//NdisTransferData没立即返回,被挂起
ShouldReleaseBufferLock=FALSE;
}
在这种情况下,还得调用NdisTransferData库函数发送一个请求,把从底层NIC接收的数据复制到一个协议驱动提供的数据包结构中,其原型如下:
VOID NdisTransferData(
OUT PNDIS_STATUS Status,
IN NDIS_HANDLE NdisBindingHandle,
IN NDIS_HANDLE MacReceiveContext,
IN UINT ByteOffset,
IN UINT BytesToTransfer,
IN OUT PNDIS_PACKET Packet,
OUT PUINT BytesTransferred
);
上述函数中,参数Status指向调用者提供的变量,返回函数的执行状态;参数NdisBindingHandle描述NdisOpenAdapter库函数返回的句柄,标识调用者下一层驱动程序所绑定的目标NIC或者虚拟适配器;参数MacReceiveContext描述底层驱动的句柄,其作为调用者ProtocolReceive(此处为NPF_tap)函数的输入参数,协议驱动必须认为该句柄是不透明的;参数ByteOffset描述从接收网络数据包开始的偏移,将从这里开始数据传输;参数BytesToTransfer描述传输字节的数量,该值可能为0;参数Packet指针指向数据包的描述符,由调用者提供,底层NIC驱动程序将会复制数据到其中;参数BytesTransferred指向调用者提供的变量,其返回实际传输的字节数,如果NdisTransferData函数的参数Status返回NDIS_STATUS_PENDING状态,则该值无效。
如果NdisTransferData函数返回NDIS_STATUS_PENDING状态,NDIS库将调用ProtocolTransferDataComplete函数(WinPcap的为NPF_TransferDataComplete函数)来完成后续处理。下面是NPF_TransferDataComplete函数的具体实现:
VOID NPF_TransferDataComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET pPacket,IN NDIS_STATUS Status,
IN UINT BytesTransfered)
{
POPEN_INSTANCE Open;
ULONG Cpu;
CpuPrivateData*LocalData;
struct PacketHeader*Header;
ULONG increment;
/TransferData完成/
Open=(POPEN_INSTANCE)ProtocolBindingContext;
Cpu=RESERVED(pPacket)->Cpu;
LocalData=&Open->CpuData[Cpu];
/释放内存映射/
IoFreeMdl(LocalData->TransferMdl1);
if(LocalData->TransferMdl2!=NULL)
IoFreeMdl(LocalData->TransferMdl2);
NdisReinitializePacket(pPacket);
//把packet放置到空闲队列中
NdisFreePacket(pPacket);
/该数据包被正确复制到内核缓冲区中,添加数据包的头信息,设置序号与时间戳/
LocalData->Accepted++;
Header=(struct PacketHeader*)(LocalData->Buffer+LocalData->P);
GET_TIME(&Header->header.bh_tstamp,&G_Start_Time);
Header->SN=InterlockedIncrement(&Open->WriterSN)-1;
/更新LocalData->P的位置/
LocalData->P=LocalData->NewP;
/更新内核缓冲区空闲内存的数量LocalData->Free/
//修改内核缓冲区的空闲内存的数量,increment为缓冲区增加的空闲数据量
increment=Header->header.bh_caplen+sizeof(struct PacketHeader);
if(Open->Size-LocalData->P<sizeof(struct PacketHeader))
{
increment+=Open->Size-LocalData->P;
LocalData->P=0;
}
InterlockedExchangeAdd(&LocalData->Free,(ULONG)(-(LONG)increment));
/如果缓冲区数据大于最小复制数据的要求,触发数据转储或读取事件/
if(Open->Size-LocalData->Free>=Open->MinToCopy)
{
if(Open->mode&MODE_DUMP)
NdisSetEvent(&Open->DumpEvent);
else
{
if(Open->ReadEvent!=NULL)
{
KeSetEvent(Open->ReadEvent,0,FALSE);
}
}
}
LocalData->TransferMdl1=NULL;
LocalData->TransferMdl2=NULL;
//释放内核缓冲区的自旋锁
NdisDprReleaseSpinLock(&LocalData->BufferLock);
return;
}