lwIP TCP/IP协议栈的设计与实现

lwIP是一个小型的TCP/IP协议栈,适用于资源有限的系统。它采用分层设计,支持TCP、UDP、IP和ICMP协议。 lwIP通过操作系统仿真层适应不同操作系统,并管理内存和缓存以适应不同需求。网络接口处理和数据包的接收与发送通过IP、UDP和TCP模块进行。TCP处理较为复杂,包括连接管理、接收和发送数据等功能。 lwIP还提供了BSD socket API的简易实现,便于应用程序开发。
摘要由CSDN通过智能技术生成

1 介绍

    在过去几年里,人们对计算机互联和无线网络支持设备的兴趣稳步增长。计算机正在越来越多的集成到日常设备中并且价格不断下降。同时无线网络技术,比如蓝牙和IEEE802.11b WLAN也正在兴起。这也给一些领域如医疗保健、安全保密、运输和工业生产带来了诱人的前景。小型设备如传感器可以连入一个已经存在的网络的基础架构中,比如联入互联网我们就可以在任何地方进行监控。

    在过去的几十年里,Internet技术已经证明了它自己的灵活性足以适应不断改变的网络环境。Internet的最早是从象ARPANET这样的低速网络发展起来的,到今天大范围的Internet互联技术在带宽和误码率方面已经了最初有了巨大的差异。现在人们又开始在未来的无线网络中应用现有的Internet技术。全球互联网强大的互通性必将成为无线网络发展的强大动力。

    由于像传感器这样的小设备经常要求体积小成本低,因此小设备的Internet协议的实现必须考虑有限的计算资源和内存。这个报告描述了的lwIP协议栈的设计与实现,lwIP是一个小型的TCP/IP协议栈的实现,它足可以用在最低配置的小型系统。

    这个报告的结构是这样的:第234节是对lwIP协议栈的概述,第5节描述了操作系统仿真层,第6章描述了内存和缓存的管理。第7节介绍了lwIP的抽象网络接口,第8910节描述了IPUDPTCP协议的实现。第1112节描述了lwIP的接口介绍了lwIP API。第1314是对lwIP代码和实现的分析。最后第15节提供了一个lwIP API的参考手册,第1718节展示了各种代码例子。

2 协议分层

    TCP/IP协议栈的设计采用了分层的风格,每一个协议层分别解决了互联问题的一个部分。这种分层可以为设计和实现协议栈提供一个向导,因为每个协议可以分别实现。然而这种用严格的分层方式实现协议栈会导致一种情况的出现:当通信发生在两个协议层之间的时候会降低整体性能。为了克服这些问题,确定一个协议的内部样式可以被其他协议获知。必须注意只有重要的信息才能在各层之间共享。

    大多数TCP/IP实现都在应用层和底层协议之间保持了严格的划分,但是底层可以或多或少的交叉。在大多数操作系统中,底层协议是作为操作系统内核的一部分实现的,它是应用层进程通信的入口点。应用程序是用TCP/IP实现的一个抽象视图表示的,网络通信和内部进程通信或则文件I/O只有很少的不同。这意味着应用程序是不知道底层的缓存机制的,应用程序不能利用这些信息,例如无法对频繁使用的数据重用缓存。也就是说,当应用程序发送数据的时候,这些数据在处理前不得不从应用程序进程的内存空间中复制到内部缓存。

    最小配置的系统用的操作系统比如lwIP的目标系统,大部分在内核和应用程序进程之间都没有保持一个严格的屏障。这就允许用一个比较自由的模式来进行应用程序和底层协议之间的通信,这也就意味着内存共享。在特殊情况下,应用程序层能够知道底层使用的缓存处理机制。因此应用程序就可以非常有效地重用缓存了。这样应用程序进程就能够和网络通讯的代码公用相同的内存了,应用程序可以直接读写内部缓存,这就节省了拷贝的代价。

