lwIP――TCP/IP协议栈的一种实现

 

1 介绍

近几年来,对计算机互连及装备有无线联网设备的计算机的兴趣越来越流行。计算机和日常设备越来越无缝集成在一起并且价格也在下降。同时无线网络技术,如蓝牙和IEEE 802。11b无线局域网也相继出现。这将在很多领域如健康保健,安全,保险,运输及处理产业等引发更多令人振奋的前景。小设备如传感器等能接入一些已经存在的网络基础设施如全球因特网,也能在任何地方被监测到。

互联网技术已经被证实足够灵活的适应过去几十年来不断变化的网络环境。互联网技术起初是在如ARPANET的慢速网络上发展起来的,但它现在却运行在大量的连接技术及许多不同特征的带宽和比特错误率上。由于大量使用互联网技术的应用已被广泛应用,在不久的无线网络中使用现有互联网技术有很大的益处。同时,全球互联网的巨大互联性是个很大的激励。

由于小设备如传感器经常被要求小型且便宜,网络协议的实现必须处理受限制的计算资源及内存。这篇文章描述了一个成为lwIP的小TCP/IP协议栈的设计及实现,它足够小便于在最小限度的系统中使用。

文章的安排如下:2,3和4部分给出了lwIP协议栈的概要,第5部分描述了操作系统仿真层,第6部分描述了内存及缓冲区管理,第7部分介绍了lwIP网络层的抽象接口,第8,9和10部分描述了IP,UDP和TCP协议的实现,第11,12部分描述了怎样使用lwIP及API介绍,第13,14部分分析了实现,最后,第15部分提供了lwIP API的使用手册,并且第17,18部分给出了几个代码实例。

2 协议分层

TCP/IP协议族设计成分层模式,每个协议层解决不同的通信问题。分层思想主导了协议实现的设计,各个协议能被分开实现。以一种严格分层的方式实现协议导致了这么一种情况:协议层之间的通信降低了总体的性能。为了克服这个问题,一个协议的某些方面应当被其他协议所了解。必须小心,那样只有重要的信息在协议层间共享。

许多的TCP/IP实现都严格区分开应用层及其底下的协议层,不管这些层是否可以或多或少的交叉实现。在许多操作系统中,这些底层的协议被实现为操作系统内核的一部分,而只提供给应用层进程一些通信调用点。应用程序只知道TCP/IP实现的一个抽象方式,网络通信同进程间通信或文件I/O的差异很少。这样实现的原因是应用程序不会意识到底层使用缓冲区的机制,它不能使用该信息拒绝经常使用数据的缓冲区。同时,当应用程序发送数据时,该数据必须在被网络代码执行前从应用进程内存空间复制到内部缓冲区。

在最小限度系统上的操作系统通常不会维护内核及应用进程之间的严格界限保护。这就允许通过共享内存在应用层及底层协议间使用更为灵活的通信机制。特别地,应用层能意识到底层使用缓冲区的机制。因此应用程序能更高效地重用这些缓冲区。同时,由于应用进程可以使用网络层代码同样的内存,应用进程能直接读写那些内部缓冲区,因此也节省了复制的代价。

3 概要

正如许多其他的TCP/IP实现一样,已有的分层协议设计给lwIP的设计实现提供了向导。每个协议实现了自己的模块,以一些函数做为进入该协议的入口。尽管大多数协议被分开实现,但有些层却违背了这个思想,为的是上面提到的在处理速度及内存使用上提高性能。例如,当要验证到达的TCP报文段检验和,或者当要分组转发(demultiplexing)一个报文段时,TCP模块必须知道该报文段的源IP地址和目的IP地址。TCP模块清楚IP头部结构,因此能自己析取出所需的信息,而不是通过函数调用来传递这些地址。

lwIP包含好几个模块,除了那些实现TCP/IP协议族(IP,ICMP,UDP,TCP)的模块外,也实现了其他一些支持的模块。这些支持的模块包括操作系统仿真层(在第5部分描述),缓冲及内存管理子系统(在第6部分描述),网络接口函数(在第7部分描述),和计算检验和的函数。lwIP在第12部分也描述了一个抽象的API。

4 进程模型

