RTL8139 網卡性能提升分析
3162412793@qq.com
技術交流QQ群:691976956
?一、數據接收優(yōu)化
數據接收優(yōu)化,主要是從如下幾個點出發(fā)進行驅動軟件的修改:
接收中斷實現上下部方式,中斷中通過發(fā)送同步信號量,收包由一個阻塞的任務來獲取到該信號量后開始進行接收的動作。
原始方式:
當有網絡數據包接收完畢后,CPU會進中斷服務程序,中斷服務中,通過一個系統API函數來掛接收包任務,那就是netJobAdd,該函數原形如下:
?? STATUS netJobAdd
???? (
???? FUNCPTR routine, /*在工作程序隊列中要加的例行程序*/
???? int param1, /*這個例行程序的第一個參數*/
???? int param2, /*這個例行程序的第二個參數*/
???? int param3, /*這個例行程序的第三個參數*/
???? int param4, /*這個例行程序的第四個參數*/
???? int param5,/*這個例行程序的第五個參數*/
???? )
而默認情況下,netJobAdd 接口將 routine 函數和相應的參數傳遞到 tNet0 任務重被執(zhí)行,而該任務的優(yōu)先級是 50(VxWorks6.6版本是50, 不記得VxWorks5.5.1是多少了,也許是45),該優(yōu)先級固定死了,沒有提升的空間。
改進方式:
經過上述的分析后,可以在中斷服務程序中釋放一個同步信號量,通知接收數據包的任務來收包,而該收包任務的優(yōu)先級可以自定義調整,也就是具體實現中斷上下部的方式。
?
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系統下的緩沖區(qū)管理機制的研究
網絡協議存儲池使用mBlk結構、clBlk結構、簇緩沖區(qū)和netBufLib提供的函數進行組織和管理。mBlk和clBlk結構為簇緩沖區(qū)(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,以
共享數據。
?
對于發(fā)送或接收的包可以由多個分開的內存塊組成,也可以由一塊大的內存塊組成。
因此對于一個包來說,它有一個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確定數據己發(fā)送,接下來的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字節(jié)是接收的狀態(tài)標志和數據長度信息,后面才是數據的開始地址,
也就是說,拷貝數據的地址從當前的指針位置 cur_rx + 4 開始,尾部是4字節(jié)的幀校驗序列 FCS (Frame Check Sequence), FCS 采用32位CRC循環(huán)冗余校驗對從"目標MAC地址"字段到"數據"字段的數據校驗,一般該數據沒有使用。
?
數據拷貝
當前一幀數據的長度信息是存放在位置指針的頭四個字節(jié)中的,具體如上所述。
獲取到的長度包含了4字節(jié)的狀態(tài)信息,然后是數據,然后是4字節(jié)的 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字節(jié)的FIFO,然后由 DMA直接將 FIFO中的數據通過PCI Master 的方式來傳遞到主存,主存為一個環(huán)形的內存緩沖區(qū),如上述圖所示。當有完整的數據包過來后,通過中斷通知CPU,進入到中斷服務程序處理接收邏輯。
首先讀取接收狀態(tài),如果發(fā)現是發(fā)送中斷,則直接調用發(fā)送中斷服務程序,如果是接收中斷,且沒有發(fā)現接收錯誤,則發(fā)送同步信號量,收包任務被激活,進入到收包程序。
?
收包程序將讀命令寄存器,獲取到 bit 0 的數值,如果該數值一直為0,則表示緩沖區(qū)中還有數據沒有取完,通過一個循環(huán)操作解析數據包,拷貝到 Cluster, 連接到clBlk, 最后連接到 mBlk, 然后提交到協議棧,直到取完為止,中間有一系列的容錯處理,還有注意內存拷貝的指針計算等操作,都在里面。
?
改進方式:
上述的原始的方式,存在一個很明顯的問題,那就是進行了數據的拷貝。分配給DMA
的環(huán)行緩沖區(qū)地址是獨立的,數據首先會到這里,而我們最終提交給協議棧的內存空間卻是另一個內存塊,該內存塊也是由用戶自己分配的,那么就需要進行從第一個內存地址空間到另一個內存地址空間的搬移,這樣大大地浪費了操作系統的時間,效率自然就降低了。
鑒于此,可以考慮一點,能否在由DMA環(huán)行緩沖區(qū)拷貝數據到Cluster 改為直接使用DMA的環(huán)行緩沖區(qū)作為內存管理 Cluster 的新地址,說的比較多,估計也聽的有點不太明白,看下面的對比圖。
?
原始的操作方式
?
?
現在的操作方式
?
這樣直觀看了后,發(fā)現效率肯定有明顯的差別,前者需要內存拷貝,后者不需要內存拷貝。
具體的,在實現后者的設計思想上,有一些技巧需要考慮的。因為硬件設計特點,當DMA環(huán)行緩沖區(qū)到尾后,如果條件允許,數據將會自動從DMA緩沖區(qū)頭開始存放數據,目前設置的DMA緩沖區(qū)大小為 64K+16(16字節(jié)用于軟件自動調節(jié)下次存放的指針位置,確保是對齊的地址)。
這樣的話,如果按照提交指針的方式,則會有點問題,為什么呢?因為,恰巧當數據包跨緩沖區(qū)尾和區(qū)頭的時候,這樣提交指針就會出現數據錯誤。具體原因分析如下:
雖然分配了一段地址給DMA環(huán)行緩沖區(qū),看起來是一個環(huán),但實際上,地址不是連接在一起的,而是一塊內存,只是通過配置了寄存器后告訴硬件DMA,到內存的尾了,就自動從頭開始存放而已。
?
?
如果接收到的數據包沒有跨尾,則可以直接提交當前的入口指針即可,否則,如果出現了跨尾,則還是按照上述的操作會出現問題,看下圖。
?
?
如果出現跨尾,則直接提交 Data Pointer 后將使用了后面虛線的內容,而不會使用前面的數據塊,暫時無法實現自動回頭的功能。
有兩種方式解決該問題,一種就是,在網卡驅動 start函數中,動態(tài)分配一個1518+8
字節(jié)的內存塊,然后將上述出現了分離的數據拷貝到該內存塊,然后將該內存首地
址join到 clBlk 即可;另一種就是,在分配DMA環(huán)行緩沖區(qū)的時候,在原來的64K+16字節(jié)的基礎上,多分配1518+8字節(jié)的空間,如上述的虛線框所示,這樣的話,只需要將上述的最頭上的蘭色框的數據拷貝到DMA End地址之后,然后可以直接提交 Data Pointer ?join到 clBlk 即可。
??????
但是在實際中,按照后者操作的方式,出現了一些問題,需要解決。
?
問題點如下:
?
按照后者的方式操作后,PING 沒有問題,小量發(fā)送數據包也沒有問題,當使用發(fā)包工具大量發(fā)包的時候,只要在串口控制臺上按下任何的命令,都會出現如下的錯誤信息:
?
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 命令跟蹤下。
跟蹤發(fā)現,在 netMblkChainDup 函數里面出現了異常。
?
-> tt tRtlRxTask
?
0x000962f8 vxTaskEntry? +0x48 :rtlRecvTask0 (0x270d9e8)
0x00030324 rtlRecvTask0 +0x28 : semTake ()
0x0016d9d4 semTake????? +0x138:semBTake ()
value = 0 = 0x0
?
正常情況下,如上述所示。
?
如果出現了上述的數據非法訪問錯誤,則網絡收包任務將被迫停止。通過 tt 命令后就可以發(fā)現,通過一系列的函數調用后,最后的執(zhí)行函數為netMblkChainDup,估計在該函數中,釋放了一些內存,然后繼續(xù)使用導致。
?
但是有一點不太明白的是,如在 shell下不執(zhí)行任何的操作,網絡可以承受不停的沖擊,沒有什么問題,如果一旦操作后,系統就掛死了。
?
反匯編,找到該指令的地址所在:
可以使用 objdumpppc –D vxWorks>img.s
?
-> objdumpppc –D vxWorks >img.s
?
?
找到目標地址:
?
?
如何實現DMA緩沖區(qū)和 pMblk 的緩沖區(qū)管理鏈中的 Cluster的內存共享,以達到“零拷貝”。
該部分的內存共享,如上述分析,可能存在一些問題。實際測試的結果是,網絡在該情況下可以正常的收發(fā)包,但是,內存可能出現了問題,或者操作系統的任務棧遭受了破壞,shell 下無法輸入命令,只要一輸入,系統就掛死了。同上3),原因還沒有找到。
?
借助以前開發(fā)VxWorks系統下的 1394驅動經驗,能否借助于消息隊列來提升性能?
借助于 Linux 系統下面的接收機制
初始化使能中斷后,第一次無論是發(fā)送完成或接收中斷,進去后,關閉所有中斷,然后讀取中斷狀態(tài)寄存器,判斷是何種類型的中斷,如果是發(fā)送完成中斷,則在退出中斷之前,僅僅打開發(fā)送中斷,如果是接收中斷,則在退出中斷之前,不使能接收中斷,等接收數據包任務完成后再使能該中斷。
總結為一句話,發(fā)送中斷按照原始的處理方式,而接收中斷第一次進中斷后,屏蔽該中斷類型,中斷服務程序釋放一個同步信號量,激活收包任務,采用輪詢的方式直到將數據包接收完畢后再打開接收中斷。
?
二、數據發(fā)送優(yōu)化
在閱讀發(fā)送函數時,發(fā)現協議棧(以下簡稱上層),將pMblk 指針傳遞給發(fā)送函數,然后再查詢是否有可用的發(fā)送描述符后,對上層的數據包進行拷貝到發(fā)送描述符對應的緩沖區(qū),設置發(fā)送,等待完成。
這里對速度影響比較大的點就是,執(zhí)行了數據的拷貝動作,參考如下代碼:
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 */
}
上述代碼中,紅色字體部分是原始的代碼,需要進行拷貝,這樣會影響到發(fā)送性能,修改為藍色字體部分,基本上實現了“零拷貝”,經過 EtherPeek 發(fā)包軟件測試,沒有問題。
?