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.2.3 内核空间部分的实现 - 图1

图 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.2.3 内核空间部分的实现 - 图2

图 10-7 数据包数据读出现回卷

在这种情况下,NPF_Read函数中更新LocalData->C的操作如下所示:


ToCopy=Open->Size-LocalData->C;//读入缓冲区尾部的数据长度

LocalData->C=Plen-ToCopy;//读入缓冲区头部的数据长度


另外,如果写入数据后的索引会超过Open->Size位置,同样会发生写回卷现象,这时仍需要两次数据复制操作,如图10-8所示。

10.2.3 内核空间部分的实现 - 图3

图 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.2.3 内核空间部分的实现 - 图4

图 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.2.3 内核空间部分的实现 - 图5

图 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.2.3 内核空间部分的实现 - 图6

图 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.2.3 内核空间部分的实现 - 图7

图 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.2.3 内核空间部分的实现 - 图8

图 10-13 从内核缓冲区复制数据到用户缓冲区的主要步骤(一)

(2)HeaderBuffer与LookaheadBuffer中的数据不够

在这种情况下,NIC的HeaderBuffer与LookaheadBuffer缓冲区中可用的数据(LookaheadBufferSize+HeaderBufferSize)会少于所需的数据量(fres),所以除了要直接读取所需的数据外,还需调用NDIS库的NdisTransferData函数来获取不够的数据。

这种情况的处理过程如图10-14所示。

10.2.3 内核空间部分的实现 - 图9

图 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;

}