3 概述

    和许多其他的TCP/IP实现一样,分层协议仍然可以作为lwIP设计和实现的向导。每个协议作为一个单独的模块实现,每个协议都有一些功能函数作为入口点。虽然协议栈是分开实现的,还是有一些协议层不是分开实现的,像前面讨论的那样,这是为了提高性能提高处理速度和节省内存。例如,当验证一个正在接收的TCP段的检验和和解析这个TCP段的时候,源IP地址和目标IP地址就已经被TCP模块获知。而不是像传统的通过一个函数调用把这些地址传送给TCP模块。TCP模块知道IP报头的结构,所以可以自己从中解析信息。

    lwIP包含了几个模块。模块的一个部分是显示TCP/IP协议的(IPICMPUDPTCP),一部分是支持模块。支持模块包括操作系统仿真层(在第5节描述)、缓存和内存管理子系统(第6节描述)、网络接口函数(第7节描述)和计算Internet检验和功能函数。lwIP也包括了一个抽象API,这会在第12节描述。

4 进程模型

    一个协议的进程模型描述了系统将以什么方式划分不同的进程。一个已经用于实现通信协议的进程模型是让每个进程作为一个单独的进程运行。在这个模型中,协议栈是强制严格分层的,协议之间的通信点必须被严格的定义。这种方法有它的好处比如它可以在运行时刻加入,理解代码和调试一般比较容易,当然它也有缺点。严格分层的协议描述并不容易,也不是实现协议的最好方式。更重要的是,每个协议交叉引用的时候会产生一个上下文切换。接收一个TCP段将产生三个上下文切换,从网络接口的设备驱动程序到IP进程,再到TCP进程,最后再到应用程序进程。再大多数操作系统中一个上下文切换的代价是相当昂贵的。

    另一种通用的方法是让通信协议进驻操作系统的内核。在通信协议作为内核实现的情况下,应用程序和协议的通信是通过系统调用的。通信协议没有进行严格的划分,可以使用分层协议的交叉引用技术。

    lwIP的进程模型是这样的:所有协议进驻在一个单独的进程中,从而和操作系统内核分离。应用程序可以或者进驻lwIP进程,或者单独建立自己的进程。应用程序和TCP/IP协议栈通信也有两种方式:当应用程序和lwIP共享一个进程的时候可以通过函数调用来实现通信,当应用程序是一个单独的进程的时候可以通过调用lwIP API来实现。

    综合考虑优缺点lwIP作为用户进程实现要优于作为操作系统内核实现。主要的好处就是作为一个进程的话可以实现在不同操作系统下的移植。由于lwIP是为小操作系统实现的,所以一般不支持进程外交换和虚拟内存,因为lwIP进程被交换了或者内存页不再磁盘,而造成不得不等待磁盘进入激活状态的延迟不会成为问题。虽然在得到服务请求前会有一个不得不等待一个时间片的问题,但是在lwIP的设计并没有考虑这个,而把它留在操作系统内核来实现。

5 操作系统仿真层

    为了让lwIP具有可移植性,我们在代码中并没有直接使用操作系统的具体函数调用和数据结构。操作系统仿真层可以提供所需要的函数。操作系统仿真层为操作系统的服务提供了一个统一的接口,比如定时器、进程同步和消息传递机制。在这种原则下,当我们需要移植lwIP到其他操作系统的时候只需要为我们需要的特殊操作系统实现一个操作系统仿真层就可以了。

    操作系统仿真层为TCP提供了一个定时器。操作系统仿真层提供的定时器是一个周期至少为200ms的一次定时器,当出现超时的时候调用一个注册函数。

    操作系统仿真层只提供了一个进程同步机制——信号量。即使底层操作系统的信号量不可用,操作系统仿真层也可以通过其他同步原语如条件变量或者进程锁。

    消息传递是通过一个简单的机制完成的。这种机制通过调用mailboxes实现。一个mailbox有两种操作:postfetchpost操作不会阻塞进程;更合适的做法是,传送到mailbox的消息通过操作系统仿真层排队直到另外一个进程取回消息。即使底层操作系统不支持mailbox机制,他们也可以通过信号量实现。

6 缓存和内存管理

    通信系统的内存和缓存管理系统必须准备去适应非常多变的缓存大小,测量缓存中的TCP段的完整大小,这些TCP段有可能是数百字节的数据,也有可能是只有几个字节的ICMP回传。而且为了避免拷贝,它应该可以让缓存中的内容进驻没有管理的内存空间比如应用程序内存或者ROM

