为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

lwip焦版(添加了书签)

2011-05-17 42页 pdf 611KB 21阅读

用户头像

is_243143

暂无简介

举报
lwip焦版(添加了书签) TCP/IP 协议栈 LwIP的设计与实现 Design and Implementation of the LwIP TCP/IP Stack [瑞典]Adam Dunkels著 adam@sics.se 翻译 焦海波 marsstory99@hotmail.com Swedish Institute of Computer Science February 20, 2001 TCP/IP 协议栈 LwIP的设计与实现 -...
lwip焦版(添加了书签)
TCP/IP 栈 LwIP的设计与实现 Design and Implementation of the LwIP TCP/IP Stack [瑞典]Adam Dunkels著 adam@sics.se 翻译 焦海波 marsstory99@hotmail.com Swedish Institute of Computer Science February 20, 2001 TCP/IP 协议栈 LwIP的设计与实现 - 1 - 摘要 LwIP 是 TCP/IP 协议栈的一个实现。它的目的是减少内存使用率和代码大小,使 LwIP 适 用于资源受限系统比如嵌入式系统。为了减少处理和内存需求,LwIP 使用不需要任何数据 复制的经过裁剪的 API。 本文描述了 LwIP 的设计与实现。描述了在协议栈实现中以及像内存与缓冲管理这样的 子系统中使用的算法和数据结构。本文还包括 LwIP 的参考手册以及使用 LwIP 的代码例子。 1 简介 最近几年,人们对计算机互联以及计算机无线网络支撑设备的兴趣一直不断的增长。计 算机逐渐与日常使用的设备无缝集成在了一起,并且价格一直在下降。与此同时,无线网络 技术比如蓝牙(Bluetooth)[HNI+98]及 IEEE 802.11b WLAN[BIG+97]正逐渐的出现在人们 的视野中。这些新技术的出现,在许多诸如卫生保健、安全保密、运输及工业处理等领域提 供了一个非常诱人的应用前景。一些像传感器一类的轻便设备可以连入互联网,以便随时随 地进行监控。 在过去的近十年的时间里,互联网技术被证明拥有足够的灵活性以适应不断变化的网络 环境。从原始的 ARPNET 一类的低速网络发展起来的互联网,发展到今天,在带宽和误码率 方面拥有巨大差异的光纤连接技术已经使互联网实现了巨大的跨越。相当多的以互联网为基 础的应用技术被开发出来。因此,未来的无线网络——使用已经存在的互联网技术成为人们 的首选。同样,互联网在全球范围内的连通性也成为了人们选择它的动机之一。 一些轻便设备,比如在身体上使用的传感器,体积小而且便宜,内部的运算及存储资源 有限,因此就必须在资源受限的情况下实现及处理 Internet 协议。本文讲述的就是在这样 的条件下如何占用尽量少的资源实现一个轻型的TCP/IP协议栈,我们把该协议栈叫做LwIP。 本文的章节安排是这样的:第 2、3、4 节对 LwIP 做一个总体上的描述,第 5 节是关于 操作系统模拟层的内容,第 6 节是内存和缓冲区管理,第 7 节介绍 LwIP 网络接口抽象层, 第 8、9、10 介绍 IP、UDP、TCP 协议的实现,第 11、12 节介绍如何与 LwIP 协议栈接口及 LwIP 提供的 API,第 13、14 节将分析协议栈的实现,第 15、16 节提供 LwIP API 的参考手 册,17、18 节提供例子代码。 2 协议层 TCP/IP 协议族以分层的方式设计,每一层分别解决通讯问的一部分。设计实现协议 族——层可以提供指引,因为每一种协议可以被独立的实现。然而严格的按照分层的方式实 现协议族,会因为协议层之间的通讯造成总体性能下降。要解决这个问题,协议的某些内部 方面对其它协议来说应该可知,不过要注意的是,只有重要的信息在各层之间共享。 大部分的 TCP/IP 实现在应用层和底层协议层之间进行了严格的划分,而底层协议之间 却可以进行或多或少的交叉存取。在大部分的操作系统中,底层协议族作为拥有应用层进程 通讯入口的操作系统内核的一部分被实现。应用程序是 TCP/IP 实现的抽象表示,网络通讯 与进程间通讯和文件 I/O 没多少差别。这意味着,因为应用层不知道底层协议使用的缓冲机 制,那它就不能利用这些信息去做一些事情,比如,重新使用常用数据缓冲区。同样,当应 用层发送数据,在被网络代码处理之前,这些数据必须由应用层进程内存空间复制到内部缓 冲区。 像 LwIP 的目标系统这样的最小限度系统所使用的操作系统,通常不能在内核与应用层 进程之间维持一个严格的保护屏障。这就允许使用一种比较松散的通讯机制,通过共享内存 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 2 - 的方式实现应用层与底层协议族之间的通讯。特别的,应用层能够了解底层协议使用的缓冲 处理机制将使应用层可以更加有效的重复使用缓冲区。同样,既然应用层与网络代码可以使 用相同的内存区,那么应用层就可以直接读写内部缓冲区,从而避免了内存复制产生的性能 损失。 3 概览 与许多其它的 TCP/IP 实现一样,LwIP 也是以分层的协议为参照——设计实现 TCP/IP。 每一个协议作为一个模块被实现,同时还提供了几个函数作为协议的入口点。尽管这些协议 是被独立实现的,但是有些层却不是这样,就像上面讨论的,这样做的目的是为了在处理速 度与内存占用率方面提升性能。比如,当验证一个到达的 TCP段的校验和并且分解这个 TCP 段时,TCP 模块必须知道该 TCP 段的源及目的 IP 地址。因为 TCP 模块知道 IP 头的结构, 因此它就可以自己提取这个信息,从而取代了通过函数调用传递 IP 地址信息的方式。 LwIP 由几个模块组成,除 TCP/IP 协议的实现模块外(IP,ICMP,UDP,TCP),还有 包括许多相关支持模块。这些支持模块包括:操作系统模拟层(见第5节)、缓冲与内存管 理子系统(见第6节)、网络接口函数(见第7节)及一组 Internet 校验和计算函数,LwIP 还包括一个 API 概要说明,详见第 12 节. 4 进程模型 TCP/IP 协议栈的进程模型指的是采用何种方法把系统分成不同的进程。首先要说的一 种进程模型是 TCP/IP 协议族的每一个协议作为一个独立的进程存在。这种模型,必须符合 协议的每一层,同时必须指定协议之间的通讯点。虽然,这种实现方法有它的优势,比如每 一种协议可以随时参与到系统运行中,代码比较容易理解,调试方便,但是它的缺点也很明 显。像前文描述过的,这种进程模型并不是最好的 TCP/IP 协议实现方法。同样更重要的是, 数据跨层传递时将不得不产生进程切换(context switch)。对于接收一个 TCP 段来说,将会 引起三次进程切换,从网络设备驱动层进程到 IP 进程,从 IP 进程到 TCP 进程,最终到应 用层进程。对于大部分操作系统来说,进程切换得代价可是相当昂贵的。 另外一种比较普遍的方法是协议栈驻留在操作系统内核中,应用进程通过系统调用 (system calls)与协议栈通讯。各层协议不必被严格的区分,但可以使用交叉协议分层技术。 LwIP 则采取将所有协议驻留在同一个进程的方式,以便独立于操作系统内核之外。应 用程序既可以驻留在 LwIP 的进程中,也可以使用一个单独的进程。应用程序与 TCP/IP 协 议栈通讯可以采用两种方法:一种是函数调用,这适用于应用程序与 LwIP 使用同一个进程 的情况;另一种是使用更抽象的 API。 LwIP 在用户空间而不是操作系统内核实现,既有优点也有缺点。把 LwIP 作为一个进 程的主要优点是可以轻易的移植到不同的操作系统中。由于 LwIP 被设计运行在小系统里, 通常它既不支持进程换出(swapping out processes,这里译者将其翻译为进程换出,指得是操作系统将 不具备运行条件的进程从内存换出到外存,以腾出内存空间,译者注)也不支持虚拟内存。因此就不 会产生因 LwIP 进程的一部分被交换或分页到磁盘上(paged out to disk,即用到了虚拟内存,译者 注),进程因等待磁盘激活而造成延时的问题。不过在获取一个偶然发生的服务请求之前因 任务调度产生的等待延时依然是一个问题,不过在 LwIP 的设计中,这并没有妨碍它以后在 操作系统内核实现。 5 操作系统模拟层 为了方便 LwIP 移植,属于操作系统的函数调用及数据结构并没有在代码中直接使用, 而是用操作系统模拟层来代替对这些函数的使用。操作系统模拟层使用统一的接口提供定时 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 3 - 器、进程同步及消息传递机制等诸如此类的系统服务。原则上,移植 LwIP,只需针对目标 操作系统修改模拟层实现即可。 TCP 用到的定时器功能由操作系统模拟层提供。这个定时器是一个时间间隔至少为 200ms 的单脉冲定时器(one-shot timer,单脉冲定时器,指的是当时钟启动时,它把存储寄存器的值复 制到计数器中,然后晶体的每一个脉冲使计数器减 1。减至 0 时,产生一个中断,并停止工作,直至软件 重新启动它,译者注),当时间溢出发生时就会调用一个已注册的函数。 进程同步机制仅提供了信号量。即使信号量不被底层的操作系统支持也可以使用其它基 本的同步方式来模拟,比如条件变量或者加锁。 消息传递通过一个简单机制来实现,它使用一个被称作邮箱的抽象方法。邮箱有两种操 作:邮递(post)与提取(fetch),邮递操作不会阻塞进程;相反,投递到邮箱的消息被操 作系统模拟层放到队列中直至其它进程将它们取出。即使底层的操作系统本身并不支持邮箱 机制,采用信号量的方式也是很容易实现的。 6 缓冲与内存管理 通讯系统里的内存与缓冲管理模块首要考虑的是如何适应不同大小的内存需求,一个 TCP 段可能有几百个字节,而一个 ICMP 回显数据却仅有几个字节。还有,为了避免复制, 应该尽可能的让缓冲区中的数据内容驻留在不能被网络子系统管理的存储区中,比如应用程 序存储区或者 ROM。 6.1 包缓冲区 pbufs pbuf 是 LwIP 信息包的内部表示,为最小限度协议栈的特殊需求而设计。pbufs 与 BSD 实现中使用的 mbufs 相似。pbuf 结构即支持动态内存分配保存信息包内容,也支持让信息 包数据驻留在静态存储区。pbufs 可以在一个链表中链接在一起,被称作一个 pbuf 链,这样 一个信息包可以穿越几个 pbufs。 pbufs 有三种类型:PBUF RAM、PBUF ROM、PBUF POOL。图 1 所示的 pbuf 为 PBUF RAM 类型,包数据存储在由 pbuf 子系统管理的存储区中: 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 4 - 图 1 一个 PBUF RAM 类型的 pbuf,其数据保存在由 pbuf 子系统管理的存储区中 图 2 所示的 pbuf 是一个被链接的 pbuf 例子,在这个 pbuf 链中第一个 pbuf 是 PBUF RAM 类 型,第二个是 PBUF ROM 类型,这意味着它所拥有的数据存储在 pbuf 子系统不能管理的存 储区: 图 2 一个 PBUF RAM 类型的 pbuf 链接了一个数据存储在外部存储区的 PBUF ROM 类型的 pbuf 第三种 pbuf 类型,PBUF POOL,图 3 所示,它由分配自固定大小的 pbufs 池里的固定大小 的 pbufs 组成。一个 pbuf 链可以由 pbufs 的不同类型组成。 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 5 - 图 3 一个来自于 pbuf 池中的被链接的 PBUF POOL pbuf 这三种类型拥有不同的使用目的。PBUF POOL 主要用于网络设备驱动层,因为分配一 个 pbuf 的操作可以快速完成,所以非常适合用于中断处理。PBUF ROM 类型的 pbufs 用于 应用程序要发送的数据放置在应用程序管理的存储区的情况。在 pbuf 已经移交给 TCP/IP 协 议栈后,这些数据是不能被编辑修改的,因此这种 pbuf 类型主要用于数据被放置在 ROM 中 的情况(因此名字是 PBUF ROM)。为 PBUF ROM 类型的 pbuf 数据预置的包头存储在一 个 PBUF RAM 类型的 pbuf 中,这个 pbuf 被链接到这个 PBUF ROM 类型的 pbuf 前面,如 图 2 所示。 PBUF RAM 类型的 pbuf 还用于应用程序发送的数据被动态生成的情况。在这种情况下。 pbuf 系统不仅为应用数据分配内存,还要给为这些数据预置的包头分配内存,见图 1。pbuf 系统不可能预先知道为这些数据预置什么样的包头,因而考虑最坏的情况。包头大小在编译 时是可配置的。 其实,收到的 pbufs 是 PBUF POOL 类型,发送出的 pbufs 是 PBUF ROM 或 PBUF RAM 类型。 pbuf 的内部结构参见图 1 到图 3。pbuf 结构包括两个指针,两个长度字段,一个字 段和一个引用计数(reference count)。next 字段是一个指向 pbuf 链中下一个 pbuf 的指针。 payload 指针指向 pbuf 中数据的开始位置。len 字段包含 pbuf 中数据内容的长度。tot_len 字 段包含当前 pbuf 的长度与在这个 pbuf 链中随后的所有 pbufs 的 len 字段之和。换句话说, tot_len 字段是 len 字段与 pbuf 链中随后一个 pbuf 的 tot_len 字段的和。flags 字段标识 pbuf 的类型,ref 字段包含一个引用计数。next 和 payload 字段是本地指针,它们占用的字节数与 所使用的处理器架构有关。两个长度字段为 16 位无符号整形,flags 和 ref 字段是 4 位宽。 pbuf 结构的实际大小与所使用的处理器架构下的指针大小及最小对齐方式有关。在 32 位指 针及 4 字节对齐的架构里,pbuf 的大小为 16 个字节长,在 16 位指针及 1 字节对齐的架构 里,pbuf 为 9 个字节长。 pbuf 模块提供了操作 pbufs 的函数。分配一个 pbuf 使用 pbuf_alloc()函数,该函数能够 分配上面描述的三种类型中的任一类型 pbufs。pbuf_ref()函数增加引用计数。回收 pbuf 使用 pbuf_free()函数,该函数首先要减少 pbuf 索引计数(reference count)。如果引用计数已经减 为 0,这个 pbuf 被回收。pbuf_realloc()函数可以收缩 pbuf 大小,以恰好够用的内存封装数 据。pbuf_header()函数调整 payload 指针和长度字段以便为 pbuf 中的数据预置包头。 pbuf_chain()与 pbuf_dechain()函数用于链接 pbufs。 6.2 内存管理 内存管理模块支撑的 pbuf 机制很简单。它负责处理内存连续区域的分配和回收以及收 缩已分配内存块的大小。内存管理模块使用系统内存的一部分作为自己的专用区域,这确保 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 6 - 了网络系统不会使用系统中所有可用内存,即使网络系统使用了所有自己的内存,也不会扰 乱其它程序的操作。 在内部,内存管理模块通过在每一个内存分配块的顶部放置一个比较小的结构体来保存 内存分配纪录。这个结构体拥有三个成员变量,两个指针一个标志,见图 4。next 与 prev 分别指向内存的下一个和上一个分配块,used 标志标示该内存块是否已被分配。 图 4 存储区分配结构 内存管理模块根据所申请分配的大小来搜索所有未被使用的内存分配块,检索到的最先 满足条件的内存块将分配给申请者。已经分配的内存块被回收后,使用标志 used 清零。为 了防止内存碎片的产生,上一个与下一个分配块的使用标志会被检查,如果他们中的任何一 个还未被使用,这个内存块将被合并到一个更大的未使用内存块中。 7 网络接口 在 LwIP,物理网络硬件的设备驱动通过一个与 BSD 中相似的网络接口结构来表示。见 图 5。网络接口保存在一个全局链表中,它们通过结构体中的 next 指针连接。 每一个网络网络接口都拥有一个名字,保存在图 5 中的 name 字段。两个字符的名字标 识网络接口使用的设备驱动的种类并且只用于这个接口在运行时由人工操作进行配置的情 况。名字由设备驱动来设置并且应该反映通过网络接口表示的硬件的种类。比如蓝牙设备 (bluetooth)的网络接口名字可以是 bt,而 IEEE 802.11b WLAN 设备的名字就可以是 wl。 既然网络接口的名字不必具有唯一性,因此 num 字段被用来区分相同类别的不同网络接口。 三个 IP 地址 ip_addr,netmask 与 gw 用于 IP 层发送和接收信息包,有关它们的具体使 用说明见下一节。一个网络接口只能拥有一个 IP 地址,每一个 IP 地址应当创建一个网络接 口。 当收到一个信息包时,设备驱动程序调用 input 指针指向的函数。 网络接口通过 output 指针连接到设备驱动。这个指针指向设备驱动中一个向物理网络 发送信息包的函数,当信息包被发送时由 IP 层调用。这个字段由设备驱动的初始设置函数 填充。output 函数的第三个参数 ipaddr 是应该接收实际的链路层帧的主机的 IP 地址。它不 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 7 - 必与 IP 信息包的目的地址相同。特别地,当要发送 IP 信息包到一个并不在本地网络里的主 机上时,链路层帧会被发送到网络里的一个路由器上。在这种情况下,给 output 函数的 IP 地址将是这个路由器的地址。 最后,state 指针指向网络接口的设备驱动特定状态,它由设备驱动设置。 struct netif { struct netif *next; char name[2]; int num; struct ip_addr ip_addr; struct ip_addr netmask; struct ip_addr gw; void (* input)(struct pbuf *p, struct netif *inp); int (* output)(struct netif *netif, struct pbuf *p, struct ip_addr *ipaddr); void *state; }; 图 5 netif 结构 8 IP 处理 LwIP 仅实现了 IP 层大部分的基本功能,能够发送、接收以及转发信息包,但是不能接 收和发送 IP 分片包,也不能处理携带 IP 参数选项的信息包。不过对大多数的应用来说,这 不会引起任何问题。 8.1 接收信息包 对到达的 IP 信息包,由网络设备驱动调用 ip_input()函数开始处理。在这里完成对 IP 版本字段及包头长度的初始完整性检查,同时还要计算和验证包头校验和。协议栈假定代理 会重新组合 IP 分片包为一个完整的包,因此它会把收到的 IP 分片包直接丢掉。同样,信息 包携带的 IP 参数选项也被认为已经由代理处理过,这些内容会被删掉。 接下来,函数检查目的地址是否与网络接口的 IP 地址相符以确定信息包是否到达预定 主机。网络接口在链表中被排序并且采用了线性检索。因为预计接口的数量比较少,所以没 有实现比线性检索更好的检索策略。 如果一个到达的信息包被发现已经到达了目的主机,则由协议字段来决定信息包应该传 送到哪一个上层协议。 8.2 发送信息包 外发的信息包由 ip_output()函数处理,该函数使用 ip_route()函数查找适当的网络接口来 传送信息包。当外发的网络接口确定后,信息包传给以外发网络接口为参数的 ip_output_if() 函数。在这里,所有的 IP 包头字段被填充,并且计算 IP 包头校验和。IP 信息包的源及目标 地址作为参数被传递给 ip_output_if()函数。源 IP 地址可以被忽略,不过在这种情况下外发 网络接口的 IP 地址会作为 IP 信息包的源 IP 地址被使用。 ip_route()函数通过线性检索网络接口链表找到适当的网络接口。在检索期间,用网络接 口的网络掩码对 IP 信息包的目标地址进行掩码运算。如果经掩码运算的目标地址等与同样 经掩码运算的接口 IP 地址(即网络地址相等,同在一个子网中,译者注),则选择这个接口。如 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 8 - 果没有找到匹配的,则使用缺省网络接口。缺省网络接口在启动时或运行时由人工操作进行 配置(注意运行期间人工配置需要一个能配置协议栈的应用程序,LwIP 不包含这样的程序)。 如果缺省网络接口的地址不匹配目的 IP 地址,则网络接口结构体(图 5)的 gw 字段被选择 作为链路层帧的目的 IP 地址(注意,在这种情况下,IP 信息包的目标地址与链路层帧的目 标地址是不同的)。路由的基本形式掩盖这样一个事实:一个网络可能拥有很多路由器依附 它。不过对于最基本的情况,一个本地网络只拥有一个路由器进行路由工作。 因为传输层协议 UDP 与 TCP 在计算传输层校验和的时候需要拥有目标 IP 地址,因此 在有些情况下,信息包被传递给 IP 层之前必须确定外发网络接口。这可以让传输层函数直 接调用 ip_route()函数做到,因为在信息包到达 IP 层时已经知道了外发网络接口,因此就没 有必要再检索网络接口链表,而是让那些协议改为直接调用 ip_output_if()函数。因为这个函 数将网络接口作为参数,从而避免了对外发接口的检索。 8.3 转发信息包 如果没有网络接口的地址与到达的信息包的目标地址相同,信息包应该被转发。这项工 作由 ip_forward()函数完成。在这里,TTL 字段值被减少(Time To Live 的简写,生存时间的意 思,译者注),当减为 0 的时候,将会给 IP 信息包的最初发送者发送 ICMP 错误信息,并抛弃 该信息包。因为 IP 包头被改变,因此需要调整 IP 包头校验和。不过,不必重新计算完整的 校验和,因为可以使用简单的算法调整原始 IP 校验和。最后,信息包被转发到适当的网络 接口。查找适当的网络接口的算法与发送信息包使用的算法相同。 8.4 ICMP 处理 ICMP 处理相当简单。ip_input()函数收到的 ICMP 信息包被移交给 icmp_input()函数对 ICMP 包头解码,然后进行适当的动作。某些 ICMP 消息被传递给上层协议,由传输层的特 定函数处理。ICMP 目标不可到达消息可以由传输层发送,特别是通过 UDP,使用 icmp_dest_unreach()函数完成这项工作。 图 6 ICMP 处理 用 ICMP ECHO 消息来探测网络被广泛的使用,因而 ICMP 回显处理性能最优。实际处 理被放置在 icmp_input()函数,由交换到达包的源与目的 IP 地址,改变 ICMP 类型为回显应 答以及调整 ICMP 校验和组成。然后,信息包被回传给 IP 层传送。 9 UDP 处理 UDP 是被用来在不同的进程间分解信息包的一个简单协议。每一个 UDP 会话的状态保 存在一个 PCB 结构体中,如图 7 所示。UDP PCBs 保存在一个链表中,当一个 UDP 数据包 到达时对这个链表进行匹配检索。 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 9 - struct udp_pcb { struct udp_pcb *next; struct ip_addr local_ip, dest_ip; u16_t local_port, dest_port; u8_t flags; u16_t chksum_len; void (* recv)(void *arg, struct udp_pcb *pcb, struct pbuf *p); void *recv_arg; }; 图 7 udp_pcb 结构 UDP PCB 结构体包含一个指向 UDP PCBs 全局链表中下一个 PCB 的指针。一个 UDP 会话 由终端 IP 地址和端口号来定义,这些信息保存在 local_ip,dest_ip,local_port 以及 dest_port 字段中。flags 字段标识什么样的 UDP 校验和策略应该用于这个会话。或者可以 完全关闭 UDP 校验和,或者使用 UDP 简化版(UDP Lite)[LDP99]校验和只覆盖数据包的一 部分。如果使用 UDP Lite,chksum_len 字段指出应该进行校验和计算的数据段的长度。 最后两个参数 recv 与 recv_arg 是在由 PCB 指定的会话收到一个数据包时使用。在收到 UDP 数据包时调用 recv 指向的函数。由于 UDP 的简单性,输入输出处理比较简单,并且遵 循图 8 所示的处理流程。要发送数据,由应用程序调用 udp_send()函数,然后再由该函数 请求调用 udp_output()函数来完成。在此处进行必须的校验和计算以及填充 UDP 包头。因 为校验和包括 IP 信息包的源地址,因此在某些情况下会调用 ip_route()函数以查找信息包 将被传输到哪一个网络接口。网络接口的 IP 地址将作为信息包的源地址被使用。最后,信 息包被移交给 ip_output_if()函数传送。 图 8 UDP 处理 当一个 UDP 数据包到达,IP 层调用 udp_input()函数。这里,如果校验和在这个会话中 应该被使用,则 UDP 校验和被检查并分解数据包。当找到了相应的 UDP PCB,recv 函数被调 用。 10 TCP 处理 TCP 属于传输层协议,它为应用层提供了可靠的字节流服务。对它的描述要比对其它协 议的描述复杂的多,单从代码量来说,它就占了 LwIP 代码总量的 50%。 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 10 - 10.1 概览 基本的 TCP 处理过程被分割为六个功能函数来实现(如图 9 所示):tcp_input()、 tcp_process()及 tcp_receive()函数与 TCP 输入有关,tcp_write()、tcp_enqueue()及 tcp_output()则用于 TCP 输出。 图 9 TCP 处理 现在,让我们先看看数据发送的过程是如何进行的。如上图所示,整个过程的发起者是 应用层。应用层调用 ip_write()函数,接着 tcp_write()函数再将控制权交给 tcp_enqueue()函数, 这个函数会在必要时将数据分割为适当大小的 TCP 段,然后再把这些 TCP 段放到所属连接 的传输队列中。这时,tcp_output()函数会检查现在是不是能够发送数据,也就是判断接收器 窗口是否拥有足够大的空间,阻塞窗口是否也足够大,如果条件满足,就使用 ip_route()及 ip_output_if()函数发送数据。 接下来,我们再看看数据的接收过程。图 9 所示,过程的发起者是网络接口层,这里不 做描述。网络接口层将数据包传递给 ip_input()函数,该函数验证 IP 头后移交 TCP 段给 tcp_input()函数。tcp_input()函数完成两项工作:其一,初始完整性检查(也就是校验和验证 与 TCP 选项解析);其二,判定这个 TCP 段属于哪个 TCP 连接。接着,这个 TCP 段到达 tcp_process()函数,这个函数实现了 TCP 状态机,任何必要的状态转换在这里实现。当该 TCP 所属的连接正处于接受网络数据的状态,tcp_receive()函数将被调用。最终,tcp_receive() 函数将数据传给上层的应用程序,完成接收过程。如果这个 TCP 段由一个不被承认的 ACK 应答数据构成,数据将会从缓冲区移走,它所占用的存储区被收回。同样,如果收到一个 ACK 应答确认数据,接收器同意接收更多的数据,如图 9 所示,tcp_output()函数将会被调 用。 10.2 数据结构 由于 LwIP 被设计运行于内存受限的最小限度系统(即嵌入式系统,译者注),所以用于 TCP 实现的数据结构应该尽量较小。为此,我们必须在结构复杂性与使用这些结构的代码 复杂性之间做出选择,为了能够让这些数据结构占用较少的内存单元,我们只能选择代码复 杂性。 实际上,TCP PCB 结构还是相当大,见图 10。因为 TCP 连接在 LISTEN 和 TIME-WAIT 状态相对于其它状态的连接要保留的信息比较少,所以设计了一个较小的数据结构用于这种 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 11 - 状态下的连接。这个小数据结构包含在一个完整的 PCB 结构里,因此图 10 所示的结构成员 列表显得有些笨拙。 图 10 tcp_pcb 结构 这些 TCP PCB 结构保存在一个链表中,next 指针将它们链接在一块。state 变量包含网 络连接的当前 TCP 状态。IP 地址和端口号存储着网络连接信息,而 mss 变量则包含网络连 接所允许的段的最大容量。 rcv_nxt 与 rcv_wnd 字段在接收数据时使用。rcv_nxt 字段包含从远程终端期望得到的下 一个包序号,因而该字段被用于向远程主机发送 ACK 包。接收器窗口由 rcv_wnd 字段保存 并且字段值是在将要发送的 TCP 段中获取的。tmr 字段被作为一个定时器使用,当指定的计 时结束后,连接应该被取消,比如处于 TIME-WAIT 状态的连接。连接允许的段的最大容量 由 mss 字段指定。flags 字段包含了连接的附加状态信息,比如连接是快速恢复还是一个被 延迟的 ACK 是否被发送。 rttest,rtseq,sa,sv 字段被用于 RTT(round-trip time:往返时间,指数据包在 TCP 链路上发 送和收到确认的延时时间,译者注)评估。rtseq 保存段序号,rttest 保存段的发送时间。sa 及 sv 分别保存平均往返时间及时间差。这些字段被用于计算重发超时时间,得到的值保存在 rto 字段。 lastack 与 dupacks 字段用于快速重发及快速恢复的实现。lastack 字段包含收到的最后一 个 ACK 包的序列号,dupacks 字段则对后续收到的与该序列号相等的 ACK 重复包计数。连 接的当前阻塞窗口保存在 cwnd 字段,慢速启动阀值保存在 ssthresh 字段。 snd_ack、snd_nxt、snd_wnd、snd_wl1、snd_wl2、snd_lbb 这六个字段用于发送数据。 接收器应答的最高顺序编号保存在 snd_ack 字段,下一个要发送的序号保存在 snd_nxt 字段。 接收器公开窗口(advertised window)保存在 snd_wnd 字段。snd_wl1 与 snd_wl2 两个字段 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 12 - 用于更新 snd_wnd 字段。snd_lbb 字段保存传输队列最后一个字节的顺序编号。 函数指针 recv 及 recv_arg 用于向应用层传递收到的数据。三个队列字段 unsent、unacked 及 ooseq 用于发送和接收数据。从应用层接收但还未被发送的数据,被放置在 unsent 队列排 队等待发送,已经发送但还未收到远程主机应答确认的数据保存在 unacked 队列。接收到序 列以外的数据由 ooseq 缓冲。 图 11 tcp_seg 处理结构 图 11 所示的 tcp_seg 结构成员列表是 TCP 报文段的内部表示方法。结构的第一个成员 是指向其自身的 next 指针,该指针用于将接收到的多个报文段链结在一起以形成一个等待 队列。len 字段包含 TCP 报文段的长度。这意味着对于一个数据段来说,len 字段包含段中 数据的长度,对于具备 SYN 或者 FIN 标志的空段来说,len 值为 1。pbuf 类型的指针 p 指向 TCP 报文段的存储缓冲区。tcphdr 及 data 指针分别指向 TCP 头和段中的数据。对于外发报 文段,rtime 字段用于报文段的重发超时。而接收报文段是不需要重发的,因此,rtime 字段 是不需要的,所以接收报文段该字段不分配内存。 10.3 顺序编号计算 用于对TCP字节流的每一个字节进行顺序编号的TCP序号是 32 位无符号整形,因此范 围为[0,232-1]。如果TCP序号到到达 232-1,则从 0 开始重新编号,也就是说序号以 232为模 进行计算。这意味着普通的比较操作符不能用于TCP序号。修改过的比较操作符叫做seq,关系定义如下: sseqt Ù s-t>0 s 与 t 为 TCP 序号。比较操作符≤与≥同样被等效定义。这些操作符作为 C 的宏定义包含在 头文件里。 10.4 队列与数据传输 tcp_enqueue()函数对要发送的数据按照适当大小进行分割,并对分割后的数据块进行 顺序编号。在这里,数据被封装进 pbufs 并附加到 tcp_seg 结构。在 pbuf 内,TCP 头被创 建,并且除应答数量,ackno、广播窗口,wnd 以外的其它所有字段都将被填充。tcp_output() 函数可以在报文段排队时重新设置这些字段的值,这个函数完成实际的报文段传输。报文段 被创建之后,它们被放入 PCB 内的 unsent 列表排队。tcp_enqueue()函数会尝试使用最大段 大小的数据填充每一个报文段,当发现 unsent 队列的末尾存在一个低于最大容量的段时, 函数会使用 pbuf 链表的功能将新数据附加到该段。 在 tcp_enqueue()函数格式化完成及排队报文段之后,tcp_output()函数被调用。它将 检查当前窗口是否还有空间存放更多的数据。它通过获取阻塞窗口及发布的接收器窗口的最 大数量来计算。接着,它会填充未被 tcp_enqueue()函数填充的 TCP 报头(TCP header)字段, 然后使用 ip_route()与 ip_output_if()函数发送报文段。发送之后,报文段被放入 unacked 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 13 - 列表,并一直保留至收到相应的 ACK 应答包。 处于 unacked 列表的报文段,会像 10.8 节所描述的进行重发计时。当一个报文段被重 发时,最初的 TCP 与 IP 报头被保留,TCP 报头需要作较小的改动。TCP 报头的 ackno 与 wnd 字段被设置为当前值,因为在报文段最初发送与重发期间,我们可能接收了数据。这仅改变 报头的两个 16 位字,整体的 TCP 校验和不必被重新计算,因为简单算法[Rij94]可以用于更 新校验和。报文段在最初发送时 IP 层已经加上了 IP 报头,并且没有理由去改变它。因而, 重发报文段不需要重新计算 IP 报头校验和。 10.4.1 糊涂窗口的避免 糊涂窗口综合症[Cla82b](SWS,请参考《TCP-IP 详解卷 1:协议》第 22 章 22.3 节,译者注)是一 种能够导致网络性能严重下降的 TCP 现象。当 TCP 接收方通告了一个小窗口并且 TCP 发送方 立即发送数据填充该窗口时,SWS 就会发生。当一个小的报文段被确认,窗口再一次以较小 单元被打开而发送方将再一次发送一个小的报文段填充这个窗口。这样就会造成 TCP 数据流 包含一些非常小的报文段情况的发生。为了避免 SWS 的发生,在发送方和接收方必须设法消 除这种情况。接收方不必通告小窗口更新,并且发送方在只有小窗口提供时不必发送小的报 文段。 在 LwIP,SWS 在发送端就被自然的避免了,因为 TCP 报文段在建立和排队时不知道通告 的接收器窗口。在大数据量发送中,输出队列将包括最大尺寸的报文段。这意味着,如果 TCP 接收方通告了一个小窗口,发送方将不会发送队列中的第一个报文段,因为它比通告的 窗口要大。相反,它会一直等待直至窗口有足够大的空间容下它。 当作为 TCP 接收方时,LwIP 将不会通告小于连接允许的最大报文段尺寸的接收器窗口。 10.5 接收报文段 10.5.1 解析 当 TCP 报文段到达 tcp_input()函数,它们会在 TCP PCBs 之间被解析。解析的关键是 源及目的 IP 地址和 TCP 端口号。解析报文段时,有两种 PCBs 类型必须被区分:相对于开放 连接的 PCB 类型与相对于半开放连接的 PCB 类型。半开放连接指的是那些处于监听状态并且 只有指定的本地 TCP 端口号及本地 IP 地址为任意值的连接;而开放连接则指拥有两个指定 的 IP 地址及两个端口号的连接。许多 TCP 实现,像早期的 BSD 实现,使用具有单一入口缓 存的 PCB 链表技术。在这之后的基本理论是:大部分的 TCP 连接组成代表性的显示一个大量 位置[Mog92]的批量传输,这样就会导致一个高的缓冲区命中率。其它的缓冲包括保存 两个单一的缓冲入口,一个用作已经被发送的最后一个包的 PCB,另一个用作已经收到的最 后一个包的 PCB [PP93]。通过移动最近用过的 PCB 到链表的前端可以使用二者之中的任何一 个方案。两种方案已经表明[MD92]通要胜过单一入口的方案。 对于 LwIP,只要在解析一个报文段时发现一个 PCB 匹配,这个 PCB 就要被移动到 PCB 链表的前端。不过用于监听状态连接的 PCB 不再被移动到前端,因为这种连接并不期望接收 报文段。 10.5.2 接收数据 对到达报文段的实际处理在 tcp_receive()函数里进行。报文段应答序号与处在连接 unacked 队列里的报文段进行比较,如果应答序号比 unacked 队列里的报文段序号高,则这 个报文段会被移出队列,并且为其分配的内存也被收回。 如果到达段的序号要比 PCB 中的 rcv_nxt 变量高,则该段脱离序列。脱离队列的报文段 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 14 - 会被放入 PCB 中的 ooseq 队列。如果到达段的序号等于 rcv_nxt,则通过调用 PCB 中的 recv 指向的函数将报文段转交到上层,并且通过到达段的长度来增加 rcv_nxt 值。因为序列中报 文段的接收可能意味着先前收到的脱离序列的报文段是被期望接收的下一个段,ooseq 队列 被检查。如果它包含一个序号等于 rcv_nxt 值的报文段,则通过调用 recv 指向的函数将该 段转交给应用程序,并且 rcv_nxt 值被更新。这个过程会一直持续至 ooseq 队列为空或者 ooseq 队列中的下一个报文段脱离序列。 10.6 接受新的连接 处于监听状态也就是被动开放的连接,准备着接受远程主机新的连接请求。为了那些连 接,必须建立新的 TCP PCB,并传递给打开初始监听 TCP 连接的应用程序。对于 LwIP,这个 过程是通过让应用程序注册一个回调函数来实现的,这个回调函数在新的连接建立时调用。 当处于监听状态的连接收到一个 SYN 标志设置的 TCP 段时,一个新的连接被建立并且一 个携带 SYN 与 ACK 标志的段被发送以响应收到的 SYN 段。连接进入 SYN-RCVD 状态并且等待 发送的 SYN 段的应答。当应答到达,连接进入 ESTABLISHED 状态,并且 accept 指向的函数 被调用(accept 函数指针请参考图 10 所示的 PCB 结构图)。 10.7 快速重发 LwIP 通过保存最后一个应答 ACK 来实现快速重发与恢复。这样,当收到相同序号的 ACK, TCP PCB 结构中的 dupacks 计数会加一。当 dupacks 值为 3,unacked 队列中的第一个报文 段被重发且快速恢复被初始化。快速恢复按照[APS99]描述的过程实现。无论何时收到新数 据的应答 ACK,dupacks 计数都将复位为 0。 10.8 定时器 与 BSD 中的 TCP 实现一样,LwIP 使用两个周期性定时器,周期分别为 200ms 和 500ms。 这两个定时器又被用于实现更复杂的逻辑定时器,比如重发定时器,TIME-WAIT 定时器及延 迟 ACK 定时器(delayed-ACK timer)。 细粒度定时器(fine grained timer)tcp_timer_fine()会遍历每一个 TCP PCB,检查 是否存在应该被发送的被延迟的 ACKs,就像 tcp_pcb 结构里 flag 字段所指示的(图 10)。 如果延迟 ACK 标志被设置,一个空的 TCP ACK 应答段被发送,并且标志被清除。 粗粒度定时器在 tcp_timer_coarse()函数里实现,同样扫描 PCB 列表。对每一个 PCB, 将遍历未应答报文段列表(unacked 指针详细信息见图 10 及 11,译者注),并且 rtime 变量 值被增加。如果 rtime 值变得比 PCB 中 rto 变量给出的当前重发超时值大,报文段被重发并 且重发超时加倍。只有在阻塞窗口与通告的接收器窗口的值允许的情况下报文段才被重发。 重发之后,阻塞窗口被设置为最大段尺寸大小,慢启动阀值被设置为有效窗口大小的一半, 并且慢启动在连接中被初始化。 对于 TIME-WAIT 状态的连接,粗粒度定时器也会增加 PCB 结构中的 tmr 字段值。当定时 器到达 2×MSL阀值,连接被取消。粗粒度定时器还会增加一个全局的 TCP 时钟值,tcp_ticks。 这个时钟用于 RTT(往返时间:round-trip time)估算及重发超时(retransmission time-outs)。 10.9 往返时间估算 RTT 估算是 TCP 的主要部分,因为估算出的往返时间用于确定适当的重发超时。在 LwIP, 往返时间算法与 BSD 实现类似,每一次往返时间就被测量一次并且使用[Jac88] 描述的 smoothing 函数计算适当的重发超时。 TCP PCB 中的 rttseq 变量保存着被测量过往返时间的报文段的序号。PCB 中的 rttest 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 15 - 变量保存报文段被第一次重发时的 tcp_ticks 值。当收到的 ACK 包的序号等于或者大于 rtseq,往返时间通过从 tcp_ticks 减去 rttest 来测量。如果重发发生在往返时间测量期间, 测量值不被采纳。 10.10 阻塞控制 阻塞控制的实现令人惊讶的简单,其在输入与输出中仅有几行代码。当收到一个新数据 的ACK应答,阻塞窗口cwnd值会加上最大段大小或mss2/cwnd的大小,这取决于连接是慢速启 动还是阻塞控制。当发送数据时,接收器通告的窗口与阻塞窗口的最小值被用于确定每个窗 口能够发送多少数据。 11 协议栈接口 使用 TCP/IP 协议栈提供的服务有两种方法:1)直接调用 TCP 与 UDP 模块的函数;2)使 用下一节将要介绍的 LwIP API 函数。 TCP 与 UDP 模块提供网络服务的一个基本接口,该接口基于函数回调技术,因此使用该 接口的应用程序可以不用进行连续操作。不过,这会使应用程序编写难度加大且代码不易被 理解。为了接收数据,应用程序会向协议栈注册一个回调函数。该回调函数与特定的连接相 关联,当该关联的连接到达一个信息包,该回调函数就会被协议栈调用。 此外,与 TCP 和 UDP 模块直接接口的应用程序必须(至少部分地)驻留在像 TCP/IP 协 议栈这样的进程中。这应归结于回调函数不能跨进程调用的事实(译者注:这里的意思是函数 只能在进程内调用,不允许一个进程去调用另外一个进程的函数)。这即有优点也有缺点。优点是既 然应用程序和 TCP/IP 协议栈驻留在同一个进程中,那么发送和接收数据就不再产生进程切 换。主要缺点是应用程序不能使自己陷入长期的连续运算中,这样会导致通讯性能下降,原 因是 TCP/IP 处理与连续运算是不能并行发生的。这个缺点可以通过把应用程序分为两部分 来克服,一部分处理通讯,一部分处理运算。通讯部分驻留在 TCP/IP 进程,进行大量运算 的部分放在一个单独的进程。将在下一节介绍的 LwIP API 提供了以这样一种方式分割应用 程序的构造方法。 12 应用程序接口 由 BSD 提供的高级别的 Socket API(Socket:一个通过 UNIX 文件描述符和其他程序通讯 的方法,可以认为 Socket 就是一个插座,用于网络的时候,Socket 提供一个网络插口,远程终端的连接 请求可以当做是一条延伸的虚拟电缆需要找到一个可以连接的插座,当虚拟电缆插到这个插口上,通讯链 路就建立了,译者注)不适合用于受限系统的 TCP/IP 实现,特别是 BSD Socket 需要将要发送 的数据从应用程序复制到 TCP/IP 协议栈的内部缓冲区。复制数据的原因是应用程序与 TCP/IP 协议栈通常驻留在不同的受保护空间。大多数情况是应用程序是一个用户进程,而 TCP/IP 协议栈则驻留在操作系统内核。通过避免额外的复制操作,API 的性能可以大幅度提 升。同样,为了复制数据,系统还需要为此分配额外的内存,这样每一个信息包都需要使用 双倍的内存。 LwIP API 专为 LwIP 设计,所以它可以充分利用 LwIP 的内部结构以实现其设计目标。 LwIP API 与 BSD API 类似,但操作相对低级。API 不需要在应用程序和协议栈之间复制数据, 因为应用程序可以巧妙的直接处理内部缓冲区。 因为 BSD Socket API 易于理解,并且很多应用程序为它而写,所以 LwIP 保留一个 BSD Socket 兼容层是很有用的。在 17 节将介绍如何使用 LwIP API 重写 DSD Socket 函数。15 翻译 焦海波 6/2/2006 TCP/IP 协议栈 LwIP的设计与实现 - 16 - 节是 LwIP API 的参考手册。 12.1 基本概念 从应用的角度来说,BSD Socket API 在连续的内存区域处理数据非常便于编写应用程 序。因为应用程序内的数据处理通常是在这样的连续内存区域内进行的。但是,对于 LwIP, 采用这种机制不具备任何优势。因为 LwIP 通常要处理的缓存中的数据是被分割放置在更小 的内存块里的,所以当这些数据在被传递给应用程序之前必须先被复制到一块连续的内存区 域内,这样即浪费处理时间又浪费内存。因此 LwIP 允许应用
/
本文档为【lwip焦版(添加了书签)】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索