RTL8139 網(wǎng)卡性能提升分析
3162412793@qq.com
技術(shù)交流QQ群:691976956
?一、數(shù)據(jù)接收優(yōu)化
數(shù)據(jù)接收優(yōu)化,主要是從如下幾個點出發(fā)進行驅(qū)動軟件的修改:
接收中斷實現(xiàn)上下部方式,中斷中通過發(fā)送同步信號量,收包由一個阻塞的任務(wù)來獲取到該信號量后開始進行接收的動作。
原始方式:
當(dāng)有網(wǎng)絡(luò)數(shù)據(jù)包接收完畢后,CPU會進中斷服務(wù)程序,中斷服務(wù)中,通過一個系統(tǒng)API函數(shù)來掛接收包任務(wù),那就是netJobAdd,該函數(shù)原形如下:
?? STATUS netJobAdd
???? (
???? FUNCPTR routine, /*在工作程序隊列中要加的例行程序*/
???? int param1, /*這個例行程序的第一個參數(shù)*/
???? int param2, /*這個例行程序的第二個參數(shù)*/
???? int param3, /*這個例行程序的第三個參數(shù)*/
???? int param4, /*這個例行程序的第四個參數(shù)*/
???? int param5,/*這個例行程序的第五個參數(shù)*/
???? )
而默認情況下,netJobAdd 接口將 routine 函數(shù)和相應(yīng)的參數(shù)傳遞到 tNet0 任務(wù)重被執(zhí)行,而該任務(wù)的優(yōu)先級是 50(VxWorks6.6版本是50, 不記得VxWorks5.5.1是多少了,也許是45),該優(yōu)先級固定死了,沒有提升的空間。
改進方式:
經(jīng)過上述的分析后,可以在中斷服務(wù)程序中釋放一個同步信號量,通知接收數(shù)據(jù)包的任務(wù)來收包,而該收包任務(wù)的優(yōu)先級可以自定義調(diào)整,也就是具體實現(xiàn)中斷上下部的方式。
?
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)
{
/*釋放信號量,開始接收數(shù)據(jù)*/
semGive(rtlNetTaskSemId0);
}
}
}
?????
收包任務(wù)的實現(xiàn)
?????
/*數(shù)據(jù)接收任務(wù)0*/
void rtlRecvTask0(RTL81X9END_DEVICE *?? pDrvCtrl)
{
??? FOREVER
??? {
??????? /* wait for somebody to wakeus up */
??? ???semTake (rtlNetTaskSemId0, WAIT_FOREVER);
??? ???rtl81x9HandleRecvInt(pDrvCtrl);
??? }
}
?
在start 函數(shù)中,啟動一個接受數(shù)據(jù)包的任務(wù),如下代碼所示。
?
VxWorks系統(tǒng)下的緩沖區(qū)管理機制的研究
網(wǎng)絡(luò)協(xié)議存儲池使用mBlk結(jié)構(gòu)、clBlk結(jié)構(gòu)、簇緩沖區(qū)和netBufLib提供的函數(shù)進行組織和管理。mBlk和clBlk結(jié)構(gòu)為簇緩沖區(qū)(cluster)中數(shù)據(jù)的緩沖共享和緩沖鏈接提供必要的信息。netBufLib例程使用mBlk和clBlk來管理cluster和引用cluster中的數(shù)據(jù),這些結(jié)構(gòu)體中的信息用于管理cluster中的數(shù)據(jù)并且允許他們通過引用的形式來實現(xiàn)數(shù)據(jù)共享,從而達到數(shù)據(jù)“零拷貝”的目的。
?
結(jié)構(gòu)體mBlk和clBlk及其數(shù)據(jù)結(jié)構(gòu)
mBlk是訪問存儲在內(nèi)存池中數(shù)據(jù)的最基本對象,由于mBlk僅僅只是通過clBlk來引用數(shù)據(jù),這使得網(wǎng)絡(luò)層在交換數(shù)據(jù)時就可以避免數(shù)據(jù)復(fù)制。只需把一個mBlk連到相應(yīng)mBlk鏈上就可以存儲和交換任意多的數(shù)據(jù)。一個mBlk結(jié)構(gòu)體包括兩個成員變量mNext和mNextPkt,由它們來組成縱橫兩個鏈表:mNext來組成橫的鏈表,這個鏈表中的所有結(jié)點構(gòu)成一個包(packet);mNextPkt來組成縱的鏈表,這個鏈表中的每個結(jié)點就是一個包(packet),所有的結(jié)點鏈在一起構(gòu)成一個包隊列,如圖1所示。
?
結(jié)構(gòu)體mBlk和clBlk的數(shù)據(jù)結(jié)構(gòu)如下所示:
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;
?
網(wǎng)絡(luò)協(xié)議存儲池的初始化
VxWorks在網(wǎng)絡(luò)初始化時給網(wǎng)絡(luò)協(xié)議分配存儲池并調(diào)用netPoolInit()函數(shù)對其初
始化,由于一個網(wǎng)絡(luò)協(xié)議通常需要不同大小的簇,因此它的存儲池也必須包含很多
簇池(每一個簇池對應(yīng)一個大小的簇)。如圖2所示。另外,每個簇的大小必須為2
的方冪,最大可為64KB(65536),存儲池的常用簇的大小為64,128,256,512,
1024比特,簇的大小是否有效取決于CL_DESC表中的相關(guān)內(nèi)容,CL_DESC表是由
netPoolInit()函數(shù)調(diào)用設(shè)定的。
?
網(wǎng)絡(luò)協(xié)議存儲池初始化后的結(jié)構(gòu)
?
使用netBufLib進行內(nèi)存池管理
?
netBufLib提供了mBlks與clBlks結(jié)構(gòu),其中mBlks指向clBlks,而clBlks指向
實際存貯數(shù)據(jù)的Cluster。不同層次之間交互數(shù)據(jù)可以直接通過傳遞mBlks鏈來進
行,而不用進行多余的數(shù)據(jù)拷貝。其中clBlks的作用是,記錄有多少個mBlks對其
進行了引用,當(dāng)引用為零時才可以釋放。不同的mBlks可以指向相同的clBlks,以
共享數(shù)據(jù)。
?
對于發(fā)送或接收的包可以由多個分開的內(nèi)存塊組成,也可以由一塊大的內(nèi)存塊組成。
因此對于一個包來說,它有一個mBlks鏈,鏈接著這個包的所有clusters。一個包
也應(yīng)該可以由一個大cluster組成,要是這樣的話,一個包就只要有一個mBlks就
行了。mBlks除鏈接著本身的所有的mBlks外,mBlks頭還鏈接著下一個包的mBlks
鏈的頭。
?
Clusters大小:
對于Clusters的大小,可以有不同型號。用于protocol的內(nèi)存池,可以有不同大
小的Clusters型號,但型號大小仍有限定(見參考資料)。用于driver的內(nèi)存池,
只有一種大小的Cluster。其大小與MTU(max transport unit)類似。
?
建立內(nèi)存池內(nèi)驟:
調(diào)用netPoolInit(),初始化緩沖池參數(shù)。預(yù)留mBlk,clBlk,cluster結(jié)構(gòu)空間等。
此
步應(yīng)在初始化時進行。
?
在Clusters中保存數(shù)據(jù):
1、在初始化時,調(diào)用netClusterGet()來預(yù)留Clusters空間。
2、當(dāng)組裝好數(shù)據(jù)或接收到數(shù)據(jù)則裝進Clusters中的一個。
3、調(diào)用netClBlkGet()來預(yù)留clBlk結(jié)構(gòu)。
4、調(diào)用netClBlkJoin()連接clBlk到包含數(shù)據(jù)的Cluster。
5、調(diào)用netMblkGet()預(yù)留mBlk結(jié)構(gòu)。
6、調(diào)用netMblkClJoin()連接mBlk結(jié)構(gòu)到clBlk。
?
釋放mBlks,clBlks,Clusters:
釋放mBlks鏈:netMblkClChainFree().這將釋放鏈中所有的mBlks。同時減少clBlks
中mBlks對其的引用,若減少至零,則clBlks及Clusters被釋放。釋放單獨
mBlk,clBlk,Cluster: netMblkClFree();
?
protocol與driver間傳數(shù)據(jù):
driver調(diào)用MUX的muxReceive();MUX調(diào)用protocol的stackRcvRtn()函數(shù);當(dāng)
muxReceive()正確返回后,driver確定數(shù)據(jù)己發(fā)送,接下來的buffer釋放,由協(xié)
議棧上層來完成。(The upper layers of the stack are responsible for freeing
the memory back to the driver’s memory pool.)
?
三、網(wǎng)絡(luò)收包分析
網(wǎng)卡收包有一系列需要注意的地方。
3.1 數(shù)據(jù)包的格式定義
數(shù)據(jù)包的格式如下。
??? /* cur_rx:
?????????? 31?????????? 16 15???????????? 0
??????????------------------------------------------------------????
??? 0:???? |????WORD1??? |???? WORD2????|? -----
??????????------------------------------------------------------???????
???????????? ???????????????????????????????????????_/
??? +4:??? ------------------------------------------------------
?????????? |??????????? DWORD3??????????? |
??????????------------------------------------------------------
?????????? WORD1:? Receive Status Flag;
?????????? WORD2:? Receive Package Length;
?????????? DWORD3: Receive PackageData Start Address.
???? */
??
?? 數(shù)據(jù)包開頭的4字節(jié)是接收的狀態(tài)標志和數(shù)據(jù)長度信息,后面才是數(shù)據(jù)的開始地址,
也就是說,拷貝數(shù)據(jù)的地址從當(dāng)前的指針位置 cur_rx + 4 開始,尾部是4字節(jié)的幀校驗序列 FCS (Frame Check Sequence), FCS 采用32位CRC循環(huán)冗余校驗對從"目標MAC地址"字段到"數(shù)據(jù)"字段的數(shù)據(jù)校驗,一般該數(shù)據(jù)沒有使用。
?
數(shù)據(jù)拷貝
當(dāng)前一幀數(shù)據(jù)的長度信息是存放在位置指針的頭四個字節(jié)中的,具體如上所述。
獲取到的長度包含了4字節(jié)的狀態(tài)信息,然后是數(shù)據(jù),然后是4字節(jié)的 FCS。
拷貝數(shù)據(jù)動作:
如果沒有跨尾,則直接拷貝即可。
??? memcpy (pNewCluster, readPtr + 4,len);???
??? 計算下一次DMA數(shù)據(jù)存放的地址;
??? 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數(shù)據(jù)存放的地址;
??? cur_rx = (wrapSize + 4 + 3) &~3;
3.2 數(shù)據(jù)接收由硬件DMA從FIFO到主內(nèi)存后,提交給 pMblk 的內(nèi)存鏈之Cluster,
最理想的是實現(xiàn)“零拷貝”。
?
?
?
原始方式:
網(wǎng)卡MAC接收到數(shù)據(jù)后,先進入到內(nèi)部的64K+16字節(jié)的FIFO,然后由 DMA直接將 FIFO中的數(shù)據(jù)通過PCI Master 的方式來傳遞到主存,主存為一個環(huán)形的內(nèi)存緩沖區(qū),如上述圖所示。當(dāng)有完整的數(shù)據(jù)包過來后,通過中斷通知CPU,進入到中斷服務(wù)程序處理接收邏輯。
首先讀取接收狀態(tài),如果發(fā)現(xiàn)是發(fā)送中斷,則直接調(diào)用發(fā)送中斷服務(wù)程序,如果是接收中斷,且沒有發(fā)現(xiàn)接收錯誤,則發(fā)送同步信號量,收包任務(wù)被激活,進入到收包程序。
?
收包程序?qū)⒆x命令寄存器,獲取到 bit 0 的數(shù)值,如果該數(shù)值一直為0,則表示緩沖區(qū)中還有數(shù)據(jù)沒有取完,通過一個循環(huán)操作解析數(shù)據(jù)包,拷貝到 Cluster, 連接到clBlk, 最后連接到 mBlk, 然后提交到協(xié)議棧,直到取完為止,中間有一系列的容錯處理,還有注意內(nèi)存拷貝的指針計算等操作,都在里面。
?
改進方式:
上述的原始的方式,存在一個很明顯的問題,那就是進行了數(shù)據(jù)的拷貝。分配給DMA
的環(huán)行緩沖區(qū)地址是獨立的,數(shù)據(jù)首先會到這里,而我們最終提交給協(xié)議棧的內(nèi)存空間卻是另一個內(nèi)存塊,該內(nèi)存塊也是由用戶自己分配的,那么就需要進行從第一個內(nèi)存地址空間到另一個內(nèi)存地址空間的搬移,這樣大大地浪費了操作系統(tǒng)的時間,效率自然就降低了。
鑒于此,可以考慮一點,能否在由DMA環(huán)行緩沖區(qū)拷貝數(shù)據(jù)到Cluster 改為直接使用DMA的環(huán)行緩沖區(qū)作為內(nèi)存管理 Cluster 的新地址,說的比較多,估計也聽的有點不太明白,看下面的對比圖。
?
原始的操作方式
?
?
現(xiàn)在的操作方式
?
這樣直觀看了后,發(fā)現(xiàn)效率肯定有明顯的差別,前者需要內(nèi)存拷貝,后者不需要內(nèi)存拷貝。
具體的,在實現(xiàn)后者的設(shè)計思想上,有一些技巧需要考慮的。因為硬件設(shè)計特點,當(dāng)DMA環(huán)行緩沖區(qū)到尾后,如果條件允許,數(shù)據(jù)將會自動從DMA緩沖區(qū)頭開始存放數(shù)據(jù),目前設(shè)置的DMA緩沖區(qū)大小為 64K+16(16字節(jié)用于軟件自動調(diào)節(jié)下次存放的指針位置,確保是對齊的地址)。
這樣的話,如果按照提交指針的方式,則會有點問題,為什么呢?因為,恰巧當(dāng)數(shù)據(jù)包跨緩沖區(qū)尾和區(qū)頭的時候,這樣提交指針就會出現(xiàn)數(shù)據(jù)錯誤。具體原因分析如下:
雖然分配了一段地址給DMA環(huán)行緩沖區(qū),看起來是一個環(huán),但實際上,地址不是連接在一起的,而是一塊內(nèi)存,只是通過配置了寄存器后告訴硬件DMA,到內(nèi)存的尾了,就自動從頭開始存放而已。
?
?
如果接收到的數(shù)據(jù)包沒有跨尾,則可以直接提交當(dāng)前的入口指針即可,否則,如果出現(xiàn)了跨尾,則還是按照上述的操作會出現(xiàn)問題,看下圖。
?
?
如果出現(xiàn)跨尾,則直接提交 Data Pointer 后將使用了后面虛線的內(nèi)容,而不會使用前面的數(shù)據(jù)塊,暫時無法實現(xiàn)自動回頭的功能。
有兩種方式解決該問題,一種就是,在網(wǎng)卡驅(qū)動 start函數(shù)中,動態(tài)分配一個1518+8
字節(jié)的內(nèi)存塊,然后將上述出現(xiàn)了分離的數(shù)據(jù)拷貝到該內(nèi)存塊,然后將該內(nèi)存首地
址join到 clBlk 即可;另一種就是,在分配DMA環(huán)行緩沖區(qū)的時候,在原來的64K+16字節(jié)的基礎(chǔ)上,多分配1518+8字節(jié)的空間,如上述的虛線框所示,這樣的話,只需要將上述的最頭上的蘭色框的數(shù)據(jù)拷貝到DMA End地址之后,然后可以直接提交 Data Pointer ?join到 clBlk 即可。
??????
但是在實際中,按照后者操作的方式,出現(xiàn)了一些問題,需要解決。
?
問題點如下:
?
按照后者的方式操作后,PING 沒有問題,小量發(fā)送數(shù)據(jù)包也沒有問題,當(dāng)使用發(fā)包工具大量發(fā)包的時候,只要在串口控制臺上按下任何的命令,都會出現(xiàn)如下的錯誤信息:
?
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方式,依次查找出現(xiàn)問題的原因。
上述錯誤信息指令地址在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
?
問題點應(yīng)該可以鎖定在 netMblkChainDup 這個函數(shù)中。
?
可以使用 tt 命令跟蹤下。
跟蹤發(fā)現(xiàn),在 netMblkChainDup 函數(shù)里面出現(xiàn)了異常。
?
-> tt tRtlRxTask
?
0x000962f8 vxTaskEntry? +0x48 :rtlRecvTask0 (0x270d9e8)
0x00030324 rtlRecvTask0 +0x28 : semTake ()
0x0016d9d4 semTake????? +0x138:semBTake ()
value = 0 = 0x0
?
正常情況下,如上述所示。
?
如果出現(xiàn)了上述的數(shù)據(jù)非法訪問錯誤,則網(wǎng)絡(luò)收包任務(wù)將被迫停止。通過 tt 命令后就可以發(fā)現(xiàn),通過一系列的函數(shù)調(diào)用后,最后的執(zhí)行函數(shù)為netMblkChainDup,估計在該函數(shù)中,釋放了一些內(nèi)存,然后繼續(xù)使用導(dǎo)致。
?
但是有一點不太明白的是,如在 shell下不執(zhí)行任何的操作,網(wǎng)絡(luò)可以承受不停的沖擊,沒有什么問題,如果一旦操作后,系統(tǒng)就掛死了。
?
反匯編,找到該指令的地址所在:
可以使用 objdumpppc –D vxWorks>img.s
?
-> objdumpppc –D vxWorks >img.s
?
?
找到目標地址:
?
?
如何實現(xiàn)DMA緩沖區(qū)和 pMblk 的緩沖區(qū)管理鏈中的 Cluster的內(nèi)存共享,以達到“零拷貝”。
該部分的內(nèi)存共享,如上述分析,可能存在一些問題。實際測試的結(jié)果是,網(wǎng)絡(luò)在該情況下可以正常的收發(fā)包,但是,內(nèi)存可能出現(xiàn)了問題,或者操作系統(tǒng)的任務(wù)棧遭受了破壞,shell 下無法輸入命令,只要一輸入,系統(tǒng)就掛死了。同上3),原因還沒有找到。
?
借助以前開發(fā)VxWorks系統(tǒng)下的 1394驅(qū)動經(jīng)驗,能否借助于消息隊列來提升性能?
借助于 Linux 系統(tǒng)下面的接收機制
初始化使能中斷后,第一次無論是發(fā)送完成或接收中斷,進去后,關(guān)閉所有中斷,然后讀取中斷狀態(tài)寄存器,判斷是何種類型的中斷,如果是發(fā)送完成中斷,則在退出中斷之前,僅僅打開發(fā)送中斷,如果是接收中斷,則在退出中斷之前,不使能接收中斷,等接收數(shù)據(jù)包任務(wù)完成后再使能該中斷。
總結(jié)為一句話,發(fā)送中斷按照原始的處理方式,而接收中斷第一次進中斷后,屏蔽該中斷類型,中斷服務(wù)程序釋放一個同步信號量,激活收包任務(wù),采用輪詢的方式直到將數(shù)據(jù)包接收完畢后再打開接收中斷。
?
二、數(shù)據(jù)發(fā)送優(yōu)化
在閱讀發(fā)送函數(shù)時,發(fā)現(xiàn)協(xié)議棧(以下簡稱上層),將pMblk 指針傳遞給發(fā)送函數(shù),然后再查詢是否有可用的發(fā)送描述符后,對上層的數(shù)據(jù)包進行拷貝到發(fā)送描述符對應(yīng)的緩沖區(qū),設(shè)置發(fā)送,等待完成。
這里對速度影響比較大的點就是,執(zhí)行了數(shù)據(jù)的拷貝動作,參考如下代碼:
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ā)送性能,修改為藍色字體部分,基本上實現(xiàn)了“零拷貝”,經(jīng)過 EtherPeek 發(fā)包軟件測試,沒有問題。
?