6.1 包缓存——pbufs

    pbuf是包在lwIP内部的表示,它是为最小的栈的具体需要设计的。pbufBSD实现中的mbuf很像。pbuf结构支持动态分配内存去保存包内容,也支持让包数据进驻静态内存。pubf可以用一个表链接在一起组成一个pbuf链,这样一个包就可以跨越多个pbuf了。

    pbuf有三个类型,PBUF_RAMPBUF_ROMPBUF_POOL。图1就是一个PBUF_RAM的表示,它的包数据存储在通过pbuf子系统管理的内存中。图2是一个链接pbuf的例子,链中的第一个pbufPBUF_RAM类型,第二个是PBUF_ROM类型,PBUF_ROM类型的数据定位在没有通过pbuf系统管理的内存中。第三种类型PBUF_POOL如图3所示,它由从固定大小的pbuf池中分配的固定大小的pbuf组成。一个pbuf链可以由多种类型的pbuf组成。

    这三种类型有不同的用处。PBUF_POOL类型主要由网络设备驱动使用,因为分配单个pbuf快速且适合中断句柄使用。PBUF_ROM类型由应用程序发送那些在应用程序内存空间中的数据时使用。这些数据不会在pbuf递交给TCP/IP栈后被修改,因此这个类型主要用于当数据在ROM中时。PBUF_ROM中指向数据的头部被存在链表中其前一个PUBF_RAM类型的pbuf中,如图2所示。

1 PBUF_RAM

2 pub

3 PBUF_POLL

    PBUF_RAM类型也用于应用程序发送动态产生的数据。这情况下,pbuf系统不仅为应用程序数据分配内存,也为将指向数据的头部分配内存。如图1所示,Pbuf系统不能预知哪种头部将指向那些数据,只假定最坏的情况。头部的大小在编译时确定。

    基本上,进来的pbufPBUF_POOL类型,而出去的pbufPBUF_ROMPBUF_RAM类型。

    从图1到图3我们可以看到pbuf的内部结构。pbuf结构包含了两个指针、两个长度域、一个标志域和一个引用计数。next域是一个指针,在一个pbuf链中它指向了下一个pbufpayload指针指向了pbuf中数据的开始位置。len域包含了pbuf数据内容的长度。tot_len域包含了当前pbuflenpbuf链中接下来的所有pbuflen域的总和。换句话说,tot_len域的值是当前len域与pbuf链中接在它之后的pbuftot_len的值的和。nextpayload都是本地指针,它们的大小依赖于运行它的处理器架构。两个长度域是16位无符号整数,标志域和参考计数域都是4位长度。pbuf结构的整体大小依赖于处理器架构和最小校正位。在32位指针的架构和4字节校正中,整体大小是16字节。在一个16位指针的架构和1字节的校正中,整体大小是9字节。

    Pbuf模块提供了操作pbuf的函数。Pbuf_alloc()可以分配前面提到的三种类型的pbufPbuf_ref()增加引用计数,pbuf_free()释放已分配的空间,它先减少引用计数,当引用计数为0时就释放pbufPbuf_realloc()收缩空间以使pbuf只占用刚好的空间保存数据。Pbuf_header()调整payload指针和长度字段,以使一个头部指向pbuf中的数据。Pbuf_chain()pbuf_dechain()用于链表化pbuf

6.2 内存管理

内存管理对pbuf方案的支持非常简单。它处理连续内存的分配合释放,并能收缩已经分配的内存块。系统从整个内存中分割出专门的部分用于内存管理。这保证了网络系统不会用掉所有的可用内存,并且如果网络系统使用了它的所有内存,其他程序的操作也不会受到影响。

在内部,内存管理在每个分配的内存块头部记录一个小的结构以跟踪所有已分配的内存。图4所示结构保存两个指针分别指向前一个和后一个分配的内存块。它同时有一个已使用标志指示该内存块是否已经分配。

内存分配是通过搜索一个未使用的足够大小的内存块给所请求的空间分配。首次适应法使搜索时第一块足够大小的内存被分配出去。当一内存块被释放时,已用标志被设为0。为了防止碎片,通过检查前一个和后一个内存块的已用标志,可以把未用的块结合起来形成更大的未用的块。

7 网络接口

lwIP中物理网络设备驱动是用一个网络接口结构来表示的,这个网络接口类似BSD中的网络接口结构。图5所示就是lwIP中的网络接口结构。网络接口被保存在一个全局链接表中,通过结构中的next指针链接起来。

4 网络接口链表

5 网络接口结构