一个协议实现的进程模型是这样描述的:系统被分开为不同的进程。一种已经被用于实现通信协议的进程模型是让每个进程作为一个独立的进程运行。在这种模型中,协议分层是严格的,并且协议层间的通信接口也被严格定义。尽管这种方法有其优点,如协议能在运行是加入进来,清楚代码且容易调试,它也有些缺点。前面提到过,严格分层并不是实现协议的最好方法。同时,更重要的是,层间交互会引起上下文切换。对一个到达的TCP报文段意味着三个上下文切换,从网络接口层的设备驱动,到IP进程,再到TCP进程,最后才到应用进程。大多数操作系统的上下文切换是相当昂贵的。

另一个通常的方法是让通信协议驻留在操作系统的内核中。在这种内核级实现通信协议的情况下,应用进程通过系统调用和这些协议进行通信。这些通信协议并不严格区分彼此,反而会使用某些技术来跨过协议分分层。

lwIP使用了进程模型。在这个模型中,所有的协议驻留在单一的进程中,显然和操作系统内核是分开的。应用进程可以驻留在lwIP进程中,也可以在分开的不同进程中。应用程序和TCP/IP协议栈之间的通信有两种:一个是当应用程序和lwIP共享一个进程时可以通过函数调用;另一种是使用一个更为抽象的API。

让lwIP实现为用户空间的进程而不是驻留操作系统内核有其优缺点。主要优点是这种实现在不同操作系统间是可移植的。因为lwIP被设计为运行在小的操作系统上,这些操作系统不支持交换进出进程和虚拟内存,所以由不得不等待磁盘操作(假如部分lwIP进程被交换出到磁盘中)引起的延迟不再是个问题。在有机会服务一个请求前必须等待一定的调度这仍然是个问题,但在lwIP的设计中并没有什么去排除将来会在操作系统内核中实现。

5 操作系统仿真层

为了使lwIP可移植,操作系统特定函数调用及数据结构并没直接在代码中使用。当需要这样的函数时,操作系统仿真层就被使用。操作系统仿真层向操作系统服务提供了统一的接口,这些服务有定时器,进程同步,消息传递机制,等等。原理上,要移植lwIP到其他操作系统时,只需实现该特定操作系统的仿真层就可以了。

操作系统仿真层提供了一个功能性定时器让TCP使用。该定时器是一个时间间隔至少200ms的一次性定时,它在定时结束时将调用一个已注册的函数。

信号量是进程同步机制的唯一一个实现。即使下层的操作系统并不提供信号量实现,它也能由其他的同步原语如临界变量或锁机制实现。

