目录
摘要:
1.介绍
2.协议分层
3.综述
4.进程模型
5.操作系统仿真层
6.缓冲与存储管理
6.1包缓冲----pbufs
6.2内存管理
LWIP是TCP/IP协议栈的实现。LWIP协议栈关注于减少内存的使用和代码尺寸,从而使得LWIP适用于像嵌入式系统这样资源非常有限的小客户端。为了减少对处理能力和内存的要求,LWIP使用一个不需要任何数据复制的剪裁后的API。
这篇文章将描述LWIP的设计与实现。协议的实现以及像内存与缓冲管理这样的子系统中用到的算法与数据结构在这里都有描述。另外,这里也包括LWIP API的使用参考和一些使用LWIP的代码实例。
纵观过去的几年,连接计算机以及支持计算的设备到无线网络的需求在稳步增长。计算机正在越来越无缝的集成于每日所见设备中的,并且价格呈飞速下降趋势。同时,无线网络技术,像蓝牙[HNI+98]和IEEE802.11bWLAN[BIG+97],正在浮现。这在保健事业、安全与保密、传输以及工业处理等领域产生了许多新的让人感兴趣的局面。像传感器这样的小设备就能够连接到像全球Internet这样已经存在的网络基础设施,并且能够在任何地方进行监控。
网络技术已经证明,它自己的灵活性足够应对过去十几年不断变化着的网络环境。比起最初的像ARPANET这种低速的网络,当今的网络技术可以运行于各种不同的链路上,而这些链路不管是在带宽还是位错误率等特征上有很大的不同。由于大量的网络应用程序使用现有的网络技术,所以将已存在的网络技术运用于日后的无线网路中具有巨大的优势。同样,全球Internet的广泛的互联也是一种巨大的鼓励。由于像传感器这样的小设备常常需要较小的物理尺寸和便宜的价格,所以,一个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节提供了使用LWIP的API的参考并在17、18节中给出代码示例。
TCP/IP协议栈中的协议是基于分层的设计,每一层解决通信中的一部分相对独立的问题。这种分层可以帮助设计与实现协议,因为每一个协议都可以相对于其他协议独立的实现。尽管如此,严格的按照分层的方式来实现协议,会导致这样一种情况出现,那就是协议层之间的通信会降低整体的性能[Cla82a]。为了克服这些问题,一个协议内部的某些方面可以让其他协议了解。但是必须注意到,只有重要的信息才可以在层之间共享。
由于较低层协议之间或多或少的交叉,几乎所有的TCP/IP的实现都保持应用层与较低协议层的严格区分。在大部分操作系统中,较低的协议层也作为操作系统内核的一部分而加以实现,并带有和应用程序层进程进行通信的入口点。应用程序对于TCP/IP的实现呈现一种抽象的观点,在那里,网络通信与内部进程通信或者文件I/O并没有太大的不同。这意味着由于应用程序并没有意识到底层协议所使用的缓冲机制,因而它不能够使用这些信息,比如利用频繁使用过的数据重复利用缓冲区。也就是说,当应用程序发送数据时,在它们被网络代码处理之前,数据必须从应用进程空间复制一份到内部缓冲区。
对于像目标系统为LWIP这样的小系统中的操作系统,在内核与应用进程之间几乎没有保持一个严格的保护栅栏。这就允许使用共享内存这种简单的方案实现应用程序与底层协议之间的通讯。特别的,可以使应用程序意识到底层所使用的缓冲捕获机制。这样,应用程序可以更加有效的重复利用缓冲区。同样,由于应用进程可以与网络代码使用同一个内存缓冲区,所以,应用程序可以直接的读写内部缓存,从而节省了复制数据的性能花费。
就像许多其他TCP/IP的实现,分层的协议设计也被用作指导来设计LWIP的实现。每一个协议被作为一个独立模块来设计实现,通过一些函数作为进入其他协议的入口点。尽管这些协议是独立实现的,但如上所述,为了提升性能,不管是处理速度还是内存占用,仍然有一些违反分层的做法。比如,当确认一个传送进来的TCP段的校验和时,还有,当多路分解一个段时,这个段的源和目的的IP地址必须被TCP模块知道。在这里不采用通过函数调用将这些地址传给TCP的做法,而是让TCP模块意识到IP头的数据结构,从而能够自己来设法得到这些信息。
LWIP由许多模块组成。除了实现TCP/IP协议栈的模块(IP、ICMP、UDP和TCP),一些支持模块也被实现了。支持模块包括操作系统仿真层(在第5节描述),缓冲和内存管理子系统(第6节),网络接口函数(第7节),以及为计算Internet校验和的函数。LWIP也包括一个抽象的API,在12节讲述。
协议实现的进程模型将系统分解为不同的进程的方式来进行讲述。被用来实现通信协议的进程模型将使得每一个协议以一个单独的进程的方式来运行。通过这种模型,严格的协议分层被强化,不同协议之间的通信点必须严格限制。这种方式有它自身的优点,像协议可以在运行时加载,理解代码以及调试将变得更加容易。除此之外,它也有缺点。就像先前所叙述的,严格的分层并不总是实现协议的最好方式。更重要的是,对于各个层之间的访问,一个上下文开关必须提供。对于一个传送进来的TCP段来说,这将意味着必须有三个上下文开关,从设备驱动到网络接口,从IP进程到TCP进程,最后到应用进程。在几乎所有的操作系统中,一个上下文开关是不小的花费。
另外一种通常的方法是让协议之间的通信存在于操作系统内核中。一旦内核实现了协议之间的通信,应用进程与协议之间的通信就可以通过系统调用来实现。虽然协议之间的通信彼此之间并没有严格的划分,但是,可能要使用到穿过不同协议层的技术。
LWIP使用进程模型,所有协议存在于一个单进程中,并且独立于操作系统内核。应用程序可能存在于LWIP进程中,或者存在于独立的进程中。TCP/IP协议栈与应用程序之间的通信或者通过函数调用来实现,比如应用程序与LWIP共享一个进程;或者通过一个更加抽象的API的方式。
当然,将LWIP的实现作为一个用户空间的进程而不是在操作系统内核有它的优点和缺点。将LWIP作为一个进程主要的优点在于能够便于访问不同的操作系统。因为LWIP被设计为运行在小的操作系统上,而这样的操作系统通常都不支持交换区处理和虚拟存储,因此由于LWIP进程被交换或换页出到硬盘而不得不等待磁盘活动所造成的延时将不再是需要考虑的问题。而问题在于得到机会去服务请求之前进行调度所要等待的总的时间,但是在LWIP的设计中,并没有什么能够妨碍它以后在一个操作系统的内核中来实现。
为了使得LWIP更加便携,操作系统相关的具体函数和数据结构并不直接在代码中使用,相反,当这样的功能函数需要时,操作系统仿真层会被用到。操作系统仿真层提供一个唯一的接口给操作系统服务,这些服务包括像时间、进程同步和消息传递机制。原则上讲,当将LWIP移植到其他操作系统上时,对于该特定的操作系统,仅仅需要操作系统仿真层的实现即可。
操作系统仿真层提供一个时间功能函数供TCP使用。这些由操作系统仿真层提供的计时器是至少保持200ms时间间隔的时间片,在这期间当超时发生时调用注册的函数。
进程同步机制仅提供信号量。即使信号量在下层的操作系统中不可用,它们也能够通过其他原始的同步机制像条件变量或者锁来仿真。
消息传递通过使用一个称为mailboxes的抽象调用这样一种简单机制来完成。一个mailboxes有两种操作:发送与获取。发送将不会阻塞该进程,更确切的说,发送到邮箱的消息通过操作系统仿真层被排成队列直到其他进程来取得它们。即使下层的操作系统对于邮箱没有自然的支持,他们也能够通过使用信号量很容易的实现。
通信系统中的存储与缓冲管理系统必须被很好的准备来适应缓冲区在一个较大范围内的变化,从缓冲包含有几百字节的完整的TCP段到只包含几个字节的短ICMP响应。另外,为了避免数据复制,有可能会使缓冲区中的数据内容存在于内存中,这部分内存并不由像应用程序存储区或者ROM由网络子系统来管理。
在LWIP内部,一个pbuf代表一个数据包,并且根据最小栈的特殊需要而设计。pbufs类似于BSD实现中所使用的mbufs。pbuf数据结构即支持动态内存分配来捕获数据包中的内容,同时,也能够使得包数据存在于静态内存。pbufs能够通过一个链连接在一起,称作pbuf链,因此通过链,一个数据包可能存在于多个pbufs中。
有三种类型的pbufs,pbuf ram、pbuf rom和pbuf pool。图一中所示的pbuf代表pbuf ram 类型的pbuf,包数据存储于由pbuf子系统管理的内存中。图二则是一个pbuf链的实例,链中第一个为pbuf ram 型的pbuf,第二个为pbuf rom型的pbuf,这意味着内存中有数据不被pbuf子系统管理。第三种类型的pbuf,即pbuf pool,在图三中可以看到,它由固定大小的pbuf构成,这些pbufs来自于由固定大小的pubf构成的池中。一个pbuf链可能包含多种类型的pbufs。
这三种pbufs都有各自不同的用途。pubf pool型的pbuf主要由网络设备驱动来使用。因为这样分配一个pbuf的操作将是迅速的,并且因此也适合于在中断捕获中使用。pubf rom型的pbuf在应用程序发送数据时使用,而这些数据定位于由应用程序管理的内存中。这些数据在pbuf被交到TCP/IP协议栈之后可能不会再被改变,因此,这种类型的pbuf主要在数据定位于rom中时使用(这也就是为什么叫pubf Rom)。预先计划到pubf rom中的数据头被存储到pbuf ram型的pubf中,并被连到pbuf rom型pbuf的开头,如图2所示。
Ram型的pubfs也在应用程序发送动态生成的数据的时候使用。在这种情况下,pbuf系统不仅给应用程序数据分配内存,同时也为预先计划好的数据头分配内存。这在图一中可以看到。包缓冲系统不可能预先知道那些头将被计划为数据,通常就假设最糟糕的情况。头的大小在编译时被配置。
本质上,输入包缓冲是pbuf pool类型的,而输出包缓冲是pbuf rom或者pbuf ram型的。
一个pbuf的内部数据结构可以通过图一到图三看出。pbuf由两个指针、两个长度域、一个标识域以及一个参考计数域构成。next域在pbuf链中用来指向下一个pbuf。payload指针指向pbuf中数据的开始。len域包含有pbuf数据内容的长度。tot_len域是当前pbuf的长度和pbuf链中其他pbuf的len域的长度之和。换句话说,tot len就是当前的len域与pbuf链中下一个pbuf的tot len中的值之和。标识域用来指示pbuf的类型,而ref域包含有一个参考计数。next和payload域是原始的指针,它们的大小取决于所使用处理器的架构。两个长度域是16位的无符号的整型数据,标识和参考计数域是4个位的宽度。整个pbuf结构的大小依赖于所使用处理器架构中指针的大小以及处理器架构中最小的可能的对齐方式。对于32位指针和4字节对齐的结构,总共的大小为16字节,而对于16位指针和1字节对齐的结构,则只需9个字节。
包缓冲模型提供了操作缓冲包的功能函数。函数pbuf_alloc()用来分配一个pbuf,它可以分配前面描述的任何一种类型的pbuf。函数pbuf_ref()用来将参考计数加一。pbuf_free()用来进行释放分配操作,它首先将pbuf的参考计数减一。当参考计数为0时,pbuf就被释放。函数pbuf_realloc()用来为pbuf瘦身,以使得它能够拥有足够的内存来满足数据存储。pbuf_hader()函数用来调节改变payload指针和长度域,以使得一个数据头能够按照计划加入到pbuf中。pbuf_chain()和pbuf_dechain()函数用来构链和拆链。
存储管理者对于pbuf的支持方案是非常简单的。它持有分配的与释放的内存相邻区域的句柄,并且能够缩减先前已分配的内存块的大小。内存管理者使用系统所有内存的一个专用的部分,这将保证网络系统不会使用所有其他可用内存,并且如果网络系统已经使用了它的所有内存其他程序的操作也不会被干扰。
在内部,内存管理者通过在每一个已分配内存块上使用一个小的数据结构来跟踪已分配的内存。这个结构体(如图4)含有两个指针用来指向其之前与之后已分配块的内存。它同样还有一个标识域用来指示分配块已经被分配了还是没有。
当在内存中查找到一个还没有被使用的分配块,并且对于分配请求而言其足够大,该内存块将被分配。这里,首次匹配原则被使用,也就是第一块满足条件的内存块将被首先使用。当一个分配块被释放时,标示域被置为0表示该内存块不再被使用,属于可分配块。为了避免碎片,之前与之后分配块的标示域将被检查,如果它们中的任何一个都没有被使用,这些块将被合并为一个更大的未使用块。
TCP/IP协议栈Lwip的设计与实现:之二_龙赤子的博客-CSDN博客