每个网络接口都有一个名字,存储在如图5所示的name域中。这里用两个字符的名字识别用于网络接口的设备驱动,并且只在运行时通过人工配置网络接口。Name字段由设备驱动设置,并且能反映出由网络接口表示的硬件类型。例如,蓝牙驱动的网络驱动接口可以名字设置为btIEEE 802.11b WLAN的网络接口名字可以被设置为wl。因为name域可以不是唯一的,所以同种类型的不同网络接口可以通过num域区别。

三个IP地址ip_addrnetmaskgw在发送和接受包的时候被IP层使用,下一节将描述他们。不能用超过一个IP地址来配置一个网络接口。更合适的做法是为每一个IP地址新建一个网络接口。

当一个包被接收到的时候,设备驱动中input指针所指向的函数将被调用。

一个网络接口通过output指针连接一个设备驱动。这个指针指向设备驱动中的一个函数,当一个包需要在物理网络传送的时候,IP层就会调用这个函数。这个字段由设备驱动初始化函数设置。Output函数的第三个参数,ipaddr,是将接收到的实际链路层帧的主机IP地址,它没必要和IP包的目的地址一样。特别地,当发送一个IP到不在本地网络的主机时,链路层帧将被送到网络中的一台路由器上。这种情况下,传递给output函数的IP地址将会是该路由器的IP地址。

最后,state指针指向设备驱动指示网络接口的特定状态,由设备驱动设置。

8 IP层处理

lwIP只实现了最基本的IP层功能。它可以发送、接收和转发包,但是不能发送和接收分段的IP包,也不能处理有IP选项的包。不过这对大多数应用来说不会造成什么问题。

8.1 接收包

接收一个IP包是从网络设备驱动调用ip_input()函数开始的。在这里要完成IP版本和头部长度的完整的初始化检查,以及计算和检查头部的检验和。这里假定协议栈将不会接收到任何的IP分段,因为它假定代理会收集任何的分段包,因此所有的IP分段都会被丢掉。

包所携带的IP选项也是假定由代理处理,所以也会被丢弃。

 下一步,ip_input()函数将会用网络接口的ip地址去检查目标地址以确定目标地址是否是本机。网络接口被顺序链接到一个链表中,被线性搜索。网络接口的数量预期会很小,所以lwIP没有实现比线性搜索更高级的搜索。

如果接收到的包确定是传给本机的,则根据协议域的值把它传递给相应的高层协议。

8.2 发送包

 发送包是由ipz_output()函数处理的,该函数通过调用ip_route()来查找适合的网络接口来传送数据包。当决定了发送该包的网络接口后,该包被传递给ip_output_if()函数,同时发送该包的网络接口也将作为该函数的一个参数被传递。在这里还将完成所有IP头域的填充和检验和的计算。该IP包的源和目的地址都被作为一个参数传递给ip_output_if()函数。另外可以省略传递该IP包的源地址,但这种情况下,发送该包的网络接口的IP地址将作为该包的源IP地址。

ip_route()函数通过线性搜索网络接口列表,以找到合适的网络接口。搜索过程中,IP包的目的IP地址由该网络接口的子网掩码所掩盖。假如该被掩盖的目的地址等于一个网络接口的掩盖后的IP地址,那么该网络接口就被选择,否则将使用一个默认的网络接口。默认网络接口可以在系统引导是设置或在运行时人工设置。如果默认网络接口的网络地址和目的IP地址不品配,则网络接口结构(图5)中的gw域值将被作为链路帧的目的IP地址。(注意:在这种情况下IP包的目的地址和链路层帧的目的地址是不同的)这种原始的路由解析形式是基于一个网络可以有多个依附于它的路由的假设。大多数情况下一个本地网络只有一个路由,这种情况下也可以工作的很好。

由于传输层协议UDPTCP需要目的IP地址以计算传输层校验和,在一些情况下,该包被传递给IP层前就应该确定发送该包的网络接口。可以让传输层函数直接调用ip_route()来解决。于是在该包到达IP层时已经知道了发送该包的网络接口,因此无需再搜索网络接口列表。而是让这些协议直接调用ip_output_if()。因为该函数把该网络接口作为一个参数,因此就避免了搜索该网路接口。

8.3 转发包