消息传递的实现是一个抽象为“邮箱”的简单机制。一个邮箱有两种操作:邮递和收取。邮递操作不会阻塞该进程,而邮递给一个邮箱的信息会由操作系统仿真层列入队列,直到另外的进程收取该信息。即使下层操作系统不支持邮箱机制,它也能容易地由信号量实现。
6 缓冲及内存管理
在一个通信系统中,缓冲及内存管理系统必须能准备好分配不同大小的缓冲区,从包含有几百字节有用数据的全长 TCP报文段,到只含几个字节的ICMP 应答响应。同时,为了避免拷贝,应该尽可能地让数据类型缓冲区驻留在内存中,而不是由网络子系统,如应用内存或ROM,来管理。
6 .1 包缓冲区――pbuf
Pbuf是 lwIP包的内部表示,被设计为最小化栈的特殊需要。Pbufs类似于BSD实现中的mbufs。Pbuf结构支持为包内容动态分配内存和让包数据驻留在静态内存中。Pbufs能被一个称为pbuf链的链接到一个链表中,以至一个包能跨越多个pbufs。
Pbufs有三种类型 :PBUF_RAM,PBUF_ROM和PBUF_POOL。图1表示PBUF_RAM类型,包含有存在内存中由pbuf子系统管理的包数据。图2显示了一个pbuf链表,第1个是PBUF_RAM类型,第2个是PBUF_ROM类型,意味着它包含有不被pubf子系统管理的内存数据。图3描述了PBUF_POOL,其包含有从固定大小pbuf池中分配来的pbuf。一个pbuf链可以包含多个不同类型的pbuf。
这三种类型有不同的用处。 PBUF_POOL类型主要由网络设备驱动使用,因为分配单个pbuf快速且适合中断句柄使用。PBUF_ROM类型由应用程序发送那些在应用程序内存空间中的数据时使用。这些数据不会在pbuf递交给TCP/IP栈后被修改,因此这个类型主要用于当数据在ROM中时。PBUF_ROM中指向数据的头部被存在链表中其前一个PUBF_RAM类型的pbuf中,如图2所示。
PBUF_RAM类型也用于应用程序发送动态产生的数据。这情况下, pbuf系统不仅为应用程序数据分配内存,也为将指向(prepend)数据的头部分配内存。如图1所示。Pbuf系统不能预知哪种头部将指向(prepend)那些数据,只假定最坏的情况。头部的大小在编译时确定。
本质上,进来的 pbuf是PBUF_POOL类型,而出去的pbuf是PBUF_ROM或PBUF_RAM类型。
从图 1,图2可以看出pbuf的内部结构。Pbuf结构包含有两个指针,两个长度字段,一个标志字段,和一个参考计数。Next字段指向统一链表中的下一个pbuf。有效载荷指针指向该pbuf中数据的起始点。Len字段包含有该pbuf数据内同的长度。Tot_len字段是当前pbuf和所有链表接下来中的len字段值的总和。简单说,tot_len字段是len字段及下一个pbuf中tot_len字段值的总和。Flags字段表示pbuf类型而ref字段包含一个参考计数。Next和payload字段是本地指针,其大小由处理器体系结构决定。两个长度字段是16位无符号整数,而flags和ref字段都是4比特大小。Pbuf的总大小决定于使用的处理器体系结构。在32位指针和4字节校正的体系结构上,总大小是16字节,而在16位指针和1自己校正的体系结构上,总大小是9字节。
Pbuf模块提供了操作 pbuf的函数。Pbuf_alloc()可以分配前面提到的三种类型的pbuf。Pbuf_ref()增加引用计数,pbuf_free()释放分配的空间,它先减少引用计数,当引用计数为0时就释放pbuf。Pbuf_realloc()收缩空间以使pbuf只占用刚好的空间保存数据。Pbuf_header()调整payload指针和长度字段,以使一个头部指向pbuf中的数据。Pbuf_chain()和pbuf_dechain()用于链表化pbuf。
6 .2 内存管理
内存管理只是简单地支持 pbuf机制。它处理连续内存的分配和释放,并能收缩先前分配的内存块。内存管理使用系统总内存贡献出的小部分,这确保网络系统不会使用所有可用的内存,并且其他程序的操作不会因为网络系统使用完所有内存而受打断。
在内部,内存管理在每个分配的内存块头部记录一个小的结构以跟踪所有已分配的内存。图 4所示结构保存两个指针分别指向前一个和后一个分配的内存块。它同时有一个已使用标志指示该内存块是否已经分配。
内存分配是通过搜索一个未使用的足够大小的内存块给所请求的空间分配。首次适应法使搜索时第一块足够大小的内存被分配出去。当一内存块被释放时,已用标志被设为 0。为了防止碎片,通过检查前一个和后一个内存块的已用标志,可以把未用的块结合起来形成更大的未用的块。
 
7 网络层接口
lwIP中物理网络硬件的设备驱动被表示为一个像 BSD中那样的一个网络接口结构。图5显示了网路接口结构。网络接口被保存在一个全局的链表中,通过结构中的next指针连接起来。每个网络接口都有一个名字,存在name字段中。两字符名字指示网路接口使用的设备驱动种类,只在运行时通过人工操作完成接口配置。Name字段由设备驱动设置,并且能反映出由网络接口表示的硬件类型。例如,蓝牙驱动的网路接口其name值应为bt,IEEE 802.11b WLAN其name值应为wl。因为name值没必要是唯一的,因此num字段用来区别统一类型驱动的不同网络接口。
 
