Vxworks网络协议栈.docx
- 文档编号:29840583
- 上传时间:2023-07-27
- 格式:DOCX
- 页数:13
- 大小:88.19KB
Vxworks网络协议栈.docx
《Vxworks网络协议栈.docx》由会员分享,可在线阅读,更多相关《Vxworks网络协议栈.docx(13页珍藏版)》请在冰豆网上搜索。
Vxworks网络协议栈
《嵌入式操作系统VxWorks中网络协议存储池原理及实现》
周卫东蔺妍刘利强
(哈尔滨工程大学自动化学院,黑龙江哈尔滨,150001)
摘 要 本文讨论了网络协议存储池的基本原理和在嵌入式操作系统中的实现方法。
为在嵌入式系统中实现TCP/IP协议栈,提供了一种有效、简洁、可靠的缓冲区管理。
关键词 VxWorks;mBlk;clBlk;网络协议存储池
VxWorks操作系统是美国WindRiver公司于1983年设计开发的一种嵌入式实时操作系统(RTOS)。
它以良好的持续发展能力、高性能的内核以及卓越的实时性被广泛的应用在通信、军事、航空、航天等高精尖技术及实时性要求极高的领域中。
VxWorks操作系统有着优越的网络性能,而缓冲区的数据拷贝是影响网络性能的主要因素。
众所周知,缓冲区在网络协议栈中有两个作用:
第一,提供载体,使分组或报文可以在各协议层中流动;第二,为各级缓冲区提供空间。
缓冲区的设立使得TCP/IP协议栈支持异步I/O操作,异步操作对于协议栈的I/0性能是非常重要的。
在网络输出的过程中每一层需要在数据的首部或者尾部添加数据头和数据尾来对数据进行封装使得接收端对应的层能够进行正确的接收,在输入的过程中每层都需要将本层的数据头和数据尾去掉而最终还原成发送端发送的数据。
上述的封装/去封装和拷贝操作使得网络协议对内核的存储器管理能力提出了很多要求。
这些要求包括能方便地操作可变长缓存,能在缓存头部和尾部添加数据(如低层封装来自高层的数据),能从缓存中移去数据(如当数据包向上经过协议栈时要去掉首部),并能尽量减少这些操作所作的数据复制。
1 使用netBufLib管理存储池的基本原理
网络协议存储池使用mBlk结构、clBlk结构、簇缓冲区和netBufLib提供的函数进行组织和管理。
mBlk和clBlk结构为簇缓冲区(cluster)中数据的缓冲共享和缓冲链接提供必要的信息。
netBufLib例程使用mBlk和clBlk来管理cluster和引用cluster中的数据,这些结构体中的信息用于管理cluster中的数据并且允许他们通过引用的形式来实现数据共享,从而达到数据“零拷贝”的目的。
1.1 结构体mBlk和clBlk及其数据结构
mBlk是访问存储在内存池中数据的最基本对象,由于mBlk仅仅只是通过clBlk来引用数据,这使得网络层在交换数据时就可以避免数据复制。
只需把一个mBlk连到相应mBlk链上就可以存储和交换任意多的数据。
一个mBlk结构体包括两个成员变量mNext和mNextPkt,由它们来组成纵横两个链表:
mNext来组成横的链表,这个链表中的所有结点构成一个包(packet);mNextPkt来组成纵的链表,这个链表中的每个结点就是一个包(packet),所有的结点链在一起构成一个包队列,如图1所示。
图1包含两个数据包的mBlk链
结构体mBlk和clBlk的数据结构如下所示:
structmBlk
{
M_BLK_HDR mBlkHdr; /*header*/
M_PKT_HDR mBlkPktHdr; /*pkthdr*/
CL_BLK* pClBlk; /*pointertoclusterblk*/
}M_BLK;
structclBlk
{
CL_BLK_LIST clNode;/*unionofnextclBlk*/
UINT clSize;/*clustersize*/
int clRefCnt;/*countofthecluster*/
structnetPool* pNetPool; /*pointertothenetPool*/
}CL_BLK;
/*headeratbeginningofeachmBlk*/
structmHdr
{
structmBlk* mNext;/*nextbufferinchain*/
structmBlk*mNextPkt;/*nextchaininqueue/record*/
char*mData; /*locationofdata*/
int mLen;/*amountofdatainthismBlk*/
UCHAR mType;/*typeofdatainthismBlk*/
UCHAR mFlags; /*flags;seebelow*/
}M_BLK_HDR;
/*record/packetheaderinfirstmBlkofchain;validifM_PKTHDRset*/
struct pktHdr
{
structifnet* rcvif;/*rcvinterface*/
int len; /*totalpacketlength*/
}M_PKT_HDR;
1.2 网络协议存储池的初始化
VxWorks在网络初始化时给网络协议分配存储池并调用netPoolInit()函数对其初始化,由于一个网络协议通常需要不同大小的簇,因此它的存储池也必须包含很多簇池(每一个簇池对应一个大小的簇)。
如图2所示。
另外,每个簇的大小必须为2的方幂,最大可为64KB(65536),存储池的常用簇的大小为64,128,256,512,1024比特,簇的大小是否有效取决于CL_DESC表中的相关内容,CL_DESC表是由netPoolInit()函数调用设定的。
图2 网络协议存储池初始化后的结构
1.3 存储池的链接及释放
存储池在初始化后,由netPool结构组织几个下一级子池:
一个mBlk池、一个clBlk池和一个cluster池。
mBlk池就是由很多mBlk组成的一条mBlk链;clBlk池就是由很多clBlk组成的一条clBlk链。
cluster池由很多的更下一级cluster子池构成,每一个cluster子池就是一个cluster链。
每一个cluster链中的所有cluster的大小相同,不同链中的cluster大小不同。
但要实现不同进程访问同一簇而不需要作数据的拷贝,还需要把mBlk结构,clBlk结构和簇结构链接在一起。
创建这三级结构一般要遵循这样五步:
a.调用系统函数netClusterGet()预定一块簇缓冲区;
b.调用系统函数netClBlkGet()预定一个clBlk结构;
c.调用系统函数netMblkGet()预定一个mBlk结构;
d.调用系统函数netClBlkJoin()把簇添加到clBlk结构中;
e.调用系统函数netMblkClJoin()把clBlk结构添加到mBlk结构中。
这样,就构成了最后的缓冲区。
在缓冲区中的数据使用完毕后要及时的释放内存,这一过程只需要调用系统函数netMblkC1ChainFree()释放存有数据的mBlk链表。
例如当数据向上层传送时,在本层中可以释放已经不再使用的mBlk链表,由于在clBlk中记录着指向本模块的mBlk的个数,虽然释放了mBlk链表,但是这并不表示将cluster中的数据释放掉了,上层复制的链表仍然控制着这些数据,直到clBlk中的mBlk计数为0时才真正的将数据占用的簇释放掉,将数据占用的内存空间释放、归还给系统将来使用。
2 网络协议存储池与数据的封装处理
VxWorks操作系统之所以采用mBlk—clBlk—cluster这样的网络数据存储结构,目的就是减少数据拷贝的次数,提高网络数据的传输速率。
图3存储带有1460个字节数据的mBlk
在网络输出的过程中当从上层向下层传递数据时,下层协议需要对数据进行封装使得接收端对应的层能够进行正确的接收。
下面通过实例分析网络数据的封装过程。
例如要在如图3所示的mBlk链中添加IP和UDP的首部。
在mBlk链表中封装数据的方法是分配另外一个mBlk,把它放在链首,并将分组首部复制到这个mBlk。
IP首部和UDP首部被放置在新mBlk的最后,这个新mBlk就成了整个链表的首部。
如果需要,它允许任何其它低层协议(例如添加以太网首部)在IP首部前添加自己的首部,不需要再复制IP和UDP首部。
在第一个mBlk中的mBlkHdr.mData指针指向首部的起始位置,mBlkHdr.mLen的值是28。
在分组首部和IP首部之间有72字节的未用空间留给以后的首部,通过适当地修改mBlkHdr.mData指针和mBlkHdr.mLen添加在IP首部的前面。
注意,分组首部已经移到新mBlk中了,分组首部必须放在mBlk链表的第一个mBlk中。
在移动分组首部的同时,在第一个mBlk设置M_PKTHDR标志。
在第二个mBlk中分组首部占用的空间现在未用。
最后,改变在此分组首部中的长度成员mBlkPktHdr.len,成员mBlkPktHdr.len的值是这个分组的mBlk链表中所有数据的总长度:
即所有通过mBlkHdr.mNext指针链接的mbuf的mBlkHdr.mLen值的和。
本例中由于增加了28个字节变成了1488。
如图4所示。
图4添加完IP和UDP首部的mBlk
这样,当报文在协议栈中流动时,不会拷贝报文链,而只需把指向mBlk的指针通过参数传递。
当报文需要进人缓冲区时,也是通过链表的指针操作将报文插入或添加到队列中。
3 结论
网络协议存储池的职责有两个:
为协议栈提供合适的缓冲区,如果太大会浪费系统资源,太小会影响协议栈的吞吐量;提供合适的数据结构装载网络报文,既可以使协议栈方便地处理报文,又可以减少缓冲区拷贝的次数。
减少拷贝次数不仅降低了CPU的负荷,还可以降低存储器的消耗。
本文剖析了嵌入式操作系统VxWorks中网络协议存储池的原理,实现了数据能够动态增删、但在逻辑上又呈现连续性的数据结构。
能够满足在各协议层之间传递数据而不需要进行内存拷贝。
参考文献
[1]翟东海,李力.mbuf的实现原理剖析及其在网络编程中的应用[J].计算机工程与应,2004(8):
104-106.
[2][美]DouglasE.Comer著.张娟等译.用TCP/IP进行网际互联第二卷:
设计、实现与内核(第三版)[M].北京:
电子工业出版社,2001.05.
[3][美]GaryR.WrightW.RichardStevens著.陆雪莹,蒋慧等译.TCP/IP详解卷2:
实现[M].北京:
机械工业出版社.2000.07:
10—50.
[4]WindRiverSystemInc.VxWorksNetwork
收稿日期:
8月25日 修改日期:
9月2日
《VxWorks任务追踪实例分析》
VxWorks中如果稍有不慎,就可能导致tasksuspend,如果运气好,shell没有被挂起,则可以通过系统的一些命令追踪一下挂起的原因。
其中用到的主要命令是i、tt、ti、d等。
首先从出错信息开始:
0xfc8125b8(t_Lcd):
memPartFree:
invalidblock0xfdfc6f38inpartition0xfe508894.
由于memPartFree了一个非法的内存块,导致了任务挂起,我们需要确定到底是哪条语句导致了这个异常的产生,可能是什么原因引起的。
首先,需要通过“i”命令察看任务状态:
->i
NAMEENTRYTIDPRISTATUSPCSPERRNODELAY
-----------------------------------------------------------------------
tExcTaskexcTaskfdffec800PENDfe3c5f50fdffeb603006b0
tLogTasklogTaskfdffc2980PENDfe3c5f50fdffc18800
tShellshellfdf5fa481READYfe1f3afcfdf5f62800
……
t_Lcdfe392a30fc8125b8100SUSPENDfe1f24b0fc8120b8d00030
……
可以看到任务t_Lcd的状态为SUSPEND,即被挂起的状态。
其他各项的含义都比较清楚,ENTRY是任务的入口函数,如果没有symbol,则直接显示地址,TID是任务的ID号,一般用任务的栈底地址表示,PC是当前的指令位置,SP是当前栈顶位置。
然后通过”tt”来追溯函数调用过程:
->tt"t_Lcd"
fe3c14f4vxTaskEntry+68:
fe392a30()
fe392b48initLcdComponent+2e8:
fe392bdc()
fe392becinitLcdComponent+38c:
fe392bfc()
fe39333cinitLcdComponent+adc:
lcdShowPassWord()
fe398590lcdShowPassWord+84:
saveModifiedSetting()
fe3a3790saveModifiedSetting+220:
saveSettingValue
(1)
fe1fd6d0saveSettingValue+148:
fe1fc428(0,5,ffffffff)
fe1fc888getDeviceSettingValue+64c:
fclose()
fe1aab48fclose+ec:
free()
fe1bb7d4free+1c:
memPartFree()
fe1bb2dcmemPartFree+148:
taskSuspend()
我们可以了解到函数的调用过程,vxTaskEntry()?
->fe392a30()->fe392bdc()->fe392bfc()->lcdShowPassWord()->saveModifiedSetting()->saveSettingValue
(1)->fe1fc428(0,5,ffffffff)->fclose()->free()->memPartFree()->taskSuspend()。
其他相关信息:
第一栏是发生跳转(即函数调用)后的返回地址,稍后会作详细解释,第二栏是离返回地址最近的symbol和偏移量,一般情况下会是发起调用的那个函数的名称,除非该函数是内部函数,系统中没有symbol,第三栏是被调用的函数。
再来查看一下t_Lcd任务的栈里的内容,从前面的任务信息里已经得知当前栈顶位置为fc8120b8,通过“d”命令显示该地址的内容。
->d0xfc8120b0
fc8120b0:
fc8125b8fc8125b8fc8120c8fe1f24b0*..%...%......$.*
fc8120c0:
fe508894fdfc6f30fc8120e8fe1bb2dc*.P....o0.......*
fc8120d0:
fdf26b08000001000000000c00000000*..k.............*
fc8120e0:
00000000fdf1ed80fc8120f8fe1bb7d4*...............*
fc8120f0:
fd9a3538fdf1ed80fc812108fe1aab48*..58......!
....H*
fc812100:
fd9a3538fc812108fc812238fe1fc888*..58..!
..."8....*
fc812110:
0000000000000005ffffffff00000600*................*
fc812120:
0000000afdf1ed804230312e78746373*........B01.xtcs*
fc812130:
5f6275662e696c320000000000000000*_buf.il2........*
这里我们可以看到一些熟悉的地址,注意看最后两列,在这里可以找到”tt”中显示的第一栏地址即函数调用的返回地址。
两个返回地址之间是该函数的栈空间,用于保存栈指针、局部变量或者相关寄存器的值。
要具体了解这些值是怎么来的,就要用到反汇编了。
反汇编通过objdump命令来实现,不同类型的cpu会有不同的可执行文件,例如
objdumpppc-Dvxworks>xx.s
反汇编的结果可能会很大,耐心等待吧。
我们来看一下fclose()里调用free()的这一过程,在汇编代码里查找返回地址fe1aab48。
fe1aaa5c
……
fe1aab44:
48010c75blfe1bb7b8
fe1aab48:
38000000lir0,0
bl是无条件跳转指令,free()执行完之后,应返回fe1aab48继续执行。
查找fe1bb7b8,看看free被调用时干了些什么?
fe1bb7b8
fe1bb7b8:
9421fff0stwur1,-16(r1)
fe1bb7bc:
7c0802a6mflrr0
fe1bb7c0:
90010014stwr0,20(r1)
fe1bb7c4:
7c641b78mrr4,r3
fe1bb7c8:
3c60fe51lisr3,-431
fe1bb7cc:
38638894addir3,r3,-30572
fe1bb7d0:
4bfff9c5blfe1bb194
fe1bb7d4:
80010014lwzr0,20(r1)
fe1bb7d8:
7c0803a6mtlrr0
fe1bb7dc:
38210010addir1,r1,16
fe1bb7e0:
4e800020blr
虽然不同类型cpu的汇编指令不同,但还是可以大致猜出其中的含义。
stwu指令将r1保存到地址(r1-16)位置,然后让将r1减去16保存到r1中,完成了保存并更新栈指针的过程;第2、3条语句将返回地址保存到r1+20的位置;下面几条语句实际上是准备参数的过程,r3,r4一般用来保存函数的形参值,随后调用了memPartFree。
从memPartFree返回后,先从堆栈上读取返回地址,然后将栈顶下移16字节,即恢复到原来的位置,最后跳转到返回地址。
对着内存内容校验一下,fclose调用free后的返回地址为fe1aab48,按照上面的分析,这个地址会被free()函数保存在r1+20的位置,因此r1+20=fc8120fc,r1=fc8120e8,而free中将r1减去了16,所以刚进入free()时,r1应该等于fc8120f8,这个数值会被保存在fc8120e8处,事实正是如此。
这里栈内空间的利用有点交叉混杂,暂时没有弄的太明白,并且free()中没有用到过fc8120f0-fc8120f8空间的内存,推测可能是栈的大小至少为16字节所以留空了,里面的内容是历史遗留产物,是否如此,还有待进一步的研究。
以上是比较通用的分析过程,本来还涉及到结合c代码的分析,跟具体的例子结合太紧密,就不赘述了,有一点可以提一下,想追溯函数调用过程中某一参数的运行值,可能会在调用者的栈中,也可能会在调用者的调用者的栈中,这个需要结合具体的汇编码来分析。
因为函数的栈开辟出来是为了保存一些临时的乱七八糟的东西,比如需要用到r38,就会把r38临时保存到栈上,返回时再恢复。
而对本函数有用的变量,通常会被优化到寄存器中保存,除非寄存器不够用了,才会用到栈空间。
《VxWorks5.5的内存结构》
内存结构
VxWorks5.5中的内存是平板式的,基本的结构如上图所示,整个内存分区被划分成一个个区块(Block)。
每一个区块都有一个区块头,如果是空闲的内存块,是FREE_BLOCK,如果是已分配的,则是BLOCK_HDR。
这两个结构在private/memPartLibP.h中定义。
FREE_BLOCK和BLOCK_HDR的前半部分的结构是一样的,先是32bit的pPrevHdr指针,指向前一个区块头,接着是31bit的nWords,余下的1bit用于指示该区块是否空闲。
这里nWords的单位是Word,等于2Byte。
当前的区块头地址加上2倍的nWords就是下一个区块的地址。
这最后的1bit看起来有些讨厌,使得nWords的数值并不那么直观,但实际上我们用到的总是2*nWords,也就是nWords<<1,刚好空出了最后一位,从一个例子可以说的更明白些,数据格式为BigEndian。
0x0088ca00:
0088c9c000001125……
我们可以知道前一个区块的地址为0×88c9c0,这个区块的长度为0×1124Byte,这个区块是空闲的,通过计算我们还能得出下一个区块的地址为0×88ca00+0×1124=0×88db24。
如果内存分配时需要按16Bytes对齐,BLOCK_HDR还包含8Byte的空白区,然后才是实际的数据区。
FREE_BLOCK则还包含一个DL_NODE,即双向链表节点,这些节点被串在memSysPartId->freelist中,这些节点并不一定按照地址高低排列。
操作函数
memPartLibP.h中提供了一些宏和函数来操作这些结构,本质上都是指针的移动。
∙NEXT_HDR(pHdr)和PREV_HDR(pHdr),得到前后区块的地址
∙HDR_TO_BLOCK(pHdr)和BLOCK_TO_HDR(pBlock),在区块头和数据区之间转换;
∙HDR_TO_NODE(pHdr)和NODE_TO_HDR(pNode),在空闲节点和区块头之间转换。
对于freelist,则采用dllLib中的双向链表操作函数来维护。
∙DLL_FIRST和DLL_LAST,得到链表头或尾;
∙DLL_NEXT(pNode)和DLL_PREVIOUS(pNode),得到下一个或前一个节点;
∙dllAdd和dllRemove,增加或删除一个节点;
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Vxworks 网络 协议