如果没有一个网路接口的IP地址和一个到来的包的目的地址相匹配,该包就应该被转发。这是由ip_forward()函数完成的。这时,TTL域的值被减少,并且如果该值为0,一个ICMP错误消息将被发送到该包的原始发送者,同时丢弃该包。因为IP头部已改变了,IP头部校验和必须重新校正。但是没必要重新计算整个校验和,因为可以应用简单的算法来校正原始IP校验和[MK90,Rij94]。最后,该包被转发给一个合适的网路接口。寻找一个合适的网络接口的算法和发送一个IP包是一样的。

8.4 ICMP处理

ICMP处理过程相当简单。由ip_input()函数收到的ICMP包被转交给icmp_input()函数处理更,该函数对ICMP头部进行解码并作出相应的响应。一些ICMP消息被传递给更高层的协议,一些特殊的传输层协议也可能会关注这些消息。ICMP目的不可达消息可由传输层协议发送,这种情况下UDP.icmp_dest_unreach()函数会被调用处理。

6 ICMP处理

网络中,广泛使用ICMP Echo消息来探测网络,因此ICMP Echo在性能上应该尽量的优化。实际的处理发生在icmp_input(),包含的处理有:对到来的包的源和目的IP地址进行交换,改变Echo回复的ICMP类型并校正ICMP校验和。然后,该包会被传递回IP层以进一步发送出去。

9 UDP处理

UDP是用来在不同进程间多路分解包的简单协议。每个UDP会话的状态被保存在一个如图7所示的进程控制块(PCB)中。UDP进程控制块被保存在一个链表中。当一个UDP数据报到来时将搜索该链表以找到一个相比配的控制块(PCB)

7 udp_pcb结构

在全局UDP控制块链表中,每个UDP控制块都包含有指向下一个控制块的指针。一个UDP会话由两终端的IP地址和端口号决定,而这些信息分别被保存在local_ipdest_iplocal_portdest_port域中。flags域标识将对当前会话应用何种UDP校验和。在这里允许关闭UDP检验和,或者使用UDP Lite来计算部分数据报的校验和。当使用UDP Lite,chesum_len域指明将计算多少的数据报。

最后的两个参数recvrecv_arg将在一个数据报到达时被使用。

由于UDP的简单性,而且输入和输出处理也十分简单,完全以直线流程方式处理(如图8)。为了发送数据,应用程序调用udp_send()函数,接着该函数会调用udp_output()函数。这其中将计算必要的校验和,以及填充UDP头部字段的值。由于检验和包含IP包的源地址,因此在某种情况下将调用ip_route()来找出发送该IP包的网络接口。该网络接口的IP地址被作为该包的源IP地址。最后,该包被转交给ip_output_if()进行传输。

8 UDP处理

当一个UDP数据报到来时,IP层调用udp_input()函数。这时,如果该会话应该使用检验和,则检查该数据报的UDP校验和,接着多路分解该包。当找到相应的UDP控制块后,recv函数就被调用。

10 TCP处理

TCP是一个为应用层提供可靠的字节流服务的传输层协议。TCP比其它在这里描述的任何协议都要复杂,其实现代码占总代码量的50%左右。

101 概述

基本的TCP处理(如图9)被分为6个函数。tcp_input()tcp_process()tcp_receive()三个函数与TCP接收处理相关,tcp_write()tcp_enqueue()tcp_output()三个函数则与TCP发送处理有关。

9 TCP处理

应用程序调用tcp_write()来发送TCP数据。tcp_write()函数传递控制给tcp_enqueue()函数。如果需要,tcp_enqueue()会将把数据分成合适大小的TCP报文段,最终把TCP报文段放到连接的传输队列中。tcp_output()函数将检查是否可以发送该数据,比如,接收窗口是否有足够的空间并且拥塞窗口是否足够大等等,如果可以的话,该函数将调用ip_route()ip_output_if()发送该数据。

ip_input()确认完IP头部并转交TCP报文段给tcp_input()函数时,接收处理便开始了。tcp_input()函数做好初步完整的检查(如校验和以及TCP选项解析等),同时确认该报文段属于哪个TCP连接。接着,该数据报就被转交给tcp_process(),其中实现了TCP状态机和任何必要的状态转换。当该连接正处于从网络中接收数据的状态时,将调用tcp_receive()函数。这种情况下,tcp_reveive()将传递给报文给上层的应用程序。假如该报文包含一个未确认数据(当然是当前缓冲的数据)ACK应答,那么该数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值