三个 IP地址,ip_addr, netmask和gw由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()函数。这里,初步全面(initial sanity)的IP版本域和头部长度的检查,加上计算和检查头部检验和。它假定协议栈将不会接收到任何的IP分片,因为它假定代理会收集热合的分片包,因此所有的IP分片都会被丢掉。包含IP选项的包也假定由代理处理因此同样被丢掉。
接下来,函数用该网络接口的 IP地址检查目的地址,看该包是否是传送给该主机的。网络接口被顺序链接到一个链表中,被线性搜索。网络接口的数量预期很少所以没有用比线性搜索更好的搜索策略。
如果到来的包被认定是发给本主机的,则协议域值被用来决定该包被传递给哪个高层协议。
8 .2 发送网络包
一个发送的包由函数 ip_output()处理,该函数使用ip_route()来找到合适的网络接口以传输该包。当决定了发送该包的网络接口后,该包被传递给ip_output_if()函数,同时决定发送该包的网络接口也是该函数的一个参数被传递。这时,所有的IP头部字段都被填写,并且计算了IP头部校验和。该IP包的源和目的地址都被作为一个参数传递给ip_output_if()函数。另外可以省略传递该IP包的源地址,但这种情况下,发送该包的网络接口的IP地址将作为该包的源IP地址。
Ip_route()函数线性搜索网络接口列表,以找到合适的网络接口。搜索过程中, IP包的目的IP地址由该网络接口的子网掩码所掩盖(masked)。假如该被掩盖的目的地址相等于一个网络接口的掩盖后的IP地址,那么该网络接口就被选择,否则将使用一个默认的网络接口。该默认网络接口可以在系统引导时设置,或者运行时由人工配置。如果默认网络接口的网络地址和目的IP地址不品配,则网络接口结构(图5)中的gw字段值将被作为链路帧的目的IP地址。(注意:在这种情况下,IP包的目的地址和链路帧的目的地址将会不同)。这种原始形式的路由假象下的事实是一个网络可能有多个路由器相连。对于大多数的基本情况,如一个本地网络只有一台路由器,这工作的很好。
由于传输层协议 UDP和TCP需要目的IP地址以计算传输层校验和,在某种情况下,在该包被传递给IP层之前就应该确定发送该包的网络接口。解决的办法是让传输层函数直接调用ip_route()。由于在该包到达IP层时已经知道了发送该包的网络接口,因此无需再搜索网络接口列表。相反,这些协议直接调用ip_output_if()。因为该函数把该网络接口作为一个参数没,因此就避免了搜索该网路接口。
8 .3 转发网络包
    如果没有一个网路接口的 IP地址和一个到来的包的目的地址相匹配,该包就应该被转发。这由ip_forward()函数处理。这时,TTL字段的值被减少,并且如果该值为0,一个ICMP错误消息将被发送到该包的原始发送者,同时丢弃该包。因为IP头部已改变了,IP头部校验和必须重新校正。因为可以应用[MK90,Rij94]简单的算法来校正原始IP校验和,因此没必要重新计算整个校验和。最后,该包被转发给一个合适的网路接口。用于找到一个合适的网络接口的算法和发送一个IP包一样.
8 .4 ICMP 处理
ICMP处理过程相当简单 .有ip_input()收到的ICMP包被转交给icmp_input()函数,该函数对ICMP头部进行解码并作出相应的响应.一些ICMP消息被传递给高层协议,以及一些特殊的传输层协议可能会关注这些消息.ICMP目的不可达消息可由传输层协议发送,特别是UDP.icmp_dest_unreach()被用来处理此种情况.
网络中 ,广泛使用ICMP Echo消息来探测网络,因此ICMP Echo在性能上尽量的优化.实际的处理发生在icmp_input()中,包含的处理有:对到来的包的源和目的IP地址进行交换,改变Echo回复的ICMP类型并校正ICMP校验和.此时,该包被传递回IP层以进一步发送出去.
9 .UDP处理
UDP是用来在不同进程间多路转发 (demultiplexing)包的简单协议.每个UDP会话的状态被保存在一个PCB结构体中(如图7所示).UDP控制块被保存在一个链表中.当一个UDP数据报到来时将搜索该链表以找到一个相比配的控制块(pcb).
在全局 UDP控制块链表中,每个UDP控制块都包含有指向下一个控制块的指针.一个UDP会话由两终端的IP地址和端口号决定,而这些信息分别被保存在local_ip、dest_ip、local_port、dest_port字段中.flags字段指示将对当前会话应用何种UDP校验和.这允许关闭UDP检验和,或者使用UDP Lite来计算部分数据报的校验和.当使用UDP Lite时,chesum_len字段指明将计算多少的数据报.
最后的两个参数 recv和recv_arg将在一个数据报到达时被使用.
由于 UDP的简单性,输入和输出处理也十分简单,完全以直线方式流程处理(如图8).为了发送数据,应用程序调用udp_send()函数,而该函数则接着调用udp_output()函数.其中将计算必要的校验和,以及填充UDP头部字段的值.由于检验和包含IP包的源地址,因此在某种情况下将调用ip_route()来找出发送该IP包的网络接口.该网络接口的IP地址被作为该包的源IP地址.最后,该包被转交给ip_output_if()进行传输.
当一个 UDP数据报到来时,IP层调用udp_input()函数.这时,如果该会话应该使用检验和,则检查该数据报的UDP校验和,接着多路转发该包.当找到相应的UDP控制后,recv函数就被调用.
 
