rtl8139 END驱动性能分析
扫描二维码
随时随地手机看文章
RTL8139 网卡性能提升分析
3162412793@qq.com
技术交流QQ群:691976956
一、数据接收优化
数据接收优化,主要是从如下几个点出发进行驱动软件的修改:
接收中断实现上下部方式,中断中通过发送同步信号量,收包由一个阻塞的任务来获取到该信号量后开始进行接收的动作。
原始方式:
当有网络数据包接收完毕后,CPU会进中断服务程序,中断服务中,通过一个系统API函数来挂接收包任务,那就是netJobAdd,该函数原形如下:
STATUS netJobAdd
(
FUNCPTR routine, /*在工作程序队列中要加的例行程序*/
int param1, /*这个例行程序的第一个参数*/
int param2, /*这个例行程序的第二个参数*/
int param3, /*这个例行程序的第三个参数*/
int param4, /*这个例行程序的第四个参数*/
int param5,/*这个例行程序的第五个参数*/
)
而默认情况下,netJobAdd 接口将 routine 函数和相应的参数传递到 tNet0 任务重被执行,而该任务的优先级是 50(VxWorks6.6版本是50, 不记得VxWorks5.5.1是多少了,也许是45),该优先级固定死了,没有提升的空间。
改进方式:
经过上述的分析后,可以在中断服务程序中释放一个同步信号量,通知接收数据包的任务来收包,而该收包任务的优先级可以自定义调整,也就是具体实现中断上下部的方式。
LOCAL void rtl81x9Int
(
RTL81X9END_DEVICE *pDrvCtrl
)
{
…
#if 0
if (stat & RTL_IPT_RX_OK)
{
if (netJobAdd ((FUNCPTR)rtl81x9HandleRecvInt,(int) pDrvCtrl,0, 0, 0, 0) != OK)
DRV_LOG(DRV_DEBUG_INT, "xl: netJobAdd (rtl81x9HandleRecvInt) failedn",
0, 0, 0, 0, 0, 0);
DRV_LOG(DRV_DEBUG_RX, "RTL_IPT_RX_OKn", 0, 0, 0, 0, 0, 0);*/
}
#endif
if (stat & RTL_IPT_RX_OK)
{
if(pDrvCtrl->unit == 0)
{
/*释放信号量,开始接收数据*/
semGive(rtlNetTaskSemId0);
}
}
}
收包任务的实现
/*数据接收任务0*/
void rtlRecvTask0(RTL81X9END_DEVICE * pDrvCtrl)
{
FOREVER
{
/* wait for somebody to wakeus up */
semTake (rtlNetTaskSemId0, WAIT_FOREVER);
rtl81x9HandleRecvInt(pDrvCtrl);
}
}
在start 函数中,启动一个接受数据包的任务,如下代码所示。
VxWorks系统下的缓冲区管理机制的研究
网络协议存储池使用mBlk结构、clBlk结构、簇缓冲区和netBufLib提供的函数进行组织和管理。mBlk和clBlk结构为簇缓冲区(cluster)中数据的缓冲共享和缓冲链接提供必要的信息。netBufLib例程使用mBlk和clBlk来管理cluster和引用cluster中的数据,这些结构体中的信息用于管理cluster中的数据并且允许他们通过引用的形式来实现数据共享,从而达到数据“零拷贝”的目的。
结构体mBlk和clBlk及其数据结构
mBlk是访问存储在内存池中数据的最基本对象,由于mBlk仅仅只是通过clBlk来引用数据,这使得网络层在交换数据时就可以避免数据复制。只需把一个mBlk连到相应mBlk链上就可以存储和交换任意多的数据。一个mBlk结构体包括两个成员变量mNext和mNextPkt,由它们来组成纵横两个链表:mNext来组成横的链表,这个链表中的所有结点构成一个包(packet);mNextPkt来组成纵的链表,这个链表中的每个结点就是一个包(packet),所有的结点链在一起构成一个包队列,如图1所示。
结构体mBlk和clBlk的数据结构如下所示:
struct mBlk
{
M_BLK_HDR mBlkHdr; /* header */
M_PKT_HDR mBlkPktHdr; /* pkthdr */
CL_BLK * pClBlk; /* pointer to cluster blk */
} M_BLK;
struct clBlk
{
CL_BLK_LIST clNode;/* union of next clBlk */
UINT clSize;/* cluster size*/
int clRefCnt;/*countof thecluster */
struct netPool * pNetPool; /* pointer to the netPool */
} CL_BLK;
/* header at beginning of each mBlk */
struct mHdr
{
struct mBlk * mNext;/* nextbuffer in chain */
struct mBlk * mNextPkt;/* next chain inqueue/record */
char *mData; /* location of data */
int mLen;/* amount of data in this mBlk */
UCHAR mType;/* type of data in this mBlk */
UCHAR mFlags; /* flags; see below */
} M_BLK_HDR;
/* record/packet header in first mBlk of chain; valid if M_PKTHDR set */
struct pktHdr
{
struct ifnet * rcvif;/* rcvinterface */
int len; /* total packet length */
} M_PKT_HDR;
网络协议存储池的初始化
VxWorks在网络初始化时给网络协议分配存储池并调用netPoolInit()函数对其初
始化,由于一个网络协议通常需要不同大小的簇,因此它的存储池也必须包含很多
簇池(每一个簇池对应一个大小的簇)。如图2所示。另外,每个簇的大小必须为2
的方幂,最大可为64KB(65536),存储池的常用簇的大小为64,128,256,512,
1024比特,簇的大小是否有效取决于CL_DESC表中的相关内容,CL_DESC表是由
netPoolInit()函数调用设定的。
网络协议存储池初始化后的结构
使用netBufLib进行内存池管理
netBufLib提供了mBlks与clBlks结构,其中mBlks指向clBlks,而clBlks指向
实际存贮数据的Cluster。不同层次之间交互数据可以直接通过传递mBlks链来进
行,而不用进行多余的数据拷贝。其中clBlks的作用是,记录有多少个mBlks对其
进行了引用,当引用为零时才可以释放。不同的mBlks可以指向相同的clBlks,以
共享数据。
对于发送或接收的包可以由多个分开的内存块组成,也可以由一块大的内存块组成。
因此对于一个包来说,它有一个mBlks链,链接着这个包的所有clusters。一个包
也应该可以由一个大cluster组成,要是这样的话,一个包就只要有一个mBlks就
行了。mBlks除链接着本身的所有的mBlks外,mBlks头还链接着下一个包的mBlks
链的头。
Clusters大小:
对于Clusters的大小,可以有不同型号。用于protocol的内存池,可以有不同大
小的Clusters型号,但型号大小仍有限定(见参考资料)。用于driver的内存池,
只有一种大小的Cluster。其大小与MTU(max transport unit)类似。
建立内存池内骤:
调用netPoolInit(),初始化缓冲池参数。预留mBlk,clBlk,cluster结构空间等。
此
步应在初始化时进行。
在Clusters中保存数据:
1、在初始化时,调用netClusterGet()来预留Clusters空间。
2、当组装好数据或接收到数据则装进Clusters中的一个。
3、调用netClBlkGet()来预留clBlk结构。
4、调用netClBlkJoin()连接clBlk到包含数据的Cluster。
5、调用netMblkGet()预留mBlk结构。
6、调用netMblkClJoin()连接mBlk结构到clBlk。
释放mBlks,clBlks,Clusters:
释放mBlks链:netMblkClChainFree().这将释放链中所有的mBlks。同时减少clBlks
中mBlks对其的引用,若减少至零,则clBlks及Clusters被释放。释放单独
mBlk,clBlk,Cluster: netMblkClFree();
protocol与driver间传数据:
driver调用MUX的muxReceive();MUX调用protocol的stackRcvRtn()函数;当
muxReceive()正确返回后,driver确定数据己发送,接下来的buffer释放,由协
议栈上层来完成。(The upper layers of the stack are responsible for freeing
the memory back to the driver’s memory pool.)
三、网络收包分析
网卡收包有一系列需要注意的地方。
3.1 数据包的格式定义
数据包的格式如下。
/* cur_rx:
31 16 15 0
------------------------------------------------------
0: | WORD1 | WORD2 | -----
------------------------------------------------------
_/
+4: ------------------------------------------------------
| DWORD3 |
------------------------------------------------------
WORD1: Receive Status Flag;
WORD2: Receive Package Length;
DWORD3: Receive PackageData Start Address.
*/
数据包开头的4字节是接收的状态标志和数据长度信息,后面才是数据的开始地址,
也就是说,拷贝数据的地址从当前的指针位置 cur_rx + 4 开始,尾部是4字节的帧校验序列 FCS (Frame Check Sequence), FCS 采用32位CRC循环冗余校验对从"目标MAC地址"字段到"数据"字段的数据校验,一般该数据没有使用。
数据拷贝
当前一帧数据的长度信息是存放在位置指针的头四个字节中的,具体如上所述。
获取到的长度包含了4字节的状态信息,然后是数据,然后是4字节的 FCS。
拷贝数据动作:
如果没有跨尾,则直接拷贝即可。
memcpy (pNewCluster, readPtr + 4,len);
计算下一次DMA数据存放的地址;
cur_rx = (cur_rx + len + 4 + 3)& ~3;
如果有跨尾,则分两次拷贝
wrapSize = (int) ((readPtr + len) - (pDrvCtrl->ptrRxBufSpace +RTL_RXBUFLEN));
/* Copy in first section of message as stored */
/* at the end of the ring buffer */
memcpy (pNewCluster, readPtr + 4, len-wrapSize-4);
/* Copy in end of message as stored */
/* at the start of the ring buffer */
memcpy (pNewCluster +len - wrapSize - 4, pDrvCtrl->ptrRxBufSpace,wrapSize);
/* there have some error compiler's bug in this line*/
/* If I just copy the correct bytes the last two bytes will*/
/* have some trouble, so I copy extra bytes to fix the CPU or*/
/* OS's bug vic */
计算下一次DMA数据存放的地址;
cur_rx = (wrapSize + 4 + 3) &~3;
3.2 数据接收由硬件DMA从FIFO到主内存后,提交给 pMblk 的内存链之Cluster,
最理想的是实现“零拷贝”。
原始方式:
网卡MAC接收到数据后,先进入到内部的64K+16字节的FIFO,然后由 DMA直接将 FIFO中的数据通过PCI Master 的方式来传递到主存,主存为一个环形的内存缓冲区,如上述图所示。当有完整的数据包过来后,通过中断通知CPU,进入到中断服务程序处理接收逻辑。
首先读取接收状态,如果发现是发送中断,则直接调用发送中断服务程序,如果是接收中断,且没有发现接收错误,则发送同步信号量,收包任务被激活,进入到收包程序。
收包程序将读命令寄存器,获取到 bit 0 的数值,如果该数值一直为0,则表示缓冲区中还有数据没有取完,通过一个循环操作解析数据包,拷贝到 Cluster, 连接到clBlk, 最后连接到 mBlk, 然后提交到协议栈,直到取完为止,中间有一系列的容错处理,还有注意内存拷贝的指针计算等操作,都在里面。
改进方式:
上述的原始的方式,存在一个很明显的问题,那就是进行了数据的拷贝。分配给DMA
的环行缓冲区地址是独立的,数据首先会到这里,而我们最终提交给协议栈的内存空间却是另一个内存块,该内存块也是由用户自己分配的,那么就需要进行从第一个内存地址空间到另一个内存地址空间的搬移,这样大大地浪费了操作系统的时间,效率自然就降低了。
鉴于此,可以考虑一点,能否在由DMA环行缓冲区拷贝数据到Cluster 改为直接使用DMA的环行缓冲区作为内存管理 Cluster 的新地址,说的比较多,估计也听的有点不太明白,看下面的对比图。
原始的操作方式
现在的操作方式
这样直观看了后,发现效率肯定有明显的差别,前者需要内存拷贝,后者不需要内存拷贝。
具体的,在实现后者的设计思想上,有一些技巧需要考虑的。因为硬件设计特点,当DMA环行缓冲区到尾后,如果条件允许,数据将会自动从DMA缓冲区头开始存放数据,目前设置的DMA缓冲区大小为 64K+16(16字节用于软件自动调节下次存放的指针位置,确保是对齐的地址)。
这样的话,如果按照提交指针的方式,则会有点问题,为什么呢?因为,恰巧当数据包跨缓冲区尾和区头的时候,这样提交指针就会出现数据错误。具体原因分析如下:
虽然分配了一段地址给DMA环行缓冲区,看起来是一个环,但实际上,地址不是连接在一起的,而是一块内存,只是通过配置了寄存器后告诉硬件DMA,到内存的尾了,就自动从头开始存放而已。
如果接收到的数据包没有跨尾,则可以直接提交当前的入口指针即可,否则,如果出现了跨尾,则还是按照上述的操作会出现问题,看下图。
如果出现跨尾,则直接提交 Data Pointer 后将使用了后面虚线的内容,而不会使用前面的数据块,暂时无法实现自动回头的功能。
有两种方式解决该问题,一种就是,在网卡驱动 start函数中,动态分配一个1518+8
字节的内存块,然后将上述出现了分离的数据拷贝到该内存块,然后将该内存首地
址join到 clBlk 即可;另一种就是,在分配DMA环行缓冲区的时候,在原来的64K+16字节的基础上,多分配1518+8字节的空间,如上述的虚线框所示,这样的话,只需要将上述的最头上的兰色框的数据拷贝到DMA End地址之后,然后可以直接提交 Data Pointer join到 clBlk 即可。
但是在实际中,按照后者操作的方式,出现了一些问题,需要解决。
问题点如下:
按照后者的方式操作后,PING 没有问题,小量发送数据包也没有问题,当使用发包工具大量发包的时候,只要在串口控制台上按下任何的命令,都会出现如下的错误信息:
data access
Exception current instruction address: 0x001ebce4
Machine Status Register: 0x00003032
Data Access Register: 0x5fff0017
Condition Register: 0x42048042
Data storage interrupt Register: 0x40000000
Task: 0x273b470 "tRtlRxTask0"
0x273b470 (tRtlRxTask0): task 0x273b470 has had a failure and has beenstopped.
0x273b470 (tRtlRxTask0): fatal kernel task-level exception!
通过DEBUG方式,依次查找出现问题的原因。
上述错误信息指令地址在0x001ebce4,一般而言,如果修改代码,重新编译,该地址将会变化。
在命令行下查看了该地址信息,如下:
-> lkAddr 0x001ebce4
0x001eb290 netMblkChainDup text
0x001ec398 muxTxRestart text
0x001ec48c muxReceive text
0x001ec6a0 muxSend text
0x001ec6d8 _muxTkSendNpt text
0x001ec7c0 _muxTkSendEnd text
0x001eca98 muxTkSend text
0x001ecac8 muxTkReceive text
0x001ecd04 ipcom_sem_wait text
0x001ecd28 ipcom_mutex_lock text
0x001ecd4c ipcom_sem_interrupt_flush text
0x001ecd6c ipcom_sem_flush text
value = 0 = 0x0
问题点应该可以锁定在 netMblkChainDup 这个函数中。
可以使用 tt 命令跟踪下。
跟踪发现,在 netMblkChainDup 函数里面出现了异常。
-> tt tRtlRxTask
0x000962f8 vxTaskEntry +0x48 :rtlRecvTask0 (0x270d9e8)
0x00030324 rtlRecvTask0 +0x28 : semTake ()
0x0016d9d4 semTake +0x138:semBTake ()
value = 0 = 0x0
正常情况下,如上述所示。
如果出现了上述的数据非法访问错误,则网络收包任务将被迫停止。通过 tt 命令后就可以发现,通过一系列的函数调用后,最后的执行函数为netMblkChainDup,估计在该函数中,释放了一些内存,然后继续使用导致。
但是有一点不太明白的是,如在 shell下不执行任何的操作,网络可以承受不停的冲击,没有什么问题,如果一旦操作后,系统就挂死了。
反汇编,找到该指令的地址所在:
可以使用 objdumpppc –D vxWorks>img.s
-> objdumpppc –D vxWorks >img.s
找到目标地址:
如何实现DMA缓冲区和 pMblk 的缓冲区管理链中的 Cluster的内存共享,以达到“零拷贝”。
该部分的内存共享,如上述分析,可能存在一些问题。实际测试的结果是,网络在该情况下可以正常的收发包,但是,内存可能出现了问题,或者操作系统的任务栈遭受了破坏,shell 下无法输入命令,只要一输入,系统就挂死了。同上3),原因还没有找到。
借助以前开发VxWorks系统下的 1394驱动经验,能否借助于消息队列来提升性能?
借助于 Linux 系统下面的接收机制
初始化使能中断后,第一次无论是发送完成或接收中断,进去后,关闭所有中断,然后读取中断状态寄存器,判断是何种类型的中断,如果是发送完成中断,则在退出中断之前,仅仅打开发送中断,如果是接收中断,则在退出中断之前,不使能接收中断,等接收数据包任务完成后再使能该中断。
总结为一句话,发送中断按照原始的处理方式,而接收中断第一次进中断后,屏蔽该中断类型,中断服务程序释放一个同步信号量,激活收包任务,采用轮询的方式直到将数据包接收完毕后再打开接收中断。
二、数据发送优化
在阅读发送函数时,发现协议栈(以下简称上层),将pMblk 指针传递给发送函数,然后再查询是否有可用的发送描述符后,对上层的数据包进行拷贝到发送描述符对应的缓冲区,设置发送,等待完成。
这里对速度影响比较大的点就是,执行了数据的拷贝动作,参考如下代码:
LOCAL STATUS rtl81x9Send
(
RTL81X9END_DEVICE *pDrvCtrl, /* device ptr */
M_BLK_ID pMblk /* data to send */
)
{
…
/* Replace this code !! */
#if 0
len = netMblkToBufCopy(pMblk, pBuf, NULL);
netMblkClChainFree(pMblk);
#endif
len =pMblk->mBlkHdr.mLen;
/*pMblk->mBlkPktHdr.len = len;*/
len = max (len, ETHERSMALL);
tx_val = len + (0x38<< 16);
/* pass pointer to the TX desc register */
pBuf =pMblk->mBlkHdr.mData;
…
/* Flush the writepipe */
CACHE_PIPE_FLUSH ();
netMblkClChainFree(pMblk); /* Add byalex */
}
上述代码中,红色字体部分是原始的代码,需要进行拷贝,这样会影响到发送性能,修改为蓝色字体部分,基本上实现了“零拷贝”,经过 EtherPeek 发包软件测试,没有问题。