10.TCP 处理
    TCP是一个为应用层提供可靠的字节流服务的传输层协议 .TCP必任何其它在这里描述的协议都要复杂,其实现代码占总代码量的50%左右.
10.1 概要
    基本的 TCP处理(如图9)被分为6个函数.tcp_input()、tcp_process()和tcp_receive()三个函数与TCP接收处理相关,而tcp_write()、tcp_enqueue()和tcp_output()三个函数则与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应答,那么该数据将从缓冲区中移出,其所占空间则可以重新使用了.同时,当受到一个ACK应答时,接收者可能想要接收更多的数据,因此将调用tcp_output()函数.
(待续)
 

lwIP――TCP/IP协议栈的一种实现
    近来,自己重点关注于TCP/IP技术的原理及实现,也曾看过 Douglas E.Comer和David L.Stevens写的《Internetworking with TCP/IP》三卷本,但遗憾的是自己只是“看过”而不是“研读”过,对其中的原理只限于表面上的理解,而未曾深入或者研读实现的代码.幸运的是,我无意中找到国外一些对TCP/IP研究的论文,其中的lwIP算是一个基本的实现,对初学者深入了解TCP/IP应该有很大的帮助。因此,自己打算翻译一些关于lwIP的论文(介绍及实现等等),让自己能真正的理解该协议,也希望能给志同道合者一些帮助。由于自己是第一次翻译这类计算机科技文章,而自己的英语水平有限,还望见谅。
    文章主要基于Adam Dunkels的《Design and Implemantation of the lwIP TCP/IP Stack》。以下是摘要和目录结构,希望自己能及时的翻译完这篇文章,并阅读其源代码。
摘要
lwIP是TCP/IP协议栈的一种实现,它注重减少内存使用量及代码大小,以便在像嵌入式系统这样的资源有限的小客户端系统中使用。为了减少处理及内存需求,lwIP使用裁剪后的API不要求任何的数据复制。
这篇文章描述了lwIP的设计及实现,包括协议实现及底层系统如内存及缓冲管理中所涉及到的算法及数据结构。同时,文章中也提供了lwIP API使用手册及使用lwIP的一些代码实例。
1 介绍
2 协议分层
3 概要
4 进程模型
5 操作系统仿真层
6 缓冲及内存管理
 6.1 包缓冲区
 6.2 内存管理
7 网络层接口
8 IP 处理
 8.1 接受网络包
 8.2 发送网络包
 8.3 转发网络包
 8.4 ICMP 处理
9 UDP处理
10 TCP 处理
 10.1 概要
 10.2 数据结构
 10.3 序列号计算
 10.4 队列及数据传输
    10.4.1 糊涂窗口症避免
 10.5 接收报文段
    10.5.1多路转发(Demultiplexing)
    10.5.2 接收数据
10.6 接受新连接
10.7 快速重传
10.8 定时器
10.9 往返时间估计
10.10 拥塞控制
11 协议栈接口
12 应用程序接口
 12.1 基本思想
 12.3 API的实现
13 代码统计分析
 13.1 代码行数
 13.2 目标代码大小
14 性能分析
15 API参考
 15.1 数据类型
 15.2 缓冲区函数
16 网络连接函数
17 BSD socket 库
18 代码实例
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值