You are on page 1of 887

第1章 概 述

1.1 引言

本章介绍伯克利 ( B e r k e l e y )联网程序代码。开始我们先看一段源代码并介绍一些通篇要用
的印刷约定。对各种不同代码版本的简单历史回顾让我们可以看到本书中的源代码处于什么
位置。接下来介绍了两种主要的编程接口,它们在 Unix与非Unix系统中用于编写 TCP/IP协议。
然后我们介绍一个简单的用户程序,它发送一个 U D P数据报给一个位于另一主机上的日
期/时间服务器,服务器返回一个 U D P数据报,其中包含服务器上日期和时间的 A S C I I码字符
串。这个进程发送的数据报经过所有的协议栈到达设备驱动器,来自服务器的应答从下向上
经过所有协议栈到达这个进程。通过这个例子的这些细节介绍了很多核心数据结构和概念,
这些数据结构和概念在后面的章节中还要详细说明。
本章的最后介绍了在本书中各源代码的组织,并显示了联网代码在整个组织中的位置。

1.2 源代码表示

不考虑主题,列举 15 000 行源代码本身就是一件难事。下面是所有源代码都使用的文本


格式:

1.2.1 将拥塞窗口设置为 1

387-388 这是文件tcp_subr.c中的函数tcp_quench。这些源文件名引用 4.4BSD-Lite发


布的文件。 4 . 4 B S D在1 . 1 3节中讨论。每个非空白行都有编号。正文所描述的代码的起始和结
束位置的行号记于行开始处,如本段所示。有时在段前有一个简短的描述性题头,对所描述
的代码提供一个概述。
这些源代码同 4 . 4 B S D - L i t e发行版一样,偶尔也包含一些错误,在遇到时我们会提出来并
加以讨论,偶尔还包括一些原作者的编者评论。这些代码已通过了 G N U缩进程序的运行,使
它们从版面上看起来具有一致性。制表符的位置被设置成 4个栏的界线使得这些行在一个页面
中显示得很合适。在定义常量时,有些 # i f d e f语句和它们的对应语句 # e n d i f被删去 (如:
G A T E W A Y和M R O U T I N G,因为我们假设系统被作为一个路由器或多播路由器 )。所有register说

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
2 计计TCP/IP详解 卷 2:实现

明符被删去。有些地方加了一些注释,并且一些注释中的印刷错误被修改了,但代码的其他
部分被保留下来。
这些函数大小不一,从几行 (如前面的 t c p _ q u e n c h)到最大11 0 0行(t c p _ i n p u t)。超过
大约4 0行的函数一般被分成段,一段一段地显示。虽然尽量使代码和相应的描述文字放在同
一页或对开的两页上,但为了节约版面,不可能完全做到。
本书中有很多对其他函数的交叉引用。为了避免给每个引用都添加一个图号和页码,书
封底内页中有一个本书中描述的所有函数和宏的字母交叉引用表和描述的起始页码。因为本
书的源代码来自公开的 4 . 4 B S D _ L i t e版,因此很容易获得它的一个拷贝:附录 B详细说明了各
种方法。当你阅读文章时,有时它会帮助你搜索一个在线拷贝 [例如Unix程序grep (1)]。
描述一个源代码模块的各章通常以所讨论的源文件的列表开始,接着是全局变量、代码
维护的相关统计以及一个实际系统的一些例子统计,最后是与所描述协议相关的 S N M P变量。
全局变量的定义通常跨越各种源文件和头文件,因此我们将它们集中到的一个表中以便于参
考。这样显示所有的统计,简化了后面当统计更新时对代码的讨论。卷 1的第 2 5章提供了
S N M P的所有细节。我们在本文中关心的是由内核中的 T C P / I P例程维护的、支持在系统上运
行的SNMP代理的信息。

1.2.2 印刷约定

通篇的图中,我们使用一个等宽字体表示变量名和结构成员名 (m _ n e x t),用斜体等宽字
体表示定义常量 ( N U L L)或常量的值 (5 1 2)的名称,用带花括号的粗体等宽字体表示结构名称
(mbuf{})。这里有一个例子:

在表中,我们使用等宽字体表示变量名称和结构成员名称,用斜体等宽字体表示定义的
常量。这里有一个例子:
m_flags 说 明

M_BCAST 以链路层广播发送 /接收

通常用这种方式显示所有的 # d e f i n e符号。如果必要,我们显示符号的值 (M _ B C A S T的
值无关紧要 )并且所列符号按字母排序,除非对顺序有特殊要求。
通篇我们会使用像这样的缩进的附加说明来描述历史的观点或实现的细节。
我们用有一个数字在圆括号里的命令名称来表示 Unix命令,如 g r e p(1)。圆括号中的数字
是4.4BSD手册“manual page”中此命令的节号,在那里可以找到其他的信息。

1.3 历史

本书讨论在伯克利的加利福尼亚大学计算机系统研究组的 T C P / I P实现的常用引用。历史
上,它曾以 4.x BSD系统(伯克利软件发行 )和“B S D联网版本”发行。这个源代码是很多其他
实现的起点,不论是 Unix或非Unix操作系统。
图1 - 1显示了各种 B S D版本的年表,包括重要的 T C P / I P特征。显示在左边的版本是公开可

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 3
用源代码版,它包括所有联网代码:协议本身、联网接口的内核例程及很多应用和实用程序
(如Telnet和FTP)。
4.2BSD (1983)
第一次被广泛应用的
TCP/IP版本

4.3BSD (1996)
改进了TCP性能

4.3BSD Tahoe (1988)


慢启动,防拥塞,快速
重传
BSD联网软件
版本1.0(1989):Net/1
4.3BSD Reno (1990)
快速恢复, T C P首部预
测,SLIP首部压缩,路
由表改变
BSD联网软件
版本2.0(1991):Net/2
4.4BSD(1993)
多播,长肥管道的修改

4.4BSD-Lite(1994)
在正文中用Net/3表示

图1-1 带有重要 TCP/IP特征的各种 BSD版本

虽然本文描述的软件的官方名称为 4.4BSD-Lite发行软件,但我们简单地称它为 Net/3。


虽然源代码由 U. C. Berkeley发行并被称为伯克利软件发行,但 T C P / I P代码确实是各种研
究者的工作的融合,包括伯克利和其他地区的研究人员。
通篇我们会使用术语源于伯克利的实现来谈及各厂商的实现,如 SunOS 4.x、系统V版本
4 ( S V R 4 )和AIX 3.2,它们的 T C P / I P代码最初都是从伯克利源代码发展而来的。这些实现有很
多共同之处,通常包括同样的错误!
在图1 - 1中没有显示的伯克利联网代码的第 1版实际上是1 9 8 2年的4 . 1 c B S D,但是
广泛发布的是1983年的版本4.2BSD。
在4 . 1 c B S D 之前的 B S D版本使用的一个 T C P / I P实现,是由 Bolt Beranek and
Newman(BBN)的Rob Gurwitz和Jack Haverty开发的。[Salus 1994]的第18章提供了另
外一些合并到 4 . 2 B S D中的B B N代码细节。其他对伯克利 T C P / I P代码有影响的实现是
由Ballistics研究室的Mike Muuss为PDP-11开发的TCP/IP实现。
描述联网代码从一个版本到下一个版本的变化的文档有限。 [Karels and
McKusick 1986]描述了从 4 . 2 B S D到4 . 3 B S D的变化,并且 [Jacobson 1990d]描述了从
4.3BSD Tahoe到4.3BSD Reno的变化。

1.4 应用编程接口

在互联网协议中两种常用的应用编程接口 ( A P I )是插口 ( s o c k e t )和T L I (运输层接口 )。前者

Linux公社 www.linuxidc.com
www.linuxidc.com
4 计计TCP/IP详解 卷 2:实现

有时称为伯克利插口 (Berkeley socket),因为它被广泛地发布于 4.2BSD系统中(见图1-1)。但它


已被移植到很多非 BSD Unix系统和很多非 U n i x系统中。后者最初是由 AT & T开发的,由于被
X / O p e n承认,有时叫作 X T I ( X / O p e n传输接口 )。X / O p e n是一个计算机厂商的国际组织,它制
定自己的标准。 XTI是TLI的一个有效超集。
虽然本文不是一本程序设计书,但既然在 N e t / 3 (和所有 B S D版本)中应用编程用插口来访
问TCP/IP,我们还是说明一下插口。在各种非 Unix系统中也实现了插口。插口和 TLI的编程细
节在[Stevens 1990]中可以找到。
系统Ⅴ版本 4(SVR4)也为应用编程提供了一组插口 API,在实现上与本文中列举的有所不
同。在SVR4中的插口基于“流”子系统,在 [Rago 1993]中有所说明。

1.5 程序示例

在本章我们用一个简单的 C程序(图1-2)来介绍一些 BSD网络实现的很多特点。

图1-2 程序示例:发送一个数据报给 UDP日期/时间服务器并读取一个应答

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计5
1. 创建一个数据报插口
19-20 socket函数创建了一个 UDP 插口,并且给进程返回一个保存在变量 sockfd中的描
述符。差错处理函数 e r r _ s y s在[Stevens 1992]的附录 B . 2中给出。它接收任意数量的参数,
并用v s p r i n t f对它们格式化,将系统调用产生的 e r r n o值对应的 U n i x错误信息打印出来,
并中断进程。

我们在不同的地方使用术语插口: ( 1 )为4 . 2 B S D开发的程序用来访问网络协议的


A P I通常叫插口 A P I或者就叫插口接口; (2) socket是插口 A P I中的一个函数的名字;
(3)我们把调用 socket创建的端点叫做一个插口,如评注“创建一个数据报插口”。
但是这里还有一些地方也使用术语插口: (4) socket函数的返回值叫一个插口描
述符或者就叫一个插口; ( 5 )在内核中的伯克利联网协议实现叫插口实现,相比较其
他系统如:系统 V的流实现。(6)一个IP地址和一个端口号的组合叫一个插口, IP地址
和端口号对叫一个插口对。所幸的是引用哪一种术语是很明显的。

2. 将服务器地址放到结构 sockaddr_in中
21-24 在一个互联网插口地址结构中存放日期 /时间服务器的 IP地址(140.252.1.32)和端口号
( 1 3 )。大多数 T C P / I P实现都提供标准的日期 /时间服务器,它的端口号为 13 [Stevens 1994,图
1-9]。我们对服务器主机的选择是随意的—直接选择了提供此服务的本地主机 (图1-17)。
函数i n e t _ a d d r将一个点分十进制表示的 I P地址的 A S C I I字符串转换成网络字节序的 3 2
b i t二进制整数。 ( I n t e r n e t协议族的网络字节序是高字节在后 )。函数 h t o n s把一个主机字节序
的短整数(可能是低字节在后 )转换成网络字节序 (高字节在后 )。在Sparc这种系统中,整数是高
字节在后的格式, h t o n s 典型地是一个什么也不做的宏。但是在低字节在后的 8 0 3 8 6上的
B S D / 3 8 6系统中, h t o n s可能是一个宏或者是一个函数,来完成一个 16 bit整数中的两个字节
的交换。
3. 发送数据报给服务器
25-27 程序调用 s e n d t o发送一个 1 5 0字节的数据报给服务器。因为是运行时栈中分配的未初
始化数组,1 5 0字节的缓存内容是不确定的。但没有关系,因为服务器根本就不看它收到的报
文的内容。当服务器收到一个报文时,就发送一个应答给客户端。应答中包含服务器以可读
格式表示的当前时间和日期。
我们选择的 1 5 0字节的客户数据报是随意的。我们有意选择一个报文长度在 1 0 0 ~ 2 0 8之间
的值,来说明在本章的后面要提到的 m b u f链表的使用。为了避免拥塞,在以太网中,我们希
望长度要小于1472。
4. 读取从服务器返回的数据报
28-32 程序通过调用 r e c v f r o m来读取从服务器发回的数据报。 U n i x服务器典型地发回一
个如下格式的26字节字符串
Sat Dec 11 11:28:05 1993\r\n

\ r是一个 A S C I I回车符, \ n是A S C I I换行符。我们的程序将回车符替换成一个空字节,然后


调用printf输出结果。
在本章和下一章我们在分析函数 s o c k e t、s e n d t o和r e c v f r o m的实现时,要进入这个
例子的一些细节部分。

Linux公社 www.linuxidc.com
www.linuxidc.com
6 计计TCP/IP详解 卷 2:实现

1.6 系统调用和库函数

所有的操作系统都提供服务访问点,程序可以通过它们请求内核中的服务。各种 U n i x都
提供精心定义的有限个内核入口点,即系统调用。我们不能改变系统调用,除非我们有内核
的源代码。 U n i x第7版提供大约 5 0个系统调用, 4 . 4 B S D提供大约 1 3 5个,而 S V R 4大约有 1 2 0
个。
在《U n i x程序员手册》第 2节中有系统调用接口的文档。它是以 C语言定义的,在任何给
定的系统中无需考虑系统调用是如何被调用的。
在各种Unix系统中,每个系统调用在标准 C函数库中都有一个相同名字的函数。一个应用
程序用标准C的调用序列来调用此函数。这个函数再调用相应的内核服务,所使用的技术依赖
于所在系统。例如,函数可能把一个或多个 C参数放到通用寄存器中,并执行几条机器指令产
生一个软件中断进入内核。对于我们来说,可以把系统调用看成 C函数。
在《U n i x程序员手册》的第 3节中为程序员定义了一般用途的函数。虽然这些函数可能调
用一个或多个内核系统调用但没有进入内核的入口点。如函数 p r i n t f可能调用了系统调用 w r i t e
去执行输出,而函数 strcpy(复制一个串 )和atoi(将ASCII码转换成整数)完全不涉及操作系统。
从实现者的角度来看,一个系统调用和库函数有着根本的区别。但在用户看来区别并不
严重。例如,在 4 . 4 B S D 中我们运行图1 - 2 中的程序。程序调用了三个函数: s o c k e t 、
s e n d t o和r e c v f r o m,每个函数最终调用了一个内核中同样名称的函数。在本书的后面我们
可以看到这三个系统调用的 BSD内核实现。
如果我们在 S V R 4中运行这个程序,在那里,用户库中的插口函数调用“流”子系统,那
么三个函数同内核的相互作用是完全不同的。在 SVR4中对s o c k e t的调用最终调用内核 o p e n
系统调用,操作文件 / d e v / u d p并将流模块 s o c k m o d放置到结果流。调用 s e n d t o导致一个
p u t m s g系统调用,而调用 r e c v f r o m导致一个 g e t m s g系统调用。这些 SVR 4的细节在本书
中并不重要,我们仅仅想指出的是:实现可能不同但都提供相同的 API给应用程序。
最后,从一个版本到下一个版本的实现技术可能会改变。例如,在 Net/1中,send和sendto
是分别用内核系统调用实现的。但在 Net/3中,send是一个调用系统调用sendto的库函数:
send(int s, char *msg, int len, int flags)
{
return(sendto(s, msg, len, flags, (struct sockaddr *) NULL, 0));
}

用库函数实现 s e n d的好处是仅调用 s e n d t o,减少了系统调用的个数和内核代码的长度。缺


点是由于多调用了一个函数,增加了进程调用 send的开销。
因为本书是说明 T C P / I P的伯克利实现的,大多数进程调用的函数 (s o c k e t、b i n d、
connect等)是直接由内核系统调用来实现。

1.7 网络实现概述

N e t / 3通过同时对多种通信协议的支持来提供通用的底层基础服务。的确, 4 . 4 B S D支持四
种不同的通信协议族:
1) TCP/IP(互联网协议族),本书的主题。
2) XNS(Xerox网络系统 ),一个与 TCP/IP相似的协议族;在 80年代中期它被广泛应用于连

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 7
接X e r o x设备(如打印机和文件服务器 ),通常使用的是以太网。虽然 N e t / 3仍然发布它的代码,
但今天已很少使用这个协议了,并且很多使用伯克利 T C P / I P代码的厂商把 X N S代码删去了 (这
样他们就不需要支持它了 )。
3) OSI协议[Rose 1990;Piscitello and Chapin 1993]。这些协议是在 80年代作为开放系统
技术的最终目标而设计的,来代替所有其他通信协议。在 9 0年代初它没有什么吸引力,以致
于在真正的网络中很少被使用。它的历史地位有待进一步确定。
4) Unix域协议。从通信协议是用来在不同的系统之间交换信息的意义上来说,它还不算
是一套真正的协议,但它提供了一种进程间通信 (IPC)的形式。
相对于其他 I P C,例如系统 V消息队列,在同一主机上两个进程间的 I P C使用U n i x域协议
的好处是 U n i x域协议用与其他三种协议同样的 A P I (插口)访问。另一方面,消息队列和大多数
其他形式 I P C的A P I与插口和 T L I完全不同。在同一主机上的两进程间的 I P C使用网络 A P I,更
容易将一个客户 /服务器应用程序从一台主机移植到多台主机上。在 U n i x域中提供两个不同的
协议 —一个是可靠的,面向连接的,与 T C P相似的字节流协议;一个是不可靠的,无连接
的,与UDP相似的数据报协议。
虽然U n i x域协议可以作为一种同一主机上两进程间的 I P C,但也可以用 T C P / I P来
完成它们之间的通信。进程间通信并不要求使用在不同的主机上的互联网协议。
内核中的联网代码组织成三层,如图 7. 应用
1- 3所示。在图的右侧我们注明了 O S I参考 进 程 6. 表示
模型[ P i s c i t e l l o和Chapin 1994] 的七层分别 5. 会话

对应到BSD组织的哪里。 系统调用 (socket, bind, connect, etc.)

1) 插口层是一个到下面协议相关层的
插口层
协议无关接口。所有系统调用从协议无关
的插口层开始。例如:在插口层中的 b i n d 协议层 4. 运输

系统调用的协议无关代码包含几十行代码, (TCP/IP, XNS, OSI, Unix) 3. 网络

它们验证的第一个参数是一个有效的插口 接口层
2. 数据链路
(以太网、SLIP、环回等等)
描述符,并且第二个参数是一个进程中的
有效指针。然后调用下层的协议相关代码,
协议相关代码可能包含几百行代码。 媒体 1. 物理
2) 协议层包括我们前面提到的四种协
图1-3 Net/3联网代码的大概组织
议族(TCP/IP,XNS,OSI和Unix域)的实现。
每个协议族可能包含自己的内部结构,在图 1-3中我们没有显示出来。例如,在 Internet协议族
中,IP(网络层)是最低层, TCP和UDP两运输层在 IP的上面。
3) 接口层包括同网络设备通信的设备驱动程序。

1.8 描述符

图1 - 2中,一开始调用 s o c k e t,这要求定义插口类型。 I n t e r n e t协议族(P F _ I N E T)和数据


报插口(SOCK_DGRAM)组合成一个 UDP协议插口。
s o c k e t的返回值是一个描述符,它具有其他 U n i x描述符的所有特性:可以用这个描述
符调用 r e a d和w r i t e;可以用 d u p 复制它,在调用了 f o r k后,父进程和子进程可以共享
Linux公社 www.linuxidc.com
www.linuxidc.com
8 计计TCP/IP详解 卷 2:实现

它;可以调用 f c n t l来改变它的属性,可以调用 c o l s e来关闭它,等等。在我们的例子中可


以看到插口描述符是函数 s e n d t o 和r e c v f r o m的第一个参数。当程序终止时 (通过调用
exit),所有打开的描述符,包括插口描述符都会被内核关闭。
我们现在介绍在进程调用 s o c k e t时被内核创建的数据结构。在后面的几章中会更详细地
描述这些数据结构。
首先从进程的进程表表项开始。在每个进程的生存期内都会有一个对应的进程表表项存
在。
一个描述符是进程的进程表表项中的一个数组的下标。这个数组项指向一个打开文件表
的结构,这个结构又指向一个描述此文件的 i-node或v-node结构。图1-4说明了这种关系。

图1-4 从一个描述符开始的内核数据结构的基本关系

在这个图中,我们还显示了一个涉及插口的描述符,它是本书的焦点。由于进程表表项
是由以下 C语言定义的,我们把记号 p r o c { }放在进程表项的上面。并且在本书所有的图中都
用它来标注这个结构。
struct proc {
...
}

[Stevens 1992,3 . 1 0节]显示了当进程调用 d u p和f o r k时,描述符、文件表结构和 i - n o d e


或v - n o d e之间的关系是如何改变的。这三种数据结构的关系存在于所有版本的 U n i x中,但不
同的实现细节有所变化。在本书中我们感兴趣的是 s o c k e t结构和它所指向的 I n t e r n e t专用数
据结构。但是既然插口系统调用以一个描述符开始,我们就需要理解如何从一个描述符导出
一个socket结构。
如果程序如此执行
a.out

不重定向标准输入 (描述符 0 )、标准输出 (描述符 1 )和标准错误处理 (描述符 2 ),图1 - 5显示


了程序示例中的 N e t / 3数据结构的更多细节。在这个例子中,描述符 0、1和2连接到我们的终
端,并且当 socket被调用时未用描述符的最小编号是 3。
当进程执行了一个系统调用,如 s o c k e t,内核就访问进程表结构。在这个结构中的项
p _ f d 指向进程的 f i l e d e s c 结构。在这个结构中有两个我们现在关心的成员:一个是

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 9
f d _ o f i l e f l a g s ,它是一个字符数组指针 ( 每个描述符有一个描述符标志 ) ;一个是
f d _ o f i l e s,它是一个指向文件表结构的指针数组的指针。描述符标志有 8 bit,只有两位可
为任何描述符设置: c l o s e - o n - e x e c标志和m a p p e d - f r o m - d e v i c e标志。在这里我们显示的所有标
志都是0。
由于U n i x描述符与很多东西有关,除了文件外,还有:插口、管道、目录、设

所有UDP Internet协议控制
块的双向循环链表

图1-5 在程序示例中调用 socket后的内核数据结构

Linux公社 www.linuxidc.com
www.linuxidc.com
10 计计TCP/IP详解 卷 2:实现

备等等,因此,我们有意把本节叫做“描述符”而不是“文件描述符”。但是很多
U n i x文献在谈到描述符时总是加上“文件”这个修饰词,其实没有必要。虽然我们
要说明的是插口描述符,但这个内核数据结构叫 f i l e d e s c { }。我们尽可能地使用
描述符这个未加修饰的术语。
项f d _ o f i l e s指向的数据结构用 * f i l e { } [ ]来表示。它是一个指向 f i l e结构的指针数
组。这个数组及描述符标志数组的下标就是描述符本身: 0、1、2等等,是非负整数。在图 1 -
5中我们可以看到描述符 0、1、2对应的项指向图底部的同一个 f i l e结构(由于这三个描述符都
对应终端设备)。描述符3对应的项指向另外一个 file结构。
结构 f i l e的成员 f _ t y p e指示描述符的类型是 D T Y P E _ S O C K E T和D T Y P E _ V N O D E。v -
n o d e是一个通用机制,允许内核支持不同类型的文件系统—磁盘文件系统、网络文件系统
(如N F S )、C D - R O M文件系统、基于存储器的文件系统等等。在本书中关心的不是 v - n o d e,因
为TCP/IP插口的类型总是 DTYPE_SOCKET。
结构f i l e的成员 f _ d a t a指向一个 s o c k e t结构或者一个 v n o d e结构,根据描述符类型
而定。成员 f _ o p s 指向一个有 5个函数指针的向量。这些函数指针用在 r e a d 、r e a d v、
w r i t e、w r i t e v、i o c t l、s e l e c t和c l o s e系统调用中,这些系统调用需要一个插口描述
符或非插口描述符。这些系统调用每次被调用时都要查看 f_type的值,然后做出相应的跳转,
实现者选择了直接通过 fileops结构的相应项来跳转的方式。
我们用一个等宽字体 ( f o _ r e a d )来醒目地表示一个结构成员的名称,用斜体等宽字体
(s o o _ r e a d)来表示一个结构成员的内容。注意,有时我们用一个箭头指向一个结构的左上角
(如结构 f i l e d e s c),有时用一个箭头指向右上角 (如结构 f i l e和f i l e o p s)。我们用这些方
法来简化图例。
下面我们来查看结构 s o c k e t,当描述符的类型是 D T Y P E _ S O C K E T时,结构 f i l e指向结
构socket。在我们的例子中, socket的类型(数据报插口的类型是 SOCK_DGRAM)保存在成
员s o _ t y p e中。还分配了一个 I n t e r n e t协议控制块 ( P C B ):一个 i n p c b结构。结构 s o c k e t的
成员s o _ p c b指向i n p c b,并且结构 i n p c b的成员i n p _ s o c k e t指向结构 s o c k e t。对于一个
给定插口的操作可能来自两个方向:“上”或“下”,因此需要有指针来互相指向。
1) 当进程执行一个系统调用时,如 s e n d t o,内核从描述符值开始,使用 f d _ o f i l e s索
引到 f i l e结构指针向量,直到描述符所对应的 f i l e结构。结构 f i l e指向s o c k e t结构,结
构socket带有指向结构inpcb的指针。
2) 当一个 U D P数据报到达一个网络接口时,内核搜索所有 U D P协议控制块,寻找一个合
适的,至少要根据目标 U D P端口号,可能还要根据目标 I P地址、源 I P地址和源端口号。一旦
定位所找的 inpcb,内核就能通过 inp_socket指针来找到相应的 socket结构。
成员 i n p _ f a d d r 和 i n p _ l a d d r 包含远地和本地 I P 地址,而成员 i n p _ f p o r t 和
inp_lport包含远地和本地端口号。 IP地址和端口号的组合经常叫做一个插口。
在图1 - 5的左边,我们用名称 u d b来标注另一个 i n p c b结构。这是一个全局结构,它是所
有UDP PCB组成的链表表头。我们可以看到两个成员 i n p _ n e x t和i n p _ p r e v把所有的 U D P
P C B组成了一个双向环型链表。为了简化此图,我们用两条平行的水平箭头来表示两条链,
而不是用箭头指向 PCB的顶角。右边的 inpcb结构的成员 inp_prev指向结构udb,而不是它
的成员 i n p _ p r e v。来自 u d b . i n p _ p r e v和另一个 P C B成员in p _ n e x t的虚线箭头表示这里

Linux公社 www.linuxidc.com
www.linuxidc.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
第1章 概 述计计 11
还有其他PCB在这个双链表上,但我们没有画出。
在本章,我们已看了不少内核数据结构,大多数还要在后续章节中说明。现在要理解的
关键是:
1) 我们的进程调用 s o c k e t,最后分配了最小未用的描述符 (在我们的例子中是 3 )。在后
面,所有针对此 socket的系统调用都要用这个描述符。
2) 以下内核数据结构是一起被分配和链接起来的:一个 D T Y P E _ S O C K E T类型f i l e结构、
一个s o c k e t结构和一个 i n p c b结构。这些结构的很多初始化过程我们并没有说明: f i l e结
构的读写标志 (因为调用 s o c k e t总是返回一个可读或可写的描述符 );默认的输入和输出缓
存大小被设置在 socket结构中,等等。
3) 我们显示了标准输入、输出和标准错误处理的非 socket描述符的目的是为了说明所有
描述符最后都对应一个 file结构,虽然 socket描述符和其他描述符之间有所不同。

1.9 mbuf与输出处理

在伯克利联网代码设计中的一个基本概念就是存储器缓存,称作一个 m b u f,在整个联网
代码中用于存储各种信息。通过我们的简单例子 (图1-2)分析一些mbuf的典型用法。在第 2章中
我们会更详细地说明 mbuf。

1.9.1 包含插口地址结构的 mbuf

在s e n d t o调用中,第 5个参数指向一个 I n t e r n e t插口地址结构 (叫s e r v ),第6个参数指示它


的长度(后面我们将要看到是 16个字节)。插口层为这个系统调用做的第一件事就是验证这些参
数是有效的 (即这个指针指向进程地址空间的一段存储器 ),并且将插口地址结构复制到一个
mbuf中。图1-6所示的是这个所得到的 mbuf。

20字节

带有目标IP地址和端口号的
128字节 16字节的sockaddr_in{}

图1-6 mbuf中针对sendto的目的地址

m b u f的前2 0个字节是首部,它包含关于这个 m b u f的一些信息。这 2 0个字节的首部包括四


个4字节字段和两个 2字节字段。 mbuf的总长为128个字节。
稍后我们会看到, m b u f可以用成员 m _ n e x t和m _ n e x t p k t链接起来。在这个例子中都是

Linux公社 www.linuxidc.com
www.linuxidc.com
12 计计TCP/IP详解 卷 2:实现

空指针,它是一个独立的 mbuf。
成员m _ d a t a指向m b u f中的数据,成员 m _ l e n指示它的长度。对于这个例子, m _ d a t a
指向mbuf中数据的第一个字节 (紧接着mbuf首部)。mbuf后面的92个字节(108-16)没有用(图1-6
的阴影部分 )。
成员m _ t y p e指示包含在 m b u f中数据的类型,在本例中是 M T _ S O N A M E(插口名称 )。首部
的最后一个成员 m_flags,在本例中是零。

1.9.2 包含数据的 mbuf

下面继续讨论我们的例子,插口层将 sendto调用中指定的数据缓存中的数据复制到一个或
多个m b u f中。s e n d t o的第二个参数指示了数据缓存 ( b u ff )的开始位置,第三个参数是它的大
小(150字节)。图1-7显示了150字节的数据是如何存储在两个 mbuf 中的。

指向链中下一个mbuf的指针

mbuf分组首部
50字节的数据

100字节的数据

图1-7 用两个mbuf来存储150字节的数据

这种安排叫做 m b u f链表。在每个 m b u f中的成员 m _ n e x t把链表中所有的 m b u f都链接在一


起。
我们看到的另一个变化是链表中第一个 mbuf的mbuf首部的另外两个成员:m_pkthdr.len
和m _ p k t h d r . r c v i f。这两个成员组成了分组首部并且只用在链表的第一个 m b u f中。成员
m _ f l a g s的值是M _ P K T H D R,指示这个mbuf包含一个分组首部。分组首部结构的成员 len包含
了整个m b u f链表的总长度 (在本例中是1 5 0 ),下一个成员 r c v i f在后面我们会看到,它包含了
一个指向接收分组的接收接口结构的指针。
因为m b u f总是1 2 8个字节,在链表的第一个 m b u f中提供了 1 0 0字节的数据存储能力,而后
面所有的 m b u f有1 0 8字节的存储空间。在本例中的两个 m b u f需要存储 1 5 0字节的数据。我们稍
后会看到当数据超过 208字节时,就需要 3个或更多的 mbuf。有一种不同的技术叫“簇”,一种
大缓存,典型的有 1024或2048字节。
在链表的第一个 m b u f中维护一个带有总长度的分组首部的原因是,当需要总长度时可以
避免查看所有mbuf中的m_len来求和。

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 13
1.9.3 添加IP和UDP首部

在插口层将目标插口地址结构复制到一个 m b u f中,并把数据复制到 m b u f链中后,与此插


口描述符 (一个 U D P描述符 )对应的协议层被调用。明确地说, U D P输出例程被调用,指向
mbuf的指针被作为一个参数传递。这个例程要在这 150字节数据的前面添加一个 IP首部和一个
UDP首部,然后将这些 mbuf传递给IP输出例程。
在图1-7中的mbuf链表中添加这些数据的方法是分配另外一个 mbuf,把它放在链首,并将
分组首部从带有 100字节数据的 mbuf复制到这个 mbuf。在图1-8中显示了这三个 mbuf。
链中的
链中的下一个mbuf 下一个mbuf

mbuf
分组首部
50字节的数据

100字节的数据

28字节用于IP首
部和UDP首部

图1-8 在图1-7中的mbuf链表中添加另一个带有 IP和UDP首部的mbuf

I P首部和 U D P首部被放置在新 m b u f的最后,这个新 m b u f就成了整个链表的首部。如果


需要,它允许任何其他低层协议 (例如接口层 )在I P首部前添加自己的首部,而不需要再复制
I P和U D P首部。在第一个 m b u f中的m _ d a t a指针指向这两个首部的起始位置, m _ l e n的值是
2 8 。在分组首部和 I P 首部之间有 7 2 字节的未用空间留给以后的首部,通过适当地修改
m _ d a t a指针和 m _ l e n添加在 I P首部的前面。稍后我们会看见以太网首部就是用这种方法建
立的。
注意,分组首部已从带有 1 0 0字节数据的 m b u f中移到新 m b u f中去了。分组首部必须放在
m b u f链表的第一个 m b u f中。在移动分组首部的同时,在第一个 m b u f设置M _ P K T H D R标志并且
在第二个 m b u f中清除此标志。在第二个 m b u f中分组首部占用的空间现在未用。最后,在此分
组首部中的长度成员由于增加了 28 字节而变成了178。
然后U D P输出例程填写 U D P首部和 I P首部中它们所能填写的部分。例如, I P首部中的目
标地址可以被设置,但 IP检验和要留给IP输出例程来计算和存放。
UDP检验和计算后存储在 UDP首部中。注意,这要求遍历存储在 mbuf链表中的所有150字
节的数据。这样,内核要对这 1 5 0字节的用户数据做两次遍历:一次是把用户缓存中的数据复
制到内核中的 mbuf中,而现在是计算 UDP检验和。对整个数据的额外遍历会降低协议的性能,
在后续章节中我们会介绍另一种可选的实现技术,它可以避免不必要的遍历。
接着,UDP输出例程调用IP输出例程,并把此 mbuf链表的指针传递给 IP输出例程。

Linux公社 www.linuxidc.com
www.linuxidc.com
14 计计TCP/IP详解 卷 2:实现

1.9.4 IP输出

IP输出例程要填写 IP首部中剩余的字段,包括 IP检验和;确定数据报应发到哪个输出接口


(这是IP路由功能 );必要时,对IP报文分片;以及调用接口输出函数。
假设输出接口是一个以太网接口,再次把此 m b u f链表的指针作为一个参数,调用一个通
用的以太网输出函数。

1.9.5 以太网输出

以太网输出函数的第一个功能就是把 3 2位I P地址转换成相应的 4 8位以太网地址。在使用


ARP(地址解析协议 )时会使用这个功能,并且会在以太网上发送一个 ARP请求并等待一个 ARP
应答。此时,要输出的 mbuf链表已得到,并等待应答。
然后以太网输出例程把一个 1 4字节的以太网首部添加到链表的第一个 m b u f中,紧接在 I P
首部的前面(图1-8)。以太网首部包括 6字节以太网目标地址、 6字节以太网源地址和 2字节以太
网帧类型。
之后此 m b u f链表被加到此接口的输出队列队尾。如果接口不忙,接口的“开始输出”例
程立即被调用。若接口忙,在它处理完输出队列中的其他缓存后,它的输出例程会处理队列
中的这个新 mbuf。
当接口处理它输出队列中的一个 m b u f时,它把数据复制到它的传输缓存中,并且开始输
出。在我们的例子中, 1 9 2字节被复制到传输缓存中: 1 4字节以太网首部、 2 0字节I P首部、 8
字节UDP首部及150字节用户数据。这是内核第三次遍历这些数据。一旦数据从 mbuf链表被复
制到设备传输缓存, m b u f链表就被以太网设备驱动程序释放。这三个 m b u f被放回到内核的自
由缓存池中。

1.9.6 UDP输出小结

我们在图 1 - 9中给出了一个进程调用 s e n d t o传输一个 U D P数据报时的大致处理过程。在图


中我们说明的处理过程与三层内核代码 (图1-3)的关系也显示出来了。

进程

系统调用

把目标插口地址结构和用户数
插口层
据(150字节)复制到mbuf链中

UDP输出
协议层
IP输出

以太网输出(ARP)
接口层
设备驱动器输出

以太网帧

图1-9 三层处理一个简单 UDP输出的执行过程

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 15
函数调用控制从插口层到 U D P输出例程,到 I P输出例程,然后到以太网输出例程。每个
函数调用传递一个指向要输出的 m b u f的指针。在最低层,设备驱动程序层, m b u f链表被放置
到设备输出队列并启动设备。函数调用按调用的相反顺序返回,最后系统调用返回给进程。
注意,直到 U D P数据报到达设备驱动程序前, U D P数据没有排队。高层仅仅添加它们的协议
首部并把mbuf传递给下一层。
这时,在我们的程序示例中调用 recvfrom去读取服务器的应答。因为该插口的输入队列是
空的(假设应答还没有到达 ),进程就进入睡眠状态。

1.10 输入处理

输入处理与刚讲过的输出处理不同,因为输入是异步的。就是说,它是通过一个接收完
成中断驱动以太网设备驱动程序来接收一个输入分组,而不是通过进程的系统调用。内核处
理这个设备中断,并调度设备驱动程序进入运行状态。

1.10.1 以太网输入

以太网设备驱动程序处理这个中断,假定它
表示一个正常的接收已完成,数据从设备读到
一个 m b u f链表中。在我们的例子中,接收了 5 4
字节的数据并复制到一个 m b u f中: 2 0字节 I P首
部、8字节UDP首部及26字节数据 (服务器的时间
与日期)。图1-10所示的是这个mbuf的格式。
指向接口结构的指针
这个m b u f是一个分组首部 (m _ f l a g s被设置
成M_PKTHDR),它是一个数据记录的第一个mbuf。 分配了16字节(但未用)

分组首部的成员 l e n包含数据的总长度,成员 20字节IP首部


8字节UDP首部
rcvif包含一个指针,它指向接收数据的接口的 26字节的数据
接口结构 (第3章)。我们可以看到成员 r c v i f用
于接收分组而不是输出分组(图1-7和图1-8)。
m b u f的前 1 6字节数据空间被分配给一个接
图1-10 用一个mbuf存储输入的以太网数据
口层首部,但没有使用。数据就存储在这个
mbuf中,54字节的数据存储在剩余的 84字节的空间中。
设备驱动程序把 m b u f传给一个通用以太网输入例程,它通过以太网帧中的类型字段来确
定哪个协议层来接收此分组。在这个例子中,类型字段标识一个 I P数据报,从而 m b u f被加入
到I P输入队列中。另外,会产生一个软中断来执行 I P输入例程。这样,这个设备中断处理就
完成了。

1.10.2 IP输入

I P输入是异步的,并且通过一个软中断来执行。当接口层在系统的一个接口上收到一个
I P数据报时,它就设置这个软中断。当 I P输入例程执行它时,循环处理在它的输入队列中的
每一个IP数据报,并在整个队列被处理完后返回。
IP输入例程处理每个接收到的 IP数据报。它验证 IP首部检验和,处理 IP选项,验证数据报

Linux公社 www.linuxidc.com
www.linuxidc.com
16 计计TCP/IP详解 卷 2:实现

被传递到正确的主机 (通过比较数据报的目标 I P地址与主机I P地址),并当系统被配置为一个路


由器,且数据报被表注为其他的 I P地址时,转发此数据报。如果 I P数据报到达它的最终目标,
调用I P首部中标识的协议的输入例程: I C M P,I G M P,T C P或U D P。在我们的例子中,调用
UDP输入例程去处理 UDP数据报。

1.10.3 UDP输入

UDP输入例程验证UDP首部中的各字段 (长度与可选的检验和 ),然后确定是否一个进程应


该接收此数据报。在第 2 3章我们要详细讨论这个检查是如何进行的。一个进程可以接收到一
指定U D P端口的所有数据报,或让内核根据源与目标 I P地址及源与目标端口号来限制数据报
的接收。
在我们的例子中, U D P输入例程从一个全局变量 u d b(图1 - 5 )开始,查看所有 U D P协议控
制块链表,寻找一个本地端口号 ( i n p _ l p o r t)与接收的 U D P数据报的目标端口号匹配的协议
控制块。这个 P C B是由我们调用 s o c k e t创建的,它的成员 i n p _ s o c k e t指向相应插口结构,
并允许接收的数据在此插口排队。
在程序示例中,我们从未为应用程序指定本地端口号。在习题 23.3中,我们会看
到在写第一个 U D P程序时创建一个插口而不绑定一个本地端口号会导致内核自动地
给此插口分配一个本地端口号 ( 称为短期端口 ) 。这就是为什么插口的 P C B 成员
inp_lport不是一个空值的原因。
因为这个 U D P数据报要传递给我们的进程,发送方的 I P地址和 U D P端口号被放置到一个
m b u f中,这个 m b u f和数据 (在我们的例子中是 2 6字节)被追加到此插口的接收队列中。图 1 - 11
所示的是被追加到这个插口的接收队列中的这两个 mbuf。

指向链中下一个mbuf的指针
插口的接
收队列

16字节sockaddr_in{}
带有发送方IP地址和端口号 指向接口结构的指针

44字节(未用)

26字节(未用)

图1-11 发送方地址和数据

比较这个链表中的第二个 m b u f (M T _ D A T A 类型 ) 与图 1 - 1 0中的 m b u f,成员 m _ l e n 和


m _ p k t h d r . l e n都减小了 2 8字节( 2 0字节的I P首部和8字节U D P首部),并且指针 m _ d a t a也减
小了28字节。这有效地将 IP和UDP首部删去,只保留了 26字节数据追加到插口接收队列。

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 17
在链表的第一个 m b u f中包括一个 1 6字节I n t e r n e t插口地址结构,它带有发送方 I P 地址和
U D P端口号。它的类型是 M T _ S O N A M E,与图 1 - 6中的m b u f类似。这个 m b u f是插口层创建的,
将这些信息返回给通过调用系统调用 r e c v f o r m或r e c v m s g的调用进程。即使在这个链表的
第二个 m b u f中有空间 ( 1 6字节)存储这个插口地址结构,它也必须存放到它自己的 m b u f中,因
为它们的类型不同 (一个是MT_SONAME,一个是MT_DATA)。
然后接收进程被唤醒。如果进程处于睡眠状态等待数据的到达 (我们例子中的情况 ),进程
被标志为可运行状态等待内核的调度。也可以通过 s e l e c t系统调用或 S I G I O信号来通知进
程数据的到达。

1.10.4 进程输入

我们的进程调用 r e c v f r o m时被阻塞,在内核中处于睡眠状态,现在进程被唤醒。 U D P
层追加到插口接收队列中的 2 6字节的数据 (接收的数据报 )被内核从 m b u f复制到我们程序的缓
存中。
注意,我们的程序把 r e c v f o r m的第5,第6个参数设置为空指针,告诉系统在接收过程
中不关心发送方的 I P地址和 U D P端口号。这使得系统调用 r e c v f r o m时,略过链表中的第一
个mbuf(图1-11),仅返回第二个 mbuf中的26字节的数据。然后内核的 r e c v f r o m代码释放图 1-
11中的两个mbuf,并把它们放回到自由 mbuf池中。

1.11 网络实现概述 (续)

图1 - 1 2总结了在各层间为网络输入输出而进行的通信。图 1 - 1 2是对图 1 - 3进行了重画,它


只考虑Internet协议,并且强调层间的通信。符号 splnet与splimp在下一节讨论。
我们使用复数术语插口队列 (socket queues)和接口队列 (interface queues),因为每个插口

进程

系统调用

插口层

函数调用 插口队列

协议层 软件中断@splnet
(TCP, UDP, IP, ICMP, IGMP) (由接口层产生)

函数调用 协议队列
接口队列 (IP输入队列)
来启动输出

接口层 硬件中断@splimp
(由网络设备产生)

图1-12 网络输入输出的层间通信

Linux公社 www.linuxidc.com
www.linuxidc.com
18 计计TCP/IP详解 卷 2:实现

和每个接口 (以太网、环回、 S L I P、P P P等)都有一个队列,但我们使用单数术语协议队列


(protocol queue),因为只有一个 I P输入队列。如果考虑其他协议层,我们就会有一个队列用
于XNS协议,一个队列用于 OSI协议。

1.12 中断级别与并发

我们在1.10节看到网络代码处理输入分组用的是异步和中断驱动的方式。首先,一个设备
中断引发接口层代码执行,然后它产生一个软中断引发协议层代码执行。当内核完成这些级
别的中断后,执行插口代码。
在这里给每个硬件和软件中断分配一个优先级。图 1 - 1 3所示的是 8个优先级别的顺序,从
最低级别(不阻塞中断 )到最高级别 (阻塞所有中断)。
函 数 说 明

spl0 正常操作方式,不阻塞中断 (最低优先级 )


Splsoftclock 低优先级时钟处理
splnet 网络协议处理
spltty 终端输入输出
splbio 磁盘与磁带输入输出
splimp 网络设备输入输出
splclock 高优先级时钟处理
splhigh 阻塞所有中断 (最高优先级 )

splx(s) (见正文)

图1-13 阻塞所选中断的内核函数

[Leffler et al. 1989]的表4-5显示了用于 VAX实现的优先级别。 386的Net/3的实现


使用图1 - 1 3所示的8个函数,但 s p l s o f t c l o c k与s p l n e t在同一级别, s p l c l o c k
与splhigh也在同一级别。
用于网络接口级的名称 i m p来自于缩写I M P (接口报文处理器 ),它是在 ARPANET
中使用的路由器的最初类型。
不同优先级的顺序意味着高优先级中断可以抢占一个低优先级中断。看图 1 - 1 4所示的事
被抢占

被抢占

插口 插口

协议(IP输入) 协议(IP输入)

SLIP设备驱动程序
以太网设备
驱动程序

step 1 2 3 4 5 6 7

图1-14 优先级示例与内核处理

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 19
件顺序。
1) 当插口层以级别 s p l 0执行时,一个以太网设备驱动程序中断发生,使接口层以级别
splimp执行。这个中断抢占了插口层代码的执行。这就是异步执行接口输入例程。
2) 当以太网设备驱动程序在运行时,它把一个接收的分组放置到 I P输出队列中并调度一
个s p l n e t级别的软中断。软中断不会立即有效,因为内核正在一个更高的优先级 (s p l i m p)
上运行。
3) 当以太网设备驱动程序完成后,协议层以级别splnet执行。这就是异步执行IP输入例程。
4) 一个终端设备中断发生 (完成一个 S L I P分组),它立即被处理,抢占协议层,因为终端
输入/输出(spltty)优先级比图 1-13中的协议层 (splnet)更高。
5) SLIP驱动程序把接收的分组放到 IP输入队列中并为协议层调度另一个软中断。
6) 当SLIP驱动程序结束时,被抢占的协议层继续以级别 splnet执行,处理完从以太网设备
驱动程序收到的分组后,处理从 SLIP驱动程序接收的分组。仅当没有其他输入分组要处理时,
它会把控制权交还给被它抢占的进程 (在本例中是插口层 )。
7) 插口层从它被中断的地方继续执行。
对于这些不同优先级,一个要关心的问题就是如何处理那些在不同级别的进程间共享的
数据结构。在图 1 - 2中显示了三种在不同优先级进程间共享的数据结构—插口队列、接口队
列和协议队列。例如,当 I P输入例程正在从它的输入队列中取出一个接收的分组时,一个设
备中断发生,抢占了协议层,并且那个设备驱动程序可能添加另一个分组到 I P输入队列。这
些共享的数据结构 (本例中的IP输入队列,它共享于协议层和接口层 ),如果不协调对它们的访
问,可能会破坏数据的完整性。
Net/3的代码经常调用函数 s p l i m p和s p l n e t。这两个调用总是与 s p l x成对出现,s p l x
使处理器返回到原来的优先级。例如下面这段代码,被协议层 I P输入函数执行,去检查是否
有其他分组在它的输入队列中等待处理:
struct mbuf *m;
int s;
s = splimp ();
IF_DEQUEUE (&ipintrq, m);
splx(s);
if (m == 0)
return;

调用s p l i m p把CPU的优先级升高到网络设备驱动程序级,防止任何网络设备驱动程序中断发
生。原来的优先级作为函数的返回值存储到变量 s中。然后执行宏 I F _ D E Q U E U E把I P输入队列
(i p i n t r q)头部的第二个分组删去,并把指向此 m b u f链表的指针放到变量 m中。最后,通过
调用带有参数 s (其保存着前面调用 s p l i m p 的返回值 )的s p l x ,C P U的优先级恢复到调用
splimp前的级别。
由于在调用 s p l i m p和s p l x之间所有的网络设备驱动程序的中断被禁止,在这两个调用
间的代码应尽可能的少。如果中断被禁止过长的时间,其他设备会被忽略,数据会被丢失。
因此,对变量m的测试(看是否有其他分组要处理 )被放在调用 splx之后而不是之前。
当以太网输出例程把一个要输出的分组放到一个接口队列,并测试接口当前是否忙时,
若接口不忙则启动接口,这时例程需要调用这些 spl调用。

Linux公社 www.linuxidc.com
www.linuxidc.com
20 计计TCP/IP详解 卷 2:实现

在这个例子中,设备中断被禁止的原因是防止在协议层正在往队列添加分组时,设备驱
动程序从它的发送队列中取走下一个分组。设备发送队列是一个在协议层和接口层共享的数
据结构。
在整个源代码中到处都会看到 spl函数。

1.13 源代码组织

图1-15所示的是Net/3网络源代码的组织,假设它位于目录 /usr/src/sys。
Intel 80x86专用

通用内核

通用联网

HDLC, X.25

TCP/IP协议

OSI协议

XNS协议

NFS

内核头文件

文件系统

虚拟存储器

图1-15 Net/3源代码组织

本书的重点在目录 n e t i n e t,它包含所有 TCP/IP源代码。在目录 k e r n和n e t中我们也可


找到一些文件。前者是协议无关的插口代码,而后者是一些通用联网函数,用于 TCP/IP例程,
如路由代码。

Linux公社 www.linuxidc.com
www.linuxidc.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
第1章 概 述计计21
包含在每个目录中的文件简要地列于下面:
• i 3 8 6:因特8 0 x 8 6专用目录。例如,目录 i 3 8 6 / i s a包含专用于 I S A总线的设备驱动程
序。目录 i386/stand包含单机引导程序代码。
• k e r n:通用的内核文件,不属于其他目录。例如,处理系统调用 f o r k和e x e c的内核
文件在这个目录。在这个目录中,我们只考察少数几个文件—用于插口系统调用的文
件(插口层在图 1-3)。
• n e t:通用联网文件,例如,通用联网接口函数, B P F ( B S D分组过滤器 )代码、 S L I P驱
动程序和路由代码。在这个目录中我们考察一些文件。
• netccitt:OSI协议接口代码,包括 HDLC(高级数据链路控制 )和X.25驱动程序。
• n e t i n e t:I n t e r n e t协议代码: I P,I C M P,I G M P,T C P和U D P。本书的重点集中在这
个目录中的文件。
• netiso:OSI协议。
• netns:施乐(Xerox)XNS协议。
• nfs:SUN公司的网络文件系统代码。
• s y s:系统头文件。在这个目录中我们考察几个头文件。这个目录中的文件还出现在目
录/usr/include/sys中。
• ufs:Unix文件系统的代码,有时叫伯克利快速文件系统。它是标准磁盘文件系统。
• vm:虚拟存储器系统代码。
图1 - 1 6所示的是源代码组织的另一种表现形式,它映射到我们的三个内核层。忽略
netimp和nfs这样的目录,在本书中我们不关心它们。

插口层

4000行C代码

net/ netinet/ netns/ netiso/ kern/


协议层
选路 (TCP/IP) (XNS) (OSI) (Unix域)

2 100 13 000 6 000 26 000 750

net/ net/if_sl* net/if_loop* net/bpf* 以太网设备


接口层
(以太网,ARP) (SLIP) (boopback) (BPF) 驱动程序

500 1 750 250 2 000 1 000每驱动程序

图1-16 映射到三个内核层的 Net/3源代码组织

在每个表格框底下的数字是对应功能的 C代码的近似行数,包括源文件中的所有注释。
我们不考察图中所有的源代码。显示目录 n e t n s与n e t i s o是为了与 I n t e r n e t协议比较。
我们仅考虑有阴影的表格框。

1.14 测试网络

图1 - 1 7所示的测试网络用于本书中所有的例子。除了在图顶部的主机 v a n g o g h,所有的

Linux公社 www.linuxidc.com
www.linuxidc.com
22 计计TCP/IP详解 卷 2:实现

I P地址属于 B类网络地址 1 4 0 . 2 5 2,并且所有主机名属于域 . t u c . n o a o . e d u (n o a o代表“国


家光学天文台”,t u c代表Tu c s o n )。例如,在右下角的系统的主机全名是 s v r 4 . t u c . n o a o .
edu,IP地址是140.252.13.34。在每个框图顶上的记号是运行在此系统上操作系统的名称。
在图顶的主机的全名是 v a n g o g h . c s . b e r k e l e y . e d u,其他主机通过 I n t e r n e t可以连接
到它。
这个图与卷 1中的测试网络几乎一样,有一些操作系统升级了,在 s u n与n e t b之间的拨号
链路现在用 P P P取代了 S L I P。另外,我们用 N e t / 3网络代码代替了 BSD/386 V1.1提供的 N e t / 2
网络代码。

路由器

以太网
二进制遥测系统

拨号

以太网

图1-17 用于本书中所有例子的测试网络

1.15 小结

本章是对 N e t / 3网络代码的概述。通过一个简单的程序示例 (图1 - 2 )—发送一个 U D P数据


报给一个日期时间服务器并接收应答,我们分析了通过内核进行输入输出的过程。 m b u f中保
存要输出的信息和接收的 IP数据报。下一章我们要查看 mbuf的更多细节。
当进程执行 s e n d t o系统调用时,产生 UDP输出,而 IP输入是异步的。当一个设备驱动程

Linux公社 www.linuxidc.com
www.linuxidc.com
第1章 概 述计计 23
序接收了一个 I P数据报,数据报被放到 I P输入队列中并且产生一个软中断使 I P输入函数执行。
我们考察了在内核中用于联网代码的不同中断级别。由于很多联网数据结构被不同的层所共
享,而这些层在不同的中断级别上执行,因此当访问或修改这些共享结构时要特别小心。几
乎所有我们要查看的函数中都会遇到 spl函数。
本章结束时我们查看了 Net/3源代码的整个组织结构,及本书关注的代码。

习题

1.1 输入程序示例 (图1 - 2 )并在你的系统上运行。如果你的系统有系统调用跟踪能力,如


trace (SunOS 4.x)、truss (SVR4)或ktrace (4.4BSD),用它检测本例中调用的
系统调用。
1.2 在1.12节调用I F _ D E Q U E U E的例子中,我们注意到调用 s p l i m p来防止网络设备驱动
程序的中断。当以太网驱动程序以这个级别执行时, SLIP驱动程序会发生什么?

Linux公社 www.linuxidc.com
www.linuxidc.com
下载

第2章 mbuf:存储器缓存
2.1 引言

网络协议对内核的存储器管理能力提出了很多要求。这些要求包括能方便地操作可变长
缓存,能在缓存头部和尾部添加数据 (如低层封装来自高层的数据 ),能从缓存中移去数据 (如,
当数据分组向上经过协议栈时要去掉首部 ),并能尽量减少为这些操作所做的数据复制。内核
中的存储器管理调度直接关系到联网协议的性能。
在第1章我们介绍了普遍应用于 Net/3内核中的存储器缓存: mbuf,它是“memory buffer”
的缩写。在本章,我们要查看 m b u f和内核中用于操作它们的函数的更多的细节,在本书中几
乎每一页我们都会遇到 mbuf。要理解本书的其他部分必须先要理解 mbuf。
m b u f的主要用途是保存在进程和网络接口间互相传递的用户数据。但 m b u f也用于保存其
他各种数据:源与目标地址、插口选项等等。
图2 - 1显示了我们要遇到的四种不同类型的 m b u f,它们依据在成员 m _ f l a g s中填写的不
同标志M_PKTHDR和M_EXT而不同。图 2-1中四个mbuf的区别从左到右罗列如下:
1) 如果m_flags等于0,mbuf只包含数据。在mbuf中有108字节的数据空间 (m_dat数组)。
指针m _ d a t a指向这1 0 8字节缓存中的某个位置。我们所示的 m _ d a t a指向缓存的起始,
但它能指向缓存中的任意位置。成员 m _ l e n指示了从 m _ d a t a开始的数据的字节数。
图1-6是这类mbuf的一个例子。
在图2 - 1中,结构 m _ h d r中有六个成员,它的总长是 2 0字节。当我们查看此结构的 C语
言定义时,会看见前四个成员每个占用 4字节而后两个成员每个占用 2字节。在图 2 - 1中
我们没有区分4字节成员和 2字节成员。
2) 第二类m b u f的m _ f l a g s值是M _ P K T H D R,它指示这是一个分组首部,描述一个分组数
据的第一个 m b u f。数据仍然保存在这个 m b u f中,但是由于分组首部占用了 8字节,只
有1 0 0字节的数据可存储在这个 m b u f中(在m _ p k t d a t数组中)。图1 - 1 0是这种m b u f的一
个例子。
成员m _ p k t h d r . l e n的值是这个分组的 m b u f链表中所有数据的总长度:即所有通过
m _ n e x t 指针链接的 m b u f的m _ l e n 值的和,如图 1 - 8所示。输出分组没有使用成员
m _ p k t h d r . r c v i f,但对于接收的分组,它包含一个指向接收接口 i f n e t结构(图3 -
6)的指针。
3) 下一种m b u f不包含分组首部 (没有设置 K _ P K T H D R),但包含超过 2 0 8字节的数据,这时
用到一个叫“簇”的外部缓存 (设置 M _ E X T)。在此 m b u f中仍然为分组首部结构分配了
空间,但没有用—在图2 - 1中,我们用阴影显示出来。 N e t / 3分配一个大小为 1 0 2 4或
2 0 4 8字节的簇,而不是使用多个 m b u f来保存数据 (第一个带有 1 0 0字节数据,其余的每
个带有108字节数据)。在这个mbuf中,指针m_data指向这个簇中的某个位置。
N e t / 3版本支持七种不同的结构。定义了四种 1 0 2 4字节的簇 (惯例值),三种 2 0 4 8字节的

Linux公社 www.linuxidc.com
www.linuxidc.com
第 2章 mbuf:存储器缓存计计 25
簇。传统上用 1 0 2 4字节的原因是为了节约存储器:如果簇的大小是 2 0 4 8,对于以太网
分组 (最大 1 5 0 0字节 ),每个簇大约有四分之一没有用。在 2 7 . 5节中我们会看到 N e t / 3
T C P发送的每个 T C P报文段从来不超过一簇大小,因为当簇的大小为 1 0 2 4时,每个
1 5 0 0字节的以太网帧几乎三分之一未用。但是 [Mogul 1993,图1 5 - 1 5 ]显示了当在以太
网中发送最大帧而不是 1 0 2 4字节的帧时能明显提高以太网的性能。这就是一种性能 /存
储器互换。老的系统使用 1 0 2 4字节簇来节约存储器,而拥有廉价存储器的新系统用
2048字节的簇来提高性能。在本书中我们假定一簇的大小是 2048字节。

m_hdr{}
(20字节)

pkthdr{}
(8字节)

m_ext{}
(12字节)

108字节的 100字节的
缓存: 缓存:
m_dat m.pktdat

2048字节的簇
2048字节的簇 (外部缓存)
(外部缓存)

图2-1 根据不同 m_flags 值的四种不同类型的 mbuf

不幸的是,我们所说的“簇 ( c l u s t e r )”用过不同的名字。常量 M C L B Y T E S是这些


缓存 ( 1 0 2 4或2 0 4 8 )的大小,操作这些缓存的宏的名字是 M C L G E T、M C L A L L O C 和
M C L F R E E。这就是为什么称它们为“簇”的原因。但我们还看到 m b u f的标志是
M_EXT,它代表“外部的”缓存。最后, [Leffler et al. 1989]称它们为映射页 (mapped
p a g e )。这后一种称法来源于它们的实现,在 2 . 9节我们会看到当要求一个副本时,这
些簇是可以共享的。
我们可能会希望这种类型的 mbuf的m _ l e n的最小值是209而不是我们在图中所示

Linux公社 www.linuxidc.com
bbs.theithome.com
26 计计TCP/IP详解 卷 2:实现

的2 0 8。这是指, 2 0 8字节数据的记录是可以存放在两个 m b u f中的,第一个 m b u f存放


100字节,第二个 mbuf存放108字节。但在源代码中有一个差错:若超过或等于 208就
分配一个簇。
4) 最后一类 m b u f包含一个分组首部,并包含超过 2 0 8 字节的数据。同时设置了标志
M_PKTHDR和M_EXT。
对于图2-1,我们还有另外几点需要说明:
• m b u f结构的大小总是 1 2 8字节。这意味着图 2 - 1右边两个 m b u f在结构 m _ e x t后面的未用
空间为88字节(128-20-8-12)。
• 既然有些协议(例如UDP)允许零长记录,当然就可以有 m_len为0的数据缓存。
• 在每个 m b u f中的成员 m _ d a t a指向相应缓存的开始 ( m b u f缓存本身或一个簇 )。这个指针
能指向相应缓存的任意位置,不一定是起始。

队头
队尾
链中的下一个
链中的下一个mbuf mbuf

mbuf
分组首部
50字节的数据

100字节的数据

以太网首部,IP
首部,TCP首部

链中的下一个mbuf

2048字节的簇

1460字节的数据
mbuf
分组首部

以太网首部,IP
首部,TCP首部

图2-2 在一个队列中的两个分组:第一个带有 192字节数据,第二个带有 1514字节数据

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 27
• 带有簇的mbuf总是包含缓存的起始地址(m_ext.ext_buf)和它的大小(m_ext.ext_size)。
我们在本书采用的大小为 2048。成员 m _ d a t a和m _ e x t . e x t _ b u f值是不同的(如我们所
示),除非 m _ d a t a 也指向缓存的第一个字节。结构 m _ e x t的第三个成员 e x t _ f r e e,
Net/3当前未用。
• 指针m_next把mbuf链接在一起,把一个分组 (记录)形成一条 mbuf链表,如图 1-8所示。
• 指针 m _ n e x t p k t把多个分组 (记录)链接成一个 m b u f链表队列。在队列中的每个分组可
以是一个单独的 m b u f,也可以是一个 m b u f链表。每个分组的第一个 m b u f包含一个分组
首部。如果多个 m b u f定义一个分组,只有第一个 m b u f的成员 m _ n e x t p k t被使用—链
表中其他mbuf的成员m_nextpkt全是空指针。
图2 - 2所示的是在一个队列中的两个分组的例子。它是图 1 - 8的一个修改版。我们已经把
UDP数据报放到接口输出队列中 (显示出14字节的以太网首部已经添加到链表中第一个 mbuf的
I P首部前面 ),并且第二个分组已经被添加到队列中: T C P段包含 1 4 6 0字节的用户数据。 T C P
数据包含在一个簇中,并且有一个 m b u f包含了它的以太网、 I P与T C P首部。通过这个簇,我
们可以看到指向簇的数据指针 (m _ d a t a)不需要指向簇的起始位置。我们所示的队列有一个头
指针和一个尾指针。这就是 N e t / 3处理接口输出队列的方法。我们给有 M _ E X T标志的m b u f还添
加了一个 m_ext结构,并且用阴影表示这个 mbuf中未用的pkthdr结构。

带有U D P数据报分组首部的第一个 m b u f的类型是 M T _ D A T A,但带有 T C P报文段


分组首部的第一个 m b u f的类型是 M T _ H E A D E R。这是由于U D P和T C P采用了不同的方
式往数据中添加首部造成的,但没有什么不同。这两种类型的 m b u f本质上一样。链
表中第一个 mbuf的m_flags的值M_PKTHDR指示了它是一个分组首部。
仔细的读者可能会注意到我们显示一个 mbuf的图(Net/3 mbuf,图2-1)与显示一个
Net/1 mbuf的图[Leffler et al. 1989,p.290]的区别。这个变化是在 Net/2中造成的:添
加了成员 m _ f l a g s,把指针 m _ a c t改名为 m _ n e x t p k t,并把这个指针移到这个
mbuf的前面。
在第一个 m b u f中,U D P与T C P协议首部位置的不同是由于 U D P调用M _ P R E P E N D
(图23-15和习题23.1)而TCP调用MGETHDR(图26-25)造成的。

2.2 代码介绍

mbuf函数在一个单独的 C文件中,并且 mbuf宏与各种mbuf定义都在一个单独的头文件中,


如图2-3所示。
文 件 说 明

sys/mbuf.h mbuf结构、mbuf宏与定义
kern/uipc_mbuf.c mbuf函数

图2-3 本章讨论的文件

2.2.1 全局变量

在本章中有一个全局变量要介绍,如图 2-4所示。

Linux公社 www.linuxidc.com
bbs.theithome.com
28 计计TCP/IP详解 卷 2:实现

变 量 数 据 类 型 说 明

mbstat struct mbstat mbuf的统计信息 (图2-5)

图2-4 本章介绍的全局变量

2.2.2 统计

在全局结构 mbstat中维护的各种统计,如图 2-5所示。


m b s t a t成员 说 明

m_clfree 自由簇
m_clusters 从页池中获得的簇
m_drain 调用协议的 drain函数来回收空间的次数
m_drops 寻找空间(未用)失败的次数
m_mbufs 从页池(未用)中获得的mbuf数
m_mtypes[256] 当前mbuf的分配数: M T _xxx索引
m_spare 剩余空间(未用)
m_wait 等待空间(未用)的次数

图2-5 在结构mbstat 中维护的 mbuf统计

这个结构能被命令netstat -m检测;图2-6所示的是一些输出示例。关于所用映射页的数
量的两个值是:m_clusters(34)减m_clfree (32)—当前使用的簇数(2)和m_clusters(34)。
分配给网络的存储器的千字节数是 m b u f存储器 ( 9 9×1 2 8字节)加上簇存储器 ( 3 4×2 0 4 8字
节)再除以1 0 2 4。使用百分比是 m b u f存储器 ( 9 9×1 2 8字节)加上所用簇的存储器 ( 2×2 0 4 8字节)
除以网络存储器总数 (80千字节),再乘100。

图2-6 mbuf统计例子

2.2.3 内核统计

mbuf统计显示了在Net/3源代码中的一种通用技术。内核在一个全局变量 (在本例中是结构
m b s t a t)中保持对某些统计信息的跟踪。当内核在运行时,一个进程 (在本例中是 n e t s t a t程
序)可以检查这些统计。
不是提供系统调用来获取由内核维护的统计,而是进程通过读取链接编辑器在内核建立
时保存的信息来获得所关心的数据结构在内核中的地址。然后进程调用函数 k v m ( 3 ),通过使
用特殊文件 / d e v / m e m读取在内核存储器中的相应位置。如果内核数据结构从一个版本改变

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计29
为下一版本,任何读取这个结构的程序也必须改变。

2.3 mbuf的定义

处理m b u f时,我们会反复遇到几个常量。它们的值显示在图 2 - 7中。除了 M C L B Y T E S定义


在文件/usr/include/machine/param.h中外,其他所有常量都定义在文件 mbuf.h中。
常 量 值(字节数) 说 明

MCLBYTES 2048 一个mbuf簇(外部缓存)的大小


MHLEN 100 带分组首部的 mbuf的最大数据量
MINCLSIZE 208 存储到簇中的最小数据量
MLEN 108 在正常mbuf中的最大数据量
MSIZE 128 每个mbuf的大小

图2-7 mbuf.h 中的mbuf常量

2.4 mbuf结构

图2-8所示的是mbuf结构的定义。

图2-8 mbuf 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
30 计计TCP/IP详解 卷 2:实现

图2-8 (续)

结构m b u f是用一个 m _ h d r结构跟着一个联合来定义的。如注释所示,联合的内容依赖于


标志M_PKTHDR和M_EXT。
93-103 这11个#define语句简化了对mbuf结构中的结构与联合的成员的访问。我们会看
到这种技术普遍应用于 Net/3源代码中,只要是一个结构包含其他结构或联合这种情况。
我们在前面说明了在结构 m b u f中前两个成员的目的:指针 m _ n e x t把m b u f链接成一个
mbuf链表,而指针m_nextpkt把mbuf链表链接成一个 mbuf队列。
图1 - 8显示了每个 m b u f的成员 m _ l e n与分组首部中的成员 m _ p k t h d r . l e n的区别。后者
是链表中所有mbuf的成员m_len的和。
图2-9所示的是成员m_flags的五个独立的值。
m_flags 说 明

M_BCAST 作为链路层广播发送 /接收


M_EOR 记录结束
M_EXT 此mubf带有簇(外部缓存 )
M_MCAST 作为链路层多播发送 /接收
M_PKTHDR 形成一个分组 (记录)的第一个mbuf
M_COPYFLAGS M_PKTHDR/M_EOR/M_BCAST/M_MCAST

图2-9 m_flags 值

我们已经说明了标志 M _ E X T和M _ P K T H D R。M _ E O R 在一个包含记录尾的 m b u f中设置。


Internet协议(例如TCP)从来不设置这个标志,因为 TCP提供一个无记录边界的字节流服务。但
是OSI与XNS运输层要用这个标志。在插口层我们会遇到这个标志,因为这一层是协议无关的,
并且它要处理来自或发往所有运输层的数据。
当要往一个链路层广播地址或多播地址发送分组,或者要从一个链路层广播地址或多播
地址接收一个分组时,在这个 m u b f中要设置接下来的两个标志 M _ B C A S T和M _ M C A S T。这两
个常量是协议层与接口层之间的标志 (图1-3)。
对于最后一个标志值 M _ C O P Y F L A G S,当一个 m b u f包含一个分组首部的副本时,这个标
志表明这些标志是复制的。
图2 - 1 0所示的常量 M T _x x x用于成员 m _ t y p e,指示存储在 m b u f中的数据的类型。虽然我
们总认为一个 m b u f是用来存放要发送或接收的用户数据,但 m b u f可以存储各种不同的数据结
构。回忆图 1 - 6中的一个 m b u f被用来存放一个插口地址结构,其中的目标地址用于系统调用
sendto。它的m_type成员被设置为MT_SONAME。

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
第 2章 mbuf:存储器缓存计计 31
不是图2 - 1 0中所有的 m b u f类型值都用于 N e t / 3。有些已不再使用 (M T _ H T A B L E),还有一些
不用于 T C P / I P代码中,但用于内核的其他地方。例如, M T _ O O B D A T A用于O S I和X N S协议,
但是T C P用不同方法来处理带外 ( o u t - o f - b a n d )数据(我们在 2 9 . 7节说明 )。当我们在本书的后面
遇到其他mbuf类型时会说明它们的用法。
Mbuf m_t y p e 用于Net/3 TCP/IP代码 说 明 存储类型

MT_CONTROL • 外部数据协议报文 M_MBUF


MT_DATA • 动态数据分配 M_MBUF
MT_FREE 应在自由列表中 M_FREE
MT_FTABLE • 分片重组首部 M_FTABLE
MT_HEADER • 分组首部 M_MBUF
MT_HTABLE IMP主机表 M_HTABLE
MT_IFADDR 接口地址 M_IFADDR
MT_OOBDATA 加速(带外)数据 M_MBUF
MT_PCB 协议控制块 M_PCB
MT_RIGHTS 访问权限 M_MBUF
MT_RTABLE 路由表 M_RTABLE
MT_SONAME • 插口名称 M_MBUF
MT_SOOPTS • 插口选项 M_SOOPTS
MT_SOCKET 插口结构 M_SOCKET

图2-10 成员m_type 的值

本图的最后一列所示的 M_xxx值与内核为不同类型 mbuf分配的存储器片有关。这里有大约


6 0个可能的 M_x x x值指派给由内核函数 m a l l o c和宏M A L L O C分配的不同类型的存储器空间。
图2 - 6所示的是来源于命令 n e t s t a t -m的m b u f分配统计信息,它包括每种 M T _x x x类型的统
计。命令 v m s t a t - 显
m 示了内核的存储分配统计,包括每个 M_xxx类型的统计。

由于m b u f有一个固定长度 ( 1 2 8字节),因此对于 m b u f的使用有一个限制—包含


的数据不能超过 1 0 8字节。 N e t / 3用一个 m b u f来存储一个 T C P协议控制块 (在第2 4章我
们会涉及到 ),这个 m b u f的类型为 M T _ P C B。但是 4 . 4 B S D把这个结构的大小从 1 0 8字
节增加到140字节,并为这个结构使用一种不同的内核存储器分配类型。
仔细的读者会注意到图 2 - 1 0中我们表明未使用 M T _ P C B类型的 m b u f,而图 2 - 6显
示这个类型的计数不为零。 U n i x域协议使用这种类型的 m b u f,并且m b u f的统计功能
用于所有协议,而不只是 Internet协议,记住这一点很重要。

2.5 简单的mbuf宏和函数

有超过两打的宏和函数来处理 m b u f (分配一个 m b u f,释放一个 m b u f,等等)。让我们来查


看几个宏与函数的源代码,看看它们是如何实现的。
有些操作既提供了宏也提供了函数。宏版本的名称是以 M开头的大写字母名称,而函数是
以m _开始的小写字母名称。两者的区别是一种典型的时间 -空间互换。宏版本在每个被用到的
地方都被 C预处理器展开 (要求更多的代码空间 ),但是它在执行时更快,因为它不需要执行函
数调用(对于有些体系结构,这是费时的 )。而对于函数版本,它在每个被调用的地方变成了一
些指令(参数压栈,调用函数等 ),要求较少的代码空间,但会花费更多的执行时间。

Linux公社 www.linuxidc.com
bbs.theithome.com
32 计计TCP/IP详解 卷 2:实现

2.5.1 m_get函数

让我们先看一下图 2-11中分配mbuf的函数:m_get。这个函数仅仅就是宏 MGET的展开。

图2-11 m_get 函数:分配一个 mbuf

注意,Net/3代码不使用ANSI C 参数声明。但是,如果使用一个ANSI C 编译器,所


有Net/3系统头文件为所有的内核函数都提供了ANSI C函数原型。例如,<sys/mbuf.h>
头文件中包含这样的行:
struct mbuf *m_get(int, int);

这些函数原型为所有内核函数的调用提供编译期间的参数与返回值的检查。
这个调用表明参数 n o w a i t的值为M _ W A I T或M _ D O N T W A I T,它取决于在存储器不可用时
是否要求等待。例如,当插口层请求分配一个 m b u f来存储 s e n d t o系统调用 (图1 - 6 )的目标地
址时,它指定 M _ W A I T,因为在此阻塞是没有问题的。但是当以太网设备驱动程序请求分配一
个m b u f来存储一个接收的帧时 (图1 - 1 0 ),它指定 M _ D O N T W A I T,因为它是作为一个设备中断
处理来执行的,不能进入睡眠状态来等待一个 m b u f。在这种情况下,若存储器不可用,设备
驱动程序丢弃这个帧比较好。

2.5.2 MGET宏

图2 - 1 2所示的是 M G E T宏。调用 M G E T来分配存储 s e n d t o系统调用 (图1 - 6 )的目标地址的


mbuf如下所示:
MGET(m, M_WAIT, MT_SONAME);
if (m == NULL)
return(ENOBUFS);

图2-12 MGET 宏

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 33
虽然调用指定了 M _ W A I T,但返回值仍然要检查,因为,如图 2-13所示,等待一个 mbuf并
不保证它是可用的。
154-157 M G E T一开始调用内核宏 M A L L O C,它是通用内核存储器分配器进行的。数组
m b t y p e s把mbuf的M T _xxx值转换成相应的 M _xxx值(图2-10)。若存储器被分配,成员 m _ t y p e
被设置为参数中的值。
158 用于跟踪统计每种mbuf类型的内核结构加1(mbstat)。当执行这句时,宏MBUFLOCK把它作
为参数来改变处理器优先级(图1-13),然后把优先级恢复为原值。这防止在执行语句mbstat.m_
mtypes[type]++;时被网络设备中断,因为mbuf可能在内核中的各层中被分配。考虑这样一
个系统,它用三步来实现一个C中的++运算:(1)把当前值装入一个寄存器;(2)寄存器加1;(3)把
寄存器值存入存储器。假设计数器值为77并且MGET在插口层执行。假设执行了步骤1和2(寄存器
值为78),并且一个设备中断发生。若设备驱动也执行MGET来获得同种类型的mbuf,在存储器中
取值(77),加1(78),并存回在存储器。当被中断执行的MGET的步骤3继续执行时,它将寄存器的
值(78)存入存储器。但是计数器应为79,而不是78,这样计数器就被破坏了。
159-160 两个mbuf指针,m_next和m_nextpkt,被设置为空指针。若必要,由调用者把
这个mbuf加入到一个链或队列。
161-162 最后,数据指针被设置为指向 108字节的mbuf缓存的起始,而标志被设置为 0。
163-164 若内核的存储器分配调用失败,调用 m_retry(图2-13)。第一个参数是 M_WAIT或
M_DONTWAIT。

2.5.3 m_retry函数

图2-13所示的是m_retry函数。

图2-13 m_retry 函数

92-97 被m _ r e t r y调用的第一个函数是 m _ r e c l a i m。在7 . 4节我们会看到每个协议都能定


义一个“ d r a i n”函数,在系统缺乏可用存储器时能被 m _ r e c l a i m调用。在图 1 0 - 3 2中我们还
会发现当 I P的d r a i n函数被调用时,所有等待重新组成 I P数据报的 I P分片被丢弃。 T C P的d r a i n
函数什么都不做,而 UDP甚至就没有定义一个 drain函数。
98-102 因为在调用了 m _ r e c l a i m后有可能有机会得到更多的存储器,因此再次调用宏
M G E T,试图获得 m b u f。在展开宏 M G E T(图2 - 1 2 )之前, m _ r e t r y被定义为一个空指针。这可
以防止当存储器仍然不可用时的无休止的循环:这个 M G E T展开会把 m设置为空指针而不是调
用m _ r e t r y函数。在 M G E T展开以后,这个 m _ r e t r y的临时定义就被取消了,以防在此之后

Linux公社 www.linuxidc.com
bbs.theithome.com
34 计计TCP/IP详解 卷 2:实现

有对MGET的其他引用。

2.5.4 mbuf锁

在本节中我们所讨论的函数和宏并不调用 s p l函数,而是调用图 2-12中的M B U F L O C K来保


护这些函数和宏不被中断。但在宏 M A L L O C的开始包含一个 s p l i m p,在结束时有一个 s p l x。
宏M F R E E中包含同样的保护机制。由于 m b u f在内核的所有层中被分配和释放,因此内核必须
保护那些用于存储器分配的数据结构。
另外,用于分配和释放 m b u f簇的宏M C L A L L O C与M C L F R E E要用一个s p l i m p和一个s p l x
包括起来,因为它们修改的是一个可用簇链。
因为存储器分配与释放及簇分配与释放的宏被保护起来防止被中断,我们通常在 M G E T和
m_get这样的函数和宏的前后不再调用 spl函数。

2.6 m_devget和m_pullup函数
我们在讨论 IP、ICMP、IGMP、UDP和TCP的代码时会遇到函数 m _ p u l l u p。它用来保证
指定数目的字节 (相应协议首部的大小 )在链表的第一个 m b u f中紧挨着存放;即这些指定数目
的字节被复制到一个新的 m b u f并紧接着存放。为了理解 m _ p u l l u p的用法,必须查看它的实
现及相关的函数 m _ d e v g e t和宏m t o d与d t o m。在分析这些问题的同时我们还可以再次领会
Net/3中mbuf的用法。

2.6.1 m_devget函数

当接收到一个以太网帧时,设备驱动程序调用函数 m _ d e v g e t来创建一个 m b u f链表,并


把设备中的帧复制到这个链表中。根据所接收的帧的长度 (不包括以太网首部 ),可能导致 4种
不同的mbuf链表。图2-14所示的是前两种。

16字节
(未用)

52字节的数据 85字节的数据
(以IP首部开始) (以IP首部开始)

32字节
(未用) 15字节
(未用)

0≤数据长度≤84 85≤数据长度≤100
图2-14 m_devget 创建的前两种类型的 mbuf

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 35
链中的下一个mbuf

4字节的数据

100字节的数据 104字节
(以IP首部开始) (未用)

101≤数据长度≤207

图2-15 m_devget 创建的第 3种mbuf

1) 图2 - 1 4左边的 m b u f用于数据的长度在 0 ~ 8 4字
节之间的情况。在这个图中,我们假定有 5 2
字节的数据:一个20字节的IP首部和一个32字
节的 T C P首部(标准的 2 0字节的 T C P首部加上
1 2字节的 T C P选项),但不包括 T C P数据。既
然m _ d e v g e t返回的m b u f数据从I P首部开始,
m _ l e n的实际最小值是 2 8:2 0字节的I P首部,
8字节的UDP首部和一个0长度的UDP数据报。
m _ d e v g e t在这个 m b u f的开始保留了 1 6字节
未用。虽然 1 4字节的以太网首部不存放在这
里,还是分配了一个 1 4字节的用于输出的以 88字节
太网首部,这是同一个 m b u f,用于输出。我 (未用)

们会遇到两个函数: i c m p _ r e f l e c t 和
t c p _ r e s p o n d,它们通过把接收到的 m b u f
作为输出 m b u f来产生一个应答。在这两种情
208≤数据长度≤2048
况中,接收的数据报应该少于 84 字节,因此
2048字节簇
很容易在前面保留 16 字节的空间,这样在建
立输出数据报时可以节省时间。分配 16 字节 1500字节数据
(以IP首部开始)
而不是 1 4字节的原因是为了在 m b u f中用长字
对准方式存储IP首部。
548字节(未用)
2) 如果数据在 8 5 ~ 1 0 0字节之间,就仍然存放在
一个分组首部 m b u f中,但在开始没有 1 6字节 图2-16 m_devget 创建的第4种mbuf

Linux公社 www.linuxidc.com
bbs.theithome.com
36 计计TCP/IP详解 卷 2:实现

的空间。数据存储在数组 m _ p k t d a t的开始,并且任何未用的空间放在这个数组的后
面。例如在图2-14的右边的mbuf显示的就是这个例子,假设有 85字节数据。
3) 图2 - 1 5所示的是 m _ d e v g e t创建的第 3种m b u f。当数据在 1 0 1 ~ 2 0 7字节之间时,要求有
两个mbuf。前100字节存放在第一个 mbuf中(有分组首部的mbuf),而剩下的存放在第二
个m b u f中。在此例中,我们显示的是一个 1 0 4字节的数据报。在第一个 m b u f的开始没
有保留16字节的空间。
4) 图 2 - 1 6所示的是 m _ d e v g e t 创建的第四种 m b u f。如果数据超过或等于 208 字节
(M I N C L B Y T E S),要用一个或多个簇。图中的例子假设了一个 1 5 0 0字节的以太网帧。
如果使用 1 0 2 4字节的簇,本例子需要两个 m b u f,每个m b u f都有标志 M _ E X T,和指向一
个簇的指针。

2.6.2 mtod和dtom宏

宏mtod和dtom也定义在文件mbuf.h中。它们简化了复杂的 mbuf结构表达式。

m t o d(“m b u f到数据” )返回一个指向 m b u f数据的指针,并把指针声名为指定类型。例如


代码

存储在mbuf的数据(m _ d a t a)指针i p中。C编译器要求进行类型转换,然后代码用指针 i p引用


IP首部。我们可以看到当一个C结构(通常是一个协议首部 )存储在一个 mbuf中时会用到这个宏。
当数据存放在mbuf本身(图2-14和图2-15)或存放在一个簇中 (图2-16)时,可以用这个宏。
宏dtom (“数据到mbuf”)取得一个存放在一个 mbuf中任意位置的数据的指针,并返回这
个mbuf结构本身的一个指针。例如,若我们知道 ip指向一个mbuf的数据区,下面的语句序列

把指向这个 m b u f开始的指针存放到 m中。我们知道 M S I Z E(1 2 8)是2的幂,并且内核存储


器分配器总是为 mbuf分配连续的 M S I Z E字节的存储块, d t o m仅仅是清除参数中指针的低位来
发现这个mbuf的起始位置。
宏d t o m有一个问题:当它的参数指向一个簇,或在一个簇内,如图 2 - 1 6时,它不能正确
执行。因为那里没有指针从簇内指回 m b u f结构, d t o m不能被使用。这导致了下一个函数:
m_pullup。

2.6.3 m_pullup函数和连续的协议首部

函数m _ p u l l u p有两个目的。第一个是当一个协议 ( I P、I C M P、I G M P、U D P或T C P )发现


在第一个 m b u f的数据量 (m _ l e n)小于协议首部的最小长度 (例如: I P是2 0,U D P是8,T C P是
2 0 ) 时。调用 m _ p u l l u p 是基于假定协议首部的剩余部分存放在链表中的下一个 m b u f。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 37
m _ p u l l u p重新安排 m b u f链表,使得前 N字节的数据被连续地存放在链表的第一个 m b u f中。N
是这个函数的一个参数,它必须小于或等于 1 0 0( M H L E N )。如果前 N字节连续存放在第一个
mbuf中,则可以使用宏 mtod和dtom。
例如,我们在IP输入例程中会遇到下面这样的代码:

如果第一个mbuf中的数据少于20(标准IP首部的大小 ),m _ p u l l u p被调用。函数 m _ p u l l u p有


两个原因会失败: ( 1 )如果它需要其他 m b u f并且调用 M G E T失败;或者 ( 2 )如果整个 m b u f链表中
的数据总数少于要求的连续字节数 (即我们所说的 N,在本例中是 2 0 )。通常,失败是因为第二
个原因。在此例中,如果 m_pullup失败,一个 IP计数器加 1,并且此IP数据报被丢弃。注意,
这段代码假设失败的原因是 mbuf 链表中数据少于 20字节。
实际上,在这种情况下, m _ p u l l u p很少能被调用 (注意,C语言的 & &操作符仅当 m b u f长
度小于期待值时才调用它 ),并且当它被调用时,它通常会失败。通过查看图 2-14~图2-16,我
们可以找到它的原因:在第一个 mbuf中,或在簇中,从IP首部开始有至少 100字节的连续字节。
这允许 6 0字节的最大 I P首部,并且后面跟着 4 0字节的 T C P首部(其他协议—— I C M P,I G M P和
U D P——它们的协议首部不到 4 0字节)。如果 m b u f链表中的数据可用 (分组不小于协议要求的
最小值 ),则所要求的字节数总能连续地存放在第一个 m b u f中。但是,如果接收的分组太小
(m _ l e n小于期待的最小值 ),则m _ p u l l u p被调用,并且它返回一个差错,因为在 m b u f链表
中没有所要求数目的可用数据。
源于伯克利的内核维护一个叫 M P F a i l的变量,每次 m _ p u l l u p失败时,它都加
1。在一个 N e t / 3系统中曾经接收了超过 2 7 0 0万的I P数据报,而 M P F a i l只有9。计数
器i p s t a t . i p s _ t o o s m a l l也是9,并且所有其他协议计数器 ( I C M P、I G M P、U D P
和T C P等)所计的m _ p u l l u p失败次数为 0。这证实了我们的断言:大多数 m _ p u l l u p
的失败是因为接收的 IP数据报太小。

2.6.4 m_pullup和IP的分片与重组

使用m _ p u l l u p的第二个用途涉及到 I P和T C P的重组。假定 I P接收到一个长度为 2 9 6的分


组,这个分组是一个大的 I P数据报的一个分片。这个从设备驱动程序传到 I P输入的 m b u f看起
来像我们在图 2 - 1 6中所示的一个 m b u f:2 9 6字节的数据存放在一个簇中。我们将这显示在图 2 -
17中。
问题在于, IP的分片算法将各分片都存放在一个双向链表中,使用 IP首部中的源与目标 IP
地址来存放向前与向后链表指针 (当然,这两个 I P地址要保存在这个链表的表头中,因为它们
还要放回到重组的数据报中。我们在第 10章讨论这个问题 )。但是如果这个IP首部在一个簇中,
如图2 - 1 7所示,这些链表指针会存放在这个簇中,并且当以后遍历链表时,指向 I P首部的指
针(即指向这个簇的起始的指针 )不能被转换成指向 m b u f的指针。这是我们在本节前面提到的
问题:如果 m _ d a t a指向一个簇时不能使用宏 d t o m,因为没有从簇指回 m b u f的指针。 I P分片

Linux公社 www.linuxidc.com
bbs.theithome.com
38 计计TCP/IP详解 卷 2:实现

不能如图2-17所示的把链指针存储在簇中。

IP分片的 IP分片的
双向链表 双向链表

276字节的数据

2048字节簇

图2-17 一个长度为 296的IP分片

为解决这个问题,当接收到一个分片时,若分片存放在一个簇中, I P分片例程总是调用
m_pullup。它强行将 20字节的IP首部放到它自己的 mbuf中。代码如下:

图2 - 1 8所示的是在调用了 m _ p u l l u p后得到的 m b u f链表。 m _ p u l l u p分配了一个新的


m u b f,挂在链表的前面,并从簇中取走 4 0字节放入到这个新的 m b u f中。之所以取 4 0字节而不
是仅要求的 2 0字节,是为了保证以后在 I P把数据报传给一个高层协议 (例如: I C M P,I G M P,
U D P或T C P )时,高层协议能正确处理。采用不可思议的 4 0 (图7 - 1 7中的m a x _ p r o t o h d r)是因
为最大协议首部通常是一个 20 字节的 I P首部和 2 0字节的 T C P首部的组合 (这假设其他协议族,
例如OSI协议,并不编译到内核中 )。
在图 2 - 1 8中, I P分片算法在左边的 m b u f中保存了一个指向 I P首部的指针,并且可以用
dtom将这个指针转换成一个指向 mbuf本身的指针。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 39

IP分片的 IP分片的
IP首部
双向 链表 双向 链表

数据报的下20字节

数据报的下256字节

2048字节的簇
图2-18 调用m_pullup 后的长度为 296的IP分片

2.6.5 TCP重组避免调用m_pullup

重组TCP报文段使用一个不同的技术而不是调用 m _ p u l l u p。这是因为 m _ p u l l u p开销较


大:分配存储器并且数据从一个簇复制到一个 mbuf中。TCP试图尽可能地避免数据的复制。
卷1的第1 9章提到大约有一半的 T C P数据是批量数据 (通常每个报文段有 5 1 2或更多字节的
数据),并且另一半是交互式数据 (这里面有大约 9 0%的报文段包含不到 1 0字节的数据 )。因此,
当T C P从I P接收报文段时,它们通常是如图 2 - 1 4左边所示的格式 (一个小量的交互数据,存储
在m b u f本身 )或图 2 - 1 6所示的格式 (批量数据,存储在一个簇中 )。当T C P报文段失序到达时,
它们被TCP存储到一个双向链表中。如 IP分片一样,在 IP首部的字段用于存放链表的指针,既
然这些字段在 TCP接收了IP数据报后不再需要,这完全可行。但当 IP首部存放在一个簇中,要
将一个链表指针转换成一个相应的 mbuf指针时,会引起同样的问题 (图2-17)。
为解决这个问题,在 27.9节中我们会看到 TCP把mbuf指针存放在 TCP首部中的一些未用的
字段中,提供一个从簇指回 m b u f的指针,来避免对每个失序的报文段调用 m _ p u l l u p。如果
I P首部包含在 m b u f的数据区 (图2 - 1 8 ),则这个回指指针是无用的,因为宏 d t o m对这个链表指
针会正常工作。但如果 IP首部包含在一个簇中,这个回指指针将被使用。当我们在讨论 27.9节
的tcp_reass时,会研究实现这种技术的源代码。

Linux公社 www.linuxidc.com
bbs.theithome.com
40 计计TCP/IP详解 卷 2:实现

2.6.6 m_pullup使用总结

我们已经讨论了关于使用 m_pullup的三种情况:
• 大多数设备驱动程序不把一个 IP数据报的第一部分分割到几个 mbuf中。假设协议首部都
紧挨着存放,则在每个协议 ( I P、I C M P、I G M P、U D P和T C P )中调用 m _ p u l l u p的可能
性很小。如果调用 m _ p u l l u p,通常是因为 I P数据报太小,并且 m _ p u l l u p返回一个差
错,这时数据报被丢弃,并且差错计数器加 1。
• 对于每个接收到的IP分片,当IP数据报被存放在一个簇中时, m_pullup被调用。这意味
着,几乎对于每个接收的分片都要调用m_pullup,因为大多数分片的长度大于208 字节。
• 只要 T C P 报文段不被 I P 分片,接收一个 T C P 报文段,不论是否失序,都不需调用
m_pullup。这是避免 IP对TCP分片的一个原因。

2.7 mbuf宏和函数的小结
在操作mbuf的代码中,我们会遇到图 2-19中所列的宏和图 2-20中所列的函数。图 2-19中的
宏以函数原型的形式显示,而不是以 # d e f i n e形式来显示参数的类型。由于这些宏和函数主
要用于处理 m b u f数据结构并且不涉及联网问题,因此我们不查看实现它们的源代码。还有另
外一些m b u f宏和函数用于 N e t / 3源代码的其他地方,但由于我们在本书中不会遇到它们,因此
没有把它们列于图中。

宏 描 述

MCLGET 获得一个簇 (一个外部缓存 )并将m指向的m b u f中的数据指针 ( m _ d a t a)设置为指向这个


簇。如果存储器不可用,返回时不设置 mbuf中的M _ E X T标志
void MCLGET(struct mbuf * m, int nowait);
MFREE 释放一个m指向的mbuf。若m指向一个簇 (设置了M _ E X T),这个簇的引用计数器减 1,但
这个簇并不被释放,直到它的引用计数器降为 0 (如 2 . 9 节所述 ) 。返回 m 的后继 ( 由 m
->m_n e x t指向,可以为空 )存放在n中
void MFREE(struct mbuf * m, struct mbuf *n);
MGETHDR 分配一个mbuf,并把它初始化为一个分组首部。这个宏与 M G E T(图2-12)相似,但设置
了标志M _ P K T H D R,并且数据指针 (m _ d a t a)指向紧接分组首部后的 100字节的缓存
void MGETHDR(struct mbuf * m, int nowait, int type);
MH_ALIGN 设置包含一个分组首部的 mbuf的m _ d a t a,在这个mbuf数据区的尾部为一个长度为 l e n
字节的对象提供空间。这个数据指针也是长字对准方式的
void MH_ALIGN(struct mbuf * m, int len);
M_PREPEND 在m指向的mbuf中的数据的前面添加len字节的数据。如果mbuf有空间,则仅把指针(m_data)
减len字节,并将长度 (m_len)增加len字节。如果没有足够的空间,就分配一个新的 mbuf,
它的m_next指针被设置为m。一个新mbuf的指针存放在m中。并且新mbuf的数据指针被设置,
这样len字节的数据放置到这个mbuf的尾部(例如,调用MH_ALIGN)。如果一个新mbuf被分配,
并且原来的mbuf的分组首部标志被设置,则分组首部从老mbuf中移到新mbuf中
void M_PREPEND(struct mbuf * m, int len, int nowait);
dtom 将指向一个 mbuf数据区中某个位置的指针 x转换成一个指向这个 mbuf的起始的指针。
struct mbuf *dtom(void *x);
mtod 将m指向的mbuf的数据区指针的类型转换成 type类型
t y p e m t o d( s t r u c t m b u f m,
* type) ;

图2-19 我们在本书中会遇到的 mbuf宏

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
第 2章 mbuf:存储器缓存计计 41
函 数 说 明

m_adj 从m指向的m b u f中移走len字节的数据。如果 len是正数,则所操作的是紧排在这个


mbuf的开始的len字节数据;否则是紧排在这个 mbuf的尾部的 len绝对值字节数据
void m_adj(struct mbuf *m, int len);
m_cat 把由n指向的mbuf链表链接到由 m指向的m b u f链表的尾部。当我们讨论 IP重组时(第10
章)会遇到这个函数
void m_cat(struct mbuf *m, struct mbuf *n);
m_copy 这是m _ c o p y m的三参数版本,它隐含的第 4个参数的值为 M _ D O N T W A I T
struct mbuf * m_copy(struct mbuf *m, int offset, int len);
m_copydata 从m指向的mbuf链表中复制 len字节数据到由 cp指向的缓存。从 mbuf链表数据区起始的
offset字节开始复制
void m_copydata(struct mbuf *m, int offset, int len, caddr_t cp);
m_copyback 从cp指向的缓存复制len字节的数据到由 m指向的mbuf,数据存储在 mbuf链表起始
offset字节后。必要时, mbuf链表可以用其他 mbuf来扩充
void m_copyback(struct mbuf *m, int offset, int len, caddr_t cp);
m_copym 创建一个新的 m b u f链表,并从 m指向的 m b u f链表的开始 o f f s e t处复制 l e n字节的数据。
一个新 m b u f链表的指针作为此函数的返回值。如果 l e n等于常量 M _ C O P Y A L L ,则从这
个m b u f链表的 o f f s e t开始的所有数据都将被复制。在 2 . 9节中,我们会更详细地介绍这个
函数
struct mbuf *m_copym(struct mbuf *m, int offset, int len, int nowait);
m_devget 创建一个带分组首部的 mbuf链表,并返回指向这个链表的指针。这个分组首部的 len和
rcvif字段被设置为 len和ifp。调用函数copy从设备接口 (由buf指向)将数据复制到 mbuf中。
如果c o p y是一个空指针,调用函数 b c o p y。由于尾部协议不再被支持, o f f为0。我们在
2.6节讨论了这个函数
struct mbuf * m_devget(char *buf, int len, int off, struct ifnet * ifp,
void (*copy)(const void *, void *, u_int));
m_free 宏M F R E E的函数版本
struct mbuf *m_free(struct mbuf * m);
m_freem 释放m指向的链表中的所有 mbuf
void m_freem(struct mbuf * m);
m_get 宏M G E T的函数版本。我们在图 2-12中显示过此函数
struct mbuf *m_get(int nowait, int type);
m_getclr 此函数调用宏 M G E T来得到一个 mbuf,并把108字节的缓存清零
struct mbuf *m_getclr(int nowait, int type);
m _ g ethdr 宏M G E T H D R的函数版本
struct mbuf *m_gethdr(int nowait, int type);
m_pullup 重新排列由 m指向的mbuf中的数据,使得前 len字节的数据连续地存储在链表中的第一
个m b u f中。如果这个函数成功,则宏 m t o d能返回一个正好指向这个大小为 l e n的结构。
我们在2 . 6节讨论了这个函数
s t r u c t m b u f m*_ p u l l u p( s t r u c t m b u f m,
* int len) ;

图2-20 在本书中我们要遇到的 mbuf函数

所有原型的参数 n o w a i t是M _ W A I T或M _ D O N T W A I T,参数t y p e是图2 - 1 0中所示的 M T _x x x中


的一个。

Linux公社 www.linuxidc.com
bbs.theithome.com
42 计计TCP/IP详解 卷 2:实现

M _ P R E P E N D的一个例子是,从图 1 - 7转换到图 1 - 8的过程中,当 I P和U D P首部被添加到数


据的前面时要调用这个宏,因为另一个 m b u f要被分配。但当这个宏再次被调用 (从图1 - 8转换
成图2-2)来添加以太网首部时,在那个 mbuf中已有存放这个首部的空间。
M _ c o p y d a t a的最后一个参数的类型是 c a d d r _ t,它代表“内核地址”。这个
数据类型通常定义在 <sys/types.h>中,为char *。它最初在内核中使用,但被
某些系统调用使用时被外露出来。例如, m m a p系统调用,不论是 4 . 4 B S D或S V R 4都
把caddr_t作为第一个参数的类型并作为返回值类型。

2.8 Net/3联网数据结构小结

本节总结我们在 N e t / 3联网代码中要遇到的数据结构类型。在 N e t / 3内核中用到其他数据结


构(感兴趣的读者可以查看头文件 <sys/queue.h>),但下面这些是我们在本书中要遇到的。
1) 一个mbuf链:一个通过m_next指针链接的 mbuf链表。我们已经看过几个这样的例子。
2) 只有一个头指针的 m b u f链的链表。 m b u f链通过每个链的第一个 m b u f中的m _ n e x t p k t
指针链接起来。
图2-21所示的就是这种链表。这种数据结构的例子是一个插口发送缓存和接收缓存。

图2-21 只有头指针的 mbuf链的链表

顶部的两个 m b u f形成这个队列中的第一个记录,底下三个 m b u f形成这个队列的第二个


记录。对于一个基于记录的协议,如 U D P,我们在每个队列中能遇到多个记录。但对
于像T C P这样的协议,它没有记录的边界,每个队列我们只能发现一个记录 (一个m b u f
链可能包含多个 mbuf)。
把一个 m b u f追加到队列的第一个记录中要遍历所有第一个记录的 m b u f,直到遇到
m _ n e x t为空的 m b u f。而追加一个包含新记录的 m b u f链到这个队列中,要查找所有记
录直到遇到 m_nextpkt为空的记录。
3) 一个有头指针和尾指针的 mbuf链的链表。
图2 - 2 2显示的是这种类型的链表。我们在接口队列中会遇到它 (图3 - 1 3 ),并且在图 2 - 2
中已显示过它的一个例子。
在图2-21中仅有一点改变:增加了一个尾指针,来简化增加一个新记录的操作。
4) 双向循环链表。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 43

图2-22 有头指针和尾指针的链表

图2 - 2 3所示的是这种类型的链表,我们在 I P分片与重装 (第1 0章)、协议控制块 (第2 2章)


及TCP失序报文段队列 (第27.9节)中会遇到这种数据结构。

列表表头

图2-23 双向循环链表

在这个链表中的元素不是 m b u f— 它们是一些定义了两个相邻的指针的结构:一个
n e x t指针跟着一个 p r e v i o u s指针。两个指针必须在结构的起始处。如果链表为空,表头
的next和previous指针都指向这个表头本身。
在图中我们简单地把向后指针指向另一个向后指针。显然所有的指针应包含它所指向
的结构的地址,即向前指针的地址 (因为向前和向后指针总是放在结构的起始处 )。
这种类型的数据结构能方便地向前向后遍历,并允许方便地在链表中任何位置进行插
入与删除。
函数insque和remque(图10-20)被调用来对这个链表进行插入和删除。

2.9 m_copy和簇引用计数

使用簇的一个明显的好处就是在要求包含大量数据时能减少 m b u f的数目。例如,如果不
使用簇,要有 1 0个m b u f才能包含 1 0 2 4字节的数据:第一个 m b u f带有1 0 0字节的数据,后面 8个
每个存放 1 0 8字节数据,最后一个存放 6 0字节数据。分配并链接 1 0个m b u f比分配一个包含
1 0 2 4字节簇的 m b u f开销要大。簇的一个潜在缺点是浪费空间。在我们的例子中使用一个簇
(2048 + 128)要2176字节,而1280字节不到1簇(10×128)。
簇的另外一个好处是在多个 mbuf间可以共享一个簇。在 TCP输出和m _ c o p y函数中我们遇
到过这种情况,但现在我们要更详细地说明这个问题。

Linux公社 www.linuxidc.com
bbs.theithome.com
44 计计TCP/IP详解 卷 2:实现

例如,假设应用程序执行一个 w r i t e,把4 0 9 6字节写到 T C P插口中。假设插口发送缓存


原来是空的,接收窗口至少有 4 0 9 6,则会发生以下操作。插口层把前 2 0 4 8字节的数据放在一
个簇中,并且调用协议的发送例程。 T C P发送例程把这个 m b u f追加到它的发送缓存后,如图
2 - 2 4所示,并调用 t c p _ o u t p u t。结构 s o c k e t中包含 s o c k b u f结构,这个结构中存储着发送
缓存mbuf链的链表的表头: so_snd.sb_mb。

2048字节簇

2048字节的数据

图2-24 包含2048字节数据的 TCP插口发送缓存

假设这个连接 (典型的是以太网 )的一个T C P最大报文段大小 ( M S S )为1 4 6 0,t c p _ o u t p u t


建立一个报文段来发送包含前 1 4 6 0字节的数据。它还建立一个包含 I P和T C P首部的 m b u f,为
链路层首部 ( 1 6字节)预留了空间,并将这个 m b u f链传给 I P输出。在接口输出队列尾部的 m b u f
链显示在图 2-25中。
在1.9节的UDP例子中, UDP用mbuf链来存放数据报,在前面添加一个 mbuf来存放协议首
部,并把此链传给 I P输出。U D P并不把这个m b u f保存在它的发送缓存中。而 T C P不能这样做,
因为TCP是一个可靠协议,并且它必须维护一个发送数据的副本,直到数据被对方确认。
在这个例子中, t c p _ o u t p u t调用函数 m _ c o p y,请求复制 1 4 6 0字节的数据,从发送缓
存起始位置开始。但由于数据被存放在一个簇中, m _ c o p y创建一个 m b u f (图2 - 2 5的右下侧 )并
且对它初始化,将它指向那个已存在的簇的正确位置 (此例中是簇的起始处 )。这个 m b u f的长
度是1 4 6 0,虽然有另外 5 8 8字节的数据在簇中。我们所示的这个 m b u f链的长度是 1 5 1 4,包括
以太网首部、IP首部和TCP首部。
在图2 - 2 5的右下侧我们还显示了这个 m b u f包含一个分组首部,但它不是链中的
第一个 m b u f。当m _ c o p y复制一个包含一个分组首部的 m b u f并且从原来 m b u f的起始
地址开始复制时,分组首部也被复制下来。因为这个 m b u f不是链中的第一个 m b u f,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计45
这个额外的分组首部被忽略。而在这个额外的分组首部中的 m _ p k t h d r . l e n的值
2048也被忽略。

TCP发送缓存

2048字节簇

2048字节的数据

有要发送的
TCP报文段
的接口输出
队列 以太网首部,
IP首部,
TCP首部

图2-25 TCP插口发送缓存和接口输出队列中的报文段

这个共享的簇避免了内核将数据从一个 m b u f复制到另一个 m b u f中— 这节约了很多开


销。它是通过为每个簇提供一个引用计数来实现的,每次另一个 m b u f指向这个簇时计数加 1,
当一个簇释放时计数减 1。仅当引用计数到达 0时,被这个簇占用的存储器才能被其他程序使
用(见习题2.4)。
例如,当图 2 - 2 5底部的 m b u f链到达以太网设备驱动程序并且它的内容已被复制给这个设
备时,驱动程序调用 m _ f r e e m。这个函数释放带有协议首部的第一个 m b u f,并注意到链中第

Linux公社 www.linuxidc.com
bbs.theithome.com
46 计计TCP/IP详解 卷 2:实现

二个m b u f指向一个簇。簇引用计数减 1,但由于它的值变成了 1,它仍然保存在存储器中。它


不能被释放,因为它仍在 TCP发送缓存中。

2048字节簇 2048字节簇

1460字节的数据
2048字节的数据

588字节的数据

以太网首部,
IP首部,
TCP首部

图2-26 用于发送 1460字节TCP报文段的mbuf链

继续我们的例子,由于在发送缓存中剩余的 588字节不能组成一个报文段, t c p _ o u t p u t
在把1 4 6 0字节的报文段传给 I P后返回(在第2 6章我们要详细说明在这种条件下 t c p _ o u t p u t发
送数据的细节 )。插口层继续处理来自应用程序的数据:剩下的 2 0 4 8字节被存放到一个带有一
个簇的 m b u f中,T C P发送例程再次被调用,并且新的 m b u f被追加到插口发送缓存中。因为能
发送一个完整的报文段,tcp_output建立另一个带有协议首部和 1460字节数据的 mbuf链表。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 2章 mbuf:存储器缓存计计 47
m _ c o p y的参数指定了 1 4 6 0字节的数据在发送缓存中的起始位移和长度 ( 1 4 6 0字节)。这显示在
图2-26中,并假设这个 mbuf链在接口输出队列中 (这个链中的第一个 mbuf的长度反映了以太网
首部、IP首部及TCP首部)。
这次1 4 6 0字节的数据来自两个簇:前 5 8 8字节来自发送缓存的第一个簇而后面的 8 7 2字节
来自发送缓存的第二个簇。它用两个 m b u f来存放 1 4 6 0字节,但 m _ c o p y还是不复制这 1 4 6 0字
节的数据 —它引用已存在的簇。
这次我们没有在图 2 - 2 6右下侧的任何 m b u f中显示一个分组首部。原因是调用
m _ c o p y的起始位移为零。但在插口发送缓存中的第二个 mbuf包含一个分组首部,而
不是链中的第一个 mbuf。这是函数 s o s e n d的特点,这个额外的分组首部被简单地忽
略了。
我们在通篇中会多次遇到函数 m _ c o p y。虽然这个名字隐含着对数据进行物理复制,但如
果数据被包含在一个簇中,却是仅引用这个簇而不是复制。

2.10 其他选择

m b u f远非完美,并且时常遭到批评。但不管怎样,它们形成了所有今天正使用着的伯克
利联网代码的基础。
一种由Van Jacobson [Partridge 1993]完成的I n t e r n e t协议的研究实现,它废除了支持大量
连续缓存的复杂的 m b u f数据结构。 [Jacobson 1993]提出了一种速度能提高一到两个数量级的
改进方案,还包括其他改进,及废除 mbuf。
这个m b u f的复杂性是一种权衡,以避免分配大的固定长度的缓存,这样的大缓存很少能
被装满。而在这种情况下, m b u f要进行设计,一个 VA X - 11 / 7 8 0有4兆存储器,是一个大系统,
并且存储器是昂贵的资源,需要仔细分配。今天存储器已不昂贵了,而焦点已经转向更高的
性能和代码的简单性。
mbuf的性能基于存放在 mbuf中数据量。 [Hutchinson and Peterson 1991]显示了处理 mbuf的
时间与数据量不是线性关系。

2.11 小结

在本书几乎所有的函数中我们都会遇到 m b u f。它们的主要用途是在进程和网络接口之间
传递用户数据时用来存放用户数据,但 m b u f还用于保存其他各种数据:源地址和目标地址、
插口选项等等。
根据M_PKTHDR和M_EXT标志是否被设置,这里有 4种类型的mbuf:
• 无分组首部,mbuf本身带有0~108字节数据;
• 有分组首部,mbuf本身带有0~100字节数据;
• 无分组首部,数据在簇 (外部缓存)中;
• 有分组首部,数据在簇 (外部缓存)中。
我们查看了几个 mbuf宏和函数的源代码,但不是所有的 mbuf例程源代码。图2-19和图2-20
提供了所有我们在本书中遇到的 mbuf例程的函数原型和说明。
查看了我们要遇到的两个函数的操作: m _ d e v g e t,很多网络设备驱动程序调用它来存

Linux公社 www.linuxidc.com
bbs.theithome.com
48 计计TCP/IP详解 卷 2:实现

储一个收到的帧; m_pullup,所有输入例程调用它把协议首部连续放置在一个 mbuf中。


由一个m b u f指向的簇 (外部缓存 )能通过m _ c o p y被共享。例如,用于 T C P输出,因为一个
被传输的数据的副本要被发送端保存,直到数据被对方确认。比起进行物理复制来说,通过
引用计数,共享簇提高了性能。

习题

2.1 在图2-9中定义了M_COPYFLAGS。为什么不复制标志 M_EXT?


2.2 在2 . 6节中,我们列出了两个 m _ p u l l u p失败的原因。实际上有三个原因。查看这个
函数的源代码(附录B),并发现另外一个原因。
2.3 为避免宏 d t o m遇到在 2 . 6节中我们所讨论的问题—当数据在簇中时,为什么不仅
仅给每个簇加一个指向 mbuf的回指指针?
2.4 既然一个 m b u f簇的大小是2的幂(典型的是 1 0 2 4或2 0 4 8 ),簇内的空间不能用于引用计
数。查看Net/3的源代码(附录B),并确定这些引用计数存储在什么地方。
2.5 在图2-5中,我们注意到两个计数器 m _ d r o p s和m _ w a i t现在没有实现。修改 mbuf例
程增加这些计数器。

Linux公社 www.linuxidc.com
bbs.theithome.com
第3章 接 口 层
3.1 引言

本章开始讨论 N e t / 3在协议栈底部的接口层,它包括在本地网上发送和接收分组的硬件与
软件。
我们使用术语设备驱动程序来表示与硬件及网络接口 (或仅仅是接口 )通信的软件,网络接
口是指在一个特定网络上硬件与设备驱动器之间的接口。
N e t / 3接口层试图在网络协议和连接到一个系统的网络设备的驱动器间提供一个与硬件无
关的编程接口。这个接口层为所有的设备提供以下支持:
• 一套精心定义的接口函数;
• 一套标准的统计与控制标志;
• 一个与设备无关的存储协议地址的方法;
• 一个标准的输出分组的排队方法。
这里不要求接口层提供可靠的分组传输,仅要求提供最大努力 ( b e s t - e ff o r t )的服务。更高
协议层必须弥补这种可靠性缺陷。本章说明为所有网络接口维护的通用数据结构。为了说明
相关数据结构和算法,我们参考 Net/3中三种特定的网络接口:
1) 一个AMD 7990 LANCE以太网接口:一个能广播局域网的例子。
2) 一个串行线 IP(SLIP)接口:一个在异步串行线上的点对点网络的例子。
3) 一个环回接口:一个逻辑网络把所有输出分组作为输入返回。

3.2 代码介绍

通用接口结构和初始化代码可在三个头文件和两个 C文件中找到。在本章说明的设备专用
初始化代码可在另外三个 C文件中找到。所有的 8个文件都列于图 3-1中。

文 件 说 明

sys/socket.h 地址结构定义
net/if.h 接口结构定义
net/if_dl.h 链路层结构定义
kern/init_main.c 系统和接口初始化
net/if.c 通用接口代码
net/if_loop.c 环回设备驱动程序
net/if_sl.c SLIP设备驱动程序
hp300/dev/if_le.c LANCE以太网设备驱动程序

图3-1 本章讨论的文件

3.2.1 全局变量

在本章中介绍的全局变量列于图 3-2中。
Linux公社 www.linuxidc.com
bbs.theithome.com
50 计计TCP/IP详解 卷 2:实现

变 量 数据类型 说 明

pdevinit struct pdevinit[] 伪设备如SLIP和环回接口的初始化参数数组


ifnet struct ifnet * i f n e t结构的列表的表头
ifnet_addrs struct ifaddr ** 指向链路层接口地址的指针数组
if_indexlim int 数组i f n e t _ a d d r s的大小
if_index int 上一个配置接口的索引
ifqmaxlen int 接口输出队列的最大值
hz int 这个系统的时钟频率 (次/秒)

图3-2 本章中介绍的全局变量

3.2.2 SNMP变量

N e t / 3内核收集了大量的各种联网统计。在大多数章节中,我们都要总结这些统计并说明
它们与定义在简单网络管理协议信息库 (SNMP MIB-II) 中的标准 T C P / I P信息和统计之间的关
系。RFC 1213 [McCloghrie and Rose 1991]说明了SNMP MIB-II,它组织成如图 3-3所示的10
个不同的信息组。
SNMP组 说 明

System 系统通用信息
Interfaces 网络接口信息
Address Translation 网络地址到硬件地址的映射表 (不推荐使用 )
IP IP协议信息
ICMP ICMP协议信息
TCP TCP协议信息
UDP UDP协议信息
EGP EGP协议信息
Transmission 媒体专用信息
SNMP SNMP协议信息

图3-3 MIB-II中的SNMP组

N e t / 3并不包括一个 S N M P代理。一个针对 N e t / 3的S N M P代理是作为一个进程来实现的,


它根据SNMP的要求通过 2.2节描述的机制来访问这些内核统计。
N e t / 3收集大多数 M I B - I I变量并且能被 S N M P代理直接访问,而其他的变量则要通过间接
的方式来获得。 M I B - I I变量分为三类: ( 1 )简单变量,例如一个整数值、一个时间戳或一个字
节串;( 2 )简单变量的列表,例如一个单独的路由项或一个接口描述项; ( 3 )表的列表,例如整
个路由表和所有接口实体的列表。
ISODE包含有一个 Net/3 SNMP代理例子。 ISODE的信息见附录B。
图3 - 4所示的是一个为 S N M P接口组维护的简单变量。我们在后面的图 4 - 7中描述S N M P接
口表。
SNMP变量 Net/3变量 说 明

ifNumber if_index + 1 i f _ i n d e x是系统中最后一个接口的索引值,并且起始


为0;加1来获得系统中接口个数 i f N u m b e r

图3-4 在接口组中的一个简单的 SNMP变量

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 51
3.3 ifnet结构

结构i f n e t中包含所有接口的通用信息。在系统初始化期间,分别为每个网络设备分配
一个独立的 i f n e t结构。每个 i f n e t结构有一个列表,它包含这
个设备的一个或多个协议地址。图 3-5说明了一个接口和它的地址
之间的关系。
在图3 - 5中的接口显示了 3个存放在 i f a d d r结构中的协议地
址。虽然一些网络接口,例如 S L I P,仅支持一个协议;而其他接
口,如以太网,支持多个协议并需要多个地址。例如,一个系统
可能使用一个以太网接口同时用于 I n t e r n e t和O S I两个协议。一个
类型字段标识每个以太网帧的内容,并且因为 I n t e r n e t和O S I协议
使用不同的编址方式,以太网接口必须有一个 I n t e r n e t地址和一个
O S I地址。所有地址用一个链表链接起来 (图3 - 5右侧的箭头 ),并
且每个结构包含一个回指指针指向相关的 i f n e t结构(图3 - 5左侧 图3-5 每个ifnet 结构有一个
的箭头)。 ifaddr 结构的列表

可能一个网络接口支持同一协议的多个地址。例如,在 N e t / 3中可能为一个以太网接口分
配两个Internet地址。
这个特点第一次是出现在 N e t / 2中。当为一个网络重编地址时,一个接口有两个
IP地址是有用的。在过渡期间,接口可以接收老地址和新地址的分组。
结构ifnet比较大,我们分五个部分来说明:
• 实现信息
• 硬件信息
• 接口统计
• 函数指针
• 输出队列
图3-6所示的是包含在结构 ifnet中的实现信息。

图3-6 ifnet 结构:实现信息

80-82 i f _ n e x t把所有接口的 i f n e t结构链接成一个链表。函数 i f _ a t t a c h在系统初始


化期间构造这个链表。 i f _ a d d r l i s t 指向这个接口的 i f a d d r 结构列表 (图3 - 1 6 )。每个
ifaddr结构存储一个要用这个接口通信的协议的地址信息。
1. 通用接口信息

Linux公社 www.linuxidc.com
bbs.theithome.com
52 计计TCP/IP详解 卷 2:实现

83-86 if_name是一个短字符串,用于标识接口的类型,而 if_unit标识多个相同类型的


实例。例如,一个系统有两个 S L I P接口,每个都有一个 i f _ n a m e,包含两字节的“ s 1”和
一个i f _ u n i t。对第一个接口, i f _ u n i t为0;对第二个接口为 1。i f _ i n d e x在内核中唯一
地标识这个接口,这在 sysctl系统调用(见19.14节)以及路由域中要用到。
有时一个接口并不被一个协议地址唯一地标识。例如,几个 S L I P连接可以有同
样的本地IP地址。在这种情况下, if_index明确地指明这个接口。
i f _ f l a g s表明接口的操作状态和属性。一个进程能检查所有的标志,但不能改变在图 3-
7中“内核专用”列中作了记号的标志。这些标志用 4 . 4节讨论的命令 S I O C G I F F L A G S和
SIOCSIFFLAGS来访问。
if_flags 内核专用 说 明

IFF_BROADCAST • 接口用于广播网
IFF_MULTICAST • 接口支持多播
IFF_POINTOPOINT • 接口用于点对点网络
IFF_LOOPBACK 接口用于环回网络
IFF_OACTIVE • 正在传输数据
IFF_RUNNING • 资源已分配给这个接口
IFF_SIMPLEX • 接口不能接收它自己发送的数据
IFF_LINK0 见正文 由设备驱动程序定义
IFF_LINK1 见正文 由设备驱动程序定义
IFF_LINK2 见正文 由设备驱动程序定义
IFF_ALLMULTI 接口正接收所有多播分组
IFF_DEBUG 这个接口允许调试
IFF_NOARP 在这个接口上不使用 ARP协议
IFF_NOTRAILERS 避免使用尾部封装
IFF_PROMISC 接口接收所有网络分组
IFF_UP 接口正在工作

图3-7 if_flags 值

IFF_BROADCAST和IFF_POINTOPOINT标志是互斥的。
宏I F F _ C A N T C H A N G E是对所有在“内核专用”列中作了记号的标志进行按位
“或”操作。
设备专用标志 (I F F _ L I N Kx)对于一个依赖这个设备的进程可能是可修改的,也
可能是不可修改的。例如,图 3-29显示了这些标志是如何被 SLIP驱动程序定义的。
2. 接口时钟
87 if_timer以秒为单位记录时间,直到内核为此接口调用函数 if_watchdog为止。这个
函数用于设备驱动程序定时收集接口统计,或用于复位运行不正确的硬件。
3. BSD分组过滤器
88-89 下面两个成员, if_pcount和if_bpf,支持BSD分组过滤器 (BPF)。通过BPF,一
个进程能接收由此接口传输或接收的分组的备份。当我们讨论设备驱动程序时,还要讨论分
组是如何通过BPF的。BPF在第31章讨论。
ifnet结构的下一个部分显示在图 3-8中,它用来描述接口的硬件特性。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 53

图3-8 ifnet 结构:接口特性

N e t / 3和本书使用第 1 3 8行~ 1 4 3行的# d e f i n e语句定义的短语来表示 i f n e t的成


员。
4. 接口特性
90-92 i f _ t y p e指明接口支持的硬件地址类型。图 3-9列出了n e t / i f _ t y p e s . h中几个公
共的if_type值。
if_type 说 明

IFT_OTHER 未指明
IFT_ETHER 以太网
IFT_ISO88023 IEEE 802.3以太网(CSMA/CD)
IFT_ISO88025 IEEE 802.5令牌环
IFT_FDDI 光纤分布式数据接口
IFT_LOOP 环回接口
IFT_SLIP 串行线IP

图3-9 if_type :数据链路类型

93-94 if_addrlen是数据链路地址的长度,而 if_hdrlen是由硬件附加给任何分组的首


部的长度。例如,以太网有一个长度为 6字节的地址和一个长度为 14字节的首部 (图4-8)。
95 if_mtu是接口传输单元的最大值:接口在一次输出操作中能传输的最大数据单元的字节
数。这是控制网络和传输协议创建分组大小的重要参数。对于以太网来说,这个值是 1500。
96-97 if_metric通常是0;其他更大的值不利于路由通过此接口。 if_baudrate指定接
口的传输速率,只有 SLIP接口才设置它。
接口统计由图3-10中显示的下一组 ifnet接口成员来收集。
5. 接口统计
98-111 这些统计大多数是不言自明的。当分组传输被共享媒体上其他传输中断时,
i f _ c o l l i s i o n s加1。i f _ n o p r o t o统计由于协议不被系统或接口支持而不能处理的分组数

Linux公社 www.linuxidc.com
bbs.theithome.com
54 计计TCP/IP详解 卷 2:实现

(例如:仅支持 I P的系统接收到一个 O S I分组)。如果一个非 I P分组到达一个 S L I P接口的输出队


列时,if_noproto加1。

图3-10 结构ifnet :接口统计

这些统计在 N e t / 1中不是 i f n e t 结构的一部分。它们被加入来支持接口的标准


SNMP MIB-II变量。
i f _ i q d r o p s仅被S L I P设备驱动程序访问。当 I F _ D R O P被调用时, S L I P和其
他网络驱动程序把 i f _ s n d . i f q _ d r o p s (图3 - 1 3 )加1。在 S N M P 统计加入前,
i f q _ d r o p s就已经存在于 BSD软件中了。 ISODE SNMP代理忽略i f _ i q d r o p s而使
用if_snd.ifq_drops。
6. 改变时间戳
112-113 if_lastchange记录任何统计改变的最近时间。
Net/3和本书又一次用从 144行到155行的# d e f i n e语句定义的短名来指明 i f n e t
的成员。
结构i f n e t的下一个部分,显示在图 3 - 11中,它包含指向标准接口层函数的指针,它们
把设备专用的细节从网络层分离出来。每个网络接口实现这些适用于特定设备的函数。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 55

图3-11 结构ifnet :接口过程

7. 接口函数
114-129 在系统初始化时,每个设备驱动程序初始化它自己的 i f n e t结构,包括 7个函数
指针。图3-12说明了这些通用函数。
我们在Net/3中常会看到注释 / * X X X *。它提醒读者这段代码是易混淆的,包
/
括不明确的副作用,或是一个更难问题的快速解决方案。在这里,它指示 i f _ d o n e
不在Net/3中使用。

函 数 说 明

if_init 初始化接口
if_output 对要传输的输出分组进行排队
if_start 启动分组的传输
if_done 传输完成后的清除 (未用)
if_ioctl 处理I/O控制命令
if_reset 复位接口设备
if_watchdog 周期性接口例程

图3-12 结构ifnet :函数指针

在第4章我们要查看以太网、 SLIP和环回接口的设备专用函数,内核通过 i f n e t结构中的


这些指针直接调用它们。例如,如果 ifp指向一个ifnet结构,
(*ifp->if_start)(ifp)

调用这个接口的设备驱动程序的 if_start函数。
结构ifnet中剩下的最后一个成员是接口的输出队列,如图 3-13所示。
130-137 i f _ s n d是接口输出分组队列,每个接口有它自己的 i f n e t结构,即它自己的输
出队列。 i f q _ h e a d指向队列的第一个分组 (下一个要输出的分组 ),i f q _ t a i l指向队列最后
一个分组, i f _ l e n是当前队列中分组的数目,而 i f q _ m a x l e n是队列中允许的缓存最大个
数。除非驱动程序修改它,这个最大值被设置为 5 0 (来源于全局整数 i f q m a x l e n,它在编译
期间根据 I F Q _ M A X L E N初始化而来 )。队列作为一个 m b u f链的链表来实现。 i f q _ d r o p s统计

Linux公社 www.linuxidc.com
bbs.theithome.com
56 计计TCP/IP详解 卷 2:实现

因为队列满而丢弃的分组数。图 3-14列出了那些访问队列的宏和函数。

图3-13 结构ifnet :输出队列

函 数 说 明

IF_QFULL ifq是否满
int IF_QFULL(struct ifqueue *ifq);
IF_DROP IF_DROP仅将与ifq关联的ifq_drops加1。这个名字会引起误导:调用者丢弃这个分组
void IF_DROP(struct ifqueue *ifq);
IF_ENQUEUE 把分组m追加到ifq队列的后面。分组通过 mbuf首部中的m _ n e x t p k t链接在一起
void IF_ENQUEUE(struct ifqueue *ifq, struct mbuf *m);
IF_PREPEND 把分组m插入到ifq队列的前面
void IF_PREPEND(struct ifqueue *ifq, struct mbuf *m);
IF_DEQUEUE 从ifq队列中取走第一个分组。 m指向取走的分组,若队列为空,则 m为空值
void IF_DEQUEUE(struct ifqueue *ifq, struct mbuf *m);
if_qflush 丢弃队列ifq中的所有分组,例如,当一个接口被关闭了
v o i d i f _ q f l u s h( s t r u c t i f q u e u e ifq)
* ;

图3-14 fiqueue 例程

前5个例程是定义在 n e t / i f . h中的宏,最后一个例程 i f _ q f l u s h是定义在 n e t / i f . c


中的一个函数。这些宏经常出现在下面这样的程序语句中:

这段代码试图把一个分组加到队列中。如果队列满, I F _ D R O P把i f q _ d r o p s加1,并且分组


被丢弃。可靠协议如 TCP会重传丢弃的分组。使用不可靠协议 (如UDP)的应用程序必须自己检
测和处理重传。
访问队列的语句被 s p l i m p和s p l x括起来,阻止网络中断,并且防止在不确定状态时网
络中断服务例程访问此队列。
在s p l x之前调用 m _ f r e e m,是因为这段 m b u f代码有一个临界区运行在 s p l i m p
级别上。若在 m _ f r e e m前调用 s p l x,在m _ f r e e m中进入另一个临界区 ( 2 . 5节)是浪
费效率的。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 57
3.4 ifaddr结构

我们要看的下一个结构是接口地址结构, i f a d d r,它显示在图 3 - 1 5中。每个接口维护一


个i f a d d r结构的链表,因为一些数据链路,如以太网,支持多于一个的协议。一个单独的
i f a d d r结构描述每个分配给接口的地址,通常每个协议一个地址。支持多地址的另一个原因
是很多协议,包括 T C P / I P,支持为单个物理接口指派多个地址。虽然 N e t / 3支持这个特性,但
很多TCP/IP实现并不支持。

图3-15 结构ifaddr

217-219 结构i f a d d r通过i f a _ n e x t把分配给


更多其他接口
一个接口的所有地址链接起来,它还包括一个指回
接口的i f n e t结构的指针i f a _ i f p。图3 - 1 6显示了
结构ifnet与ifaddr之间的关系。
220 i f a _ a d d r 指向接口的一个协议地址,而
i f a _ n e t m a s k 指向一个位掩码,它用于选择
i f a _ a d d r中的网络部分。地址中表示网络部分的
比特在掩码中被设置为 1,地址中表示主机的部分
被设置为 0。两个地址都存放在 s o c k a d d r结构中
( 3 . 5节)。图3 - 3 8显示了一个地址及其掩码结构。对
于IP地址,掩码选择 IP地址中的网络和子网部分。
221-223 ifa_dstaddr(或 它 的 别 名
i f a _ b r o a d a d d r)指向一个点对点链路上的另一端
的接口协议地址或指向一个广播网中分配给接口的
广播地址 (如以太网 )。接口的 i f n e t结构中互斥的
两个标志 I F F _ B R O A D C A S T和I F F _ P O I N T O P O I N T
图3-16 结构ifnet 和ifaddr
(图3-7)指示接口的类型。
224-228 ifa_rtrequest、ifa_flags和ifa_metric支持接口的路由查找。
i f a _ r e f c n t统计对结构i f a d d r的引用。宏I F A F R E E仅在引用计数降到 0时才释放这个
结构,例如,当地址被命令 S I O C D I F A D D Ri o c t l删除时。结构 i f a d d r使用引用计数是因
为接口和路由数据结构共享这些结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
58 计计TCP/IP详解 卷 2:实现

如果有其他对 i f a d d r的引用, I F A F R E E将计数器加 1并返回。这是一个通用的


方法,除了最后一个引用外,它避免了每次都调用一个函数的开销。如果是最后一
个引用, IFAFREE调用函数ifafree,来释放这个结构。

3.5 sockaddr结构

一个接口的编址信息不仅仅只包括一个主机地址。 N e t / 3在通用的 s o c k a d d r结构中维护


主机地址、广播地址和网络掩码。通过使用一个通用的结构,将硬件与协议专用的地址细节
相对于接口层隐藏起来。
图3 - 1 7显示的是这个结构的当前定义及早期 B S D版的定义—结构o s o c k a d d r。图3 - 1 8
说明了这些结构的组织。

图3-17 结构sockaddr 和osockaddr

14字节

14字节

图3-18 结构sockaddr 和osockaddr( 省略了前缀 sa_)

在很多图中,我们省略了成员名中的公共前缀。在这里,我们省略了 sa_前缀。
1. Sockaddr结构
120-124 每个协议有它自己的地址格式。 N e t / 3在一个 s o c k a d d r结构中处理通用的地址。
sa_len指示地址的长度 (OSI和Unix域协议有不同长度的地址 ),sa_family指示地址的类型。
图3-19列出了地址族(address family)常量,其中包括我们遇到的。
当指明为 A F _ U N S P E C时,一个 s o c k a d d r的内容要根据情况而定。大多数情况
下,它包含一个以太网硬件地址。
成员s a _ l e n和s a _ f a m i l y允许协议无关代码操作来自多个协议的变长的 s o c k a d d r结
构。剩下的成员 s a _ d a t a,包含一个协议相关格式的地址。 s a _ d a t a定义为一个14字节的数
组,但当 s o c k a d d r结构覆盖更大的内存空间时, s a _ d a t a可能会扩展到 2 5 3字节。s a _ l e n

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 59
仅有一个字节,因此整个地址,包括 sa_len和
sa_family 协 议
sa_family必须不超过 256字节。
AF_INET Internet
这是C语言的一种通用技术,它允许程序员把 AF_ISO,AF_OSI OSI
AF_UNIX Unix
一个结构中的最后一个成员看成是可变长的。
AF_ROUTE 路由表
每个协议定义一个专用的 s o c k a d d r结构,该结构 AF_LINK 数据链路
AF_UNSPEC (见正文)
复制成员 s a _ l e n和s a _ f a m i l y,但按那个协议的要
求来定义成员 s a _ d a t a。存储在 s a _ d a t a中的地址是 图3-19 sa_family 常量
一个传输地址;它包含足够的信息来标识同一主机上的多个通信端点。在第 6章我们要查看
Internet地址结构 sockaddr_in,它包含了一个 IP地址和一个端口号。
2. Osockaddr结构
271-274 结构osoc k a d d r是4.3BSD Reno版本以前的 s o c k a d d r定义。因为在这个定义中
一个地址的长度不是显式地可用,所以它不能用来写处理可变长地址的协议无关代码。 OSI协
议使用可变长地址,为了包括 O S I协议,使得在 N e t / 3的s o c k a d d r定义中有了我们所见的改
变。结构osockaddr是为了支持对以前编译的程序的二进制兼容。
在本书中我们省略了二进制兼容代码。

3.6 ifnet与ifaddr的专用化

结构i f n e t和i f a d d r包含适用于所有网络接口和协议地址的通用信息。为了容纳其他设


备和协议专用信息,每个设备定义了并且每个协议分配了一个专用化版本的 ifnet和ifaddr
结构。这些专用化的结构总是包含一个 i f n e t或i f a d d r结构作为它们的第一个成员,这样
无须考虑其他专用信息就能访问这些公共信息。
多数设备驱动程序通过分配一个专用化的 i f n e t结构的数组来处理同一类型的多个接口,
但其他设备(例如环回设备 )仅处理一个接口。图 3-20所示的是我们的例子接口的专用化 i f n e t
结构的组织。

以太网

环回

图3-20 设备相关的结构中的 ifnet 结构的组织

Linux公社 www.linuxidc.com
bbs.theithome.com
60 计计TCP/IP详解 卷 2:实现

注意,每个设备的结构以一个 i f n e t开始,接
下来全是设备相关的数据。环回接口只声名了一个
i f n e t 结构,因为它不要求任何设备相关的数据。
在图3 - 2 0中,我们显示的以太网和 S L I P驱动程序的
结构 s o f t c带有数组下标 0,因为两个设备都支持
多个接口。任何给定类型的接口的最大个数由内核
链路层地址
建立时的配置参数来限制。
结构a r p c o m(图3-26)对于所有以太网设备是通
用的,并且包含地址解析协议 ( A R P )和以太网多播
信息。结构 l e _ s o f t c(图3 - 2 5 )包含专用于 L A N C E
以太网设备驱动器的其他信息。 Internet地址

每个协议把每个接口的地址信息存储在一个专
用化的 i f a d d r结构的列表中。以太网协议使用一
个i n _ i f a d d r结构 ( 6 . 5节),而 O S I协议使用一个
i s o _ i f a d d r结构。另外,当接口被初始化时,内 OSI地址
核为每个接口分配了一个链路层地址,它在内核中
标识这个接口。
内 核通 过 分 配 一 个 i f a d d r 结 构 和 两 个 图3-21 一个包含链路层地址、 Internet地址
和OSI地址的接口地址列表
s o c k a d d r _ d l结构(一个是链路层地址本身,一个
是地址掩码 )来构造一个链路层地址。结构 s o c k a d d r _ d l可被OSI、ARP和路由算法访问。图
3-21显示的是一个带有一个链路层地址、一个 Internet地址和一个OSI地址的以太网接口。 3.11
节说明了链路层地址 (ifaddr和两个sockaddr_ld结构)的构造和初始化。

3.7 网络初始化概述

所有我们说明的结构是在内核初始化时分配和互相链接起来的。在本节我们大致概述一
下初始化的步骤。在后面的章节,我们说明特定设备的初始化步骤和特定协议的初始化步骤。
有些设备,例如 S L I P 和环回接口,完全用软件来实现。这些伪设备用存储在全局
p d e v i n i t数组中的一个 p d e v i n i t结构来表示 (图3 - 2 2 )。在内核配置期间构造这个数组。例
如:

图3-22 结构pdevinit

120-123 对于 S L I P和环回接口,在结构 p d e v i n i t中, p d e v _ a t t a c h分别被设置为

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 61
s l a t t a c h和l o o p a t t a c h。当调用这个 a t t a c h函数时, p d e v _ c o u n t作为传递的唯一参数,
它指定创建的设备个数。只有一个环回设备被创建,但如果管理员适当配置 S L I P项可能有多
个SLIP设备被创建。
网络初始化函数从 main开始显示在图3-23中。

图3-23 main 函数:网络初始化

70-96 cpu_startup查找并初始化所有连接到系统的硬件设备,包括任何网络接口。
97-174 在内核初始化硬件设备后,它调用包含在 p d e v i n i t数组中的每个 p d e v _ a t t a c h
函数。
175-234 ifinit和domaininit完成网络接口和协议的初始化,并且 scheduler开始内
核进程调度。ifinit和domaininit在第7章讨论。
在下面几节中,我们说明以太网、 SLIP和环回接口的初始化。

3.8 以太网初始化

作为c p u _ s t a r t u p的一部分,内核查找任何连接的网络设备。这个进程的细节超出了本
书的范围。一旦一个设备被识别,一个设备专用的初始化函数就被调用。图 3 - 2 4显示的是我
们的3个例子接口的初始化函数。

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
62 计计TCP/IP详解 卷 2:实现

每个设备驱动程序为一个网络接口初始化一个专用 设 备 初始化函数
化的i f n e t结构,并调用 i f _ a t t a c h把这个结构插入 LANCE以太网 leattach
到接口链表中。显示在图 3 - 2 5中的结构 le _ s o f t c是我 SLIP slattach
们的例子以太网驱动程序的专用化 ifnet结构(图3-20)。 环回 loopattach

1. le_softc结构 图3-24 网络接口初始化函数


69-95 在i f _ l e . c中声明了一个 l e _ s o f t c结构(有
N L E成员)的数组。每个结构的第一个成员是 s c _ a c,一个 a r p c o m结构,它对于所有以太网
接口都是通用的,接下来是设备专用成员。宏 s c _ i f和s c _ a d d r简化了对结构 i f n e t及存储
在结构arpcom(sc_ac)中的以太网地址的访问,如图 3-26所示。

图3-25 结构le_softc

图3-26 结构arpcom

2. arpcom结构
95-101 结构arpcom的第一个成员ac_if是一个ifnet结构,如图 3-20所示。ac_enaddr
是以太网硬件地址,它是在 c p u _ s t a r t u p期间内核检测设备时由 L A N C E设备驱动程序从硬
件上复制的。对于我们的例子驱动程序,这发生在函数 l e a t t a c h中(图3 - 2 7 )。a c _ i p a d d r
是上一个分配给此设备的 I P地址。我们在 6 . 6节讨论地址的分配,可以看到一个接口可以有多
个I P地址。见习题 6 . 3。a c _ m u l t i a d d r s是一个用结构 e t h e r _ m u l t i表示的以太网多播地
址的列表。 ac_multicnt统计这个列表的项数。多播列表在第 12章讨论。
图3-27所示的是LANCE以太网驱动程序的初始化代码。
106-115 内核在系统中每发现一个 LANCE卡都调用一次leattach。

只有一个指向一个 h p _ d e v i c e结构的参数,它包含了 HP专用信息,因为它是专


为HP工作站编写的驱动程序。

l e指向此卡的专用化 i f n e t结构(图3-20),i f p指向这个结构的第一个成员 s c _ i f,一个


通用的ifnet结构。图3-27并不包括设备专用初始化代码,它在本书中不予讨论。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 63

图3-27 函数leattach
3. 从设备复制硬件地址
126-137 对于LANCE设备,由厂商指派的以太网地址在这个循环中以每次半个字节 (4 bit)
从设备复制到sc_addr(即sc_ac.ac_enaddr—见图3-26)。
l e s t d是一个设备专用的位移表,用于定位 h p _ a d d r的相关信息, h p _ a d d r指
向LANCE专用信息。
通过printf语句将完整的地址输出到控制台,来指示此设备存在并且可操作。
4. 初始化ifnet结构
150-157 l e a t t a c h从h p _ d e v i c e结构把设备单元号复制到 i f _ u n i t来标识同类型的多

Linux公社 www.linuxidc.com
bbs.theithome.com
64 计计TCP/IP详解 卷 2:实现

个接口。这个设备的 i f _ n a m e是“l e”;i f _ m t u为1 5 0 0字节(E T H E R M T U),以太网的最大


传输单元; i f _ i n i t、i f _ r e s e t、i f _ i o c t l、i f _ o u t p u t和i t _ s t a r t都指向控制网络
接口的通用函数的设备专用实现。 4.1节说明这些函数。
158 所有的以太网设备都支持 I F F _ B R O A D C A S T。L A N C E设备不接收它自己发送的数据,
因此被设置为IFF_SIMPLEX。支持多播的设备和硬件还要设置 IFF_MULTICAST。
159-162 b p f a t t a c h登记有 B P F的接口,在图 3 1 - 8中说明。函数 i f _ a t t a c h把初始化了
的ifnet结构插入到接口的链表中 (3.11节)。

3.9 SLIP初始化

依赖标准异步串行设备的 S L I P接口在调用 c p u _ s t a r t u p时初始化。当 m a i n直接通过


SLIP的pdevinit结构中的指针pdev_attach调用slattach时,SLIP伪设备被初始化。
每个SLIP接口由图3-28中的一个sl_softc结构来描述。

图3-28 结构sl_softc

43-54 与所有接口结构一样, sl_softc有一个ifnet结构并且后面跟着设备专用信息。


除了在 i f n e t结构中的输出队列外,一个 S L I P设备还维护另一个队列 s c _ f a s t q,它用
于要求低时延服务的分组—典型地由交互应用产生。
s c _ t t y p指向关联的终端设备。指针 s c _ b u f和s c _ e p分别指向一个接收 S L I P分组的缓
存的第一个字节和最后一个字节。 s c _ m p指向下一个接收字节的地址,并在另一个字节到达
时向前移动。
SLIP定义的4个标志显示在图 3-29中。

常 量 s c _ s o f t c成员 说 明

SC_COMPRESS sc_if.if_flags I F F _ L I N K 0;压缩TCP通信


SC_NOICMP sc_if.if_flags I F F _ L I N K 1;禁止ICMP通信
SC_AUTOCOMP sc_if.if_flags I F F _ L I N K 2;允许TCP自动压缩
SC_ERROR sc_flags 检测到错误;丢弃接收帧

图3-29 SLIP的if_flags 和sc_flags 值

S L I P在i f n e t结构中定义了 3个接口标志预留给设备驱动程序,另一个标志定义在结构


sl_softc中。
s c _ e s c a p e 用于串行线的 I P封装机制 ( 5 . 3节),而 T C P首部压缩信息 ( 2 9 . 1 3节)保留在

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 65
sc_comp中。
指针sc_bpf指向SLIP设备的BPF信息。
结构sl_softc由slattach初始化,如图3-30所示。
1 3 5 - 1 5 2 不像 l e a t t a c h 一次仅初始化一个接口,内核只调用一次 s l a t t a c h,并且
s l a t t a c h初始化所有的 S L I P接口。硬件设备在内核执行 c p u _ s t a r t u p被发现时初始化,
而伪设备都是在 m a i n为这个设备调用 p d e v _ a t t a c h函数时被初始化的。一个 S L I P设备的
i f _ m t u为2 9 6字节(S L M T U)。这包括标准的 2 0字节I P首部、标准的 2 0字节T C P首部和2 5 6字节
的用户数据 (5.3节)。

图3-30 函数slattach

一个S L I P网络由位于一个串行通信线两端的两个接口组成。 s l a t t a c h在i f _ f l a g s中


设置IFF_POINTOPOINT、SC_AUTOCOMP和IFF_MULTICAST。
SLIP接口限制它的输出分组队列 i f _ s n d的长度为50,并且它自己的接口队列 s c _ f a s t q
的长度为 3 2。图3 - 4 2显示i f _ s n d队列的长度默认为 5 0 (i f q m a x l e n),因此,如果设备驱动
程序选择一个长度,这里的初始化是多余的。
以太网设备驱动程序不显式地设置它的输出队列的长度,它依赖于 i f i n i t (图
3-42)把它设置为系统的默认值。
i f _ a t t a c h需要一个指向一个 i f n e t结构的指针,因此 s l a t t a c h将s c _ i f的地址传递
给if_attach,sc_if是一个第一个成员为结构 sl_softc的ifnet结构。
专用程序s l a t t a c h在内核初始化后运行 (从初始化文件 / e t c / n e t s t a r t),并通过打开
串行设备和执行 ioctl命令(5.3节)添加SLIP接口和一个异步串行设备。
153-155 对于每个SLIP设备,slattach调用bpfattach来登记有BPF的接口。

3.10 环回初始化

最后显示环回接口的初始化。环回接口把输出分组放回到相应的输入队列中。接口没有
Linux公社 www.linuxidc.com
bbs.theithome.com
66 计计TCP/IP详解 卷 2:实现

相关联的硬件设备。环回伪设备在 m a i n通过环回接口的 p d e v i n i t结构中的 p d e v _ a t t a c h


指针直接调用loopattach时初始化。图3-31所示的是函数loopattach。

图3-31 环回接口初始化

41-56 环回i f _ m t u被设置为 1 5 3 6字节(L O M T U)。在i f _ f l a g s中设置I F F _ L O O P B A C K和


IFF_MULTICAST。一个环回接口没有链路首部和硬件地址,因此 if_hdrlen和if_addrlen
被设置为0。if_attach完成ifnet结构的初始化并且 bpfattach登记有BPF的环回接口。
环回MTU至少有1576(40 + 3×512)留给一个标准的 TCP/IP首部。例如 Solaris 2.3
环回M T U设置为 8232(40 + 8×1 0 2 4 )。这些计算基于 I n t e r n e t协议;而其他协议可能
有大于40字节的默认首部。

3.11 if_attach函数

前面显示的三个接口初始化函数都调用 i f _ a t t a c h来完成接口的 i f n e t结构的初始化,


并把这个结构插入到先前配置的接口的列表中。在 i f _ a t t a c h中,内核也为每个接口初始化
并分配一个链路层地址。图 3-32说明了由if_attach构造的数据结构。

图3-32 ifnet 列表

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 67
在图3 - 3 2中,i f _ a t t a c h被调用了三次:以一个 l e _ s o f t c结构为参数从 l e a t t a c h调
用,以一个 s l _ s o f t c 结构为参数从 s l a t t a c h调用,以一个通用 i f n e t结构为参数从
l o o p a t t a c h调用。每次调用时,它向 i f n e t列表中添加一个新的 i f n e t结构,为这个接口
创建一个链路层 i f a d d r 结构 ( 包含两个 s o c k a d d r _ d l 结构,图 3 - 3 3 ) ,并且初始化
ifnet_addrs数组中的一项。

图3-33 结构sockaddr_dl

图3-20显示了包含在le_softc[0]和sl_softc[0]中嵌套的结构。
初始化以后,接口仅配置链路层地址。例如, I P地址直到后面讨论的 i f c o n f i g程序才
配置(6.6节)。
链路层地址包含接口的一个逻辑地址和一个硬件地址 (如果网络支持,例如 l e 0的一个 4 8
b i t以太网地址 )。在A R P和O S I协议中要用到这个硬件地址,而一个 s o c k a d d r _ d l中的逻辑
地址包含一个名称和这个接口在内核中的索引数值,它支持用于在接口索引和关联 i f a d d r结
构(ifa_ifwithnet,图6-32)间相互转换的表查找。
结构sockaddr_dl显示在图3-33中。
55-57 回忆图 3 - 1 8,s d l _ l e n指明了整个地址的长度,而 s d l _ f a m i l y指明了地址族类,
在此例中为 AF_LINK。
58 sdl_index在内核中标识接口。图 3-32中的以太网接口会有一个为 1的索引, SLIP接口
的索引为 2,而环回接口的索引为 3。全局整数变量 i f _ i n d e x包含的是内核最近分配的一个
索引值。
60 sdl_type根据这个数据链路地址的 ifnet结构的成员 if_type进行初始化。
61-68 除了一个数字索引,每个接口有一个由结构 ifnet的成员if_name和if_unit组成
的文本名称。例如,第一个 S L I P接口叫“ s l 0”,而第二个叫“ s l 1”。文本名称存储在数组
sdl_data的前面,并且sdl_nlen为这个名称的字节长度 (在我们的SLIP例子中为 3)。
数据链路地址也存储在这个结构中。宏 L L A D D R将一个指向s o c k a d d r _ d l结构的指针转
换成一个指向这个文本名称的第一个字节的指针。 s d l _ a l e n是硬件地址的长度。对于一个
以太网设备, 48 bit硬件地址位于结构 s o c k a d d r _ d l的这个文本名称的前面。图 3 - 3 8所示的
是一个初始化了的 sockaddr_dl结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
68 计计TCP/IP详解 卷 2:实现

Net/3不使用sdl_slen。
i f _ a t t a c h更新两个全局变量。第一个是 i f _ i n d e x,它存放系统中的最后一个接口的
索引;第二个是 i f n e t _ a d d r s,它指向一个 i f a d d r指针的数组。这个数组的每项都指向一
个接口的链路层地址。这个数组提供对系统中每个接口的链路层地址的快速访问。
函数i f _ a t t a c h较长,并且有几个奇怪的赋值语句。从图 3 - 3 4开始,我们分 5个部分讨
论这个函数。
59-74 i f _ a t t a c h有一个参数:i f p,这是一个指向 i f n e t结构的指针,由网络设备驱动
程序初始化。 N e t / 3在一个链表中维护所有这些 i f n e t结构,全局指针 i f n e t指向这个链表的
首部。 w h i l e循环查找链表的尾部,并将链表尾部的空指针的地址存储到 P中。在循环后,新
i f n e t 结构被接到这个 i f n e t链表的尾部, i f _ i n d e x 加1 ,并且将新索引值赋给 i f p
->if_index。

图3-34 函数if_attach :分配接口索引

1. 必要时调整 ifnet_addrs数组的大小
75-85 第一次调用 if_attach时,数组ifnet_addrs不存在,因此要分配 16(16=8<<1)项
的空间。当数组满时,一个两倍大的新数组被分配,并且老数组中的项被复制到新的数组中。
i f _ i n d e x l i m是i f _ a t t a c h私有的一个静态变量。 i f _ i n d e x l i m通过< < =操
作符来更新。
图3-34中的函数 m a l l o c和f r e e不是同名的标准 C库函数。内核版的第二个参数指明一个

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 69
类型,内核中可选的诊断代码用它来检测程序错误。如果 malloc的第三个参数为 M_WAITOK,
且函数需要等待释放的可用内存,则阻塞调用进程。如果第三个参数为 M _ D O N T W A I T,则当
内存不可用时,函数不阻塞并返回一个空指针。
函数i f _ a t t a c h的下一部分显示在图 3 - 3 5中,它为接口准备一个文本名称并计算链路层
地址的长度。

图3-35 if_attach 函数:计算链路层地址大小

2. 创建链路层名称并计算链路层地址的长度
86-99 i f _ a t t a c h用i f _ u n i t和i f _ n a m e组装接口的名称。函数 s p r i n t _ d将i f _ u n i t
的数值转换成一个串并存储到 w o r k b u f中。m a s k l e n是s o c k a d d r _ d l数组中s d l _ d a t a前
面的信息所占用的字节数加上这个接口的文本名称的大
小(n a m e l e n + u n i t l)。函数对s
en ocksize进行上
舍入, s o c k s i z e 是 m a s k l e n 加上硬件地址长度
(i f _ a d d r l e n),上舍入为一个长整型 (R O U N D U P)。如
果它小于一个 s o c k a d d r _ d l结构的长度,就使用标准
的s o c k a d d r _ d l结构。i f a s i z e是一个 i f a d d r结构
的大小加上两倍的 s o c k s i z e ,因此,它能容纳结构
sockaddr_dl。
在函数的下一个部分中, i f _ a t t a c h分配结构并 图3-36 在if_attach 中分配的
将结构连接起来,如图 3-36所示。 链路层地址和掩码

图3 - 3 6中,在 i f a d d r结构与两个 s o c k a d d r _ d l结构间有一个空隙来说明它们


分配在一个连续的内存中但没有定义在一个 C结构中。
像图3-36所示的组织还出现在结构 in_ifaddr中;这个结构的通用 ifaddr部分中的指针
指向在这个结构的设备专用部分中的专用化 sockaddr结构,在本例中是结构 sockaddr_dl。
图3-37所示的是这些结构的初始化。
3. 地址
100-116 如果有足够的内存可用, b z e r o把新结构清零,并且 s d l指向紧接着 i f n e t结构
的第一个sockaddr_dl。若没有可用内存,代码被忽略。
s d l _ l e n被设置为结构 s o c k a d d r _ d l的长度,并且 s d l _ f a m i l y被设置为 A F _ L I N K。

Linux公社 www.linuxidc.com
bbs.theithome.com
70 计计TCP/IP详解 卷 2:实现

用 i f _ n a m e 和 u n i t n a m e 组成的文本名称存放在 s d l _ d a t a 中,而它的长度存放在
s d l _ n l e n中。接口的索引被复制到 s d l _ i n d e x中,而接口的类型被复制到 s d l _ t y p e中。
分配的结构被插入到数组 i f n e t _ a d d r s中,并通过 i f a _ i f p和i f a _ a d d r l i s t链接到结
构 i f n e t。最后,结构 s o c k a d d r _ d l 用i f a _ a d d r连接到 i f n e t 结构。以太网接口用
arp_rtrequest取代默认函数link_rtrequest 。环回接口装入函数
l o o p _ r t r e q u e s t。我们在第 1 9章和第 2 1章讨论 i f a _ r t r e q u e s t和a r p _ r t r e q u e s t。
而 l i n k r t r e q u e s t 和l o o p _ r t r e q u e s t 留给读者自己去研究。以上完成了第一个
s o c k a d d r _ d l结构的初始化。

图3-37 函数if_attach :分配并初始化链路层地址

4. 掩码
117-123 第二个s o c k a d d r _ d l结构是一个比特掩码,用来选择出现在第一个结构中的文
本名称。 i f a _ n e t m a s k从结构 i f a d d r指向掩码结构 (在这里是选择接口文本名称而不是网
络掩码)。while循环把与名称对应的那些字节的每个比特都置为 1。
图3 - 3 8所示的是我们以太网接口例子的两个初始化了的 s o c k a d d r _ d l 结构。它的
if_name为“le”,if_unit为0,if_index为1。
图3-38中所示的是 ether_ifattach对这个结构初始化后的地址 (图3-41)。
图3-39所示的是第一个接口被 if_attach连接后的结构。
在if_attach的最后,以太网设备的函数 ether_ifattach被调用,如图3-40所示。
124-127 开始不调用 ether_ifattach (例如:从leattach),是因为它要把以太网硬件
地址复制到 if_attach分配的sockaddr_dl中。
XXX注释表示作者发现在此处插入代码比修改所有的以太网驱动程序要容易。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计71

以太网地址

(3 字节) (6 字节)

全0

9字节

11字节

图3-38 初始化了的以太网 sockaddr_dl 结构(省略了前缀 sdl_ )

图3-39 第一次调用 if_attach 后的ifnet 和sockaddr_dl 结构

图3-40 函数if_attach :以太网初始化

图3-41 函数ether_ifattach

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
72 计计TCP/IP详解 卷 2:实现

图3-41 (续)

5. ether_ifattach函数
函数ether_ifattach执行对所有以太网设备通用的 ifnet结构的初始化。
338-357 对于一个以太网设备, if_type为IFT_ETHER,硬件地址为6字节长,整个以太
网首部有14字节,而以太网 MTU为1500 (ETHERMTU)。
leattach已经指派了MTU,但其他以太网设备驱动程序可能没有执行这个初始化。
4 . 3节讨论以太网帧组织的更多细节。 f o r循环定位接口的链路层地址,然后初始化结构
s o c k a d d r _ d l 中的以太网硬件地址信息。在系统初始化时,以太网地址被复制到结构
arpcom中,现在被复制到链路层地址中。

3.12 ifinit函数

接口结构被初始化并链接到一起后, main (图3-23)调用ifinit,如图3-42所示。

图3-42 函数ifinit

43-51 for循环遍历接口列表,并把没有被接口的 attach函数设置的每个接口输出队列的


最大长度设置为 50 ( ifqmaxlen)。
输出队列的大小关键要考虑的是发送最大长度数据报的分组的个数。例如以太
网,若一个进程调用 s e n d t o发送65 507字节的数据,它被分片为 4 5个数据报片,并
且每个数据报片被放进接口的输出队列。若队列非常小,由于队列没有空间,进程
可能不能发送大的数据报。
i f _ s l o w t i m o启动接口的监视计时器。当一个接口时钟到期,内核会调用这个接口的把
关定时器函数。一个接口可以提前重设时钟来阻止把关定时器函数的调用,或者,若不需要
把关定时器函数,则可以把 if_timer设置为0。图3-43所示的是函数if_slowtimo。
338-343 if_slowtimo函数有一个参数 arg没有使用,但慢超时函数的原型 (7.4节)要求有
这个参数。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 3章 接 口 层计计 73

图3-43 函数if_slowtimo

344-352 i f _ s l o w t i m o 忽 略 i f _ t i m e r 为 0的 接 口 ; 若 i f _ t i m e r不 等 于 0,
i f _ s l o w t i m o把i f _ t i m e r减1,并在这个时钟到达 0时调用这个接口关联的 i f _ w a t c h d o g
函数。在调用 i f _ s l o w t i m o时,分组处理进程被 s p l i m p阻塞。返回前, i p _ s l o w t i m o调
用t i m e o u t,来以h z / I F N E T _ S L O W H Z时钟频率调度对它自己的调用。 h z是1秒钟内时钟滴
答数(通常是 1 0 0)。它在系统初始化时设置,并保持不变。因为 I F N E T _ S L O W H Z被定义为 1,
因此内核每赫兹调用一次 if_slowtimo,即每秒一次。
函数timeout调度的函数被内核的函数 callout回调。详见[Leffler er al. 1989]。

3.13 小结

在本章我们研究了结构 i f n e t和i f a d d r,它们被分配给在系统初始化时发现的每一个网


络接口。结构 i f n e t链接成i f n e t链表。每个接口的链路层地址被初始化,并被加到 i f n e t
结构的地址链表中,还存放到数组 if_addrs中。
我们讨论了通用 s o c k a d d r结构及其成员 s a _ f a m i l y和s a _ l e n,它们标识每个地址的
类型和长度。我们还查看了一个链路层地址的 sockaddr_dl结构的初始化。
在本章中,我们还介绍了在全书中要用到的三个网络接口例子。

习题

3.1 很多U n i x系统中的 n e t s t a t程序列出网络接口及其配置信息。在你接触的系统中试


一下命令 n e t s t a t - i。那个网络接口的名称 (i f _ n a m e)是什么?传输单元的最大
长度(if_mtu)是多少?
3.2 在i f _ s l o w t i m o (图3 - 4 3 )中,调用 s p l i m p和s p l x出现在循环的外面。与把这些
调用放到循环内部相比,这样安排有何优缺点?
3.3 为什么SLIP的交互队列比它的标准输出队列要短?
3.4 为什么if_hdrlen和if_addrlen不在slattach中初始化?
3.5 为SLIP和环回设备画一个与图 3-38类似的图。

Linux公社 www.linuxidc.com
bbs.theithome.com
第4章 接口:以太网
4.1 引言

在第3章中,我们讨论了所有接口要用到的数据结构及对这些数据结构的初始化。在本章
中,我们说明以太网设备驱动程序在初始化后是如何接收和传输帧的。本章的后半部分介绍
配置网络设备的通用 ioctl命令。第5章是SLIP和环回驱动程序。
我们不准备查看整个以太网驱动程序的源代码,因为它有大约 1 000行C代码(其中有一半
是一个特定接口卡的硬件细节 ),但要研究与设备无关的以太网代码部分,及驱动程序是如何
与内核其他部分交互的。
如果读者对一个驱动程序的源代码感兴趣, N e t / 3版本包括很多不同接口的源代码。要想
研究接口的技术规范,就要求能理解设备专用的命令。图 4-1所示的是 Net/3提供的各种驱动程
序,包括在本章我们要讨论的 LANCE驱动程序。
网络设备驱动程序通过 i f n e t结构(图3 - 6 )中的7个函数指针来访问。图 4 - 2列出了指向我
们的三个例子驱动程序的入口点。
输入函数不包含在图4-2中,因为它们是网络设备中断驱动的。中断服务例程的配置与硬件相
关,并且超出了本书的范围。我们要识别处理设备中断的函数,但不是这些函数被调用的机制。

设 备 文 件

DEC DEUNA接口 vax/if/if_de.c


3Com以太网接口 vax/if/if_ec.c
Excelan EXOS 204接口 vax/if/if_ex.c
Interlan以太网通信控制器 vax/if/if_il.c
Interlan NP100以太网通信控制器 vax/if/if_ix.c
Digital Q-BUS to NI适配器 vax/if/if_qe.c
CMC ENP-20以太网控制器 tahoe/if/if_enp.c
Excelan EXOS 202 (VME) & 203 (QBUS) tahoe/if/if_ex.c
ACC VERSAbus以太网控制器 tahoe/if/if_ace.c
AMD 7990 LANCE接口 hp300/dev/if_le.c
NE2000以太网 i386/isa/if_ne.c
Western Digital 8003以太网适配器 i386/isa/if_we.c

图4-1 Net/3中可用的以太网驱动程序

ifnet 以 太 网 SLIP 环 回 说 明

if_init leinit 硬件初始化


if_output ether_output slouput looutput 接收并对传输的帧进行排队
if_start lestart 开始传输帧
if_done 输出完成 (未用)
if_ioctl leioctl slioctl lcioctl 处理来自一个进程的 i o c t l命令
if_reset lereset 把设备复位到已知的状态
if_watchdog 监视设备故障或收集统计信息

图4-2 例子驱动程序的接口函数
Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 75
只有函数 i f _ o u t p u t和i f _ i o c t l被经常地调用。而 i f _ i n i t、i f _ d o n e和
i f _ r e s e t从来不被调用或仅从设备专用代码调用 (例如:l e i n i t直接被l e i o c t l
调用)。函数if_start仅被函数ether_output调用。

4.2 代码介绍

以太网设备驱动程序和通用接口 i o c t l的代码包含在两个头文件和三个 C文件中,它们列


于图4-3中。

文 件 说 明

net/if_ether.h 以太网结构
net/if.h i o c t l命令定义
net/if_ethersubr.c 通用以太网函数
hp300/dev/if_le.c LANCE以太网驱动程序
net/if.c i o c t l处理

图4-3 在本章讨论的文件

4.2.1 全局变量

显示在图4-4中的全局变量包括协议输入队列、 LANCE接口结构和以太网广播地址。

变 量 数据类型 说 明

arpintrq struct ifqueue ARP输入队列


clnlintrq struct ifqueue CLNP输入队列
ipintrq struct ifqueue IP输入队列
le_softc struct le_softc[] LANCE以太网接口
etherbraodcastaddr u_char[] 以太网广播地址

图4-4 本章介绍的全局变量

le_softc是一个数组,因为这里可以有多个以太网接口。

4.2.2 统计量

结构ifnet中为每个接口收集的统计量如图 4-5所示。
图4-6显示了netstat命令的一些输出例子,包括 ifnet结构中的一些统计信息。
第1列包含显示为一个字符串的 if_name和if_unit。若接口是关闭的 (不设置IFF_UP),
一个星号显示在这个名字的旁边。在图 4-6中,sl0、sl2和sl3是关闭的。
第2列显示的是 if_ m t u。在表头“ N e t w o r k”和“A d d r e s s”底下的输出依赖于地址的
类型。对于链路层地址,显示了结构 s o c k a d d r _ d l的s d l _ d a t a的内容。对于 I P地址,显
示了子网和单播地址。其余的列是 i f _ i p a c k e t s 、i f _ i e r r o r s 、i f _ o p a c k e t s 、
if_oerrors和if_collisions。
• 在输出中冲突的分组大约有 3% (942 798 / 23 234 729 = 3%)。
• 这个机器的 SLIP输出队列从未满过,因为 SLIP接口的输出没有差错。
• 在传输中, LANCE硬件检测到 12个以太网的输出差错。其中有些差错可能被视为冲突。

Linux公社 www.linuxidc.com
bbs.theithome.com
76 计计TCP/IP详解 卷 2:实现

ifn e t成员 说 明 用于SNMP

if_collisions 在CSMA接口的冲突数
if_ibytes 接收到的字节总数 •
if_ierrors 接收到的有输入差错分组数 •
if_imcasts 接收到的多播分组数 •
if_ipackets 在接口接收到的分组数 •
if_iqdrops 被此接口丢失的输入分组数 •
if_lastchange 上一次改变统计的时间 •
if_noproto 指定为不支持协议的分组数 •
if_obytes 发送的字节总数 •
if_oerrors 接口上输出的差错数 •
if_omcasts 发送的多播分组数 •
if_opackets 接口上发送的分组数 •
if_snd.ifq_drops 在输出期间丢失的分组数 •
if_snd.ifq_len 输出队列中的分组数

图4-5 结构ifnet 中维护的统计

图4-6 接口统计的样本

• 硬件检测出 814个以太网的输入差错,例如分组太短或错误的检验和。

4.2.3 SNMP变量

图4 - 7所示的是 S N M P接口表(i f T a b l e)中的一个接口项对象 (i f E n t r y),它包含在每个


接口的ifnet结构中。
ISODE SNMP代理从if_type获得ifSpeed,并为ifAdminStatus维护一个内部变量。
代理的 i f L a s t C h a n g e基于结构 i f n e t中的i f _ l a s t c h a n g e,但与代理的启动时间相关,
而不是与系统的启动时间相关。代理为 ifSpecific返回一个空变量。

接口表,索引 =<ifIndex>
SNMP变量 i f n e t成员 说 明

ifIndex if_index 唯一地标识接口


ifDescr if_name 接口的文本名称
ifType if_type 接口的类型 (例如以太网、 SLIP等等)

图4-7 接口表ifTable 的变量

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 77
接口表,索引 =<ifIndex>
SNMP变量 i f n e t成员 说 明

ifMtu if_mtu 接口的MTU(字节)


ifSpeed (看正文) 接口的正常速率 (每秒比特)
ifPhysAddress ac_enaddr 媒体地址 (来自结构a r p c o m)
ifAdminStatus (看正文) 接口的期望状态 (I F F _ U P标志)
ifOperStatus if_flags 接口的操作状态 (I F F _ U P标志)
ifLastChange (看正文) 上一次统计改变时间
ifInOctets if_ibytes 输入的字节总数
ifInUcastPkts if_ipackets -if_imcasts输入的单播分组数
ifInNUcastPkts if_imcasts 输入的广播或多播分组数
ifInDiscards if_iqdrops 因为实现的限制而丢弃的分组数
ifInErrors if_ierrors 差错的分组数
ifInUnknownProtos if_noproto 指定为未知协议的分组数
ifOutOctets if_obytes 输出字节数
ifOutUcastPkts if_opackets-if_omcasts 输出的单播分组数
ifOutNUcastPkts if_omcasts 输出的广播或多播分组数
ifOutDiscards if_snd.ifq_drops 因为实现的限制而丢失的输出分组数
ifOutErrors if_oerrors 因为差错而丢失的输出分组数
ifOutQLen if_snd.ifq_len 输出队列长度
ifSpecific n/a 媒体专用信息的 SNMP对象ID(未实现)

图4-7 (续)

4.3 以太网接口

N e t / 3以太网设备驱动程序都遵循同样的设计。对于大多数 U n i x设备驱动程序来说,都是
这样,因为写一个新接口卡的驱动程序总是在一个已有的驱动程序的基础上修改而来的。在
本节,我们简要地概述一下以太网的标准和一个以太网驱动程序的设计。我们用 L A N C E驱动
程序来说明这个设计。
图4-8说明了一个 IP分组的以太网封装。

目标地址 源地址 类型 数 据 CRC

6字节 6字节 2 46~1500字节 4字节

类型 IP分组
0800
2 46~1500字节
图4-8 一个IP分组的以太网封装

以太网帧包括48 bit的目标地址和源地址,接下来是一个 16 bit的类型字段,它标识这个帧


所携带的数据的格式。对于 IP分组,类型是 0x0800(2048)。帧的最后是一个 32 bit的CRC(循环
冗余检验),它用来检查帧中的差错。
我们所讨论的最初的以太网组帧的标准在1982年由Digital设备公司、Intel公司及施
乐公司发布,并作为今天在TCP/IP网络中最常用的格式。另一个可选的格式是 IEEE(电
气电子工程师协会)规定的802.2和802.3标准。更多的IEEE标准详见[Stallings 1987]。

Linux公社 www.linuxidc.com
bbs.theithome.com
78 计计TCP/IP详解 卷 2:实现

对于以太网,IP分组的封装由RFC 894[Hornig 1984]规定,而对于802.3网,却由


RFC1042[Postel和Reynolds 1988]规定。
我们用48 bit的以太网地址作为硬件地址。 IP地址到硬件地址之间的转换用 ARP协议(RFC
826 [Plummer 1982]),这个协议在第21章讨论。而硬件地址到 IP地址的转换用 RARP协议(RFC
903 [Finlayson et al. 1984])。以太网地址有两种类型:单播和多播。一个单播地址描述一个单
一的以太网接口,而一个多播地址描述一组以太网接口。一个以太网广播是一个所有接口都
接收的多播。以太网单播地址由设备的厂商分配,也有一些设备的地址允许用软件改变。
一些D E C N E T协议要求标识一个多接口主机的硬件地址,因此 D E C N E T必须能
改变一个设备的以太网单播地址。
图4-9列举了以太网接口的数据结构和函数。

OSI ARP协议
协议 (图21-3)

输入分组 输出分组
中断

图4-9 以太网设备驱动程序

在图中:用一个椭圆标识一个函数 ( l e i n t r ) 、用一个方框标识数据结构
(le_softc[0])、le_softc及用圆角方框标识一组函数 (ARP协议)。
图4 - 9 左上角显示的是 O S I 无连接网络层 (c l n l )协议、 I P 和 A R P 的输入队列。对于
c l n l i n t r q,我们不打算讲更多,将它包含进来是为了强调 e t h e r _ i n p u t要将以太网帧分
用到多个协议队列中。
在技术上, OSI使用无连接网络协议 (CLNP而不是CLNL),但我们使用的是 Net/3
中的术语。 CLNP的官方标准是ISO 8473。[Stallings 1993]对这个标准作了概述。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 79
接口结构 l e _ s o f t c在图4 - 9的中间。我们感兴趣的是这个结构中的 i f n e t和a r p c o m,
其他是 L A N C E硬件的专用部分。我们在图 3 - 6中显示了结构 i f n e t,在图 3 - 2 6中显示了结构
arpcom。

4.3.1 leintr函数

我们从以太网帧的接收开始。现在,假设硬件已初始化并且系统已完成配置,当接口产生
一个中断时, l e i n t r被调用。在正常操作中,一个以太网接口接收发送到它的单播地址和以
太网广播地址的帧。当一个完整的帧可用时,接口就产生一个中断,并且内核调用 leintr。
在第1 2章中,我们会看见可能要配置多个以太网接口来接收以太网多播帧 (不同
于广播)。
有些接口可以配置为运行在混杂方式。在这种方式下,接口接收所有出现在网
络上的帧。在卷 1中讨论的 t c p d u m p程序可以使用 BPF (BSD分组过滤程序 )来利用这
种特性。
l e i n t r检测硬件,并且如果有一个帧到达,就调用 l e r e a d把这个帧从接口转移到一个
m b u f链中(用m _ d e v g e t)。如果硬件报告一个帧已传输完或发现一个差错 (如一个有错误的检
验和),则leintr更新相应的接口统计,复位这个硬件,并调用 lestart来传输另一个帧。
所有以太网设备驱动程序将它们接收到的帧传给 e t h e r _ i n p u t做进一步的处理。设备驱
动程序构造的 m b u f 链不包括以太网首部,以太网首部作为一个独立的参数传递给
ether_input。结构ether_header显示在图4-10中。
38-42 以太网C R C并不总是可用。它由接口硬件来计算与检验,接口硬件丢弃到达的 C R C
差错帧。以太网设备驱动程序负责 e t h e r _ t y p e的网络和主机字节序间的转换。在驱动程序
外,它总是主机字节序。

图4-10 结构ether_header

4.3.2 leread函数

函数l e r e a d(图4 - 11 )的开始是由 l e i n t r传给它的一个连续的内存缓冲区,并且构造了


一个e t h e r _ h e a d e r结构和一个 m b u f链。这个链表存储来自以太网帧的数据。 l e r e a d还将
输入帧传给 BPF。

图4-11 函数leread

Linux公社 www.linuxidc.com
bbs.theithome.com
80 计计TCP/IP详解 卷 2:实现

图4-11 ( 续)

528-539 函数l e i n t r给l e r e a d传了三个参数: u n i t,它标识接收到此帧的特定接口

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 81
卡;buf,它指向接收到的帧; len,它是帧的字节数 (包括首部和 CRC)。
函数将 e t指向这个缓存的开始,并且将以太网字节序转换成主机字节序,来构造结构
ether_header。
540-551 将len减去以太网首部和 CRC的大小得到数据的字节数。短分组 (runt packet)是一
个长度太短的非法以太网帧,它被记录、统计,并被丢弃。
5 5 2 - 5 5 7 接下来,目标地址被检测,并判断是不是以太网广播或多播地址。以太网广播地
址是一个以太网多播地址的特例;它的每一比特都被设置了。 e t h e r b r o a d c a s t a d d r是一
个数组,定义如下:
u_char etherbroadcastaddr[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };

这是C语言中定义一个 48 bit 值的简便方法。这项技术仅在我们假设字符是 8 bit


值时才起作用—ANSI C 并不保证这一点。
bcmp比较etherbroadcastaddr和ether_dhost,若相同,则设置标志 M_BCAST。
一个以太网多播地址由这个地址的首字节的低位比特来标识,如图 4-12所示。

48 bit以太网地址
标识以太网多播地址

图4-12 检测一个以太网多播地址

在第1 2章中,我们会看到并不是所有以太网多播帧都是 I P多播数据报,并且 I P必须进一


步检测这个分组。
如果这个地址的多播比特被置位,在 m b u f首部中设置 M _ M C A S T。检测的顺序是重要的:
首先e t h e r _ i n p u t将整个48 bit地址和以太网广播地址比较,若不同,则检测标识以太网多
播地址的首字节的低位比特 (习题4.1)。
5 5 8 - 5 7 3 如果接口带有 B P F,调用b p f _ t a p把这个帧直接传给 B P F。我们会看见对于 S L I P
和环回接口,要构造一个特定的 BPF帧,因为这些网络没有一个链路层首部 (不像以太网 )。
当一个接口带有 BPF时,它可以配置为运行在混淆模式,并且接收网络上出现的所有以太
网帧,而不是通常由硬件接收的帧的子集。如果分组发送给一个不与此接口地址匹配的单播
地址,则被 leread丢弃。
574-585 m_devget (2.6节)将数据从传给leread的缓存中复制到一个它分配的 mbuf链中。
传给 m _ d e v g e t的第一个参数指向以太网首部后的第一个字节,它是此帧中的第一个数据字
节。如果 m _ d e v g e t内存用完, l e r e a d立即返回。另外广播和多播标志被设置在链表中的第
一个mbuf中,ether_input处理这个分组。

4.3.3 ether_input函数

函数e t h e r _ i n p u t显示在图 4 - 1 3中,它检查结构 e t h e r _ h e a d e r来判断接收到的数据


的类型,并将接收到的分组加入到队列中等待处理。
1. 广播和多播的识别
1 9 6 - 2 0 9 传给e t h e r _ i n p u t的参数有: i f p,一个指向接收此分组的接口的 i f n e t结构
的指针; e h,一个指向接收分组的以太网首部的指针; m,一个指向接收分组的指针 (不包括
以太网首部 )。

Linux公社 www.linuxidc.com
bbs.theithome.com
82 计计TCP/IP详解 卷 2:实现

任何到达不工作接口的分组将被丢弃。可能没有为接口配置一个协议地址,或者接口可
能被程序i f c o n f i g (8) (6.6节)显式地禁用了。

图4-13 函数ether_input

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 83
210-218 变量t i m e是一个全局的 t i m e v a l结构,内核用它维护当前时间和日期,它是从
U n i x新纪元 ( 1 9 7 0年1月1日0 0 : 0 0 : 0 0,协调通用时间 [ U T C ] )开始的秒和微秒数。在 [Itano and
Ramsey 1993]中可以找到对UTC的简要讨论。我们在Net/3源代码中会经常遇到结构 timeval:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};

e t h e r _ i n p u t用当前时间更新 i f _ l a s t c h a n g e,并且把 i f _ i b y t e s加上输入分组的


长度(分组长度加上14字节的以太网首部 )。
然后,ether_input再次用leread去判断分组是否为一个广播或多播分组。
有些内核编译时可能没有包括 B P F代码,因此测试必须在 e t h e r _ i n p u t中进
行。
2. 链路层分用
219-227 e t h e r _ i n p u t根据以太网类型字段来跳转。对于一个 I P分组,s c h e d n e t i s r
调度一个 I P软件中断,并选择 I P输入队列, i p i n t r q。对于一个 A R P分组,调度 A R P软件中
断,并选择 arpintrq。
一个isr是一个中断服务例程。
在原先的BSD版本中,当处于网络中断级别时, ARP分组通过调用 a r p i n p u t立
即被处理。通过分组排队,它们可以在软件中断级别被处理。
如果要处理其他以太网类型,一个内核程序员应在此增加其他情况的处理。或
者,一个进程能用 B P F接收其他以太网类型。例如,在 N e t / 3中,R A R P服务通常用
BPF实现。
228-307 默认情况处理不识别以太网类型或按 802.3 范 围 说 明
标准(例如O S I无连接传输 )封装的分组。以太网的 t y p e
0~1500 IEEE 802.3 length字段
字段和 8 0 2 . 3的l e n g t h字段在一个以太网帧中占用同一 1501~65535 以太网type字段:
位置。两种封装能够分辨出来,因为一个以太网封装 2048 IP分组
2045 ARP分组
的类型范围和 8 0 2 . 3封装的长度范围是不同的 (图4 - 1 4 )。
我们跳过 O S I代码,在 [Stallings 1993]中有对 O S I链路 图4-14 以太网的 type字段和
层协议的说明。 802.3的length字段

有很多其他以太网类型值分配给各种协议;我们没有在图 4 - 1 4中显示。在 R F C
1700 [Reynolds and Postel 1994]中有一个有更多通用类型的列表。
3. 分组排队
308-315 最后,ether_input把分组放置到选择的队列中,若队列为空,则丢弃此分组。
我们在图7-23和图21-16中会看到IP和ARP队列的默认限制为每个50个(ipqmaxlen)分组。
当e t h e r _ i n p u t返回时,设备驱动程序通知硬件它已准备接收下一分组,这时下一分组
可能已存在于设备中。当 schednetisr调度的软件中断发生时,处理分组输入队列 (1.12节)。
准确地说,调用 i p i n t r来处理 I P输入队列中的分组,调用 a r p i n t r来处理 A R P输入队列中
的分组。

Linux公社 www.linuxidc.com
bbs.theithome.com
84 计计TCP/IP详解 卷 2:实现

4.3.4 ether_output函数

我们现在查看以太网帧的输出,当一个网络层协议,如 I P,调用此接口 i f n e t结构中指定


的函数 i f _ o u t p u t时,开始处理输出。所有以太网设备的 i f _ o u t p u t是e t h e r _ o u t p u t
(图4 - 2 )。e t h e r _ o u t p u t用1 4字节以太网首部封装一个以太网帧的数据部分,并将它放置到
接口的发送队列中。这个函数比较大,我们分 4个部分来说明:
• 验证;
• 特定协议处理;
• 构造帧;
• 接口排队。
图4-15包括这个函数的第一个部分。

图4-15 函数ether_output :验证

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 85
49-64 ether_output的参数有: ifp,它指向输出接口的 ifnet结构;m0,要发送的分
组;dst,分组的目标地址; rt0,路由信息。
65-67 在ether_output中多次调用宏senderr。
#define senderr(e) { error = (e); goto bad;}

s e n d e r r保存差错码,并跳到函数的尾部 b a d,在那里分组被丢弃,并且 e t h e r _
output返回error。
如果接口启动并在运行, e t h e r _ o u t p u t 更新接口的上次更改时间。否则,返回
ENETDOWN。
1. 主机路由
68-74 r t 0指向i p _ o u t p u t找到的路由项,并传递给 e t h e r _ o u t p u t。如果从 B P F调用
e t h e r _ o u t p u t,r t 0可以为空。在这种情况下,控制转给图 4 - 1 6中的代码。否则,验证路
由。如果路由无效,参考路由表,并且当路由不能被找到时,返回 E H O S T U N R E A C H。这时,
rt0和rt指向一个到下一跳目的地的有效路由。

图4-16 函数ether_output :网络协议处理

2. 网关路由
75-85 如果分组的下一跳是一个网关 (而不是最终目的 ),找到一个到此网关的路由,并且
r t指向它。如果不能发现一个网关路由,则返回 E H O S T U N R E A C H。这时, r t指向下一跳目
的地的路由。下一跳可能是一个网关或最终目标地址。
3. 避免ARP泛洪
86-90 当目标方不准备响应 A R P请求时, A R P代码设置标志 R T F _ R E J E C T来丢弃到达目标
Linux公社 www.linuxidc.com
bbs.theithome.com
86 计计TCP/IP详解 卷 2:实现

方的分组。这在图 21-24中描述。
e t h e r _ o u t p u t根据此分组的目标地址继续处理。因为以太网设备仅响应以太网地址,
要发送一个分组, e t h e r _ o u t p u t必须发现下一跳目的地的 I P地址所对应的以太网地址。
ARP协议(第21章)用来实现这个转换。图 4-16显示了驱动程序是如何访问 ARP协议的。
4. IP 输出
91-101 e t h e r _ o u t p u t根据目标地址中的 s a _ f a m i l y进行跳转。我们在图 4-16中仅显示
了case为AF_INET、AF_ISO和AF_UNSPEC的代码,而略过了 c a s e A F _ I S的代码。
O
case AF_INET调用arpresolve来决定与目标IP地址相对应的以太网地址。如果以太网
地址已存在于 A R P高速缓存中,则 a r p r e s o l v e返回1,并且 e t h e r _ o u t p u t继续执行。否
则,这个 I P 分组由 A R P 控制,并且 A R P 判断地址,从函数 i n _ a r p i n p u t 调用
ether_output。
假设A R P高速缓存包含硬件地址, e t h e r _ o u t p u t检查是否分组要广播,并且接口是否
是单向的 (例如,它不能接收自己发送的分组 )。如果都成立,则 m _ c o p y复制这个分组。在执
行switch后,这个复制的分组同到达以太网接口的分组一样进行排队。这是广播定义的要求,
发送主机必须接收这个分组的一个备份。
我们在第12章会看到多播分组可能会环回到输出接口而被接收。
5. 显式以太网输出
142-146 有些协议,如 A R P ,需要显式地指定以太网目的地和类型。地址族类常量
A F _ U N S P E C指出: d s t指向一个以太网首部。 b c o p y复制e d s t中的目标地址,并把以太网
类型设为 t y p e。它不必调用 a r p r e s o l v e (如A F _ I N E T),因为以太网目标地址已由调用者
显式地提供了。
6. 未识别的地址族类
147-151 未识别的地址族类产生一个控制台消息,并且ether_output返回EAFNOSUPPORT。
图4-17所示的是ether_output的下一部分:构造以太网帧。

图4-17 函数ether_output :构造以太网帧

7. 以太网首部
152-167 如果在 s w i t c h中的代码复制了这个分组,这个分组副本同在输出接口上接收到

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 87
的分组一样通过调用 looutput来处理。环回接口和 looutput在5.4节讨论。
M_PREPEND确保在分组的前面有 14字节的空间。
大多数协议要在 m b u f链表的前面留一些空间,因此, M _ P R E P E N D仅需要调整一
些指针(例如,16.7节中UDP输出的sosend和13.6节的igmp_sendreport)。
e t h e r _ o u t p u t 用t y p e 、e d s t和a c _ e n a d d r(图3 - 2 6 )构成以太网首部。
a c _ e n a d d r是与此输出接口关联的以太网单播地址,并且是所有从此接口传输的帧
的源地址。 e t h e r _ h e a d e r用a c _ e n a d d r重写调用者可能在 e t h e r _ h e a d e r结构
中指定的源地址。这使得伪造一个以太网帧的源地址变得更难。
这时, m b u f包含一个除 32 bit CRC 以外的完整以太网帧, C R C由以太网硬件在传输时计
算。图4-18所示的代码对设备要传送的帧进行排队。

图4-18 函数ether_output :输出排队

168-185 如果输出队列为空, e t h e r _ o u t p u t丢弃此帧,并返回 E N O B U F S。如果输出队


列不为空,这个帧放置到接口的发送队列中,并且若接口未激活,接口的 i f _ s t a r t函数传
输下一帧。
186-190 宏senderr跳到bad,在这里帧被丢弃,并返回一个差错码。

4.3.5 lestart函数

函数l e s t a r t从接口输出队列中取出排队的帧,并交给 LANCE以太网卡发送。如果设备


空闲,调用此函数开始发送帧。 e t h e r _ o u t p u t(图4 - 1 8 )的最后是一个例子,直接通过接口
的if_start函数调用lestart。
如果设备忙,当它完成了当前帧的传输时产生一个中断。设备调用 l e s t a r t来退队并传
输下一帧。一旦开始,协议层不再用调用 l e s t a r t来排队帧,因为驱动程序不断退队并传输
Linux公社 www.linuxidc.com
bbs.theithome.com
88 计计TCP/IP详解 卷 2:实现

帧,直到队列为空为止。
图4-19所示的是函数lestart。lestart假设已调用 splimp来阻塞所有设备中断。

图4-19 函数lestart

1. 接口必须初始化
325-333 如果接口没有初始化, lestart立即返回。
2. 将帧从输出队列中退队
335-342 如果接口已初始化,下一帧从队列中移去。如果接口输出队列为空,则 lestart
返回。
3. 传输帧并传递给 BPF
343-350 l e p u t将m中的帧复制到 l e p u t第一个参数所指向的硬件缓存中。如果接口带有
BPF,将帧传给 bpf_tap。我们跳过硬件缓存中帧传输的设备专用初始化代码。
4. 如果设备准备好,重复发送多帧

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 89
359 当l e - > s c _ t x c n t等于L E T B U F时,l e s t a r t停止给设备传送帧。有些以太网接口能
排队多个以太网输出帧。对于 LANCE驱动器,L E T B U F是此驱动器硬件传输缓存的可用个数,
并且le->sc_txcnt保持跟踪有多少个缓存被使用。
5. 将设备标记为忙
360-362 最后,lestart在ifnet结构中设置 IFF_OACTIVE来标识这个设备忙于传输帧。
在设备中将多个要传输的帧进行排队有一个负面影响。根据 [Jacobson 1998a],
L A N C E芯片能够在两个帧间以很小的时延传输排队的帧。不幸的是,有些 (差的)以
太网设备会丢失帧,因为它们不能足够快地处理输入的数据。
在一个应用如 N F S中,这会很糟糕地互相影响。 N F S发送大的 U D P数据报 (经常
是超过8192字节),数据报被I P分片,并在L A N C E设备中作为多个以太网帧排队。分
片在接收方丢失,当 NFS重传整个UDP数据报时,会导致很多未完成的数据报极大的
时延。
Jacobson提出Sun的LANCE驱动器一次只排队一个帧就可能避免这一问题。

4.4 ioctl系统调用

i o c t l系统调用提供一个通用命令接口,一个进程用它来访问一个设备的标准系统调用
所不支持的特性。 ioctl的原型为:
int ioctl(int fd, unsigned long com,…);

f d是一个描述符,通常是一个设备或网络连接。每种类型的描述符都支持它自己的一套
i o c t l命令,这套命令由第二个参数 c o m来指定。第三个参数在原型中显示为“ . . .”,因为
它是依赖于被调用的 i o c t l命令的类型的指针。如果命令要取回信息,第三个参数必须是指
向一个足够保存数据的缓存的指针。在本书中,我们仅讨论用于插口描述符的 ioctl命令。
我们显示的系统调用的原型是一个进程进行系统调用的原型。在第 1 5章中我们
会看见在内核中的这个函数还有一个不同的原型。
我们在第 1 7章讨论系统调用 i o c t l的实现,但在本书的各个部分讨论 i o c t l单个命令的
实现。
我们讨论的第一个 i o c t l命令提供对讨论过的网络接口结构的访问。我们总结的本书中
所有的ioctl命令如图4-20所示。

命 令 第三个参数 函 数 说 明

SIOCGIFCONF struct ifconf * ifconf 获取接口配置清单


SIOCGIFFLAGS struct ifreq * ifioctl 获得接口标志
SIOCGIFMETRIC struct ifreq * ifioctl 获得接口度量
SIOCSIFFLAGS struct ifreq * ifioctl 设置接口标志
SIOCSIFMETRIC struct ifreq * ifioctl 设置接口度量

图4-20 接口ioctl 的命令

第一列显示的符号常量标识 i o c t l命令(第二个参数, c o m)。第二列显示传递给第一列


所显示的命令的系统调用的第三个参数的类型。第三列是实现这个命令的函数的名称。
图4 - 2 1显示处理 i o c t l命令的各种函数的组织。带阴影的函数我们在本章中说明。其余

Linux公社 www.linuxidc.com
bbs.theithome.com
90 计计TCP/IP详解 卷 2:实现

的函数在其他章说明。

系统调用

默认

获取配置

图4-21 在本章说明的 ioctl 函数

4.4.1 ifioctl函数

系统调用ioctl将图4-20所列的5种命令传递给图 4-22所示的ifioctl函数。
394-405 对于命令 S I O C G I F C O N F,i f i o c t l调用i f c o n f来构造一个可变长 i f r e q结构
的表。
406-410 对于其他 i o c t l命令,数据参数是指向一个 i f r e q 结构的指针。 i f u n i t在
i f n e t列表中查找名称为进程在 i f r - > i f r _ n a m e中提供的文本名称 (例如:“s l 0”,“l e 1”
或“ l o 0”)的接口。如果没有匹配的接口, i f i o c t l返回E N X I O。剩下的代码依赖于 c m d,
它们在图4-29中说明。
447-454 如果接口ioctl命令不能被识别, ifioctl把命令发送给与所请求插口关联的协
议的用户要求函数。对于 I P,这些命令以一个 U D P插口发送并调用 u d p _ u s r r e q。这一类命

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 91
令在图6-10中描述。23.10节将详细讨论函数 udp_usrreq。
如果控制到达switch语句外,返回0。

图4-22 函数ifioctl :综述与 SIOCGIFCONF

4.4.2 ifconf函数

i f c o n f为进程提供一个标准的方法来发现一个系统中的接口和配置的地址。由结构
ifreq和ifconf表示的接口信息如图 4-23和图4-24所示。
262-279 一个i f r e q结构包含在i f r _ n a m e中一个接口的名称。在联合中的其他成员被各
种ioctl命令访问。通常,用宏来简化对联合的成员的访问语法。
292-300 在结构ifconf中,ifc_len是ifc_buf指向的缓存的字节数。这个缓存由一个
进程分配,但由 i f c o n f用一个具有可变长 i f r e q结构的数组来填充。对于函数 i f c o n f,
i f r _ a d d r 是结构 i f r e q 中联合的相关成员。每个 i f r e q 结构有一个可变长度,因为
i f r _ a d d r (一个s o c k a d d r结构)的长度根据地址的类型而变。必须用结构 s o c k a d d r的成员
sa_len来定位每项的结束。图 4-25说明了ifconf所维护的数据结构。
在图4 - 2 5中,左边的数据在内核中,而右边的数据在一个进程中。我们用这个图来讨论
图4-26中所示的 ifconf函数。
462-474 i f c o n f的两个参数是: c m d,它被忽略; d a t a,它指向此进程指定的 i f c o n f
结构的一个副本。

Linux公社 www.linuxidc.com
bbs.theithome.com
92计计TCP/IP详解 卷 2:实现

图4-23 结构ifreq

图4-24 结构ifconf

内核 进程

图4-25 ifconf 数据结构

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
第 4章 接口:以太网计计 93

图4-26 函数ifconf

i f c是强制为一个 i f c o n f结构指针的 d a t a。i f p从i f n e t (列表头)开始遍历接口列表,

Linux公社 www.linuxidc.com
bbs.theithome.com
94 计计TCP/IP详解 卷 2:实现

而i f a遍历每个接口的地址列表。 c p和e p控制构造在 i f r 中的接口文本名称, i f r是一个


i f r e q结构,它在接口名称和地址复制到进程的缓存前保存接口名称和地址。 i f r q指向这个
缓存,并且在每个地址被复制后指向下一个。 s p a c e是进程缓存中剩余字节的个数, c p用来
搜寻名称的结尾,而 ep标志接口名称数字部分最后的可能位置。
475-488 f o r 循环遍历接口列表。对于每个接口,文本名称被复制到 i f r _ n a m e ,在
i f r _ n a m e的后面跟着 i f _ u n i t数的文本表示。如果没有给接口分配地址,一个全 0的地址被
构造,所得的ifreq结构被复制到进程中,并减小 space,增加ifrp。
489-515 如果接口有一个或多个地址,用 f o r循环来处理每个地址。地址加到 i f r中的接
口名称中,然后 i f r被复制到进程中。长度超过标准 s o c k a d d r结构的地址不放到 i f r中,并
且直接复制到进程。在复制完每个地址后,调整 s p a c e和i f r p的值。所有接口处理完后,更
新缓存长度 ( i f c - > i f c _ l e n),并且 i f c o n f返回。系统调用 i o c t l负责将结构 i f c o n f中
新的内容复制回进程中的结构 ifconf。

4.4.3 举例

图4-27显示了以太网、 SLIP和环回接口被初始化后的接口结构的配置。

图4-27 接口和地址数据结构

图4-28显示了以下代码执行后的 ifc和buffer的内容。

这里对命令 S I O C G I F C O N F操作的插口的类型没有限制,如我们所看到的,这个命令返回
所有协议族类的地址。
在图4 - 2 8中,因为在缓存中返回的三个地址仅占用 108 (3×3 6 )字节,i o c t l将i f c _ l e n

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 95
由1 4 4改为1 0 8。返回三个 s o c k a d d r _ d l地址,并且这个缓存后面的 3 6字节未用。每项的前
16个字节包含接口的文本名称。在这里,这 16字节中只有 3个字节被使用。

(16 字节) (20字节)

以太网地址 36 字节

36 字节

36 字节

36 字节

图4-28 SIOCGIFCONF 命令返回的数据

i f r _ a d d r为一个 s o c k a d d r结构的形式,因此第一个值为长度 ( 2 0字节),且第二个值


为地址的类型 (1 8,A F _ L I N K)。接下来的一个值为 s d l _ i n d e x,与s d l _ t y p e一样,对于每
个接口,它是不同的 (与IFT_ETHER、IFT_SLIP和IFT_LOOP相对应的值为6、28和24)。
下面三个值为 s a _ n l e n(文本名称的长度 )、s a _ a l e n(硬件地址的长度 )及s a _ s l e n(未
用)。对于所有三项, s a _ n l e n都为 3。以太网地址的 s a _ a l e n为6,而S L I P和环回接口的
sa_alen为0。sa_slen总是为0。
最后,是接口的文本名称,其后面是硬件地址 ( 仅对于以太网 )。S L I P 和环回接口在
sockaddr_dl结构中不存放一个硬件级地址。
在此例中,仅返回 s o c k a d d r _ d l地址(因为在图 4 - 2 7中没有配置其他地址类型 ),因此缓
存中的每项大小一样。如果为每个接口配置其他地址 ( 例如: I P 或 O S I 地址 ) ,它们会同
sockaddr_dl地址一起返回,并且每项的大小根据返回的地址类型的不同而不同。

4.4.4 通用接口ioctl命令

图4 - 2 0中剩下的四个接口命令 (S I O C G I F F L A G S、S I O C G I F M E T R I C、S I O C S I F F L A G S


和SIOCSIFMETRIC)由函数ifioctl处理。图4-29所示的是处理这些命令的 case语句。
1. SIOCGIFLAGS和SIOCGIFMETRIC
410-416 对于两个S I O C Gxxx命令,i f i o c t l将每个接口的 i f _ f l a g s或i f _ m e t r i c值复
制到 i f r e q 结构中。对于标志,使用联合的成员 i f r _ f l a g s ;而对于度量,使用成员
i f r _ m e t r i c(图4-23)。
2. SIOCSIFFLAGS
417-429 为改变接口的标志,调用进程必须有超级用户权限。如果进程正在关闭一个运行
的接口或启动一个未运行的接口,分别调用 if_down和if_up。
3. 忽略标志IFF_CANTCHAGE
430-434 回忆图 3 - 7,有些接口标志不能被进程改变。表达式 (i f p - > i f _ f l a& g s

Linux公社 www.linuxidc.com
bbs.theithome.com
96 计计TCP/IP详解 卷 2:实现

I F F _ C A N T C H A N G E)清除能被进程改变的接口标志,而表达式 (i f r - > i f r _ f l a &


g s~
I F F _ C A N T C H A N G E)清除在请求中不被进程改变的标志。这两个表达式进行或运算并作为新
值保存在 i f p - > i f _ f l a g s中。在返回前,请求被传递给与设备相关联的 i f _ i o c t l函数(例
如:LANCE驱动器的 leioctl—图4-31)。

图4-29 函数ifioctl :标志和度量

4. SIOCSIFMETRIC
435-439 改变接口的度量要容易些;进程同样要有超级用户权限, ifioctl将接口新的度
量复制到if_metric中。

4.4.5 if_down和if_up函数

利用程序 i f c o n f i g ,一个管理员可以通过命令 S I O C S I F F L A G S 设置或清除标志


IFF_UP来启用或禁用一个接口。图 4-30显示了函数 if_down和if_up的代码。
292-302 当一个接口被关闭时, I F F _ U P 标志被清除并且对与接口关联的每个地址用
p f c t l i n p u t(7 . 7节)发送命令 P R C _ I F D O W N。这给每个协议一个机会来响应被关闭的接口。
有些协议,如 OSI,要使用接口来终止连接。对于 IP,如果可能,要通过其他接口为连接进行
重新路由。 TCP和UDP忽略失效的接口,并依赖路由协议去发现分组的可选路径。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 97
i f _ q f l u s h忽略接口的任何排队分组。 r t _ i f m s g通知路由系统发生的变化。 T C P自动
重传丢失的分组; UDP应用必须自己显式地检测这种情况,并对此作出响应。
308-315 当一个接口被启用时, IFF_UP标志被设置,并且 rt_ifmsg通知路由系统接口状
态发生变化。

图4-30 函数if_down 和if_up

4.4.6 以太网、SLIP和环回

我们看图4-29中处理SIOCSIFFLAGS命令的代码,ifioctl调用接口的if_ioctl函数。
在我们的三个例子接口中,函数 s l i o c t l 和l o i o c t l为这个被 i f i o c t l忽略的命令返回
E I N V A L。图4-31显示了函数 l e i o c t l及LANCE以太网驱动程序的 S I O C S I F F L A G S命令的处
理。

图4-31 函数leioctl :SIOCSIFFLAGS

Linux公社 www.linuxidc.com
bbs.theithome.com
98 计计TCP/IP详解 卷 2:实现

图4-31 (续)

614-623 l e i o c t l把第三个参数 d a t a转换为一个 i f a d d r结构的指针,并保存在 i f a中。


l e指针引用下标为 i f p - > i f _ u n i t的l e _ s o f t c结构。基于 c m d的s w i t c h语句构成了这个
函数的主体。
638-656 在图4 - 3 1中仅显示了 case S I O C S I F F L A G S。这次 i f i o c t l调用l e i o c t l,接
口标志被改变。显示的代码强制物理接口进入标志所配置的状态。如果要关闭接口 (没有设
置I F F _ U P),但接口正在工作,则关闭接口。若要启动未操作的接口,接口被初始化并重
启。
如果混淆比特被改变,那么就关闭接口,复位,并重启来实现这种变化。
仅当要求改变 I F F _ P R O M I S C比特时包含异或和 I F F _ P R O M I S C的表达式才为
真。
672-677 处理未识别命令的 default情况分支发送EINVAL,并在函数的结尾将它返回。

4.5 小结

在本章中,我们说明了 L A N C E以太网设备驱动程序的实现,这个驱动程序在全书中多处
引用。我们还看到了以太网驱动程序如何检测输入中的广播地址和多播地址,如何检测以太
网和8 0 2 . 3封装,以及如何将输入的帧分用到相应的协议队列中。在第 2 1章中我们会看到 I P地
址(单播、广播和多播 )是如何在输出转换成正确的以太网地址。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 4章 接口:以太网计计 99
最后,我们讨论了协议专用的 ioctl命令,它用来访问接口层数据结构。

习题

4.1 在l e r e a d中,当接收到一个广播分组时,总是设置标志 M _ M C A S T(除了M _ B C A S T


外)。与e t h e r _ i n p u t的代码比较,为什么在 l e r e a d和e t h e r _ i n p u t中设置此标
志?它至关重要吗?哪个正确?
4.2 在e t h e r _ i n p u t(图4 - 1 3 )中,如果交换广播地址和多播地址检测次序会发生什么情
况?如果在检测多播地址的 if语句前加上一个 else会发生什么情况?

Linux公社 www.linuxidc.com
bbs.theithome.com
第5章 接口:SLIP和环回
5.1 引言

在第 4章中,我们查看了以太网接口。在本章中,我们讨论 S L I P和环回接口,同样用
i o c t l命令来配置所有网络接口。 S L I P驱动程序使用的 T C P压缩算法在 2 9 . 1 3节讨论。环回驱
动程序比较简单,在这里我们要对它进行完整地讨论。
像图4-2一样,图5-1列出了针对我们三个示例驱动程序的入口点。

ifnet 以 太 网 SLIP 环 回 说 明

if_init leinit 初始化硬件


if_output ether_output sloutput looutput 接收并将要传输的分组进行排队
if_start lestart 开始传输帧
if_done 输出完成(未用)
if_ioctl leioctl slioctl loioctl 从一个进程处理i o c t l命令
if_reset lereset 将设备重新设置为一已知状态
if_watchdog 监视设备的故障或采集统计信息

图5-1 例子驱动程序的接口函数

5.2 代码介绍

SLIP和环回驱动程序的代码文件列于图 5-2中。

文 件 说 明

net/if_slvar.h SLIP定义
net/if_sl.c SLIP驱动程序函数
net/if_loop.c 环回驱动程序

图5-2 本章讨论的文件

5.2.1 全局变量

在本章讨论 SLIP和环回接口结构。全局变量见图 5-3。


变 量 数据类型 说 明

sl_softc struct sl_softc [] SLIP接口


loif struct ifnet 环回接口

图5-3 本章中介绍的全局变量

s l _ s o f t c是一个数组,因为可能有很多 S L I P接口。l o i f不是一个数组,因为只可能有


一个环回接口。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 101
5.2.2 统计量

在第4章讨论的 i f n e t结构的统计也会被 SLIP和环回驱动程序更新。采集的另一个统计量


(它不在ifnet结构中)显示在图5-4中。
变 量 说 明 被SNMP使用

tk_nin 被任何串行接口 (被SLIP驱动程序更新 )接收的字节数

图5-4 变量tk_nin

5.3 SLIP接口

一个S L I P接口通过一个标准的异步串行线与一个远程系统通信。像以太网一样, S L I P定
义了一个标准的方法对传输在串行线上的 I P分组进行组帧。图 5 - 5显示了将一个包含 S L I P保留
字符的IP分组封装到一个 SLIP帧中。
分组用 SLIP END 字符0 x c 0来分割开。如果 E N D字符出现在 I P分组中,则在它前面填充
SLIP ESC字符0 x d b,并且在传输时将它替换为 0 x d c。当E S C字符出现在 I P分组中时,就在
它前面填充 ESC字符0xdb,并在传输时将它替换为 0xdd。
因为在SLIP帧(与以太网比较)中没有类型字段, SLIP仅适用于传输IP分组。

IP分组
在分组中的END 在分组中的ESC

转义的END 转义的ESC

SLIP帧

图5-5 将一个IP分组进行 SLIP封装

在RFC 1055 [Romkey 1988]中讨论了SLIP,陈述了它的很多弱点和非标准情况。


卷1中包含了SLIP封装的详细讨论。
点对点协议 ( P P P )被设计用来解决 S L I P的问题,并提供一个标准方法来通过一个
串行链路传输帧。 PPP在RFC 1332 [McGregor 1992]和RFC 1548 [Simpson 1993]中定
义。Net/3不包含一个PPP的实现,因此我们不在本书中讨论它。关于 PPP的更多信息
见卷1的2.6节。附录B讨论在哪里获得一个 PPP实现的参考。

5.3.1 SLIP线路规程: SLIPDISC

在N e t / 3中,S L I P接口依靠一个异步串行设备驱动器来发送和接收数据。传统上,这些设
备驱动器称为 T T Y (电传机)。Net/3 TTY子系统包括一个线路规程 (Line discipline)的概念,这
个线路规程作为一个在物理设备和 I/O系统调用 (如r e a d和w r i t e)之间的过滤器。一个线路规

Linux公社 www.linuxidc.com
bbs.theithome.com
102 计计TCP/IP详解 卷 2:实现

程实现以下特性:如行编辑、换行和回车处理、制表符扩展等等。 S L I P接口作为 T T Y子系统


的一个线路规程,但它不把输入数据传给从设备读数据的进程,也不接受来自向设备写数据
的进程的输出数据。 S L I P接口将输入分组传给 I P输入队列,并通过 S L I P的i f n e t结构中的函
数i f _ o u t p u t来获得要输出的分组。内核通过一个整数常量来标识线路规程,对于 SLIP,该
常量是SLIPDISC。
图5 - 6左边显示的是传统的线路规程,右边是 S L I P规程。我们在右边用 s l a t t a c h显示进
程,因为它是初始化 S L I P接口的程序。 T T Y子系统和线路规程的细节超出了本书的范围。我
们仅介绍理解 S L I P代码工作的相关信息。对于更多关于 T T Y子系统的信息见 [ L e ffler et al.
1 9 8 9 ]。图5 - 7列出了实现 S L I P驱动程序的函数。中间的列指示函数是否实现线路规程特性和
(或)网络接口特性。

进程

进程
内核

线路规程 线路规程

设备驱动程序 设备驱动程序

串行线 串行线

图5-6 SLIP接口作为一个线路规程

函 数 网络接口 线路规程 说 明

slattach • 初始化s l _ s o f t c结构,并将它连接到 i f n e t列表


slinit • 初始化SLIP数据结构
sloutput • 对相关TTY设备上要传输的输出分组进行排队
slioctl • 处理插口i o c t l请求
sl_btom • 将一个设备缓存转换成一个 m b u f链表
slopen • 将s l _ s o f t c结构连接到 TTY设备,并初始化驱动程序
slclose • 取消TTY设备与s l _ s o f t c结构的连接,标记接口为关闭,并释放存储器
sltioctl • 处理TTY i o c t l命令
slstart • • 从队列中取分组,并开始在 TTY设备上传输数据
slinput • • 处理从TTY设备输入的字节,如果整个帧被接收,就排列输入的分组

图5-7 SLIP设备驱动程序的函数

在N e t / 3中的S L I P驱动程序通过支持 T C P分组首部压缩来得到更好的吞吐量。我们在 2 9 . 1 3


节讨论分组首部压缩,因此,图 5-7跳过实现这些特性的函数。
Net/3 SLIP接口还支持一种转义序列。当接收方检测到这个序列时,就终止 SLIP

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 103
的处理,并将对设备的控制返回给标准线路规程。我们这里的讨论忽略这个处理。
图5-8显示了作为一个线路规程的 SLIP和作为一个网络接口的 SLIP间的复杂关系。

输入字符 簇
输出字符

图5-8 SLIP设备驱动程序

在N e t / 3中,s c _ t t y p和t _ s c指向t t y结构和s l _ s o f t c [ 0 ]结构。由于使用两


个箭头会使图显得较乱,我们用一对相反的箭头表示两个指针来说明结构间的双链。
在图5-8中包含很多信息:
• 结构sl_softc表示的网络接口和结构 tty表示的TTY设备。
• 输入字节存放在簇中 (显示在结构tyy后面)。当一个完整的 SLIP帧被接收时,封装的 IP分
组被slinput放到ipintrq中。
• 输出分组从 i f _ s n d或s c _ f a s t q退队,转换成 S L I P帧,并被 s l s t a r t传给T T Y设备。
T T Y缓存将字节输出到结构 c l i s t。函数 t _ o p r o c取完,并传输在 c l i s t结构中的字
节。

5.3.2 SLIP初始化: slopen和slinit

我们在3 . 7节讨论了 s l a t t a c h是如何初始化 s l _ s o f t c结构的。接口虽然被初始化,但


还不能操作,直到一个程序 (通常是s l a t t a c h)打开一个 T T Y设备(例如:/ d e v / t t y 0 1),并
发送一个 i o c t l命令用 S L I P规程代替标准的线路规程才能操作。这时, T T Y子系统调用线路
规程的打开函数 (在此是s l o p e n),此函数在一个特定 T T Y设备和一个特定 S L I P接口间建立关
联。slopen显示在图5-9中。

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
104 计计TCP/IP详解 卷 2:实现

图5-9 函数slopen

181-193 传递给s l o p e n的两个参数为: d e v,一个内核设备标识, s l o p e n未用此参数;


t p,一个指向此 T T Y设备相关 t t y结构的指针。最开始是一些预防处理:若进程没有超级用
户权限,或 TTY的线路规程已经被设置为 SLIPDISC,则slopen立即返回。
194-205 f o r循环在s l _ s o f t c结构数组中查找第一个未用的项,调用 s l i n i t ( 5 . 1 0节),
通过 t _ s c和s c _ t t y p加进结构 t t y和s l _ s o f t c,并将 T T Y输出速率 (t _ o s p e e d)复制到
S L I P接口。 t t y f l u s h丢弃任何在 T T Y队列中追加的输入输出数据。如果一个 S L I P接口结构
不可用, slopen返回ENXIO。若成功,返回 0。
注意,第一个变量 s l _ s o f t c结构与T T Y设备相关。如果系统有多个 S L I P线路,
在T T Y设备和 S L I P接口间不需要固定的映射。实际上,这个映射依赖于 s l a t t a c h
打开和关闭 TTY设备的次序。
显示在图5-10中的函数 slinit初始化结构 sl_softc。
156-175 函数slinit分配一个mbuf簇,并将它用三个指针连接到结构 sl_softc。当一
个完整的 S L I P帧被接收后,输入字节存储在这个簇中。 s c _ b u f总是指向簇中的这个分组的
起始位置, s c _ m p 指向要接收的下一个字节的位置,并且 s c _ e p 指向这个簇的结束。
sl_compress_init为此链路初始化 TCP首部的压缩状态 (29.13节)。
在图 5 - 8 中,我们看到 s c _ b u f 不指向簇的第一个字节。 s l i n i t 保留了 1 4 8 字节
(BUFOFFSET)的空间,因为输入分组可能含有一个压缩了的首部,它会扩展来填充这个空间。
在簇中已接收的字节用阴影表示。我们看到 s c _ m p指向接收的最后一个字节的下一个字节,
并且sc_ep指向这个簇的结尾。图 5-11显示了在几个SLIP常量间的关系。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 105
使这个接口能运行,剩下的要做的工作就是给它分配一个 I P地址。同以太网驱动程序一
样,我们将地址分配的讨论推迟到 6.6节。

图5-10 函数slinit

常 量 值 说 明

MCLBYTES 2048 一个mbuf簇的大小


SLBUFSIZE 2048 一个未压缩的 SLIP分组的最大长度—包括一个BPF首部
SLIP_HDRLEN 16 SLIP BPF首部的大小
BUFOFFSET 148 一个扩展的 TCP/IP首部的最大长度加上一个 BPF首部的大小
SLMAX 1900 一个存储在簇中的压缩 SLIP分组的最大长度
SLMTU 296 SLIP分组的最佳长度;导致最小的时延,同时还有较高的批量吞吐量
SLIP_HIWAT 100 在TTY输出队列中排队的最大字节数
BUFOFFSET+SLMAX=SLBUFSIZE=MCLBYTES

图5-11 SLIP常量

5.3.3 SLIP输入处理: slinput

T T Y设备驱动程序每次调用 s l i n p u t,都将输入字符传给 S L I P线路规程。图 5 - 1 2显示了


函数slinput,但跳过了帧结束的处理,对于它我们分开讨论。
527-545 传递给slinput的参数为:c,下一个输入字符; tp,一个指向设备tty结构的指
针。全局整数tk_nin计算所有TTY设备的输入字符数。 slinput将tp->t_sc转换成sc,sc
是指向一个sl_softc结构的指针。如果这个TTY设备没有相关联的接口,slinput立即返回。
s l i n p u t的第一个参数是一个整数。除了接收的字符, c还包含从 T T Y设备驱动程序以
高位在前的比特序发送的控制字符。如果在 c中指示了一个差错,或调制解调器控制线禁用并
且不应该被忽略,则 S C _ E R R O R被置位,并且 s l i n p u t返回。之后,当 s l i n p u t处理END字
符时,此帧被丢弃。标志 C L O C A L指示系统应该把这个线路视为一个本地线路 (即不是一个拨
号线路),并且不应该看到调制解调器的控制信号。

Linux公社 www.linuxidc.com
bbs.theithome.com
106计计TCP/IP详解 卷 2:实现

图5-12 函数slinput

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 107
546-636 slinput丢弃c中的控制比特,并用 TTY_CHARMASK来屏蔽掉,更新接口上接收
字节数的计数,同时跳过接收到的字符:
• 如果c是一个转义的ESC字符,并且前一字符为ESC,则slinput用一个ESC字符替代c。
• 如果c是一个转义的END字符,并且前一字符为ESC,则slinput用一个END字符代替c。
• 如果c是SLIP ESC字符,则将s c _ e s c a p e置位,并且s l i n p u t立即返回(即,ESC字符
被丢弃)。
• 如果c是SLIP END字符,则将分组放到 IP输入队列。处理 SLIP帧结束字符的代码显示在
图5-13中。

图5-13 函数slinput :帧结束处理

Linux公社 www.linuxidc.com
bbs.theithome.com
108 计计TCP/IP详解 卷 2:实现

图5-13 (续)

通过这个switch语句的普通控制流会落到switch外(这里没有default情况)。大多数字
节是数据,并且不与这4种情况中的任何一种匹配。前两个case的控制也会落到这个switch外。
637-649 如果控制落到switch外,接收的字符为 IP分组中的一部分。这个字符被存储到簇
中(如果还有空间),指针增加,sc_escape被清除,并且slinput返回。
如果簇满,字符被丢弃,并且 s l i n p u t设置S C _ E R R O R。如果簇满或在处理帧结束时检
测到一个差错,则控制跳到 e r r o r 。程序在 n e w p a c k 为一个新的分组重设簇指针,
sc_escape被清除,并且slinput返回。
图5-13显示了图5-12中跳过的FRAME_END代码。
560-579 如果S C _ E R R O R被设置,同时正在接收分组或如果分组长度小于 3字节(记住,分
组可能被压缩),则slinput立即丢弃此输入 SLIP分组。
如果 S L I P接口带有 B P F,s l i n p u t在c h d r数组中保存这个首部的一个备份 (可能被压
缩)。
580-606 通过检查分组的第一个字节, slinput判断它是一个未压缩的 IP分组,还是一个
压缩的 T C P分段,或者一个未压缩的 T C P分段。类型存放在 c中,并且类型信息从数据的第一
个字节中移去 ( 2 9 . 1 3节)。如果分组以压缩形式出现,并且允许压缩, s l _ u n c o m p r e s s _ t c p
对分组进行解压缩。如果禁止压缩,自动允许压缩被设置,并且如果分组足够大,则仍然调

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 109
用sl_uncompress_tcp。如果是一个压缩的 TCP分组,则设置压缩标志。
若分组不被识别,slinput跳到error,丢弃此分组。29.13节详细讨论了首部压缩技术。
现在簇中包含一个完整的未压缩分组。
607-618 SLIP解压缩分组后,首部和数据传给 BPF。图5-14显示了slinput构造的缓存格式。

未用缓存空间 压缩的首部 未压缩的首部 数据


原分组
(len字节)

图5-14 BPF格式的SLIP分组

BPF首部的第一个字节是分组方向的编码,在此例中是输入 (S L I P D I R _ I N)。接下来的 15
字节包含压缩的首部。整个分组被传给 bpf_tap。
619-635 sl_btom将簇转换为一个 mbuf链表。如果分组足够小,能放到一个单独的 mbuf中,
s l _ b t o m就将分组从簇复制到一个新分配的 m b u f的分组首部;否则 s l _ b t o m将这个簇连接
到一个 m b u f,并为这个接口分配一个新簇。这样比从一个簇复制到另一个簇要快。我们在本
书中不显示 sl_btom的代码。
因为在 S L I P接口上只能传输 I P分组, s l i n p u t不必选择协议队列 (如以太网驱动程序所
做)。分组在 i p i n t r q中排队,一个 I P软件中断被调度,并且 s l i n p u t跳到n e w p a c k,更新
簇的分组指针,并清除 sc_escape。
如果分组不能在 i p i n t r q上排队, S L I P驱动程序增加 i f _ i e r r o r s,而在这种
情况下,以太网或环回驱动程序都不增加这个统计量。
即使在s p l t t y调用s l i n p u t,访问I P输入队列必须用 s p l i m p保护。回忆图 1 - 1 4,一个
splimp中断能抢占 spltty进程。

5.3.4 SLIP输出处理: sloutput

如所有的网络接口,当一个网络层协议调用接口的if_output函数时,开始处理输出。对
于以太网驱动程序,此函数是ether_output。而对于SLIP,此函数是sloutput(图5-15)。
259-289 s l o u t p u t 的4个参数为: i f p,指向 SLIP i f n e t 结构 (在此例中是一个
s l _ s o f t c结构)的指针; m,指向排队等待输出的分组的指针; d s t,分组下一跳的目标地
址; r t p ,指向一个路由表项的指针。 s l o u t p u t未用第 4个参数,但却是要求的,因为
sloutput必须匹配在 ifnet结构中的if_output函数原型。
s l o u t p u t确认d s t是一个IP地址,接口被连接到一个 TTY设备,并且这个 TTY设备是正
在运行的(即有载波信号,或应忽略它 )。如果任何检测失败,则返回差错。
290-291 SLIP为输出分组维护两个队列。默认选择标准队列 if_snd。
292-295 如果输出分组包含一个ICMP报文,并且接口的SC_NOICMP被置位,则丢弃此分组。
这防止一个SLIP链路被一个恶意用户发送的无关 ICMP分组(例如ECHO分组)所淹没(第11章)。

Linux公社 www.linuxidc.com
bbs.theithome.com
110 计计TCP/IP详解 卷 2:实现

图5-15 函数sloutput

差错码E N E T R E S E T指示分组因决策而被丢弃 (相对于网络故障 )。我们在第 11章会看到除

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 111
了在本地产生一个 I C M P报文外,此差错简单地被忽略,在这种情况下,一个差错返回给发送
此报文的进程。
N e t / 2在这种情况返回一个 0。对于一个诊断工具,如 p i n g或t r a c e r o u t e,会
出现这种情况:好像这个分组消失了,因为输出操作会报告成功完成。
通常, I C M P报文可以被丢弃。对于正确的操作,它们并不必要,但丢弃它们会
造成更多的麻烦,可能导致不佳的路由决定和较差的性能,并且会浪费网络资源。
296-297 如果在输出分组的 TOS字段指明低时延服务 (I P T O S _ L O W D E L A Y),则输出队列改
为sc_fastq。
RFC 1700 和RFC 1349 [Almquist 1992] 规定了标准协议的 TO S设置。为 Te l n e t、
R l o g i n、F T P (控制)、T F T P、S M T P (命令阶段 )和D N S ( U D P查询)指明了低时延服务。
更多细节见卷1的3.2节。
在以前的 B S D版本中, i p _ t o s不由应用程序设置。 S L I P驱动程序通过检查在 I P分
组中的传输首部来实现 TO S排队。如果发现 F T P (命令)、Te l n e t或R l o g i n端口的T C P分
组,分组就如指明了 I P T O S _ L O W D E L A Y一样被排队。很多路由器仍然这样,因为很
多这些交互服务的实现仍然不设置 ip_tos。
298-312 现在分组被放到所选择的队列中,接口统计被更新,并且 (如果T T Y输出队列为
空) sloutput调用slstart来发起对此分组的传输。
如果接口队列满,则 S L I P增加i f _ o e r r o r s;而对于 e t h e r _ o u t p u t,则不是
这样做的。
不像以太网输出函数 (e t h e r _ o u t p u t),s l o u t p u t不为输出分组构造一个数据链路首
部。因为在 S L I P网络上的另一系统在串行链路的另一端,所以不需要硬件地址或一个协议 (如
A R P )在I P地址和硬件地址间进行转换。协议标识符 (如以太网类型字段 )也是多余的,因为一
个SLIP链路仅承载 IP分组。

5.3.5 slstart函数

除了被 s l o u t p u t调用外,当 T T Y取完它的输出队列并要传输更多的字节时, T T Y设备


调用 s l s t a r t 。T T Y子系统通过一个 c l i s t 结构管理它的队列。在图 5 - 8 中,输出 c l i s t
t_outq显示在slstart下面和设备的t_oproc函数的上面。slstart把字节添加到队列中,
而t_oproc将队列取完并传输这些字节。
函数slstart显示在图5-16中。
318-358 当slstart函数被调用时, tp指向设备的 tty结构。slstart的主体由一个for
循环构成。如果输出队列 t _ o u t q不空,s l s t a r t调用设备的输出函数 t _ o p r o c,此函数传
输设备所能接收的字节数。如果 T T Y输出队列中剩余的字节超过 1 0 0字节( S L I P _ H I W A T ),
则s l s t a r t返回而不是将另一分组的字节添加到队列中。当传输完所有字节,输出设备产生
一个中断,并且当输出列表为空时, TTY子系统调用 slstart。
如果TTY输出队列为空,则一个分组从 s c _ f a s t q中退队,或者,若 s c _ f a s t q为空,则
从if_snd队列中退队,这样在其他分组前传输所有交互的分组。

Linux公社 www.linuxidc.com
bbs.theithome.com
112 计计TCP/IP详解 卷 2:实现

没有标准的 S N M P变量来统计根据 TO S字段排队的分组。在 3 5 3行的X X X注释表


示SLIP驱动程序在 if_omcasts中统计低时延分组数,而不是多播分组数。
359-383 如果S L I P接口带有 B P F,s l s t a r t在任何首部压缩前为输出分组产生一个备份。
这个备份存储在 bpfbuf数组的栈中。
384-388 如果允许压缩,并且分组包含一个 T C P报文段,则 s l o u t p u t 调用 s l _
c o m p r e s s _ t c p来压缩这个分组。得到的分组类型被返回,并与 I P首部的第一个字节 ( 2 9 . 1 3
节)进行逻辑或运算。
389-398 压缩的首部现在复制到 BPF首部,并且方向标记为 SLIPDIR_OUT。完整的BPF分
组传给bpf_tap。
483-484 如果for循环终止,则slstart返回。

图5-16 函数slstart :分组退队

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 113

图5-16 (续)

s l s t a r t的下一部分 (图5 - 1 7 )在系统存储器容量不足时丢弃分组,并且采用一种简单的


技术来丢弃由于串行线上的噪声产生的数据。这些代码在图 5-16中忽略了。
399-409 如果系统缺少 c l i s t结构,则分组被丢弃,并且作为一个冲突被统计。通过不断地
循环而不是返回, s l s t a r t快速地丢弃所有剩余的排队输出的分组。由于设备仍然有太多字
节为输出排队,每次迭代都要丢弃一个分组。高层协议必须检测丢失的分组并重传它们。
410-418 如果T T Y输出队列为空,则通信线路可能有一段时间空闲,并且接收方在另一端

Linux公社 www.linuxidc.com
bbs.theithome.com
114 计计TCP/IP详解 卷 2:实现

可能接收了线路噪声产生的无关数据。 s l s t a r t在输出队列中放置一个额外的 SLIP END字


符。一个长度为 0的帧或一个由线路噪声产生的帧应该被接收方 SLIP接口或IP协议丢弃。

图5-17 函数slstart :资源缺乏和线路噪声

图5 - 1 8说明了这个丢弃线路噪声的技术,它来源于由 Phil Karn撰写的 RFC 1055。在图 5 -


1 8中,传输第二个帧结束符 ( E N D ),因为线路空闲了一段时间。由噪声产生的无效帧和这个
END字节被接收系统丢弃。

空闲 空闲 额外的

分组 噪声 分组
帧1 帧2 帧3
(OK) (丢弃的) (OK)

图5-18 Karn的丢弃SLIP线路噪声的方法

在图5-19中,线路上没有噪声并且 0长度帧被接收系统丢弃。

空闲 额外的

分组 分组
帧1 帧3
(OK) (OK)

帧2
(丢弃的)

图5-19 无噪声的Karn方法

slstart的下一部分 (图5-20)将数据从一个mbuf传给TTY设备的输出队列。
419-467 在这部分的外部 w h i l e循环对链表中的每个 mbuf执行一次。中间的 w h i le循环将
数据从每个 m b u f传给输出设备。内部的 w h i l e循环不断递增 c p,直到它找到一个 E N D或E S C
字符。 b _ t o _ q传输b p到c p之间的数据。 END和ESC字符被转义,并且两次通过调用 p u t c放

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 115
入队列。中间的循环直到 m b u f的所有字节都传给 T T Y设备输出队列才停止。图 5 - 2 1说明了对
包含了一个 SLIP END字符和一个 SLIP ESC字符的mbuf的处理。
b p标记用b _ t o _ q传输的m b u f的第一部分的开始, c p标记这个部分的结束。 e p标记这个
mbuf中数据的结束位置。

图5-20 函数slstart :传输分组

Linux公社 www.linuxidc.com
bbs.theithome.com
116 计计TCP/IP详解 卷 2:实现

mbuf
数据 数据 数据
首部

第一次调用b_to_q 第二次调用b_to_q 第三次调用b_to_q

图5-21 单个mbuf的SLIP传输

如果b _ t o _ q或p u t c失败(即,数据不能在 T T Y设备排队 ),则b r e a k导致s l s t a r t退出


内部 w h i l e循环。这种失败表示内核 c l i s t资源用完。在每个 m b u f被复制到 T T Y设备后,或者
当一个差错发生时, m b u f被释放, m增加到链表的下一个 m b u f,并且外部 w h i l e循环继续执
行直到链表中所有 mbuf被处理。
图5-22显示了slstart完成输出帧的处理。

图5-22 函数slstart :帧结束处理

468-482 当外部w h i l e循环处理完对输出队列中的字节排队时,控制到达这段代码。驱动


程序发送一个SLIP END字符,来终止这个帧。
如果这些字节在排队时发生差错,则输出帧无效,并会因为“无效的检验和”或“无效
的长度”被接收系统检测出来。
无论这个帧是不是因为一个差错而终止,如果 E N D字符没有填充到输出队列中,队列的
最后一个字符就要被丢弃,并且 s l s t a r t将使这个帧结束。这保证传输了一个 E N D字符。这
个无效帧在目标站被丢弃。

5.3.6 SLIP分组丢失

S L I P接口提供了一个尽最大努力服务的好例子。如果 T T Y超载,则 S L I P丢弃分组;在分


组开始传输后,如果资源不可用,则它截断分组,并且为了检测和丢弃线路噪声插入无关的
空分组。对以上的每一种情况都不产生差错报文。 S L I P依靠 I P层和运输层来检测损坏的和丢
失的分组。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 117
在一个路由器上从一个高速接口例如以太网,发送帧到一个低速的 S L I P线路上。如果发
送方不能意识到瓶颈并相应调节数据速率,则会有大比例的分组被丢弃。在 2 5 . 11节我们会看
到T C P是如何检测并对此响应的。应用程序使用一个无流量控制的协议,如 U D P,必须自己
识别和响应这种情况 (习题5.8)。

5.3.7 SLIP性能考虑

一个S L I P帧的M T U (S L M T U)、c l i s t高水位标记 (high-water mark)(S L I P _ H I W A T)和S L I P的


TOS排队策略都是用来设计交互通信的低速串行链,使得固有的时延最小。
1) 一个小的 M T U能够改进交互数据的时延 (如敲键和回显 ),但有损批量数据传输的吞吐
量。一个大的 M T U能改进批量数据的吞吐量,但增加了交互时延。 S L I P链路的另一个
问题是键入一个字符就要有 4 0字节的开销来写入 T C P首部和 I P首部的信息,这就增加
了通信的时延。
解决办法是挑选一个足够大的 M T U来提供好的交互响应时间和适当的批量数据吞吐
量,并压缩 T C P / I P首部来减小每个分组的负荷。 RFC 1144 [Jacobson 1990a]描述了一
个压缩方案和时间计算,它为一个典型的 9600 b/s异步S L I P链路选择了一个数值为 2 9 6
的M T U。我们在 2 9 . 1 3节讨论压缩的 S L I P ( C S L I P )。卷1的2 . 1 0节和7 . 2节总结了这种定
时考虑,并说明了在 SLIP链路上的时延。
2) 如果有太多的字节缓存在 clist中(因为SLIP_HIWAT设置得太高 ),TOS排队会受到阻碍,
因为新的交互式通信等在大量缓存数据的后面。如果 S L I P一次传给 T T Y驱动程序一个
字节(因为S L I P _ H I W A T设置得太低 ),设备为每个字节调用 s l s t a r t,并在每个字节
传输后线路空闲一段时间。把SLIP_HIWAT设置为100可使在设备排队的数据量最小化,
并且减小了 TTY子系统调用 slstart的频率,大约每 100字符必须调用slstart一次。
3) 如前所述, S L I P驱动程序提供了 TO S排队,其策略是先从 s c _ f a s t q队列中发送交互
式通信数据,然后在标准接口队列 if_snd中发送其他的通信数据。

5.3.8 slclose函数

为了完整性,我们显示函数 s l c l o s e。当s l a t t a c h程序关闭 S L I P的T T Y设备,并且中


断对远程系统的连接时,调用它。

图5-23 函数slclose

Linux公社 www.linuxidc.com
bbs.theithome.com
118 计计TCP/IP详解 卷 2:实现

图5-23 (续)

210-230 t p指向要关闭的 T T Y设备。 s l c l o s e清除任何残留在串行设备中的数据,中断


T T Y和网络处理,并且将 T T Y复位到默认的线路规程。如果 T T Y设备被连接到一个 S L I P接口,
则关闭这个接口,在这两个结构间的链接被切断,与此接口关联的 m b u f簇被释放,并且指向
现在被丢弃的簇的指针被复位。最后, splx重新允许TTY中断和网络中断。

5.3.9 sltioctl函数

回忆一下, SLIP在内核中有两种作用:
• 作为一个网络接口;
• 作为一个TTY线路规程。
图5 - 7显示了 s l i o c t l处理通过一个插口描述符发送给一个 S L I P接口的 i o c t l命令。在
4 . 4节中,我们显示了 i f i o c t l是如何调用 s l i o c t l的。我们会看到一个处理 i o c t l命令的
相似模型,并且在后面的章节中会讨论到。
图5 - 7还表示了 s l t i o c t l处理发送给与一个 S L I P网络接口关联的 T T Y设备的 i o c t l命
令。这个被 sltioctl识别的命令显示在图 5-24中。
命 令 参 数 函 数 说 明

SLIOCGUNIT int * sltioctl 返回与TTY设备关联的接口联合

图5-24 sltioctl 命令

函数sltioctl显示在图5-25中。

图5-25 函数sltioctl

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 119
236-252 t t y结构的 t _ s c指针指向关联的 s l _ s o f t c结构。这个 S L I P接口的设备号从
if_unit被复制到*data,它最后返回给进程 (17.5节)。
当系统被初始化时, s l a t t a c h初始化 i f _ u n i t,并且当 s l a t t a c h程序为此 T T Y设备
选择SLIP线路规程时, s l o p e n初始化t _ s c。因为一个TTY设备和一个 SLIP s l _ s o f t c结构
间的关系是在运行时建立的,一个进程能通过 SLIOCGUNIT命令发现所选择的接口结构。

5.4 环回接口

任何发送给环回接口 (图5-26)的分组立即排入输入队列。接口完全用软件实现。

OSI协议

图5-26 环回设备驱动程序

环回接口的 i f _ o u t p u t指向的函数l o o u t p u t,将输出分组放置到分组的目的地址指明


的协议的输入队列中。
我们已经看到当设备被设置为 I F F _ S I M P L E X时,e t h e r _ o u t p u t会调用l o o u t p u t来
排队一个输出广播分组。在第 1 2 章中,我们会看到多播分组也可能以这种方式环回。
looutput显示在图5-27中。

图5-27 函数looutput

Linux公社 www.linuxidc.com
bbs.theithome.com
120 计计TCP/IP详解 卷 2:实现

图5-27 (续)

57-66 l o o u t p u t的参数同 e t h e r _ o u t p u t一样,因为都是通过它们的 i f n e t结构中的


i f _ o u t p u t指针直接调用的。 i f p,指向输出接口的 i f n e t结构的指针; m,要发送的分
组; d s t,分组的目的地址; r t,路由信息。如果链表中的第一个 m b u f不包含一个分组,
looutput调用panic。
图5-28所示的是一个BPF环回分组的逻辑格式。
69-83 驱动程序在堆栈上的 m 0中构造BPF环回分组,并且把 m 0连接到包含原始分组的 mbuf

Linux公社 www.linuxidc.com
bbs.theithome.com
第 5章 接口:SLIP和环回计计 121
链表中。注意 m 0的声明不同往常。它是一个 m b u f,而不是一个 m b u f指针。 m 0的m _ d a t a指向
af,它也分配在这个堆栈中。图 5-29显示了这种安排。
BPF
首部

地址簇 原分组

4字节

图5-28 BPF环回分组:逻辑格式

由looutput 在内核mbuf
在栈上分配 池中分配
图5-29 BPF环回分组: mbuf格式

l o o u t p u t将目的地址族复制到 af,并且将新 mbuf链表传递给 b p f _ m t a p,去处理这个分


组。与 b p f _ t a p相比,它在一个单独的连续缓存中接收这个分组而不是在一个 m b u f链表中。
如图中注释所示, BPF从来不释放一个链表中的 mbuf,因此将m 0 (它指向栈中的一个 m b u f)传
给bpf_mtap是安全的。
84-89 l o o u t p u t剩下的代码包含 i n p u t对此分组的处理。虽然这是一个输出函数,但分组
被环回到输入。首先, m - > m _ p k t h d r . r c v i f设置为指向接收接口。如果调用方提供一个路
由项, l o o u t p u t 检查是否它指示此分组应该被拒绝 ( R T F _ R E J E C T )或直接被丢弃
(R T F _ B L A C K H O L E)。通过丢弃 m b u f并返回0来实现一个黑洞。从调用者看来就好像分组已经
被传输了。要拒绝一个分组,如果路由是一个主机,则 l o o u t p u t返回E H O S T U N R E A C H;如
果路由是一个网络则返回 ENETUNREACH。
各种RTF_xxx标志在图18-25中描述。
90-120 然后l o o u t p u t通过检查分组目的地址中的 s a _ f a m i l y来选择合适的协议输入队
列和软件中断。接着把识别的分组进行排队,并用 schednetisr来调度一个软件中断。

5.5 小结

我们讨论了两个剩下的接口,它们在书中多次引用: s l 0,一个SLIP接口; l o 0,标准的


环回接口。
我们显示了在 S L I P接口和S L I P线路规程之间的关系,讨论了 S L I P封装方法,并且讨论了
TOS处理交互式通信和 SLIP驱动程序的其他性能考虑。
我们显示了环回接口是如何按目的地址分用输出分组及将分组放到相应的输入队列中
去。

Linux公社 www.linuxidc.com
bbs.theithome.com
122 计计TCP/IP详解 卷 2:实现

习题

5.1 为什么环回接口没有输入函数?
5.2 你认为为什么图 5-27中的m0要分配在堆栈中?
5.3 分析一个19 200 b/s的串行线的 SLIP特性。对于这个线路, SLIP MTU应该改变吗?
5.4 导出一个根据串行线速率选择 SLIP MTU的公式。
5.5 如果一个分组对于 SLIP输入缓存太大,会发生什么情况?
5.6 一个 s l i n p u t的早期版本,当一个分组在输入缓存溢出时,不将 S C _ E R R O R置位。
在这种情况下如何检测这种差错?
5.7 在图4 - 3 1中l e被下标为 i f p - > i f _ u n i t的l e _ s o f t c数组项初始化。你能想出另一
种初始化 le的方法吗?
5.8 当分组因为网络瓶颈被丢弃时,一个 UDP应用程序如何知道?

Linux公社 www.linuxidc.com
bbs.theithome.com
第6章 IP 编 址
6.1 引言

本章讨论 N e t / 3如何管理 I P地址信息。我们从 i n _ i f a d d r和s o c k a d d r _ i n结构开始,它


们基于通用的ifaddr和sockaddr结构。
本章其余部分讨论 IP地址的指派和几个查询接口数据结构与维护 IP地址的实用函数。

6.1.1 IP地址

虽然我们假设读者熟悉基本的 Internet编址系统,仍然有几点值得指出。
在I P模型中,地址是指派给一个系统 (一个主机或路由器 )中的网络接口而不是系统本身。
在系统有多个接口的情况下,系统有多重初始地址,并有多个 I P地址。一个路由器被定义为
有多重初始地址。如我们所看到的,这个体系特点有几个小分支。
I P地址定义了 5类。A、B和C类地址支持单播通信。 D类地址支持 I P多播。在一个多播通
信中,一个单独的源方发送一个数据报给多个目标方。 D类地址和多播协议在第 1 2章说明。 E
类地址是试验用的。接收的 E类地址分组被不参与试验的主机丢弃。
我们强调IP多播和硬件多播间的区别是重要的。硬件多播的特点是数据链路硬件用来将帧
传输给多个硬件接口。有些网络硬件,如以太网,支持数据链路多播。其他硬件可能不支持。
I P多播是一个在 I P系统内实现的软件特性,将分组传输给多个可能在 I n t e r n e t中任何位置
的IP地址。
我们假设读者熟悉 I P网络的子网划分 (RFC 950 [Mogul and Postel 1985] 和卷1的第3章)。
我们会看到每个网络接口有一个相关的子网掩码,它是判断一个分组是否到达它最后的目的
地或还需要被转发的关键。通常,当提及一个 I P地址的网络部分时,我们包括任何可能定义
的子网。当需要区分网络和子网时,我们就要明确地指出来。
环回网络, 1 2 7 . 0 . 0 . 0,是一个特殊的 A类网络。这种格式的地址是不会出现在一个主机的
外部的。发送到这个网络的分组被环回并被这个主机接收。
RFC 11 2 2要求所有在环回网络中的地址被正确地处理。因为环回接口必须指派
一个地址,很多系统选择 1 2 7 . 0 . 0 . 1 作为环回地址。如果系统不能正确识别,像
127.0.0.2这样的地址可能不能被路由到环回接口而被传输到一个连接的网络,这是不
允许的。有些系统可能正确地路由这个到环回接口的分组,但由于目标地址与地址
127.0.0.1不匹配,分组被丢弃。
图18-2显示了一个 Net/3系统配置为拒绝接收发送到一个不是 127.0.0.1的环回地址的分组。

6.1.2 IP地址的印刷规定

我们通常以点分十进制数表示法来显示一个 IP地址。图 6-1列出了每类 IP地址的范围。

Linux公社 www.linuxidc.com
bbs.theithome.com
124 计计TCP/IP详解 卷 2:实现

地址类 范 围 类 型

A 0.0.0.0到127.255.255.255
B 128.0.0.0到191.255.255.255 单播
C 192.0.0.0到223.255.255.255
D 224.0.0.0到239.255.255.255 多播
E 240.0.0.0到247.255.255.255 试验性

图6-1 不同IP地址类的范围

对于我们的有些例子,子网字段不按一个字节对齐 (即,一个网络 /子网/主机在一个B类网


络中分为 1 6 / 11 / 5 )。从点分十进制数表示法很难表示这样的地址,因此我们还是用方块图来说
明I P地址的内容。我们用三个部分显示每个地址:网络、子网和主机。每个部分的阴影指示
它的内容。图 6-2用我们网络示例 (1.14节)中的主机 s u n的以太网接口来同时说明块表示法和点
分十进制数表示法。

B类网络140.252 子网105 主机1

接口地址

全0

有1有0
网络 子网 主机
有1有0
网络掩码
全1

网络140.252 子网105 所有主机


31
定向的广播

图6-2 可选的IP地址表示法

当地址的一个部分不是全为 0或1时,我们使用两个中等程度的阴影。有两种中等
程度的阴影,这样我们就能区分网络和子网部分或用来显示如图 6 - 3 1所示的地址组
合。

6.1.3 主机和路由器

在一个I n t e r n e t上的系统通常能划分为两类:主机和路由器。一个主机通常有一个网络接
口,并且是一个 I P分组的源或目标方。一个路由器有多个网络接口,当分组向它的目标方移
动时将分组从一个网络转发到下一个网络。为执行这个功能,路由器用各种专用路由协议来
交换关于网络拓扑的信息。 IP路由问题比较复杂,在第 18章开始讨论它们。
如果一个有多个网络接口的系统不在网络接口间路由分组,仍然叫一个主机。一个系统
可能既是一个主机又是一个路由器。这种情况经常发生在当一个路由器提供运输层服务如用

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 125
于配置的 Te l n e t访问,或用于网络管理的 S N M P时。当区分一个主机和路由器间的意义并不重
要时,我们使用术语系统。
不谨慎地配置一个路由器会干扰一个网络的正常运转,因此 RFC 11 2 2规定一个系统必须
默认为一个主机来操作,并且必须显式地由一个管理员来配置作为一个路由器操作。这样做
是不鼓励管理员将通用主机作为路由器来操作而没有仔细地配置。在 N e t / 3中,如果全局整数
i p f o r w a r d i n g不为0,则一个系统作为一个路由器;如果 i p f o r w a r d i n g为0 (默认),则系
统作为一个主机。
在N e t / 3中,一个路由器通常称为网关,虽然术语网关现在更多的是与一个提供应用层路
由的系统相关,如一个电子邮件网关,而不是转发 I P分组的系统。我们在本书中使用术语路
由器,并假设 i p f o r w a r d i n g非0。在编译 N e t / 3内核期间,当 GATEWAY被定义时,我们还
有条件地包括所有代码,它们将 ipforwarding定义为1。

6.2 代码介绍

图6-3所列的两个头文件和两个 C文件包含本章中讨论的结构定义和实用函数。
文 件 说 明

netinet/in.h Internet地址定义
netinet/in_var.h Internet接口定义
netinet/in.c Internet初始化和实用函数
netinet/if.c Internet接口实用函数

图6-3 本章讨论的文件

全局变量

图6-4所列的是本章中介绍的两个全局变量。
变 量 数据类型 说 明

in_ifaddr s t r u c t i n _ i f a d d r * i n _ i f a d d r结构列表的首部
in_interfaces int 有IP能力的接口个数

图6-4 在本章中介绍的全局变量

6.3 接口和地址小结

在本章讨论的所有接口和地址结构的一个例子配置如图 6-5所示。
图6 - 5显示了我们的三个接口例子:以太网接口、 S L I P接口和环回接口。它们都有一个链
路层地址作为地址列表中的第一个结点。显示的以太网接口有两个 I P地址, S L I P接口有一个
IP地址,并且环回接口有一个 IP地址和一个 OSI地址。
注意所有的IP地址被链接到in_ifaddr列表中,并且所有链路层地址能从 ifnet_addrs
数组访问。
为了清楚起见,图 6 - 5没有画出每个 i f a d d r结构中的指针 i f a _ i f p。这些指针回指包含
此ifaddr结构的列表的首部 ifnet结构。
接下来的部分讨论图 6-5中的数据结构及用来查看和修改这些结构的 IP专用ioctl命令。

Linux公社 www.linuxidc.com
bbs.theithome.com
126 计计TCP/IP详解 卷 2:实现

每个in_ifaddr{}以
一个ifaddr{}开始

图6-5 接口和地址数据结构

6.4 sockaddr_in结构

我们在第 3章讨论了通用的 s o c k a d d r和i f a d d r结构。现在我们显示 I P专用的结构:


sockaddr_in和in_ifaddr。在Internet域中的地址存放在一个 sockaddr_in结构:
68-70 由于历史原因, Net/3以网络字节序将 Internet地址存储在一个 in_addr结构中。这个结
构只有一个成员 s _ a d d r,它包含这个地址。虽然这是多余和混乱的,但在 N e t / 3中一直保持
这种组织方式。
106-112 s i n _ l e n总是1 6(结构s o c k a d d r _ i n的大小),并且s i n _ f a m i l y为A F _ I N E T。
sin_port是一个网络字节序(不是主机字节序 )的16 bit值,用来分用运输层报文。 sin_addr
标识一个32 bit Internet地址。
图6-6显示了sock a d d r _ i n的成员s i n _ p o r t、s i n _ a d d r和s i n _ z e r o覆盖s o c k a d d r

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 127
的成员sa_data。在I n t e r n e t域中,s i n _ z e r o未用,但必须由全 0字节组成(2.7节)。将它
追加到sockaddr_in结构后面,以得到与一个 sockaddr结构一样的长度。

图6-6 结构sockaddr_in

通常,当一个 I n t e r n e t地址存储在一个 u _ l o n g中时,它以主机字节序存储,以便于地址的


压缩和位操作。在 in_addr结构(图6-7)中的s_addr是一个值得注意的例外。

图6-7 一个sockaddr_in 结构(省略sin_ )的组织

6.5 in_ifaddr结构

图6 - 8显示了为 I n t e r n e t协议定义的接口地址结构。对于每个指派给一个接口的 I P地址,分


配了一个in_ifaddr结构,并且添加到接口地址列表中和 IP地址全局列表中 (图6-5)。
41-45 in_ifaddr开始是一个通用接口地址结构 ia_ifa,跟着是IP专用成员。 ifaddr结
构显示在图 3 - 1 5中。两个宏 i a _ i f p和i a _ f l a g s简化了对存储在通用 i f a d d r结构中的接口
指针和接口地址标志的访问。 i a _ n e x t维护指派给任意接口的所有 I n t e r n e t地址的链接列表。
这个列表独立于每个接口关联的链路层 i f a d d r结构列表,并且通过全局列表 i n _ i f a d d r来
访问。
46-54 其余的成员 (除了ia_multiaddrs)显示在图6-9中,它显示了在我们的 B类网络例子
中s u n的三个接口的相应值。地址按主机字节序以 u _ l o n g 变量存储;变量 i n _ a d d r 和
s o c k a d d r _ i n按照网络字节序存储。 s u n有一个P P P接口,但显示在本表中的信息对于一个
PPP或SLIP接口是一样的。
55-56 结构i n _ i f a d d r的最后一个成员指向一个 i n _ m u l t i结构的列表 ( 1 2 . 6节),其中每
项包含与此接口有关的一个 IP多播地址。

Linux公社 www.linuxidc.com
bbs.theithome.com
128 计计TCP/IP详解 卷 2:实现

图6-8 结构in_ifaddr

变 量 类 型 以 太 网 PPP 环回 说明

网络,子网和主机号

网络号

网络号掩码

网络和子网号

网络和子网掩码

网络广播地址

定向广播地址

目的地址

像ia_subnetmask
但是用网络字节序

图6-9 sun 上的以太网、 PPP和环回in_ifaddr 结构

6.6 地址指派

在第4章中,我们显示了当接口结构在系统初始化期间被识别时的初始化。在 Internet协议
能通过这个接口进行通信前,必须指派一个 I P地址。一旦 N e t / 3内核运行,程序 i f c o n f i g就
配置这些接口, i f c o n f i g通过在某个插口上的 i o c t l系统调用来发送配置命令。这通常通
过/ e t c / n e t s t a r t s h e脚本来实现,这个脚本在系统引导时执行。
ll
图6 - 1 0显示了本章中讨论的 i o c t l命令。命令相关的地址必须是此命令指定插口所支持
的地址族类 (即,你不能通过一个 U D P插口配置一个 O S I地址)。对于I P地址,i o c t l命令在一

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 129
个UDP插口上发送。

命 令 参 数 函 数 说 明

SIOCGIFADDR struct ifreq * in_control 获得接口地址


SIOCGIFNETMASK struct ifreq * in_control 获得接口网络掩码
SIOCGIFDSTADDR struct ifreq * in_control 获得接口目标地址
SIOCGIFBRDADDR struct ifreq * in_control 获得接口广播地址
SIOCSIFADDR struct ifreq * in_control 设置接口地址
SIOCSIFNETMASK struct ifreq * in_control 设置接口网络掩码
SIOCSIFDSTADDR struct ifreq * in_control 设置接口目标地址
SIOCSIFBRDADDR struct ifreq * in_control 设置接口广播地址
SIOCDIFADDR struct ifreq * in_control 删除接口地址
SIOCAIFADDR struct i n _ a l i a s r e q * in_control 添加接口地址

图6-10 接口ioctl 命令

系统调用

默认

图6-11 本章中说明的ioctl 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
130 计计TCP/IP详解 卷 2:实现

获得地址信息的命令从 S I O C G开始,设置地址信息的命令从 S I O C S 开始。 S I O C代表


socket ioctl,G代表get,而S代表set。
在第4章中,我们看到了 5个与协议无关的 i o c t l命令。图 6 - 1 0中的命令修改一个接口的
相关地址信息。由于地址是特定协议使用的,因此,命令处理是与协议相关的。图 6 - 11强调
了与这些命令关联的 ioctl相关函数。

6.6.1 ifioctl函数

如图6-11所示,i f i o c t l将协议无关的i o c t l命令传递给此插口关联协议的 p r _ u s r r e q


函数。将控制交给 u d p _ u s r r e q,并且又立即传给 i n _ c o n t r o l,在i n _ c o n t r o l中进行大
部分的处理。如果在一个 T C P插口上发送同样的命令,控制最后也会到达 i n _ c o n t r o l。图
6-12再次显示了 ifioctl中的default代码,第一次显示在图 4-22中。

图6-12 函数ifioctl :特定协议的命令

447-454 函数将图 6 - 1 0中所列 i o c t l命令的所有相关数据传给与请求指定的插口相关联的


协议的用户请求函数。对于一个 UDP插口,调用 u d p _ u s r r e q。23.10节讨论u d p _ u s r r e q函
数的细节。现在,我们仅需要查看 udp_usrreq中的PRU_CONTROL代码:
if (req == PRU_CONTROL)
return (in_control(so, (int)m, (caddr_t)addr, (struct ifnet *)control));

6.6.2 in_control函数

图6 - 11显示了通过 s o o _ i o c t l中的d e f a u l t或i f i o c t l中的与协议相关的情况,控制


能到达 i n _ c o n t r o l 。在这两种情况中, u d p _ u s r r e q 调用 i n _ c o n t r o l ,并返回
in_control的返回值。图6-13显示了in_control。
132-145 so指向这个ioctl命令(由第二个参数cmd标识)指定的插口。第三个参数 data指
向命令所用或返回的数据 (图6 - 1 0的第二列 )。最后一个参数 i f p为空(来自s o o _ i o c t l的无接
口i o c t l)或指向结构 i f r e q或i n _ a l i a s r e q中命名的接口 (来自i f i o c t l的接口 i o c t l)。
in_control初始化ifr和ifra来访问作为一个 ifreq或in_aliasreq结构的data。
146-152 如果i f p指向一个i f n e t结构,这个f o r循环找到与此接口关联的 I n t e r n e t地址列
表中的第一个地址。如果发现一个地址, ia指向它的in_ifaddr结构;否则 ia为空。
若i f p为空, c m d就不会匹配第一个 s w i t c h中的任何情况;或第二个 s w i t c h中任何非
默认情况。在第二个 switch中的default情况中,当 ifp为空时,返回EOPNOTSUPP。
153-330 in_control中的第一个 switch确保在第二个switch处理命令之前每个命令的
前提条件都满足。在后面的章节会单独说明各个情况。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计131

图6-13 函数in_control

如果在第二个 s w i t c h中的d e f a u l t情况被执行, i f p指向一个接口结构;并且如果接


口有一个if_ioctl函数,则 in_control将ioctl命令传给这个接口进行设备的特定处理。
N e t / 3不定义任何会被 d e f a u l t情况处理的接口命令。但是,一个特定设备的驱
动程序可能会定义它自己的接口 ioctl命令,并通过这个 case来处理它们。
331-332 我们会看到这个 s w i t c h语句中的很多情况都直接返回了。如果控制落到两个
switch语句外,则 in_control返回0。第二个switch中有几个case执行了跳出语句。
我们按照下面的顺序查看这个接口 ioctl命令:
• 指派一个地址、网络掩码或目标地址;
• 指派一个广播地址;

Linux公社 www.linuxidc.com
bbs.theithome.com
132 计计TCP/IP详解 卷 2:实现

• 取回一个地址、网络掩码、目标地址或广播地址;
• 给一个接口指派多播地址;
• 删除一个地址。
对于每组命令,在第一个 s w i t c h语句中进行前提条件处理,然后在第二个 s w i t c h语句
中处理命令。

6.6.3 前提条件: SIOCSIFADDR、SIOCSIFNETMASK和SIOCSIFDSTADDR

图6 - 1 4显示了对 S I O C S I F A D D R、S I O C S I F N E T M A S K和S I O C S I F D S T A D D R的前提条件


检验。

图6-14 函数in_control :地址指派

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 133
1. 仅用于超级用户
166-172 如果这个插口不是由一个超级用户进程创建的,这些命令被禁止,并且 in_
control返回EPERM。如果此请求没有关联的接口,内核调用 panic。由于如果 ifioctl不
能找到一个接口,它就返回 (图4-22),因此,panic从来不会被调用。
当一个超级用户进程创建一个插口时, s o c r e a t e(图15-16)设置标志 S S _ P R I V。
因为这里的检验是针对标志而不是有效的进程用户 I D的,所以一个设置用户 I D的根
进程能创建一个插口,并且放弃它的超级用户权限,但仍然能发送有特权的 i o c t l
命令。
2. 分配结构
173-191 如果ia为空,命令请求一个新的地址。 in_control分配一个in_ifaddr结构,
用b z e r o清除它,并且将它链接到系统
的 i n _ i f a d d r 列表中和此接口的
if_addrlist列表中。
3. 初始化结构
192-206 代码的下一部分初始化
i n _ i f a d d r结构。首先,在此结构的
i f a d d r部分的通用指针被初始化为指
向结构in_ifaddr中的结构
s o c k a d d r _ i n。必要时,此函数还初
始化结构i a _ s o c k m a s k 和
图6-15 被in_control 初始化后的一个 in_ifaddr 结构
i a _ b r o a d a d d r。图6 - 1 5说明了初始化
后的结构 in_ifaddr。
202-206 最后,in_control建立从in_ifaddr到此接口的 ifnet结构的回指指针。
Net/3在in_interfaces中只统计非环回接口。

6.6.4 地址指派: SIOCSIFADDR

前提条件处理代码保证 i a指向一个要被 S I O C S I F A D D R命令修改的i n _ i f a d d r结构。图


6-16显示了in_control第二个switch中处理这个命令的执行代码。

图6-16 函数in_control :地址指派

159-261 i n _ i f i n i t完成所有的工作。 I P地址包含在 i f r e q结构(i f r _ a d d r)里传递给


in_ifinit。

6.6.5 in_ifinit函数

in_ifinit的主要步骤是:
• 将地址复制到此结构并将此变化通知硬件;

Linux公社 www.linuxidc.com
bbs.theithome.com
134 计计TCP/IP详解 卷 2:实现

• 忽略原地址配置的任何路由;
• 为这个地址建立一个子网掩码;
• 建立一个默认路由到连接的网络 (或主机);
• 将此接口加入到所有主机组。
从图6-17开始分三个部分讨论这段代码。
353-357 i n _ i f i n i t的四个参数为: i f p,指向接口结构的指针; i a,指向要改变的
i n _ i f a d d r结构的指针; s i n,指向请求的 I P地址的指针; s c r u b,指示这个接口如果存在
路由应该被忽略。 i保存主机字节序的 IP地址。

图6-17 函数in_ifinit :地址指派和路由初始化

1. 指派地址并通知硬件
358-374 i n _ c o n t r o l将原来的地址保存在 o l d a d d r中,万一发生差错时,必须恢复它。
如果接口定义了一个 i f _ i o c t l 函数,则 i n _ c o n t r o l 调用它。相同接口的三个函数
l e i o c t l、s l i o c t l和l o i o c t l在下一节讨论。如果发生差错,恢复原来的地址,并且
in_control返回。
2. 以太网配置
375-378 对于以太网设备, a r p _ r t r e q u e s t作为链路层路由函数被选择,并且设置
RT F _ C L O N I N G标志。 a r p _ r t r e q u e s t在2 1 . 1 3节讨论,而 RT F _ C L O N I N G在1 9 . 4节的最后

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 135
讨论。如XXX注释所建议,在此加入代码以避免改变所有以太网驱动程序。
3. 忽略原来的路由
379-384 如果调用者要求已存在的路由被清除,原地址被重新连接到 i f a _ a d d r,同时
in_ifscrub找到并废除任何基于老地址的路由。 in_ifscrub返回后,新地址被恢复。
in_ifinit显示在图6-18中的部分构造网络和子网掩码。

图6-18 函数in_ifinit :网络和子网掩码

4. 构造网络掩码和默认子网掩码
385-400 根据地址是一个 A类、B类或C类地址,在 i a _ n e t m a s k中构造了一个尝试性网络
掩码。如果这个地址没有子网掩码, i a _ s u b n e t m a s k 和i a _ s o c k m a s k 被初始化为
ia_netmask中的尝试性掩码。
如果指定了一个子网, i n _ i f i n i t将这个尝试性网络掩码和这个已存在的子网掩码进行
逻辑与运算来获得一个新的网络掩码。这个操作可能会清除该尝试性网络掩码的一些 1 bit(它
从来不设置 0 bit,因为0逻辑与任何值都得到 0 )。在这种情况下,网络掩码比所考虑的地址类
型所期望的要少一些 1 bit 。
这叫作超级联网,它在 RFC 1519 [Fuller et al. 1993]中作了描述。一个超级网络
是几个A类、B类或C类网络的一个群组。卷 1的10.8节也讨论了超级联网。
一个接口默认配置为不划分子网 ( 即,网络和子网的掩码相同 ) 。一个显式请求 ( 用
SIOCSIFNETMASK或SIOCAIFADDR)用来允许子网划分 (或超级联网 )。
5. 构造网络和子网数量
401-403 网络和子网数量通过网络和子网掩码从新地址中获得。函数 in_socktrim通过查
找掩码中包含1 bit的最后一个字节来设置in_sockmask(是一个sockaddr_in结构)的长度。
图6 - 1 9显示了 i n _ i f i n i t的最后一部分,它为接口添加了一个路由,并加入所有主机多
播组。
6. 为主机或网络建立路由
404-422 下一步是为新地址所指定的网络创建一个路由。 i n _ c o n t r o l从接口将路由度量

Linux公社 www.linuxidc.com
bbs.theithome.com
136 计计TCP/IP详解 卷 2:实现

复制到结构i n _ i f a d d r中。如果接口支持广播,则构造广播地址,并且把目的地址强制为分
配给环回接口的地址。如果一个点对点接口没有一个指派给链路另一端的 I P 地址,则
in_control在试图为这个无效地址建立路由前返回。
in_ifinit将flags初始化为RTF_UP,并与环回和点对点接口的 RTF_HOST进行逻辑或。
r t i n i t为此接口给这个网络 (不设置 R T F _ H O S T)或主机 (设置R T F _ H O S T)安装一个路由。若
rtinit安装成功,则设置ia_flags中的标志IFA_ROUTE,指示已给此地址安装了一个路由。

图6-19 函数in_ifinit :路由和多播组

7. 加入所有主机组
423-433 最后,一个有多播能力的接口当它被初始化时必须加入所有主机多播组。
in_addmulti完成此工作,并在 12.11节讨论。

6.6.6 网络掩码指派: SIOCSIFNETMASK

图6-20显示了网络掩码命令的处理。

图6-20 函数in_control :网络掩码指派

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 137
262-265 i n _ c o n t r o l 从i f r e q 结构中获取网络掩码,并将它以网络字节序保存在
ia_sockmask中,以主机字节序保存在 ia_subnetmask中。

6.6.7 目的地址指派: SIOCSIFDSTADDR

对于点对点接口,在链路另一端的系统的地址用 S I O C S I F D S T A D D R命令指定。图 6 - 1 4显
示了图6-21中的代码的前提条件处理。

图6-21 函数in_control :目的地址指派

236-245 只有点对点网络才有目的地址,因此对于其他网络, in_control返回EINVAL。


将当前目的地址保存在 o l d a d d r后,代码设置新地址,并通过函数 i f _ i o c t l通知硬件。如
果发生差错,则恢复原地址。
246-253 如果地址原来有一个关联的路由,首先调用 r t i n i t删除这个路由,并再次调用
rtinit为新地址安装一个新路由。

6.6.8 获取接口信息

图6 - 2 2显示了命令 S I O C S I F B R D A D D R的前提条件处理,它同将接口信息返回给调用进程
的ioctl命令一样。

图6-22 函数in_control :前提条件处理

Linux公社 www.linuxidc.com
bbs.theithome.com
138 计计TCP/IP详解 卷 2:实现

207-217 广播地址只能通过一个超级用户进程创建的插口来设置。命令 SIOCSIFBRDADDR


和4个S I O C Gx x x命令仅当已经为此接口定义了一个地址时才起作用,在这种情况下, i a不会
为空(ia被in_control设置,图6-13)。如果ia为空,返回 EADDRNOTAVAIL。
这5个命令(4个get命令和一个 set命令)的处理显示在图 6-23中。

图6-23 函数in_control :处理

220-235 将单播地址、广播地址、目的地址或者网络掩码复制到 i f r e q结构。只有网络接


口支持广播,广播地址才有效;并且只有点对点接口,目的地址才有效。
254-258 仅当接口支持广播,才从结构 ifreq中复制广播地址。

6.6.9 每个接口多个IP地址

S I O C Gx x x和S I O C Sx x x命令只操作与一个接口关联的第一个 I P地址—在i n _ c o n t r o l


开头的循环找到的第一个地址 ( 图 6 - 2 5 ) 。为支持每个接口的多个 I P 地址,必须用
S I O C A I F A D D R命令指派和配置其他的地址。实际上, S I O C A I F A D D R能完成所有 S I O C Gx x x
和S I O C Sx x x命令能完成的操作。程序 i f c o n f i g使用S I O C A I F A D D R来配置一个接口的所有
地址信息。
如前所述,每个接口有多个地址便于在主机或网络改号时过渡。一个容错软件系统可能
使用这个特性来准许一个备份系统充当一个故障系统的 IP地址。
Net/3的i f c o n f i g程序的- a l i a s选项将存放在一个 i n _ a l i a s r e q中的其他地址的相关
信息传递给内核,如图 6-24所示。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 139

图6-24 结构in_aliasreq

59-65 注意,不像结构 i f r e q ,在结构 i n _ a l i a s r e q中没有定义联合。在一个单独的


ioctl调用中可以为SIOCAIFADDR指定地址、广播地址和掩码。
S I O C A I F A D D R增加一个新地址或修改一个已存在地址的相关信息。 S I O C D I F A D D R删除
匹配的 I P地址的i n _ i f a d d r结构。图 6 - 2 5显示命令 S I O C A I F A D D R和S I O C D I F A D D R的前提
条件处理,它假设在 i n _ c o n t r o l ( 图 6 - 1 3 ) 开头的循环已经将 i a设置为指向与
ifra_name(如果存在)指定的接口关联的第一个 IP地址。

图6-25 函数in_control :添加和删除地址

154-165 因为S I O C D I F A D D R代码只查看 * i f r a的前两个成员,图 6 - 2 5所示的代码用于处


理S I O C A I F A D D R(当i f r a指向一个 i n _ a l i a s r e q结构时)和S I O C D I F A D D R(当i f r a指向一
个ifreq结构时)。结构in_aliasreq和ifreq的前两个成员是一样的。
对于这两个命令, i n _ c o n t r o l 开头的循环启动 f o r 循环不断地查找与 i f r a -
> i f r a _ a d d r指定的IP地址相同的i n _ i f a d d r结构。对于删除命令,如果地址没有找到,则
返回EADDRNOTAVAIL。
在这个处理删除命令的循环和检验后,控制落到我们在图 6 - 1 4中讨论的代码之外。对于
添加命令,图 6-14的代码若找不到一个与 i n _ a l i a s r e q结构中地址匹配的地址,就分配一个
新in_ifaddr结构。

6.6.10 附加IP地址:SIOCAIFADDR

这时 i a 指向一个新的 i n _ i f a d d r 结构或一个包含与请求地址匹配的 I P 地址的旧


in_ifaddr结构。SIOCAIFADDR的处理显示在图 6-26中。
266-277 因为S I O C A I F A D D R能创建一个新地址或修改一个已存在地址的相关信息,标志
m a s k I s N e w和h o s t I s N e w跟踪变化的情况。这样,在这个函数结束时,如果必要,能更新
路由。

Linux公社 www.linuxidc.com
bbs.theithome.com
140 计计TCP/IP详解 卷 2:实现

图6-26 函数in_control :SIOCAIFADDR 处理

代码在默认方式下取一个新的 IP地址指派给接口 (h o s t I s N e w以1开始)。如果新地址的长


度为0,i n _ c o n t r o l将当前地址复制到请求中,并将 h o s t I s N e w修改为0。如果长度不是 0,
并且新地址与老地址匹配,则这个请求不包含一个新地址,并且 hostIsNew被设置为0。
278-284 如果在这个请求中指定一个网络掩码,则任何使用此当前地址的路由被忽略,并
且in_control安装此新掩码。
285-290 如果接口是一个点对点接口,并且此请求包括一个新目的地址,则 i n _ s c r u b忽
略任何使用此地址的路由,新目的地址被安装,并且 m a s k I s N e w被设置为 1,以强制调用
in_ifinit来重配置接口。
291-297 如果配置了一个新地址或指派了一个新掩码,则 in_ifinit作适当的修改来支持
新的配置 (图6-17)。注意,i n _ i f i n i t的最后一个参数为 0。这表示已注意到这一点,不必刷
新所有路由。最后,如果接口支持广播,则从 in_aliasreq结构复制广播地址。

6.6.11 删除IP地址:SIOCDIFADDR

命令 S I O C D I F A D D R从一个接口删除 I P地址,如图 6 - 2 7所示。记住, i a指向要被删除的


in_ifaddr结构(即,与请求匹配的 )。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 141

图6-27 in_control 函数:删除地址

298-323 前提条件处理代码将 ia指向要删除的地址。 in_ifscrub删除任何与此地址关联


的路由。第一个 i f 删除接口地址列表的结构。第二个 i f 删除来自 I n t e r n e t 地址列表
(in_ifaddr)的结构。
324-325 IFAFREE只在引用计数降到 0时才释放此结构。
其他引用可能来自路由表中的各项。

6.7 接口ioctl处理

我们现在查看当一个地址被分配给接口时的专用 i o c t l处理,对于我们的每个例子接口,
这个处理分别在函数 leioctl、slioctl和loioctl中。
i n _ i f i n i t通过图6 - 1 6中的S I O C S I F A D D R代码和图 6 - 2 6中的S I O C A I F A D D R代码调用。
in_ifinit总是通过接口的 if_ioctl函数发送SIOCSIFADDR命令(图6-17)。

6.7.1 leioctl函数

图 4 - 3 1 显示了 L A N C E 驱动程序的 S I O C S I F F L A G S 命令的处理。图 6 - 2 8 显示了


SIOCSIFADDR命令的处理。
614-637 在处理命令前, data转换成一个 ifaddr结构指针,并且 ifp->if_unit为此请
求选择相应的le_softc结构。
l e i n i t将接口标志为启动并初始化硬件。对于 I n t e r n e t地址,I P地址保存在 a r p c o m结构

Linux公社 www.linuxidc.com
bbs.theithome.com
142 计计TCP/IP详解 卷 2:实现

中,并且为此地址发送一个免费 ARP。免费ARP在21.5节和卷1的4.7节中讨论。
未识别命令
627-677 对于未识别命令,返回 EINVAL。

图6-28 函数leioctl

6.7.2 slioctl函数

函数slioctl(图6-29)为SLIP设备驱动器处理命令 SIOCSIFADDR和SIOCSIFDSTADDR。

图6-29 函数slioctld :命令SIOCSIFADDR 和SIOCSIFDSTADDR

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 143

图6-29 (续)

663-672 对于这两个命令,如果地址不是一个 I P 地址,则返回 E A F N O S U P P O R T。


SIOCSIFADDR命令设置IFF_UP。
未识别命令
688-69 3 对于未识别命令,返回 EINVAL。

6.7.3 loioctl函数

函数loioctl和它的SIOCSIFADDR命令的实现显示在图 6-30中。

图6-30 函数loioctl :命令SIOCSIFADDR

Linux公社 www.linuxidc.com
bbs.theithome.com
144 计计TCP/IP详解 卷 2:实现

图6-30 (续)

135-151 对于Internet地址,loioctl设置IFF_UP,并立即返回。
未识别命令
167-171 对于未识别命令,返回 EINVAL。
注意,对于所有这三个例子驱动程序,指派一个地址会导致接口被标记为启用 (IFF_UP)。

6.8 Internet实用函数

图6 - 3 1列出了几个操作 I n t e r n e t地址或依赖于图 6 - 5中i f n e t结构的函数,它们通常用于发


现不能单从 32 bit IP地址中获得的子网信息。这些函数的实现主要包括数据结构的转换和操作
位掩码。读者在 netinet/in.c中可以找到这些函数。

函 数 说 明

in_netof 返回in中的网络和子网部分。主机比特被设置为 0。对于D类地址,返回 D类首标比


特和用于多播组的 0比特
u_long in_netof(struct in_addr in);
in_canforward 如果地址为 in的IP分组有资格转发,则返回真。 D类和E类地址、环回网络地址和有
一个为0网络号的地址不能转发
int in_canforward(struct in_addr in);
in_localaddr 如果主机in被定位在一个直接连接的网络,则返回真。如果全局变量subnetsarelocal
非0,则所有直接连接的网络的子网也被认为是本地的
int in_localaddr(struct in_addr in);
in_broadcast 如果in是一个由 ifp指向的接口所关联的广播地址,则返回真
int i n _ b r o a d c a s t( s t r u c t i n _ a d d rin, s t r u c t i f n e t if*
p) ;

图6-31 Internet地址函数

N e t / 2在i n _ c a n f o r w a r d中有一个错误:它允许转发环回地址。因为大多数
N e t / 2系统被配置为只承认一个环回地址,如 1 2 7 . 0 . 0 . 1,N e t / 2系统常沿着默认路由在
环回网络中转发其他地址 (例如127.0.0.2)。
一个到127.0.0.2的telnet可能不是你所希望的! (习题6.6)

6.9 ifnet实用函数

几个查找数据结构的函数显示在图 6 - 5中。列于图 6 - 3 2的函数接受任何协议族类的地址,


因为它们的参数是指向一个 s o c k a d d r结构的指针,这个结构中包含有地址族类。与图 6 - 3 1
中的函数比较,在那里的每个函数将 32 bit 的I P 地址作为一个参数。这些函数定义在文件
net/if.c中。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 6章 IP 编 址计计 145
函 数 说 明

ifa_ifwithaddr 在i f n e t列表中查找有一个单播或广播地址 addr的接口。返回一个指向这个


匹配的i f a d d r结构的指针;或者若没有找到,则返回一个空指针
s t r u c t i f a d d r *i f a _ i f w i t h a d d r( s t r u c t s o c k a d d r ad*dr) ;
ifa_ifwithdstaddr 在i f n e t列表中查找目的地址为 addr的接口。返回一个指向这个匹配的
i f a d d r结构的指针;或者若没有找到,则返回一个空指针
struct ifaddr *ifa_ifwithdstaddr(struct sockaddr a*ddr);
ifa_ifwithnet 在i f n e t列表中查找与 addr同一网络的地址。返回匹配的 i f a d d r结构的指
针;或者若没有找到,则返回一个空指针
s t r u c t i f a d d r *i f a _ i f w i t h n e t( s t r u c t s o c k a d d r ad*dr) ;
ifa_ifwithaf 在i f n e t列表中查找 与a d d具有相同地址族类的第一个地址。返回匹配的
i f a d d r结构的指针;或者若没有找到,则返回一个空指针
s t r u c t i f a d d r *i f a _ i f w i t h a f( s t r u c t s o c k a d d rad*dr) ;
ifaof_ifpforaddr 在ifp列表中查找与 addr匹配的地址。用于精确匹配的引用次序为:一个点对
点链路上的目的地址、一个同一网络上的地址和一个在同一地址族类的地址,
则返回匹配的 i f a d d r结构的指针;或者若没有找到,则返回一个空指针
struct ifaddr * i f a o f _ i f p f o r a d d(rs t r u c t s o c k a d d r *addr,
s t r u c t i f n e t if*p);
ifa_ifwithroute 返回目的地址 (dst)和网关地址 (gateway)指定的相应的本地接口的 i f a d d r结
构的指针
s t r u c ti f a d d r* i f a _ i f w i t h r o u t e ( i n t f l a g s , struct
s o c k a d d r d*st, s t r u c t s o c k a d d rga*teway);
ifunit 返回与name关联的i f n e t结构的指针
s t r u c t i f n e t *i f u n i t( c h a r *name) ;

图6-32 ifnet 实用函数

6.10 小结
在本章中,我们概述了 I P编址机制,并且说明了 I P专用的接口地址结构和协议地址结构:
结构in_ifaddr和sockaddr_in。
我们讨论了如何通过程序 ifconfig和ioctl接口命令来配置接口的 IP专用信息。
最后,我们总结了几个操作 IP地址和查找接口数据结构的实用函数。

习题
6.1 你认为为什么在结构 sockaddr_in中的sin_addr最初定义为一个结构?
6.2 ifunit("sl0")返回的指针指向图 6-5中的哪个结构?
6.3 当 I P 地址已经包含在接口的地址列表中的一个 i f a d d r 结构中时,为什么还要在
ac_ipaddr中备份?
6.4 你认为为什么IP接口地址要通过一个 UDP插口而不是一个原始的 IP插口来访问?
6.5 为什么 i n _ s o c k t r i m 要修改 s i n _ l e n 来匹配掩码的长度,而不使用一个
sockaddr_in结构的标准长度?
6.6 当一个 t e l n e t 1 2 7 . 0 . 0 .命令中的连接请求部分被一个
2 N e t / 2系统错误地转发,
并且最后被认可,同时还被默认路由上的一个系统所接收时,会发生什么情况?

Linux公社 www.linuxidc.com
bbs.theithome.com
第7章 域 和 协 议
7.1 引言

在本章中,我们讨论支持同时操作多个网络协议的 N e t / 3数据结构。用 I n t e r n e t协议来说明


在系统初始化时这些数据结构的构造和初始化。本章为我们讨论 I P协议处理层提供必要的背
景资料,IP协议处理层在第 8章讨论。
N e t / 3组把协议关联到一个域中,并且用一个协议族常量来标识每个域。 N e t / 3还通过所使
用的编址方法将协议分组。回忆图 3 - 1 9,地址族也有标识常量。当前,在一个域中的每个协
议使用同类地址,并且每种地址只被一个域使用。作为结果,一个域能通过它的协议族或地
址族常量唯一标识。图 7-1列出了我们讨论的协议和常量。

协 议 族 地 址 族 协 议

PF_INET AF_INET Internet


PF_OSI, PF_ISO AF_OSI, AF_ISO OSI
PF_LOCAL, PF_UNIX A F _ L O C A L , A F _ U N I X 本地IPC(Unix)
PF_ROUTE AF_ROUTE 路由表
n/a AF_LINK 链路层(例如以太网 )

图7-1 公共的协议和地址族常量

P F _ L O C A L和A F _ L O C A L是支持同一主机上进程间通信的协议的主要标识,它们
是P O S I X . 1 2标准的一部分。在 N e t / 3以前,用 P F _ U N I X和A F _ U N I X标识这些协议。
在Net/3中保留UNIX常量,用于向后兼容,并且要在本书中讨论。
P F _ U N I X域支持在一个单独的 U n i x主机上的进程间通信。细节见 [Stevens 1990] 。
P F _ R O U T E域支持在一个进程和内核中路由软件间的通信 (第1 8章)。我们偶尔引用 P F _ O S I协
议,它作为 N e t / 3特性仅支持 O S I 协议,但我们不讨论它们的细节。大多数讨论是关于
PF_INET协议的。

7.2 代码介绍

本章涉及两个头文件和两个 C文件。图7-2描述了这4个文件。

文 件 说 明

netinet/domain.h d o m a i n结构定义
netinet/protosw.h p r o t o s w结构定义
netinet/in_proto.c IP d o m a i n和p r o t o s w结构
kern/uipc_domain.c 初始化和查找函数

图7-2 在本章中讨论的文件

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 147
7.2.1 全局变量

图7-3描述了几个重要的全局数据结构和系统参数,它们在本章中讨论,并经常在 Net/3中
引用。

变 量 数据类型 说 明

domain struct domain * 链接的域列表


inetdomain struct domain Internet协议的d o m a i n结构
inetsw struct protosw[] Internet协议的p r o t o s w结构数组
max_linkhdr int 见图7-17
max_protohdr int 见图7-17
max_hdr int 见图7-17
max_datalen int 见图7-17

图7-3 在本章中介绍的全局变量

7.2.2 统计量

除了图7-4显示的由函数 i p _ i n i t分配和初始化的统计量表,本章讨论的代码没有收集其
他统计量。通过一个内核调试工具是查看这个表的唯一方法。

变 量 数据类型 说 明

ip_ifmatrix i n t[][] 二维数组,用来统计在任意两个接口间传送的分组数

图7-4 在本章中收集的统计量

7.3 domain结构

一个协议域由一个图 7-5所示的domain结构来表示。

图7-5 结构domain 的定义

42-57 d o m _ f a m i l y是一个地址族常量 (例如A F _ I N E T),它指示在此域中协议使用的编址


方式。dom_name是此域的一个文本名称 (例如“internet”)。

Linux公社 www.linuxidc.com
bbs.theithome.com
148 计计TCP/IP详解 卷 2:实现

除了程序 f s t a t ( 1 )在它格式化插口信息时使用 d o m _ n a m e外,成员 d o m _ n a m e


不被Net/3内核的任何部分访问。
d o m _ i n i t指向初始化此域的函数。 d o m _ e x t e r n a l i z e和d o m _ d i s p o s e指向那些管
理通过此域内通信路径发送的访问权限的函数。 U n i x域实现这个特性在进程间传递文件描述
符。Internet域不实现访问权限。
d o m _ p r o t o s w和d o m _ p r o t o s w N P R O T O S W指向一个 p r o t o s w结构的数组的起始和结
束。 d o m _ n e x t指向在一个内核支持的域链表中的下一个域。包含所有域的链表通过全局指
针domains来访问。
接下来的三个成员, d o m _ r t a t t a c h、d o m _ r t o f f s e t和d o m _ m a x r t k e y保存此域的
路由信息。它们在第 18章讨论。
图7-6显示了一个 domains列表的例子。

图7-6 domains 列表

7.4 protosw结构

在编译期间,Net/3为内核中的每个协议分配一个protosw结构并初始化,同时将在一个域
中的所有协议的这个结构组织到一个数组中。每个 domain结构引用相应的protosw结构数组。
一个内核可以通过提供多个protosw项为同一协议提供多个接口。Protosw结构的定义见图7-7。

图7-7 protosw 结构的定义

57-61 此结构中的前 4个成员用来标识和表征协议。 p r _ t y p e指示协议的通信语义。图 7 - 8

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 149
列出了pr_type可能的值和对应的 Internet协议。

pr_type 协 议 语 义 Internet协议

SOCK_STREAM 可靠的双向字节流服务 TCP


SOCK_DGRAM 最好的运输层数据报服务 UDP
SOCK_RAW 最好的网络层数据报服务 ICMP,IGMP,原始IP
SOCK_RDM 可靠的数据报服务 (未实现) n/a
SOCK_SEQPACKET 可靠的双向记录流服务 n/a

图7-8 pr_type 指明协议的语义

p r _ d o m a i n指向相关的 d o m a i n结构, p r _ p r o t o c o l为域中协议的编号, p r _ f l a g s


标识协议的附加特征。图 7-9列出了pr_flags的可能值。

pr_flags 说 明

PR_ATOMIC 每个进程请求映射为一个单个的协议请求
PR_ADDR 协议在每个数据报中都传递地址
PR_CONNREQUIRED 协议是面向连接的
PR_WANTRCVD 当一个进程接收到数据时通知协议
PR_RIGHTS 协议支持访问权限

图7-9 pr_flags 的值

如果一个协议支持 P R _ A D D R ,必须也支持 P R _ A T O M I C 。 P R _ A D D R 和
PR_CONNREQUIRED是互斥的。
当设置了 P R _ W A N T R C V D,并当插口层将插口接收缓存中的数据传递给一个进程
时(即当在接收缓存中有更多空间可用时 ),它通知协议层。
P R _ R I G H T S指示访问权限控制报文能通过连接来传递。访问权限要求内核中的
其他支持来确保在接收进程没有销毁报文时能完成正确的清除工作。仅 U n i x域支持
访问权限,在那里它们用来在进程间传递描述符。
图7-10所示的是协议类型、协议标志和协议语义间的关系。

PR_ 是否有记 可靠否 举 例


pr_type
ADDR ATOMIC CONNREQUIRED 录边界 Internet 其他
SOCK_STREAM • 否 • TCP SPP


• 显式 • TP4
SOCK_SEQPACKET
• 隐式 • SPP
SOCK_RDM • • 隐式 见正文 RDP
SOCK_DGRAM • • 隐式 UDP
SOCK_RAW • • 隐式 ICMP

图7-10 协议特征和举例

图7 - 1 0不包括标志 P R _ W A N T R C V D和P R _ R I G H T S。对于可靠的面向连接的协议,


PR_WANTRCVD总是被设置。
为了理解 N e t / 3 中一个 p r o t o s w 项的通信语义,我们必须要一起考虑 P R x x x标志和
p r _ t y p e。在图7 - 1 0中,我们用两列 (“是否有记录边界”和“可靠否” )来描述由 p r _ t y p e

Linux公社 www.linuxidc.com
bbs.theithome.com
150 计计TCP/IP详解 卷 2:实现

隐式指示的语义。图 7-10显示了可靠协议的三种类型:
• 面向连接的字节流协议,如TCP和SPP(源于XNS协议族)。这些协议用SOCK_STREAM标识。
• 有记录边界的面向连接的流协议用 S O C K _ S E Q P A C K E T标识。在这种协议类型中,
P R _ A T O M I C指示记录是否由每个输出请求隐式地指定,或者显式地通过在输出中设置
标志MSG_EOR来指定。
SPP同时支持语义SOCK_STREAM和SOCK_SEQPACKET。
• 第三种可靠协议提供一个有隐式记录边界的面向连接服务,它由 S O C K _ R D M标识。R D P
不保证按照记录发送的顺序接收记录。 R D P在[Partridge 1987] 中讨论并在 RFC 1 1 5
[Partridge and Hinden 1990]中被描述。
两种不可靠协议显示在图 7-10中:
• 一个运输层数据报协议,如 UDP,它包括复用和检验和,由 SOCK_DGRAM指定。
• 一个网络层数据报协议,如 I C M P,它由 S O C K _ R A W指定。在 N e t / 3中,只有超级用户进
程才能创建一个 SOCK_RAW插口(图15-8)。
62-68 接着的5个成员是函数指针,用来提供从其他协议对此协议的访问。 p r _ i n p u t处理
从一个低层协议输入的数据, p r _ o u t p u t处理从一个高层协议输出的数据, p r _ c t l i n p u t
处理来自下层的控制信息,而 p r _ c t l o u t p u t处理来自上层的控制信息。 p r _ u s r r e q处理
来自进程的所有通信请求。如图 7-11所示。

进程

(例如:read, write,等等)

(例如:路由域输出) (例如:插口选项)

协 议

(例如:到达TCP分组) (例如:ICMP错误)

图7-11 一个协议的 5个主要入口点

69-75 剩下的 5个成员是协议的实用函数。 p r _ i n i t 处理初始化。 p r _ f a s t t i m o和


p r _ s l o w t i m o分别每200 ms和500 ms被调用来执行周期性的协议函数,如更新重传定时器。
p r _ d r a i n在内存缺乏时被 m _ r e c l a i m调用 (图2 - 1 3 )。它请求协议释放尽可能多的内存。
p r _ s y s c t l为s y s c t l( 8 )命令提供一个接口,一种修改系统范围的参数的方式,如允许转发
分组或UDP检验和计算。

7.5 IP的domain和protosw结构

声明所有协议的结构 d o m a i n 和p r o t o s w ,并进行静态初始化。对于 I n t e r n e t协议,


i n e t s w数组包含 p r o t o s w结构。图7 - 1 2总结了在数组 i n e t s w中的协议信息。图 7 - 1 3显示了

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 151
Internet协议的数组定义和 domain结构的定义。

inetsw[] pr_protocol pr_type 说 明 缩 写

0 0 0 Internet协议 IP
1 IPPROTO_UDP SOCK_DGRAM 用户数据报协议 UDP
2 IPPROTO_TCP SOCK_STREAM 传输控制协议 TCP
3 IPPROTO_RAW SOCK_RAW Internet协议(原始) IP(原始)
4 IPPROTO_ICMP SOCK_RAW Internet控制报文协议 ICMP
5 IPPROTO_IGMP SOCK_RAW Internet组管理协议 IGMP
6 0 SOCK_RAW Internet协议(原始、默认 ) IP(原始)

图7-12 Internet域协议

图7-13 Internet 的domain 和protosw 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
152 计计TCP/IP详解 卷 2:实现

39-77 在i n e t s w数组中的 3个p r o t o s w结构提供对 I P的访问。第一个: i n e t s w [ 0 ],标


识 I P 的管理函数并且只能由内核访问。其他两项: i n e t s w [ 3 ] 和i n e t s w [ 6 ] ,除了
p r _ p r o t o c o l值以外它们是一样的,都提供到 I P的一个原始接口。 i n e t s w [ 3 ]处理接收到
的任何未识别协议的分组。 i n e t s w [ 6 ]是默认的原始协议,当没有找到其他可匹配的项时,
这个结构由函数 pffindproto返回(7.6节)。

在N e t / 3以前的版本中,通过 i n e t s w [ 3 ]传输不带 I P首部的分组,由进程负责构


造正确的首部。由内核通过 i n e t s w [ 6 ]传输带 I P首部的分组。 4.3BSD Reno引入了
I P _ H D R I N C L插口选项 (32.8节),这样在 i n e t s w [ 3 ]和i n e t s w [ 6 ]之间的区别就不
再重要了。

原始接口允许一个进程发送和接收不涉及运输层的 I P分组。原始接口的一个用途是实现
内核外的传输协议。一旦这个协议稳定下来,就能移植到内核中改进它的性能和对其他进程
的可用性。另一个用途就是作为诊断工具,如 t r a c e r o u t e,它使用原始 I P接口来直接访问
IP。第32章讨论原始 IP接口。图7-14总结了IP protosw结构。

protosw inetsw[0] i n e t s w [ 3和6 ] 说 明

pr_type 0 SOCK_RAW IP提供原始分组服务


pr_domain &inetdomain &inetdomain 两协议都是Internet域的一部分
pr_protocol 0 I P P R O T O _ R A W或0 IPPROTO_RAW(255)和0都是
预留的(RFC 1700),并且不应在
一个IP数据报中出现
pr_flags 0 PR_ATOMIC/PR_ADDR 插口层标志, IP不使用
pr_input null rip_input 从IP、ICMP或IGMP接收未识
别的数据报
pr_output ip_output rip_output 分别准备并发送数据报到 IP和
硬件层
pr_ctlinput null null IP不使用
pr_ctloutput null rip_ctloutput 响应来自进程的配置请求
pr_usrreq null rip_usrreq 响应来自进程的协议请求
pr_init ip_init n u l l或r i p _ i n i t i p _ i n i t完成所有初始化
pr_fasttimo null null IP不使用
pr_slowtimo ip_slowtimo null 用于IP重装算法的慢超时
pr_drain ip_drain null 如果可能,释放内存
pr_sysctl ip_sysctl null 修改系统范围参数

图7-14 IP inetsw 的条目

78-81 Internet协议的domain结构显示在图7-13的下部。Internet域使用AF_INET风格编址,
文本名称为“ i n t e r n e t”,没有初始化和控制报文函数,它的 p r o t o s w结构在i n e t s w数组
中。
I n t e r n e t协议的路由初始化函数是 r n _ i n i t h e a d。一个 I P地址的最大有效位数为 3 2,并
且一个Internet选路键的大小为一个 sockaddr_in结构的大小 (16字节)。

i n e t s w [ 3 ]和i n e t s w [ 6 ]的唯一区别是它们的 p r _ p r o t o c o l号和初始化函数


rip_init,它仅在inetsw[6]中定义,因此只在初始化期间被调用一次。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 153
domaininit函数

在系统初始化期间 (图3 - 2 3 ),内核调用 d o m a i n i n i t来链接结构 d o m a i n和p r o t o s w。


domaininit显示在图7-15中。

图7-15 函数domaininit

37-42 ADDDOMAIN宏声明并链接一个 domain结构。例如,ADDDOMAIN(unix)展开为:


extern struct domain unixdomain;
unixdomain.dom_next = domains;
domains = &unixdomain;

宏_C O N C A T定义在 s y s / d e f s . h 中,并且连接两个符号名。例如 _ C O N C A T


(u n i x , d o m a i)产生u
n nixdomain。

43-54 domaininit通过调用ADDDOMAIN为每个支持的域构造域列表。
因为符号 u n i x常常被C预处理器预定义,因此, N e t / 3在这里显式地取消它的定
义,使ADDDOMAIN能正确工作。

Linux公社 www.linuxidc.com
bbs.theithome.com
154 计计TCP/IP详解 卷 2:实现

图7 - 1 6显示了链接的结构 d o m a i n和p r o t o s w,它们配置在内核中来支持 I n t e r n e t、U n i x


和OSI协议族。


数据报
原始(默认)
(原始)

(原始)

图7-16 初始化后的 domain 链表和protosw 数组

55-61 两个嵌套的 f o r 循环查找内核中的每个域和协议,并且若定义了初始化函数


d o m _ i n i t和p r _ i n i t,则调用它们。对于 I n t e r n e t协议,调用下面的函数 (图7 - 1 3 ):
ip_init、udp_init、tcp_init、igmp_init和rip_init。
62-65 在d o m a i n i n i t中计算这些参数,用来控制 mbuf中分组的格式,以避免对数据的额
外复制。在协议初始化期间设置了 m a x _ l i n k h d r和m a x _ p r o t o h d r。这里, d o m a i n i n i t
将m a x _ l i n k h d r强制设置为一个下限 1 6。1 6字节用于给带有 4字节边界的 1 4字节以太网首部
留出空间。图7-17和图7-18列出了这些参数和典型的取值。

变 量 值 说 明

max_linkhdr 16 由链路层添加的最大字节数
max_protohdr 40 由网络和运输层添加的最大字节数
max_hdr 56 max_linkhdr + max_protohdr
max_datalen 44 在计算了链路和协议首部后的分组首部 m u b f中的可用数据字节数

图7-17 用来减少协议数据复制的参数

max_hdr 字节

m_har和 网络和运
链路首部 数据
m_pkthdr 输首部
28字节 16字节 40字节 44字节
MHLEN max_linkhdr max_protohdr max_datalen
128字节

图7-18 mbuf和相关的最大首部长度

m a x _ p r o t o h d r是一个软限制,估算预期的协议首部大小。在 I n t e r n e t域中,I P
和T C P首部长度通常为 2 0字节,但都可达到 6 0字节。长度超过 m a x _ p r o t o h d r的代
价是花时间将数据向后移动,以留出比预期的协议首部更大的空间。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 155
66-68 domaininit通过调用timeout启动pfslowtimo和pffasttimo。第3个参数指
明何时内核应该调用这个函数,在这里是在 1个时钟滴答内。两个函数都显示在图 7-19中。

图7-19 函数pfslowtimo 和pffasttimo

153-176 这两个相近的函数用两个 f o r循环分别为每个协议调用函数 p r _ s l o w t i m o和


p r _ f a s t t i m o,前提是如果定义了这两个函数。这两个函数每 500 ms 和200 ms 通过调用
timeout调度自己一次, timeout在图3-43中讨论过。

7.6 pffindproto和pffindtype函数

如图7 - 2 0所示,函数 p f f i n d p r o t o和p f f i n d t y p e通过编号 (例如I P P R O T O _ T C P)或类


型(例如SOCK_STREAM)来查找一个协议。如我们在第 15章要看到的,当进程创建一个插口时,
这两个函数被调用来查找相应的 protosw项。
69-84 p f f i n d t y p e线性搜索d o m a i n s,查找指定的族,然后在域内搜索第一个为此指定
类型的协议。
85-107 pffindproto像pffindtype一样搜索domains,查找由调用者指定的族、类型
和协议。如果 p f f i n d p r o t o在指定的协议族中没有发现一个 (p r o t o c o l,t y p e)匹配项,
并且 t y p e 为 S O C K _ R A W ,而此域有一个默认的原始协议 ( p r _ p r o t o c o l 等于 0 ) ,则
pffindproto选择默认的原始协议而不是完全失败。例如,一个调用如下:
pffindproto(PF_INET, 27, SOCK_RAW);

它返回一个指向 i n e t s w [ 6 ]的指针,默认的原始 I P协议,因为 N e t / 3不包括对协议 2 7的支持。


通过访问原始 I P,一个进程可以使用内核来管理 I P分组的发送和接收,从而自己实现协议 2 7

Linux公社 www.linuxidc.com
bbs.theithome.com
156 计计TCP/IP详解 卷 2:实现

服务。
协议27预留给可靠的数据报协议 (RFC 1151)。
两个函数都返回一个所选协议的 p r o t o s w结构的指针;或者,如果没有找到匹配项,则
返回一个空指针。

图7-20 函数pffindproto 和pffindtype

举例

我们在15.6节中会看到,当一个应用程序进行下面的调用时:
socket(PF_INET, SOCK_STREAM, 0); /* TCP 插口 */

pffindtype被调用如下:

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计157
pffindtype(PF_INET, SOCK_STREAM);

图7 - 1 2显示p f f i n d t y p e会返回一个指向 i n e t s w [ 2 ]的指针,因为 T C P是此数组中第一


个SOCK_STREAM协议。同样,
socket(PF_INET, SOCK_DGRAM, 0); /* UCP 插口 */

会导致
pffindtype(PF_INET, SOCK_DGRAM);

它返回一个指向 inetsw[1]中UDP的指针。

7.7 pfctlinput函数

函数p f c t l i n p u t给每个域中的每个协议发送一个控制请求 (图7-21)。当可能影响每个协


议的事件发生时,使用这个函数,例如一个接口被关闭,或路由表发生改变。当一个 I C M P重
定向报文到达时, ICMP调用p f c t l i n p u t (图11-14),因为重定向会影响所有 Internet协议(例
如UDP和TCP)。

图7-21 函数pfctlinput

142-152 两个嵌套的 for循环查找每个域中的每个协议。 pfctlinput通过调用每个协议


的p r _ c t l i n p u t 函 数来发送由c m d 指定的协议控 制命令。对于 U D P,调用
udp_ctlinput;而对于TCP,调用tcp_ctlinput。

7.8 IP初始化

如图7 - 1 3所示, I n t e r n e t域没有一个初始化函数,但单个 I n t e r n e t协议有。现在,我们仅查


看I P初始化函数 i p _ i n i t。在第2 3章和第 2 4章中,我们讨论 U D P和T C P初始化函数。在讨论
这些代码前,需要说明一下数组 ip_protox。

7.8.1 Internet传输分用

一个网络层协议像 I P必须分用输入数据报,并将它们传递到相应的运输层协议。为了完
成这些,相应的 p r o t o s w结构必须通过一个在数据报中出现的协议编号得到。对于 Internet协
议,这由数组ip_protox来完成,如图7-22所示。
数组i p _ p r o t o x的下标是来自 IP首部的协议值(i p _ p,图8-8)。被选项是i n e t s w数组中
处理此数据报的协议的下标。例如,一个协议编号为 6的数据报由 i n e t s w [ 2 ]处理,协议为
TCP。内核在协议初始化时构造 ip_protox,如图7-23所示。

Linux公社 www.linuxidc.com
bbs.theithome.com
158 计计TCP/IP详解 卷 2:实现

(原始)

(原始)

图7-22 数组ip_protox 将协议编号映射到数组 inetsw 中的一项

图7-23 函数ip_init

7.8.2 ip_init函数

domaininit (图7-15)在系统初始化期间调用函数 ip_init。


71-78 pffindproto返回一个指向原始协议 (inetsw[3],图7-14)的指针。如果找不到原
始协议, N e t / 3就调用 p a n i c,因为这是内核要求的部分。如果找不到原始协议,内核一定被
错误配置了。 I P将传输到一个未知传输协议的到达分组传递给此协议,在那里它们由内核外
部的一个进程来处理。
79-85 接着的两个循环初始化数组 ip_protox。第一个循环将数组中的每项设置为 pr,即
默认协议的下标 (图7-22中为3)。第二个循环检查 i n e t s w中的每个协议(而不是协议编号为 0或
I P P R O T O _ R A W的项),并且将 i p _ p r o t o x中的匹配项设置为引用相应的 i n e t s w项。为此,
每个protosw结构中的pr_protocol必须是期望出现在输入数据报中的协议编号。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 159
86-92 ip_init初始化IP重装队列ipq(10.6节),用系统时钟植入 ip_id,并将IP输入队列
(i p i n t r q)的最大长度设置为 5 0 (i p q m a x l e n)。i p _ i d用系统时钟设置,为数据报标识符提
供一个随机起点 (10.6节)。最后, i p _ i n i t分配一个两维数组 i p _ i f m a t r i x,统计在系统接
口之间路由的分组数。
在N e t / 3中,有很多变量可以被一个系统管理员修改。为了允许在运行时改变这
些变量而不需重新编译内核,一个常量 (在此例中是 I F Q _ M A X L E N)表示的默认值在编
译时指派给一个变量 (i p q m a x l e n)。一个系统管理员能使用一个内核调试器如 a d b,
来修改i p q m a x l e n,并用新值重启内核。如果图 7 - 2 3直接使用 I F Q _ M A X L E N,它会
要求内核重新编译来改变这个限制。

7.9 sysctl系统调用

系统调用sysctl访问并修改 Net/3系统范围参数。系统管理员通过程序 sysctl(8)修改这


些参数。每个参数由一个分层的整数列表来标识,并有一个相应的类型。此系统调用的原型为:
int sysctl(int * name, u_int namelen, void *old, size_t * oldlenp, void *new, size_t newlen);

*n a m e指向一个包含 n a m e l e n个整数的数组。 *o l d指向在此范围内返回的旧值, *n e w指向


在此范围内传递的新值。
图7-24总结了关于联网名称的组织。

图7-24 sysctl 的名称组织

在图7-24中,IP转发标志的全名为
CTL_NET、PF_INET、0、IPCTL_FORWARDING

Linux公社 www.linuxidc.com
bbs.theithome.com
160 计计TCP/IP详解 卷 2:实现

用4个整数存储在一个数组中。

net_sysctl函数

每层的sysctl命名方案通过不同函数处理。图 7-25显示了处理这些 Internet参数的函数。

图7-25 处理Internet 参数的sysctl 函数

顶层名称由 s y s c t l处理。网络层名称由 n e t _ s y s c t l处理,它根据族和协议将控制转


给此协议的 protosw项指定的pr_sysctl函数。
s y s c t l在内核中通过 _ s y s c t l函数实现,函数 _ s y s c t l不在本书中讨论。它
包含将s y s c t l参数传给内核和从内核取出 s y s c t l参数的代码及一个 s w i t c h语句,
这个switch语句选择相应的函数来处理这些参数,在这里是 net_sysctl。
图7-26所示的是函数net_sysctl。

图7-26 函数net_sysctl

Linux公社 www.linuxidc.com
bbs.theithome.com
第 7章 域 和 协 议计计 161

图7-26 (续)

108-119 n e t _ s y s c t l的参数除了增加了 p外,同系统调用 s y s c t l一样,p指向当前进程


结构。
120-134 在名称中接下来的两个整数被认为是在结构 d o m a i n和p r o t o s w中指定的协议族
和协议编号成员的值。如果没有指定族,则返回 0。如果指定了族, f o r循环在域列表中查找
一个匹配的族。如果没有找到,则返回 ENOPROTOOPT。
135-141 如果找到匹配域,第二个for循环查找第一个定义了函数 pr_sysctl的匹配协议。
当找到匹配项,将请求传递给此协议的 p r _ s y s c t l函数。注意,把 (n a m e+ 2 )指向的整数传
递给下一级。如果没有找到匹配的协议,返回 ENOPROTOOPT。
图7-27所示的是为 Internet协议定义的 pr_sysctl函数。

pr_protocol inetsw[] pr_sysctl 说 明 参 考

0 0 ip_sysctl IP 8.9节
IPPROTO_UDP 1 udp_sysctl UDP 23.11节
IPPROTO_ICMP 4 icmp_sysctl ICMP 11.14节

图7-27 Internet协议族的 pr_sysctl 函数

在路由选择域中, pr_sysctl指向函数sysctl_rtable,它在第19章中讨论。

7.10 小结

本章从说明结构 d o m a i n和p r o t o s w开始,这两个结构在 N e t / 3内核中描述及组织协议。


我们看到一个域的所有 p r o t o s w结构在编译时分配在一个数组中, i n e t d o m a i n和数组
i n e t s w描述Internet协议。我们仔细查看了三个描述 IP协议的i n e t s w项:一个用于内核访问
IP,其他两个用于进程访问 IP。
在系统初始化时, d o m a i n i n t将域链接到 d o m a i n s列表中,调用域和协议初始化函数,
并调用快速和慢速超时函数。
两个函数 p f f i n d p r o t o 和 p f f i n d t y p e 通过协议号或类型搜索域和协议列表。
pfctlinput发送一个控制命令给所有协议。
最后,我们说明了 IP初始化程序,它通过数组 ip_protox完成传输分用。

习题
7.1 由谁调用pfsfindproto会返回一个指向 inetsw[6]指针?
Linux公社 www.linuxidc.com
bbs.theithome.com
第8章 IP:网际协议
8.1 引言

本章我们介绍 I P分组的结构和基本的 I P处理过程,包括输入、转发和输出。假定读者熟


悉IP协议的基本操作,其他 IP的背景知识见卷 1的第3、9和12章。RFC 791 [Postel 1981a] 是IP
的官方规范,RFC 1122 [Braden 1989a] 中有RFC 791的说明。
第9章将讨论选项的处理,第 10章讨论分片和重装。图 8-1显示了IP层常见的组织形式。

运输协议

网络接口

网络
图8-1 IP层的处理

在第4章中,我们看到网络接口如何把到达的 I P分组放到 I P输入队列 i p i n t r q中去,并如


何调用一个软件中断。因为硬件中断的优先级比软件中断的要高,所以在发生一次软件中断
之前,有的分组可能会被放到队列中。在软件中断处理中, i p i n t r函数不断从 i p i n t r q中
移走和处理分组,直到队列为空。在最终的目的地, I P把分组重装为数据报,并通过函数调用
把该数据报直接传给适当的运输层协议。如果分组没有到达最后的目的地,并且如果主机被
配置成一个路由器,则 I P把分组传给 i p _ f o r w a r d。传输协议和 i p _ f o r w a r d把要输出的分
组传给 i p _ o u t p u t,由i p _ o u t p u t完成I P首部、选择输出接口以及在必要时对分组分片。
最终的分组被传给合适的网络接口输出函数。
当产生差错时, I P丢弃该分组,并在某些条件下向分组的源站发出一个差错报文。这些
报文是 I C M P (第11章)的一部分。 N e t / 3通过调用 i c m p _ e r r o r发出I C M P差错报文, i c m p _
e r r o r接收一个 m b u f,其中包含差错分组、发现的差错类型以及一个选项码,提供依赖于差
错类型的附加信息。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 163
本章我们讨论 I P为什么以及何时发送 I C M P报文,至于有关 I C M P本身的详细讨论将在第
11章进行。

8.2 代码介绍

本章讨论两个头文件和三个 C文件。如图 8-2所示。

文 件 描 述

net/route.h 路由入口
netinet/ip.h IP首部结构
n e t i n e t / i p _ i n p u t . c IP输入处理
n e t i n e t / i p _ o u t p u t . c IP输出处理
n e t i n e t / i p _ c k s u m . c Internet检验和算法

图8-2 本章描述的文件

8.2.1 全局变量

在IP处理代码中出现了几个全局变量,见图 8-3。

变 量 数据类型 描 述

in_ifaddr struct i n _ i f a d d r * IP地址清单


ip_defttl int IP分组的默认 TTL
ip_id int 赋给输出的 IP分组的上一个 ID
ip_protox int[] IP分组的分路矩阵
ipforwarding int 系统是否转发 IP分组?
ipforward_rt struct route 大多数最近转发的路由的缓存
ipintrq struct ifqueue IP输入队列
ipqmaxlen int IP输入队列的最大长度
ipsendredirects int 系统是否发送 ICMP重定向?
ipstat struct ipstat IP统计

图8-3 本章中引入的全局变量

8.2.2 统计量

I P收集的所有统计量都放在图 8 - 4描述的 i p s t a t结构中。图 8 - 5显示了由 n e t s t a t - s命


令得到的一些统计输出样本。统计是在主机启动 30天后收集的。

ipsta t成员 描 述 SNMP使用的

ips_badhlen IP首部长度无效的分组数 •
ips_badlen IP首部和IP数据长度不一致的分组数 •
ips_badoptions 在选项处理中发现差错的分组数 •
ips_badsum 检验和差错的分组数 •
ips_badvers IP版本不是4的分组数 •
ips_cantforward 目的站不可到达的分组数 •
ips_delivered 向高层交付的数据报数 •

图8-4 本章收集的统计

Linux公社 www.linuxidc.com
bbs.theithome.com
164 计计TCP/IP详解 卷 2:实现

ipsta t成员 描 述 SNMP使用的

ips_forward 转发的分组数 •
ips_fragdropped 分片丢失数 (副本或空间不足 ) •
ips_fragments 收到分片数 •
ips_fragtimeout 超时的分片数 •
ips_noproto 具有未知或不支持的协议的分组数 •
ips_reassembled 重装的数据报数 •
ips_tooshort 具有无效数据长度的分组数 •
ips_toosmall 无法包含IP分组的太小的分组数 •
ips_total 全部接收到的分组数 •
ips_cantfrag 由于不分片比特而丢弃的分组数 •
ips_fragmented 成功分片的数据报数 •
ips_localout 系统生成的数据报数 (即没有转发的 ) •
ips_noroute 丢弃的分组数 —到目的地没有路由 •
ips_odropped 由于资源不足丢掉的分组数 •
ips_ofragments 为输出产生的分片数 •
ips_rawout 全部生成的原始 ip分组数
ips_redirectsent 已发送的重定向报文数

图8-4 (续)
输出 成员

图8-5 IP统计样本

i p s _ n o p r o t o的值很高,因为当没有进程准备接收报文时,它能对 I C M P主机
不可达报文进行计数。见第 32.5节的详细讨论。

8.2.3 SNMP变量

图8-6显示了IP组和Net/3收集的统计中的 SNMP变量之间的关系。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 165
SNMP变量 I p s t a t成员 描 述

ipDefaultTTL ip_defttl 数据报的默认 TTL(64跳)


ipForwarding ipforwarding 系统是路由器吗?
ipReasmTimeout IPFRAGTTL 分片的重装超时 (30秒)
ipInReceives ips_total 收到的全部 IP分组数
ipInHdrErrors ips_badsum+ IP首部出错的分组数
ips_tooshort+
ips_toosmall+
ips_badhlen+
ips_badlen+
ips_badoptions+
ips_badvers
ipInAddrErrors ips_cantforward 由于错误交付而丢弃的 IP分组数(i p _ o u t p u t也失败)
ipForwDatagrams ips_forward 转发的IP分组数
ipReasmReqds ips_fragments 收到的分片数
ipReasmFails ips_fragdropped+ 丢失的分片数
ips_fragtimeout
ipReasmOKs ips_reassembled 成功地重装的数据报数
ipInDiscards (未实现) 由于资源限制而丢弃的数据报数
ipInUnknownProtos ips_noproto 具有未知或不支持的协议的数据报数
ipInDelivers ips_delivered 交付到运输层的数据报数
ipOutRequests ips_localout 由运输层产生的数据报数
ipFragOKs ips_fragmented 分片成功的数据报数
iPFragFails ips_cantfrag 由于不分片比特丢弃的 IP分组数
ipFragCreates ips_ofragments 为输出产生的分片数
ipOutDiscards ips_odropped 由于资源短缺丢失的 IP分组数
ipOutRoutes ips_noroute 由于没有路由丢弃的 IP分组数

图8-6 IP组中SNMP变量的例子

8.3 IP分组

为了更准确地讨论 I n t e r n e t协议处理,我们必须定义一些名词。图 8 - 7显示了在不同的


Internet层之间传递数据时用来描述数据的名词。
我们把传输协议交给 I P的数据称为报文。典型的报文包含一个运输层首部和应用程序数
据。图 8 - 7所示的传输协议是 U D P。I P在报文的首部前加上它自己的首部形成一个数据报。如
果在选定的网络中,数据报的长度太大, I P就把数据报分裂成几个分片,每个分片中含有它
自己的IP首部和一段原来数据报的数据。图 8-7显示了一个数据报被分成三个分片。
当提交给数据链路层进行传送时,一个 I P分片或一个很小的无需分片的 I P数据报称为分
组。数据链路层在分组前面加上它自己的首部,并发送得到的帧。
I P只考虑它自己加上的 I P首部,对报文本身既不检查也不修改 (除非进行分片 )。图8 - 8显
示了IP首部的结构。
图8-8包括ip结构(如图8-9)中各成员的名字, Net/3通过该结构访问 IP首部。
47-67 因为在存储器中,比特字段的物理顺序依机器和编译器的不同而不同,所以由 # i f s
保证编译器按照 I P标准排列结构成员。从而,当 N e t / 3把一个i p结构覆盖到存储器中的一个 I P

Linux公社 www.linuxidc.com
bbs.theithome.com
166 计计TCP/IP详解 卷 2:实现

分组上时,结构成员能够访问到分组中正确的比特。

报文

IP UDP 应用程序数据

数据报

分片1
应用程序 应用程序
IP UDP IP 应用程序数据 IP
数据 数据

分组 分片2 分片3

链路层 IP UDP 应用程序 链路层 IP 应用程序数据 链路层 IP


应用程序
数据 数据

图8-7 帧、分组、分片、数据报和报文
0 15 16 31

版本 首部长度 服务类型 全长
ip_v ip_hl ip_tos ip_len
标识符生存时间 标志和分片偏移
ip_id ip_off
生存时间 协议 首部检验和
ip_ttl ip_p ip_cksum 20字节

32位源IP地址
ip_src
32位目的IP地址
ip_dst

选项(如果有)

数据

图8-8 IP数据报,包括 ip 结构名

图8-9 ip 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计167

图8-9 (续)

IP首部中包含 IP分组格式、内容、寻址、路由选择以及分片的信息。
I P分组的格式由版本 i p _ v指定,通常为 4;首部长度 i p _ h l,通常以 4字节单元度量;分
组长度 i p _ l e n以字节为单位度量;传输协议 i p _ p生成分组内数据; i p _ s u m是检验和,检
测在发送中首部的变化。
标准的 I P首部长度是 2 0个字节,所以 i p _ h l必须大于或等于 5。大于 5表示I P选项紧跟在
标准首部后。如 i p _ h l的最大值为 15 (2 4-1 ),允许最多 4 0个字节的选项 ( 2 0 + 4 0 = 6 0 )。I P数据
报的最大长度为 65535 (2 16-1)字节,因为 ip_len是一个16 bit 的字段。图8-10是整个构成。

(ip_hl×4)字节
标准首部 选项
(20字节) (最大40字节) 数据

ip_len字节
(最大65535字节)

图8-10 有选项的 IP分组构成

因为ip_hl是以4字节为单元计算的,所以 IP选项必须常常被填充成 4字节的倍数。

8.4 输入处理:ipintr函数

在第3、第4和第5章中,我们描述了示例的网络接口如何对到达的数据报排队以等待协议
处理:
1) 以太网接口用以太网首部中的类型字段分路到达帧 (见4.3节);
2) SLIP接口只处理 IP分组,所以无需分用 (见5.3节);
3) 环回接口在l o o u t p u t函数中结合输入和输出处理,用目的地址中的 s a _ f a m i l y成员
对数据报分用(见5.4节)。
在以上情况中,当接口把分组放到 i p i n t r q上排队后,通过 s c h e d n e t i s r调用一个软
中断。当该软中断发生时,如果 IP处理过程已经由 schednetisr调度,则内核调用ipintr。
在调用ipintr之前,CPU的优先级被改变成 splnet。

8.4.1 ipintr概观

i p i n t r是一个大函数,我们将在 4个部分中讨论: (1)对到达分组验证; (2)选项处理及转


发;( 3 )分组重装; ( 4 )分用。在 i p i n t r中发生分组的重装,但比较复杂,我们将单独放在第

Linux公社 www.linuxidc.com
bbs.theithome.com
168 计计TCP/IP详解 卷 2:实现

10章中讨论。图8-11显示了ipintr的整体结构。

图8-11 ipintr 函数

100-117 标号next标识主要的分组处理循环的开始。 ipintr从ipintrq中移走分组,并


对之加以处理直到整个队列空为止。如果到函数最后控制失败, g o t o把控制权传回给 n e x t
中最上面的函数。 i p i n t r把分组阻塞在 s p l i m p内,避免当它访问队列时,运行网络的中断
程序(例如slinput和e t h e r _ i n p u)。
t
332-336 标号b a d标识由于释放相关 m b u f并返回到 n e x t中处理循环的开始而自动丢弃的
分组。在整个ipintr中,都是跳到bad来处理差错。

8.4.2 验证

我们从图 8 - 1 2开始:把分组从 i p i n t r q中取出,验证它们的内容。损坏和有差错的分组


被自动丢弃。

图8-12 ipintr 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 169

图8-12 (续)

1. IP 版本
118-134 如果i n _ i f a d d r表(见第6.5节)为空,则该网络接口没有指派 IP地址,i p i n t r必
须丢掉所有的 IP分组;没有地址, i p i n t r就无法决定该分组是否要到该系统。通常这是一种

Linux公社 www.linuxidc.com
bbs.theithome.com
170 计计TCP/IP详解 卷 2:实现

暂时情况,是当系统启动时,接口正在运行但还没有配置好时发生的。我们在 6 . 3节中介绍了
地址是如何分配的问题。
在i p i n t r访问任何 IP首部字段之前,它必须证实ip_v是4(I P V E R S I O N)。RFC 1122要求
某种实现丢弃那些具有无法识别版本号的分组而不回显信息。
Net/2不检查 i p _ v。目前大多数正在使用的 IP实现,包括 Net/2,都是在IP的版本
4之后产生的,因此无需区分不同 I P版本的分组。因为目前正在对 I P进行修正,所以
不久将来的实现都将检查 ip_v。
IEN 119 [Forgie 1979] 和RFC 1190 [Topolcic 1990] 描述了使用 IP版本5和6的实
验协议。版本 6还被选为下一个正式的 I P标准( I P v 6 )。保留版本0和1 5,其他的没有赋
值。
在C中,处理位于一个无类型存储区域中数据的最简单的方法是:在该存储区域上覆盖一
个结构,转而处理该结构中的各个成员,而不再对原始的字节进行操作。如第 2章所言, mbuf
链把一个字节的逻辑序列,例如一个 I P分组,储存在多个物理 m b u f中,各 m b u f相互连接在一
个链表上。因为覆盖技术也可用于 I P分组的首部,所以首部必须驻留在一段连续的存储区内
(也就是说,不能把首部分开放在不同的存储器缓存区 )。
135-146 下面的步骤保证 IP首部(包括选项)位于一段连续的存储器缓存区上:
• 如果在第一个 m b u f中的数据小于一个标准的 I P首部( 2 0字节),m _ p u l l u p会重新把该标
准首部放到一个连续的存储器缓存区上去。
链路层不太可能会把最大的 ( 6 0字节 ) IP 首部分在两个 m b u f中从而使用上面的
m_pullup。
• ip_hl通过乘以4得到首部字节长度,并将其保存在 hlen中。
• 如果IP分组首部的字节数长度 hlen小于标准首部(20字节),将是无效的并被丢弃。
• 如果整个首部仍然不在第一个 m b u f中(也就是说,分组包含了 I P选项),则由m _ p u l l u p
完成其任务。
同样,这不一定是必需的。
检验和计算是所有 I n t e r n e t协议的重要组成。所有的协议均使用相同的算法 ( 由函数
i n _ c k s u m完成),但应用于分组的不同部分。对 I P来说,检验和只保证 I P的首部 (以及选项,
如果有的话 )。对传输协议,如 UDP或TCP,检验和覆盖了分组的数据部分和运输层首部。
2. IP 检验和
147-150 ipintr把由in_cksum计算出来的检验和保存首部的 ip_sum字段中。一个未被
破坏的首部应该具有 0检验和。
正如我们将在 8 . 7节中看到的,在计算到达分组的检验和之前,必须对 i p _ s u m
清零。通过把 i n _ c k s u m中的结果储存在 i p _ s u m中,就为分组转发作好了准备 (尽
管还没有减小 TTL)。i p _ o u t p u t函数不依赖这项操作;它为转发的分组重新计算检
验和。
如果结果非零,则该分组被自动丢弃。我们将在 8.7节中详细讨论in_cksum。
3. 字节顺序

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 171
151-160 Internet标准在指定协议首部中多字节整数值的字节顺序时非常小心。 N T O H S把
IP首部中所有 16 bit的值从网络字节序转换成主机字节序:分组长度 (ip_len)、数据报标识符
(i p _ i d)和分片偏移 (i p _ o f f)。如果两种格式相同,则 N T O H S是一个空的宏。在这里就转换
成主机字节序,以避免 Net/3每次检查该字段时必须进行一次转换。
4. 分组长度
161-177 如果分组的逻辑长度 (i p _ l e n)比储存在 m b u f中的数据量 (m _ p k t h d r . l e n)大,
并且有些字节被丢失了,就必须丢弃该分组。如果 mbuf比分组大,则去掉多余的字节。
丢失字节的一个常见原因是因为数据到达某个没有或只有很少缓存的串口设备,
例如许多个人计算机。设备丢弃到达的字节,而 IP丢弃最后的分组。
多余的字节可能产生,如在某个以太网设备上,当一个 IP分组的大小比以太网要
求的最小长度还小时。发送该帧时加上的多余字节就在这里被丢掉。这就是为什么
IP分组的长度被保存在首部的原因之一; IP允许链路层填充分组。
现在,有了完整的 I P首部,分组的逻辑长度和物理长度相同,检验和表明分组的首部无
损地到达。

8.4.3 转发或不转发

图8 - 1 3显示了 i p i n t r的下一部分,调用 i p _ d o o p t i o n s(见第9章)来处理I P选项,然后


决定分组是否到达它最后的目的地。如果分组没有到达最后目的地, N e t / 3会尝试转发该分组
(如果系统被配置成路由器 )。如果分组到达最后目的地,就被交付给合适的运输层协议。

图8-13 续ipintr

Linux公社 www.linuxidc.com
bbs.theithome.com
172 计计TCP/IP详解 卷 2:实现

图8-13 (续)

1. 选项处理
178-186 通过对i p _ n h o p s(见9 . 6节)清零,丢掉前一个分组的原路由。如果分组首部大于
默认首部,它必然包含由 i p _ d o o p t i o n s 处理的选项。如果 i p _ d o o p t i o n s 返回 0,
i p i n t r将继续处理该分组;否则, i p _ d o o p t i o n s通过转发或丢弃分组完成对该分组的处
理,ipintr可以处理输入队列中的下一个分组。我们把对选项的进一步讨论放到第 9章进行。
处理完选项后, i p i n t r通过把IP首部内的 i p _ d s t与配置的所有本地接口的 IP地址比较,
以决定分组是否已到达最终目的地。 i p i n t r必须考虑与接口相关的几个广播地址、一个或多
个单播地址以及任意个多播地址。
2. 最终目的地
187-261 ipintr通过遍历 in_ifaddr(图6-5),配置好的 Internet地址表,来决定是否有与
分组的目的地址的匹配。对清单中的每个 i n _ i f a d d r结构进行一系列的比较。要考虑 4种常
见的情况:
• 与某个接口地址的完全匹配 (图8-14中的第一行 ),
• 与某个与接收接口相关的广播地址的匹配 (图8-14的中间4行),
• 与某个与接收接口相关的多播组之一的匹配 (图12-39),或
• 与两个受限的广播地址之一的匹配 (图8-14的最后一行 )。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 173
图8 - 1 4显示的是当分组到达我们的示例网络里的主机 s u n上的以太网接口时要测试的地
址,将在第 12章中讨论的多播地址除外。

线路
变量 以太网 SLIP 环回 (图8.13)

图8-14 为判定分组是否到达最终目的地进行的比较

对i a _ s u b n e t、i a _ n e t和I N A D D R _ A N Y的测试不是必需的,因为它们表示的


是4 . 2 B S D使用的已经过时的广播地址。但不幸的是,许多 T C P / I P实现是从 4 . 2 B S D派
生而来的,所以,在某些网络中能够识别出这些旧广播地址可能十分重要。
3. 转发
262-271 如果ip_dst与所有地址都不匹配,分组还没有到达最终目的地。如果还没有设置
ipforwarding,就丢弃分组。否则,ip_forward尝试把分组路由到它的最终目的地。
当分组到达的某个地址不是目的地址指定的接口时,主机会丢掉该分组。在这
种情况下, N e t / 3将搜索整个 i n _ i f a d d r表;只考虑那些分配给接收接口的地址。
RFC 1122 称此为强端系统 (strong end system)模型。
对多主主机而言,很少出现分组到达接口地址与其目的地址不对应的情况,除
非配置了特定的主机路由。主机路由强迫相邻的路由器把多主主机作为分组的下一
跳路由器。弱端系统 (weak end system)模型要求该主机接收这些分组。实现人员可以
随意选择两种模型。 Net/3实现弱端系统模型。

8.4.4 重装和分用

最后,我们来看 i p i n t r的最后一部分 (图8 - 1 5 ),在这里进行重装和分用。我们略去了重


装的代码,推迟到第 10章讨论。当无法重装完全的数据报时,略去的代码将把指针 ip设成空。
否则,ip指向一个已经到达目的地的完整数据报。
运输分用
325-332 数据报中指定的协议被 i p _ p用i p _ p r o t o x数组(图7-22)映射到i n e t s w数组的下

Linux公社 www.linuxidc.com
bbs.theithome.com
174 计计TCP/IP详解 卷 2:实现

标。 i p i n t r调用选定的 p r o t o s w结构中的 p r _ i n p u t函数来处理数据报包含的运输报文。


当pr_input返回时,ipintr继续处理ipintrq中的下一个分组。
注意,运输层对分组的处理发生在 i p i n t r处理循环的内部。在 I P和传输协议之间没有到
达分组的排队,这与 TCP/IP中SVR4流实现的排队不同。

图8-15 续ipintr

8.5 转发:ip_forward函数

到达非最终目的地系统的分组需要被转发。只有当 i p f o r w a r d i n g非零( 6 . 1节)或当分组


中包含源路由 ( 9 . 6节)时,i p i n t r才调用实现转发算法的 i p _ f o r w a r d函数。当分组中包含
源路由时, ip_dooptions调用ip_forward,并且第2个参数srcrt设为1。
ip_forward通过图8-16中显示的 route结构与路由表接口。

图8-16 route 结构

46-49 r o u t e 结构有两个成员: r o _ r t ,指向 r t e n t r y 结构的指针; r o _ d s t ,一个


s o c k a d d r结构,指定与 r o _ r t所指的路由项相关的目的地。目的地是在内核的路由表中用
来查找路由信息的关键字。第 18章对rtentry结构和路由表有详细的描述。
我们分两部分讨论ip_forward。第一部分确定允许系统转发分组,修改 IP首部,并为分
组选择路由。第二部分处理ICMP重定向报文,并把分组交给ip_output进行发送。见图8-17。
1. 分组适合转发吗
867-871 i p _ f r o w a r d的第1个参数是指向一个 m b u f链的指针,该 m b u f中包含了要被转发
的分组。如果第 2个参数srcrt为非零,则分组由于源路由选项 (见9.6节)正在被转发。
879-884 if语句识别并丢弃以下分组。
• 链路层广播
任何支持广播的网络接口驱动器必须为收到的广播分组把 M _ B C A S T标志置位。如果分组
寻址是到以太网广播地址,则 e t h e r _ i n p u t(图4-13)就把M _ B C A S T置位。不转发链路层的广
播分组。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 175
RFC 11 2 2不允许以链路层广播的方式发送一个寻址到单播 I P地址的分组,并在
这里将该分组丢掉。
• 环回分组
对寻址到环回网络的分组, i n _ c a n f o r w a r d返回0。这些分组将被 i p i n t r提交给 i p _
forward,因为没有正确配置反馈接口。
• 网络0和E类地址
对这些分组,in_canforward返回0。 这些目的地址是无效的,而且因为没有主机接收
这些分组,所以它们不应该继续在网络中流动。
• D类地址
寻址到 D 类地址的分组应该由多播函数 i p _ m f o r w a r d而不是由 i p _ f o r w a r d处理。
in_canforward拒绝D类(多播)地址。
RFC 791 规定处理分组的所有系统都必须把生存时间 ( T T L )字段至少减去 1,即使 T T L是
以秒计算的。由于这个要求, T T L通常被认为是对 I P分组在被丢掉之前能经过的跳的个数的
界限。从技术角度说,如果路由器持有分组超过 1秒,就必须把ip_ttl减去多于1。

图8-17 ip_forward 函数:路由选择

Linux公社 www.linuxidc.com
bbs.theithome.com
176 计计TCP/IP详解 卷 2:实现

图8-17 (续)

这就产生了一个问题:在 I n t e r n e t上,最长的路径有多长?这个度量称为网络的
直径( d i a m e t e r )。除了通过实验外无法知道直径的大小。 [Olivier 1994]中有3 7跳的路
径。
2. 减小TTL
885-890 由于转发时不再需要分组的标识符,所以标识符又被转换回网络字节序。但是当
i p _ f o r w a r d 发送包含无效 I P首部的 I C M P差错报文时,分组的标识符又应该是正确的顺
序。
N e t / 3漏做了对已被 i p i n t r转换成主机字节序的 i p _ l e n的转换。作者注意到在
大头机器上,这不会产生问题,因为从未对字节进行过转换。但在小头机器如 386上,
这个小的漏洞允许交换了字节的值在 I C M P差错中的 I P首部中。返回从运行在 3 8 6上
的S V R 4 (可能是N e t / 1码)和AIX3.2(4.3BSD Reno码)返回的I C M P分组中可以观察到这
个小的漏洞。
如果i p _ t t l达到1 (I P T T L D E C),则向发送方返回一个 I C M P超时报文,并丢掉该分组。
否则,ip_forward把ip_ttl减去IPTTLDEC。
系统不接受 T T L为0的I P数据报,但 N e t / 3在即使出现这种情况时也能生成正确的 I C M P差
错,因为p_ttl是在分组被认为是在本地交付之后和被转发之前检测的。
3. 定位下一跳
891-907 IP转发算法把最近的路由缓存在全局 route结构的ipforward_rt中,在可能时
应用于当前分组。研究表明连续分组趋向于同一目的地址 ( [ J a i n和Routhier 1986] 和[ M o g u l
1 9 9 1 ] ) ,所以这种向后一个 ( o n e - b e h i n d )的缓存使路由查询的次数最少。如果缓存为空
(i p f o r w a r d _ r t)或当前分组的目的地不是 i p f o r w a r d _ r t中的路由,就取消前面的路由,
r o _ d s t被初始化成新的目的地, r t a l l o c为当前分组的目的地找一个新路由。如果找不到
路由,则返回一个 ICMP主机不可达差错,并丢掉该分组。
908-914 由于在产生差错时, i p _ o u t p u t要丢掉分组,所以 m _ c o p y复制分组的前64个字
节,以便ip_forward发送ICMP差错报文。如果调用m_copy失败,ip_forward并不终止。
在这种情况下,不发送差错报文。 i p _ i f m a t r i x记录在接口之间进行路由选择的分组的个
数。具有接收和发送接口索引的计数器是递增的。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 177
重定向报文

当主机错误地选择某个路由器作为分组的第一跳路由器时,该路由器向源主机返回一个
I C M P重定向报文。 I P网络互连模型假定主机相对地并不知道整个互联网的拓扑结构,把维护
正确路由选择的责任交给路由器。路由器发出重定向报文是向主机表明它为分组选择了一个
不正确的路由。我们用图 8-18说明重定向报文。

重新选路

默认网络 目的网络

图8-18 路由器R1重定向主机 HS使用路由器 R2到达HD

通常,管理员对主机的配置是:把到远程网络的分组发送到某个默认路由器上。在图 8-18
中,主机 H S上R 1被配置成它的默认路由器。当 H S首次向H D发送分组时,它不知道 R 2是合适
的选择,而把分组发给 R 1。R 1识别出差错,就把分组转发给 R 2,并向 H S发回一个重定向报
文。接收到重定向报文后, HS更新它的路由表,下一次发往 HD的分组就直接发给 R2。
RFC 1122推荐只有路由器才发重定向报文,而主机在接收到 ICMP重定向报文后必须更新
它们的路由表 ( 11 . 8节)。因为 N e t / 3只在系统被配置成路由器时才调用 i p _ f o r w a r d,所以
Net/3采用RFC 1122的推荐。
在图8-19中,ip_forward决定是否发重定向报文。
1. 在接收接口上离开吗
915-929 路由器识别重定向情况的规则很复杂。首先,只有在同一接口 ( r t _ i f p和
r c v i f)上接收或重发分组时,才能应用重定向。其次,被选择的路由本身必须没有被 I C M P
重定向报文创建或修改过 (R T F _ D Y N A M I C | R T F _ M O D I F I E D),而且该路由也不能是到默认目
的地的(0.0.0.0)。这就保证系统在末授权时不会生成路由选择信息,并且不与其他系统共享自
己的默认路由。
通常,路由选择协议使用特殊目的地址 0.0.0.0定位默认路由。当到某目的地的某
个路由不能使用时,与目的地 0.0.0.0相关的路由就把分组定向到一个默认路由器上。

Linux公社 www.linuxidc.com
bbs.theithome.com
178 计计TCP/IP详解 卷 2:实现

第18章对默认路由有详细的讨论。
全局整数 i p s e n d r e d i r e c t s 指定系统是否被授权发送重定向 (第8.9节),
i p s e n d r e d i r e c t s的默认值为 1。当传给 i p _ f o r w a r d的参数s r c r t指明系统是对分组路
由选择的源时,禁止系统重定向,因为假定源主机要覆盖中间路由器的选择。

图8-19 ip_forward (续)

2. 发送重定向吗
930-931 这个测试决定分组是否产生于本地子网。如果源地址的子网掩码位和输出接口的
地址相同,则两个地址位于同一 I P网络中。如果源接口和输出的接口位于同一网络中,则该
系统就不应该接收这个分组,因为源站可能已经把分组发给正确的第一跳路由器了。 I C M P重
定向报文告诉主机正确的第一跳目的地。如果分组产生于其他子网,则前一系统是个路由器,
这个系统就不应该发重定向报文;差错由路由选择协议纠正。
在任何情况下,都要求路由器忽略重定向报文。尽管如此,当 i p f o r w a r d i n g
被置位时(也就是说,当它被配置成路由器时 ),Net/3并不丢掉重定向报文。
3. 选择合适的路由器
932-940 ICMP重定向报文中包含正确的下一个系统的地址,如果目的主机不在直接相连的
网络上时,该地址是一个路由器的地址;当目的主机在直接相连的网络中时,该地址是主机
地址。
RFC 792 描述了重定向报文的 4种类型: ( 1 )网络; ( 2 )主机; ( 3 ) TO S和网络; ( 4 ) TO S和主
机。RFC 1009推荐在任何时候都不发送网络重定向报文,因为无法保证接收到重定向报文的
主机能为目的网络找到合适的子网掩码。 RFC 1122推荐主机把网络重定向看作是主机重定向,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计179
以避免二义性。 N e t / 3只发送主机重定向报文,并省略所有对 TO S 的考虑。在图 8 - 2 0 中,
ipintr把分组和所有的 ICMP报文提交给链路层。

图8-20 ip_forward(续)

重定向报文的标准化是在子网化之前,在一个非子网化的互联网中,网络重定
向很有用,但在一个子网化的互联网中,由于重定向报文中没有有关子网掩码的信
息,所以容易产生二义性。

Linux公社 www.linuxidc.com
bbs.theithome.com
180 计计TCP/IP详解 卷 2:实现

4. 转发分组
941-954 现在, i p _ f o r w a r d有一个路由,并决定是否需要 I C M P重定向报文。 I p _
o u t p u t把分组发送到路由 i p f o r w a r d _ r t所指定的下一跳。 I P _ A L L O W B R O A D C A S T标志位
允许被转发分组是个到某局域网的广播。如果 i p _ o u t p u t成功,并且不需要发送任何重定向
报文,则丢掉分组的前 64字节,ip_forward返回。
5. 发送ICMP差错报文?
955-983 i p _ f o r w a r d可能会由于 i p _ o u t p u t失败或重定向而发送 ICMP报文。如果没有
原始分组的复制 (可能当要复制时,曾经缓存不足 ),则无法发送重定向报文, i p _ f o r w a r d
返回。如果有重定向, t y p e和c o d e以前又被置位,但如果 i p _ o u t p u t失败,s w i t c h语句
基于从 i p _ o u t p u t返回的值重新设置新的 I C M P类型和码值。 i c m p _ e r r o r发送该报文。来
自失败的i p _ o u t p u tICMP报文将覆盖任何重定向报文。
处理来自i p _ o u t p u t的差错的s w i t c h语句非常重要。它把本地差错翻译成适当的 ICMP
差错报文,并返回给分组的源站。图 8 - 2 1对差错作了总结。第 11章更详细地描述了 I C M P报
文。
当i p _ o u t p u t返回 E N O B U F S时,N e t / 3通常生成 I C M P源站抑制报文。 R o u t e r
R e q u i r e m e n t s (路由器需求 )RFC [Almquist和Kastenholz 1994]不赞成源站抑制并要求
路由器不产生这种报文。

来自ip_output的差错码 生成的I C M P报文 描 述

EMSGSIZE ICMP_UNREACH_NEEDFRAG 对所选的接口来说,发出的分组太大,


并且禁止分片 (第1 0章)
ENOBUFS ICMP_SOURCEQUENCH 接口队列满或内核运行内存不足。本报
文向源主机指示降低数据率
EHOSTUNREACH 找不到到主机的路由
ENETDOWN 路由指明的输出接口没在运行
EHOSTDOWN ICMP_UNREACH_HOST 接口无法把分组发给选定的主机
default 所有不识别的差错均作为
I C M P _ U N R E A C H _ H O S T差错报告

图8-21 来自ip_output 的差错

8.6 输出处理:iP_output函数

I P 输出代码从两处接收分组: i p _ f o r w a r d 和运输协议 ( 图8 - 1 ) 。让 i n e t s w [ 0 ]
. p r _ o u t p u t能访问到 I P输出操作似乎很有道理,但事实并非如此。标准的 I n t e r n e t传输协议
( I C M P、I G M P、U D P和T C P )直接调用 i p _ o u t p u t,而不查询 i n e t s w表。对标准 I n t e r n e t传
输协议而言, p r o t o s w结构不必具有一般性,因为调用函数并不是在与协议无关的情况下接
入IP的。在第20章中,我们将看到与协议无关的路由选择插口调用 pr_output接入IP。
我们分三个部分描述 i p _ o u t p u t :
• 首部初始化;
• 路由选择;和
• 源地址选择和分片。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 181
8.6.1 首部初始化

图8-22显示了i p _ o u t p u t的第一部分,把选项与外出的分组合并,完成传输协议提交 (不
是ip_forward提交的)的分组首部。
44-59 传给ip_output的参数包括:m0,要发送的分组; opt,包含的IP选项;ro,缓存
的到目的地的路由; flags,见图8-23;imo,指向多播选项的指针,见第 12章。
I P _ F O R W A R D I N G被i p _ f o r w a r d和i p _ m f o r w a r d ( 多播分组转发 )设置,并禁止
ip_output重新设置任何IP首部字段。

图8-22 函数ip_output

标 志 描 述

IP_FORWARDING 这是一个转发过的分组
IP_ROUTETOIF 忽略路由表,直接路由到接口
IP_ALLOWBROADCAST 允许发送广播分组
IP_RAWOUTPUT 包含一个预构 I P首部的分组

图8-23 ip_output :flag 值

Linux公社 www.linuxidc.com
bbs.theithome.com
182 计计TCP/IP详解 卷 2:实现

send、sendto和sendmsg的MSG_DONTROUTE标志使IP_ROUTETOIF有效,并进行一
次写操作 (见1 6 . 4 ),而S O _ D O N T R O U T E插口选项使 I P _ R O U T E T O I F有效,并在某个特定插口
上进行任意的写操作 (见8.8节)。该标志被传输协议传给 ip_ output。
I P _ A L L O W B R O A D C A S T标志可以被 S O _ B R O A D C A S T插口选项 (见8.8节)设置,但只被 U D P
提交。原来的 I P 默认地设置 I P _ A L L O W B R O A D C A S T 。 T C P 不支持广播,所以 I P _
ALLOWBROADCAST不能被TCP提交给ip_output。不存在广播的预请求标志。
1. 构造IP首部
60-73 如果调用程序提供任何 IP选项,它们将被ip_insertoptions(见9.8节)与分组合并,
并返回新的首部长度。
我们将在 8 . 8节中看到,进程可以设置 I P _ O P T I O N S插口选项来为一个插口指定 I P选项。
插口的运输层(TCP或UDP)总是把这些选项提交给 ip_output。
被转发分组(I P _ F O R W A R D I N G)或有预构首部 (I P _ R A W O U T P U T)分组的I P首部不能被 i p _
o u t p u t修改。任何其他分组 (例如,产生于这个主机的 U D P或T C P分组)需要有几个 I P首部字
段被初始化。 i p _ o u t p u t把i p _ v设置成4(I P V E R S I O N),把DF位需要的 i p _ o f f清零,并设
置成调用程序提供的值 (见第1 0章),给来自全局整数的 i p - > i p _ i d赋一个唯一的标识符,把
i p _ i d加1。i p _ i d是在协议初始化时由系统时钟设置的 (见7 . 8节)。i p _ h l被设置成用 32 bit
字度量的首部长度。
IP首部的其他字段—长度、偏移、 TTL、协议、TOS和目的地址—已经被传输协议初
始化了。源地址可能没被设置,因为是在确定了到目的地的路由后选择的 (图8-25)。
2. 分组已经包括首部
74-76 对一个已转发的分组 (或一个有首部的原始 IP分组),首部长度(以字节数度量 )被保存
在hlen中,留给将来分片算法使用。

8.6.2 路由选择

在完成I P首部后, i p _ o u t p u t的下一个任务就是确定一条到目的地的路由。见图 8 - 2 4所


示。

图8-24 ip_output (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 183

图8-24 (续)

1. 验证高速缓存中的路由
77-99 i p _ o u t p u t可能把一条在高速缓存中的路由作为 ro参数来提供。在第 24章中,我们
将看到UDP和TCP维护一个与各插口相关的路由缓存。如果没有路由,则 i p _ o u t p u t把r o设
置成指向临时route结构iproute。
如果高速缓存中的目的地不是去当前分组的目的地,就把该路由丢掉,新的目的地址放
在dst中。
2. 旁路路由选择
100-114 调用方可通过设置 I P _ R O U T E T O I标志(见8
F . 8节)禁止对分组进行路由选择。
i p _ o u t p u t 必须找到一个与分组中指定目的地网络直接相连的接口。
i f a _ i f w i t h d s t a d d r搜索点到点接口,而 i n _ i f w i t h n e t搜索其他接口。如果任一函数
找到与目的网络相连的接口,就返回 ENETUNREACH;否则,ifp指向选定的接口。
这个选项允许路由选择协议绕过本地路由表,并使分组通过某特定接口退出系

Linux公社 www.linuxidc.com
bbs.theithome.com
184 计计TCP/IP详解 卷 2:实现

统。通过这个方法,即使本地路由表不正确,也可以与其他路由器交换路由选择信
息。
3. 本地路由
115-122 如果分组正被路由选择 (I P _ R O U T E T O I F为关状态 ),并且没有其他缓存的路由,
则rtalloc找到一条到 dst指定地址的路由。如果 rtalloc没找到路由,则 i p _ o u t p u返
t
回E H O S T U N R E A C H。如果 i p _ f o r w a r d调用 i p _ o u t p u t,就把 E H O S T U N R E A C H转换成
ICMP差错。如果某个传输协议调用 ip_output,就把差错传回给进程 (图8-21)。
123-128 ia被设成指向选定接口的地址 (ifaddr结构),而ifp指向接口的 ifnet结构。如
果下一跳不是分组的最终目的地,则把 d s t改成下一跳路由器地址,而不再是分组最终目的
地址。IP首部内的目的地址不变,但接口层必须把分组提交给 dst,即下一跳路由器。

8.6.3 源地址选择和分片

i p _ o u t p u t的最后一部分如图 8 - 2 5所示,保证 I P首部有一个有效源地址,然后把分组提


交给与路由相关的接口。如果分组比接口的 M T U大,就必须对分组分片,然后一片一片地发
送。像前面的重装代码一样,我们省略了分片代码,并推迟到第 10章再讨论。

图8-25 ip_output(续)

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 185

图8-25 (续)

1. 选择源地址
212-239 如果没有指定ip_src,则ip_output选择输出接口的 IP地址ia作为源地址。这
不能在早期填充其他 I P首部字段时做,因为那时还没有选定路由。转发的分组通常都有一个
源地址,但是,如果发送进程没有明确指定源地址,产生于本地主机的分组可能没有源地址。
如果目的 I P地址是一个广播地址,则接口必须支持广播 (I F F _ B R O A D C A S T,图3 - 7 ),调
用方必须明确使能广播 (IP_ALLOWBROADCAST,图8-23),而分组必须足够小,无需分片。
最后的测试是一个策略决定。 IP协议规范中没有明确禁止对广播分组的分片。但
是,要求分组适合接口的 M T U,就增加了广播分组被每个接口接收的机会,因为接
收一个未损坏的分组的机会要远大于接收两个或多个未损坏分组的机会。
如果这些条件都不满足,就扔掉该分组,把 E A D D R N O T A V A I L、E A C C E S和E M S G S I Z E返
回给调用方。否则,设置输出分组的 M _ B C A S T,告诉接口输出函数把该分组作为链路级广播
发送。21.20节中,我们将看到 arpresolve把IP广播地址翻译成以太网广播地址。
如果目的地址不是广播地址,则 ip_output把M_BCAST清零。
如果M _ B C A S T没有清零,则对一个作为广播到达的请求分组的应答将可能作为
一个广播被返回。我们将在第 11章中看到, I C M P应答将以这种方式作为 TCP RST
分组(见26.9节)在请求分组内构造。
2. 发送分组
240-252 如果分组对所选择的接口足够小, ip_len和ip_off被转换成网络字节序, IP检
验和与 in_cksum(见8.7节)一起计算,把分组提交给所选接口的 if_output函数。
3. 分片分组
253-338 大分组在被发送之前必须分片。这里我们省略这段代码,推迟到第 10章讨论。
4. 清零
339-346 对每一路由入口都有一个引用计数。我们提到过,如果参数 r o 为空, i p _

Linux公社 www.linuxidc.com
bbs.theithome.com
186 计计TCP/IP详解 卷 2:实现

output可能会使用一个临时的 route结构(iproute)。如果需要,RTFREE发布iproute内
的路由入口,并把引用计数减 1。Bad处的代码在返回前扔掉当前分组。
引用计数是一个存储器管理技术。程序员必须对一个数据结构的外部引用计
数;当计数返回为 0时,就可以安全地把存储器返回给空存储器池。引用计数要求程
序员遵守一些规定,在恰当的时机增加或减小引用计数。

8.7 Internet检验和: in_cksum函数

有两个操作占据了处理分组的主要时间:复制数据和计算检验和 ([Kay和Pasquale 1993])。


m b u f数据结构的灵活性是 N e t / 3中减少复制操作的主要方法。由于对硬件的依赖,所以检验和
的有效计算相对较难。 Net/3中有几种 in_cksum的实现(图8-26)。

版 本 源 文 件

图8-26 在Net/3中的几个 in_cksum 版本

即使是可移植 C实现也已经被相当好地优化了。 RFC 1071 [Braden 、B o r m a n和P a r t r i d g e


1988] 和RFC 1141 [Mallory和K u l l b e rg 1990]讨论了 I n t e r n e t检验和函数的设计和实现。 R F C
1141被RFC 1624 [Rijsinghani 1994] 修正。从RFC 1071:
1) 把被检验的相邻字节成对配成 16 bit整数,就形成了这些整数的二进制反码的和。
2) 为生成检验和,把检验和字段本身清零,把 16 bit的二进制反码的和以及这个和的二进
制反码放到检验和字段。
3) 为检验检验和,对同一组字节计算它们的二进制反码的和。如果结果为全 1 (在二进制
反码运算中- 0,见下面的解释 ),则检验成功。
简而言之,当对用二进制反码表示的整数进行加法运算时,把两个整数相加后再加上进
位就得到加法的结果。在二进制反码运算中,只要把每一位求补就得到一个数的反。所以在
二进制反码运算中, 0有两种表示方法:全 0,和全1。有关二进制反码的运算和表示的详细讨
论见 [Mano 1982]。
检验和算法在发送分组之前计算出要放在 I P首部检验和字段的值。为了计算这个值,先
把首部的检验和字段设为 0,然后计算整个首部 (包括选项 )的二进制反码的和。把首部作为一
个16 bit整数数组来处理。让我们把这个计算结果称为 a。因为检验和字段被明确设为 0,所以
a是除了检验和字段外所有 I P首部字段的和。 a的二进制反码,用- a表示,被放在检验和字段
中,发送该分组。
如果在传输过程中没有比特位被改变,则在目的地计算的检验和应该等于 (a+-a)的二进
制反码。在二进制反码运算中 (a+-a)的和是- 0 (全1 ),而它的二进制反码应该等于 0 (全0 )。所
以在目的地,一个没有损坏分组计算出来的检验和应该总是为 0。这就是我们在图 8 - 1 2中看到

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 187
的。下面的 C代码(不是Net/3的内容)是这个算法的一种原始的实现:

图8-27 IP检验和计算的一种原始的实现

1-16 这里唯一提高性能之处在于累计 s u m高16 bit的进位。当循环结束时,累计的进位被


加在低 16 bit上,直到没有其他进位发生。 RFC 1071称此为延迟进位 (deferred carries)。在没
有有进位加法指令或检测进位代价很大的机器上,这个技术非常有效。
现在我们显示 N e t / 3的可移植 C版本。它使用了延迟进位技术,作用于存储在一个 m b u f链
中的分组。
42-140 我们的新检验和实现假定所有被检验字节存储在一个连续缓存而不是 mbuf中。这个
版本的检验和计算采用相同的底层算法来正确地处理 mbuf:用32bit整数的延迟进位对 16 bit字
作加法。对奇数个字节的 m b u f,多出来的一个字节被保存起来,并与下一个 m b u f的第一个字
节配对。因为在大多数体系结构中,对 16 bit字的不对齐访问是无效的,甚至会产生严重差错,
所以不对齐字节将被保存, i n _ c k s u m继续加上下一个对齐的字。当这种情况发生时, i n _
c k s u m总是很小心地交换字节,保证位于奇数和偶数位置的字节被放在单独的和字节中,以
满足检验和算法的要求。
循环展开
93-115 函数中的三个while循环在每次迭代中分别在和中加上 16个字、4个字和1个字。展
开的循环减小了循环的耗费,在某些体系结构中可能比一个直接循环要快得多。但代价是代
码长度和复杂性增大。

图8-28 IP检验和计算的一个优化的可移植 C程序

Linux公社 www.linuxidc.com
bbs.theithome.com
188计计TCP/IP详解 卷 2:实现

图8-28 (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 189

图8-28 (续)

其他优化

RFC 1071提到两个在Net/3中没有出现的优化:联合的有检验和的复制操作和递增的检验
和更新。对 I P首部检验和来说,把复制和检验和操作结合起来并不像对 T C P和U D P那么重要,
因为后者覆盖了更多的字节。在 2 3 . 1 2节中对这个合并的操作进行了讨论。 [ P a r t r i d g e和P i n k
1 9 9 3 ]报告了 I P首部检验和的一个内联版本比调用更一般的 i n _ c k s u m函数要快得多,只需
6~8个汇编指令就可以完成 (标准的20字节IP首部)。
检验和算法设计允许改变分组,并在不重新检查所有字节的情况下更新检验和。 RFC
1 0 7 1对该问题进行简明的讨论。 RFC 11 4 1和1 6 2 4中有更详细的讨论。该技术的一个典型应用
是在分组转发的过程中。通常情况下,当分组没有选项时,转发过程中只有 T T L字段发生变
化。在这种情况下,可以只用一次循环进位,重新计算检验和。
为了进一步提高效率,递增的检验和也有助于检测到被有差错的软件破坏的首部。如果
递增地计算检验和,则下一个系统可以检测到被破坏的首部。但是如果不是递增计算检验和,
那么检验和中就包含了差错的字节,检测不到有问题的首部。 U D P和T C P使用的检验和算法
在最终目的主机检测到该差错。我们将在第 2 3和2 5章看到 U D P和T C P检验和包含了 I P首部的
几个部分。

Linux公社 www.linuxidc.com
bbs.theithome.com
190 计计TCP/IP详解 卷 2:实现

使用硬件有进位加法指令一次性计算 32 bit检验和的检验和函数,可参见 ./ s y s / v a x /
vax/in_cksum.c文件中VAX实现的in_cksum。

8.8 setsockopt和getsockopt系统调用

N e t / 3提供s e t s o c k o p t和g e t s o c k o p t两个系统调用来访问一些网络互连的性质。这两个系统


调用支持一个动态接口,进程可用该动态接口来访问某种网络互连协议的一些性质,而标准
系统调用通常不支持该协议。这两个调用的原型是:
int setsockopt(int s, int level, int optname, void *optval, int optlen);
int getsockopt(int s, int level, int optname, const void *optval, int optlen);

大多数插口选项只影响它们在其上发布的插口。与 sysctl参数相比,后者影响整个系统。
与多播相关的插口选项是一个明显的例外,将在第 12章中讨论。
s e t s o c k o p t和g e t s o c k o p t设置和获取通信栈所有层上的选项。 N e t / 3按照与 s相关的
协议和由level指定的标识符处理选项。图 8-29列出了在我们讨论的协议中 level可能取得的值。
在第1 7章中,我们描述了 s e t s o c k o p t和g e t s o c k o p t的实现,但在其他适当章节中讨
论有关选项的实现。本章讨论访问 IP性质的选项。

字段 协议 level 函 数 参 考

任意 任意 SOL_SOCKET sosetopt和sogetopt 图17-5和图17-11


IP UDP IPPROTO_IP ip_ctloutput 图8-31
TCP IPPROTO_TCP tcp_ctloutput 30.6节
IPPROTO_IP ip_ctloutput 图8-31
原始IP r i p _ c t l o u t p u t和
ICMP IPPROTO_IP ip_ctloutput 32.8节
IGMP

图8-29 sosetopt 和sogetopt 参数

我们把本书中出现的所有插口选项总结在图 8 - 3 0中。该图显示了 I P P R O T O _ I P级的选项。


选项出现在第 1列, o p t v a l指向变量的数据类型出现在第 2列,第 3列显示的是处理该选项的
函数。

选 项 名 O p t v a l类型 函 数 描 述

IP_OPTIONS void* in_pcbopts 设置或获取发出的数据报中的 IP选项


IP_TOS int ip_ctloutput 设置或获取发出的数据报中的 IP TOS
IP_TTL int ip_ctloutput 设置或获取发出的数据报中的 IP TTL
TP_RECVDSTADDR int ip_ctloutput 使能或禁止 IP目的地址(只有UDP)的排队
IP_RECVOPTS int ip_ctloutput 使能或禁止对到达 IP选项作为控制信息的
排队(只对U D P;还没有实现 )
IP_RECVRETOPTS int ip_ctloutput 使能或禁止与到达数据报相关的逆源路由
(只对UDP;还没有实现 )

图8-30 插口选项: SOCK_RAW 、SOCK_DGRAM 和SOCK_STREAMR 插口的IPPROTO_IP 级

图8 - 3 1显示了用于处理大部分 I P P R O T O _ I P选项的i p _ c t l o u t p u t函数的整个结构。在


32.8节中我们给出与 SOCK_RAW插口一起使用的 IPPROTO_IP选项。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计191

图8-31 ip_ctloutput 函数:概貌

431-447 ip_ctloutput的第一个参数op,可以是PRCO_SETOPT或者PRCO_GETOPT。第二
个参数so,指向向其发布请求的插口。level必须是IPPROTO_IP。O p t n a m e是要改变或要检
索的选项, m p间接地指向一个含有与该选项相关数据的 m b u f,m被初始化为指向由 * m p引用
的mbuf。
448-500 如果在调用s e t s o c k o p t时指定了一个无法识别的选项 (因此,在s w i t c h中调用
PRCO_SETOPT语句),ip_ctloutput释放掉所有调用方传来的缓存,并返回 EINVAL。

Linux公社 www.linuxidc.com
bbs.theithome.com
192 计计TCP/IP详解 卷 2:实现

501-553 getsockopt传来的无法识别的选项导致ip_ctloutput返回ENOPROTOOPT。在
这种情况下,调用方释放 mbuf。

8.8.1 PRCO_SETOPT的处理

对PRCO_SETOPT的处理如图 8-32所示。

图8-32 ip_ctloutput 函数:处理 PRCO_SETOPT

450-451 IP_OPTIONS是由ip_pcbopts处理的(图9-32)。
452-484 IP_TOS、 IP_TTL、 IP_RECVOPTS、 IP_RECVERTOPTS以 及 IP_
R E C V D S T A D D R选项都需要在由 m指向的m b u f中有一个整数。该整数储存在 o p t v a l中,用来改
变 与 插口 有 关的 i p _ t o s 和 i p _ t t l 的 值, 或 者用 来 设置 或 复位 与 插口 相 关的
I N P _ R E C V O P T S、I N P _ R E C V E R T O P T S和I N P _ R E C V D S T A D D R标志位。如果 o p t v a l是非零
(或0),则宏OPTSET设置(或复位)指定的比特。
图8 - 3 0中显示没有实现 I P _ R E C V O P T S和I P _ R E C V E R T O P T S。在第2 3章中,我
Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计193
们将看到UDP忽略了这些选项的设置。

8.8.2 PRCO_GETOPT的处理

图8-33显示的一段代码完成了当指定 PRCO_GETOPT时对IP选项的检索。

图8-33 ip_ctloutput 函数:PRCO_GETOPT 的处理

503-538 对IP_OPTIONS,ip_ctloutput返回一个缓存,该缓存中包含了与该插口相关
的选项的备份。对其他选项, ip_ctloutput返回ip_tos和ip_ttl的值,或与该选项相关
的标志的状态。返回的值放在由 m指向的 m b u f中。如果在 i n p _ f l a g s中的b i t是打开 (或关
闭)的,则宏 OPTBIT将返回1(或0)。

8.9 ip_sysctl函数

图7-27显示,在调用 s y s c t l中,当协议和协议族的标识符是 0时,就调用i p _ s y s c t l函


Linux公社 www.linuxidc.com
bbs.theithome.com
194 计计TCP/IP详解 卷 2:实现

数。图8-34显示了ip_sysctl支持的三个函数。

sy s c t l常量 Net/3变量 描 述

IPCTL_FORWARDING ipforwarding 系统是否转发 IP分组?


IPCTL_SENDREDIRECTS ipsendredirects 系统是否发 ICMP重定向?
IPCTL_DEFTTL ip_defttl IP分组的默认 TTL

图8-34 sysctl 参数

图8-35显示了ip_sysctl函数。

图8-35 ip_sysctl 函数

因为i p _ s y s c t l并不把 s y s c t l请求转发给其他函数,所以在 n a m e中只能有一个成员。


否则返回 ENOTDIR。
S w i t c h 语句选择恰当的调用 s y s t l _ i n t ,它访问或修改 i p f o r w a r d i n g 、
ipsendredirects或ip_defttl。对无法识别的选项返回 EOPNOTSUPP。

8.10 小结

I P是一个最佳的数据报服务,它为所有其他 I n t e r n e t协议提供交付机制。标准 I P首部长度


为2 0字节,但可跟最多 4 0字节的选项。 I P可以把大的数据报分片发送,并在目的地重装分片。
对选项处理的讨论放在第 9章和第10章讨论分片和重装。
i p i n t r保证I P首部到达时未经破坏,通过把目的地址与系统接口地址及其他几个广播地
址比较来确定它们是否到达最终目的地。 i p i n t r把到达最终目的地的数据报传给分组内指定
的运输层协议。如果系统被配置成路由器,就把还没有到达最终目的地的分组发给

Linux公社 www.linuxidc.com
bbs.theithome.com
第 8章 IP:网际协议 计计 195
i p _ f o r w a r d 转发到最终目的地。分组有一个受限的生命期。如果 T T L字段变成 0,则
ip_forward就丢掉该分组。
许多Internet协议都使用Internet检验和函数, Net/3用i n _ c k s u m实现。IP检验和只覆盖首
部(和选项),不覆盖数据,数据必须由传输协议级的检验和保护。作为 IP中最耗时的操作,检
验和函数通常要对不同的平台进行优化。

习题

8.1 当没有为任何接口分配 IP地址时, IP是否该接收广播分组?


8.2 修改i p _ f o r w a r d和i p _ o u t p u t,当转发一个没有选项的分组时,对 IP检验和进行
递增的更新。
8.3 当拒绝转发分组时,为什么需要检测链路级广播 (某缓存中的 M _ B C A S T标志)和I P级
广播(i n _ c a n f o r w a r d)?在何种情况下,把一个具有 IP单播目的地的分组作为一个
链路层广播接收?
8.4 当一个IP分组到达时有检验和差错,为什么不向发送方返回一个差错信息?
8.5 假定一个多接口主机上的某个进程为它发出的分组选择了一个明确的源地址。而且,
假定是通过一个接口而不是作为分组源地址所选择的地址到达的。当第一跳路由器
发现分组应该到另一个路由器时,会发生什么情况?会向主机发送重定向报文吗?
8.6 一个新的主机被连到一个已划分子网的网络中,并被配置成完成路由选择的功能
(i p f o r w a r d i n g等于1),但它的网络接口没有分配子网掩码。当该主机接收一个子
网广播分组时会出现什么情况?
8.7 图8-17中,在检测 ip_ttl后(与之前相比 ),为什么需要把它减 1?
8.8 如果两个路由器都认为对方是分组的最佳下一跳目的地,将发生什么情况?
8.9 图8 - 1 4中,对一个到达 S L I P接口的分组,不检测哪些地址?有没有其他在图 8 - 1 4中
没有列出的地址被检测?
8.10 i p _ f o r w a r d在调用i c m p _ e r r o r之前,把分片的 i d从主机字节序转换成网络字
节序。为什么它不对分片的偏移进行转换?

Linux公社 www.linuxidc.com
bbs.theithome.com
第9章 IP选项处理
9.1 引言

第8章中提到, I P输入函数 (i p i n t r)将在验证分组格式 (检验和,长度等 )之后,确定分组


是否到达目的地之前,对选项进行处理。这表明,分组所遇到的每个路由器以及最终的目的
主机都要对分组的选项进行处理。
RFC 791和1122指定了IP选项和处理规则。本章将讨论大多数 IP选项的格式和处理。我们
也将显示运输协议如何指定 IP数据报内的 IP选项。
I P分组内可以包含某些在分组被转发或被接收之前处理的可选字段。 I P实现可以用任意
顺序处理选项; Net/3按照选项在分组中出现的顺序处理选项。图 9-1显示,标准IP首部之后最
多可跟40字节的选项。

ip_hl×4字节

标准首部 选 项
(20字节) (0~40字节)

最大60字节

图9-1 一个IP首部可以有 0~40字节的IP选项

9.2 代码介绍

两个首部描述了 I P选项的数据结构。选项处理的代码出现在两个 C文件中。图 9 - 2列出了


相关的文件。

文 件 描 述

netinet/ip.h i p _ t i m e s t a m p结构
netinet/ip_var.h i p o p t i o n结构
netinet/ip_input.c 选项处理
netinet/ip_output.c i p _ i n s e r t o p t i o n s函数

图9-2 本章讨论的文件

9.2.1 全局变量

图9-3描述了两个全局变量支持源路由的逆 (reversal)。

变 量 数据类型 描 述

ip_nhops int 以前的源路由跳计数


ip_srcrt struct ip_srcrt 以前的源路由

图9-3 本章引入的全局变量

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 197
9.2.2 统计量

选项处理代码更新的唯一的统计量是 ipstat结构中的ips_badoptions,如图8-4所示。

9.3 选项格式

IP选项字段可能包含 0个或多个单独选项。选项有两种类型,单字节和多字节,如图 9-4中


所示。
len字节

单字节 多字节

位移字段没有出现在每个多
字节选项中。

1 2 bit 5 bit

图9-4 单字节和多字节IP选项的结构

所有选项都以 1字节类型 (t y p e)字段开始。在多字节选项中,类型字段后面紧接着一个长


度(l e n)字段,其他的字节是数据 (d a t a )。许多选项数据字段的第一个字节是 1字节的位移
(o f f s e t)字段,指向数据字段内的某个字节。长度字节的计算覆盖了类型、长度和数据字段。
类型被继续分成三个子字段: 1 b i t 备份 ( c o p i e d) 标志、 2 b i t 类 ( c l a s s ) 字段和 5 b i t 数字
(n u m b e r)字段。图 9 - 5列出了目前定义的 I P选项。前两个选项是单字节选项;其他的是多字
节选项。
类 型 长度
常 量 Net/3 描 述
十进制 二进制 (字节)
IPOPT_EOL 0-0-0 0 0-00-00000 1 • 选项表的结尾 (EOL)
IPOPT_NOP 0-0-1 1 0-00-00001 1 • 无操作(NOP)
IPOPT_RR 0-0-7 7 0-00-00111 可变 • 记录路由
IPOPT_TS 0-2-4 68 0-10-00100 可变 • 时戳
IPOPT_SECURITY 1-0-2 130 1-00-00010 11 基本的安全
IPOPT_LSRR 1-0-3 131 1-00-00011 可变 • 宽松源路由和记录路由 (LSRR)
1-0-5 133 1-00-00101 可变 扩展的安全
IPOPT_SATID 1-0-8 136 1-00-01000 4 流标识符
IPOPT_SSRR 1-0-9 137 1-00-01001 可变 • 严格源路由和记录路由 (SSRR)

图9-5 RFC 791定义的IP选项

第1列显示了 N e t / 3的选项常量,第 2列和第 3列是该类型的十进 class 描 述


制和二进制值,第 4列是选项的长度。 Net/3列显示的是在 Net/3中由 0 控制
i p _ d o o p t i o n s实现的选项。 I P必须自动忽略所有它不识别的选 1 保留
项。我们不描述 N e t / 3没有实现的选项:安全和流 I D。流I D选项是 2 查错和措施
3 保留
过时的,安全选项主要只由美国军方使用。 RFC 791中有更多的讨
论。 图9-6 IP选项内的
class字段
当N e t / 3对一个有选项的分组进行分片时 ( 1 0 . 4节),它将检查
copied标志位。该标志位指出是否把所有选项都备份到每个分片的 IP首部。class字段把相关的

Linux公社 www.linuxidc.com
bbs.theithome.com
198 计计TCP/IP详解 卷 2:实现

选项按如图 9-6所示进行分组。图 9-5中,除时戳选项具有 class为2外,所有选项都是 class为0。

9.4 ip_dooptions函数

在图 8 - 1 3中,我们看到 i p i n t r 在检测分组的目的地址之前调用 i p _ d o o p t i o n s 。
i p _ d o o p t i o n s被传给一个指针 m,该指针指向某个分组, i p _ d o o p t i o n s处理分组中它所
知道的选项。如果 i p _ d o o p t i o n s转发该分组,如在处理 L S R R和S S R R选项时,或由于某个
差错而丢掉该分组时,它返回 1。如果它不转发分组, i p _ d o o p t i o n s返回0,由i p i n t r继
续处理该分组。
i p _ d o o p t i o n s是一个长函数,所以我们分步地显示。第一部分初始化一个 f o r循环,
处理首部中的各选项。
当处理每个选项时, c p指向选项的第一个字节。图 9 - 7显示,当可用时,如何从 c p的常
量位移访问 type、length和offset字段。

len 字节

图9-7 用常量位移访问 IP选项字段

RFC把位移(offset)字段描述作指针 (pointer),指针比位移的描述性略强一些。 offset的值是


某个字节在该选项内的序号 (从t y p e开始,序号为 1 ),不是从 t y p e开始的、且以零开始的计数。
位移的最小值是 4(IPOPT_MINOFF),它指向的是多字节选项中数据字段的第一个字节。
图9-8显示了ip_dooptions函数的整体结构。
555-566 i p _ d o o p t i o n s把I C M P差错类型 t y p e初始化为 I C M P _ P A R A M P R O B,对任何没
有特定差错类型的差错,这是一个一般值。对于 I C M P _ P A R A M P R O B,c o d e指的是出错字节
在分组内的位移。这是默认的 ICMP差错报文。某些选项将改变这些值。
i p指向一个20字节大小的i p结构,所以i p + 1指向的是跟在 IP首部后面的下一个
i p结构。因为 i p _ d o o p t i o n s需要I P首部后面字节的地址,所以就把结果指针转换
成为指向一个无符号字节 (u _ c h a r)的指针。因此, c p指向标准 I P首部以外的第一个
字节,就是 IP选项的第一个字节。
1. EOL和NOP过程
567-582 f o r循环按照每个选项在分组中出现的顺序分别对它们进行处理。 E O L选项以及
一个无效的选项长度 (也即选项长度表明选项数据超过了 IP首部)都将终止该循环。当出现 NOP
选项时,忽略它。 switch语句的default情况隐含要求系统忽略未知的选项。
下面的内容描述了 s w i t c h语句处理的每个选项。如果 i p _ d o o p t i o n s在处理分组选项
时没有出错,就把控制交给 switch下面的代码。
2. 源路由转发

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计199
719-724 如果分组需要被转发, SSRR或LSRR选项处理代码就把 forward置位。分组被传
给ip_forward,并且第2个参数为1,表明分组是按源路由选择的。

图9-8 ip_dooptions 函数

我们在8.5节中讲到,并不为源路由选择分组生成 ICMP重定向—这就是为什么
在传给ip_forward时设置第2个参数的原因。

Linux公社 www.linuxidc.com
bbs.theithome.com
200 计计TCP/IP详解 卷 2:实现

如果转发了分组,则 i p _ d o o p t i o n s 返回 1 。如果分组中没有源路由,则返回 0给
i p i n t r ,表明需要对该数据报进一步处理。注意,只有当系统被配置成路由器时
(ipforwarding等于1),才发生源路由转发。
从某种程度上说,这是一个有些矛盾的策略,但却是 RFC 1 1 2 2 的书面要求。
RFC 1127 [Braden 1989c]把它作为一个公开问题加以阐述。
3. 差错处理
725-730 如果在s w i t c h语句里出现了错误, i p _ d o o p t i o n s就跳到b a d。从分组长度中
把I P首部长度减去,因为 i c m p _ e r r o r假设首部长度不包含在分组长度里。 i c m p _ e r r o r发
出适当的差错报文, ip_dooptions返回1,避免ipintr处理被丢弃的分组。
下一节描述 Net/3处理的所有选项。

9.5 记录路由选项

记录路由选项使得分组在穿过互联网时所经过的路由被记录在分组内部。项的大小是源
主机在构造时确定的,必须足够保存所有预期的地址。我们记得在 I P分组的首部,选项最多
只能有 4 0字节。记录路由选项可以有 3个字节的开销,后面紧跟地址的列表 (每个地址 4字节)。
如果该选项是唯一的选项,则最多可以有 9个( 3+4×9=3 9 )地址出现。一旦分配给该选项的
空间被填满,就按通常的情况对分组进行转发,中间的系统就不再记录地址。
图9-9说明了一个记录路由选项的格式,图 9-10是其源程序。

4 字节 4 字节 4 字节

图9-9 记录路由选项,其中 n必须≤9

图9-10 函数ip_dooptions :记录路由选项的处理

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 201

图9-10 (续)

647-657 如果位移选项太小,则 i p _ d o o p t i o n s就发送一个 I C M P参数问题差错。如果变


量c o d e被设置成分组内无效选项的字节位移量,并且 b a d标号(图9 - 8 )语句的执行产生错误,
则发出的 I C M P参数问题差错报文中就具有该 c o d e值。如果选项中没有附加地址的空间,则
忽略该选项,并继续处理下一个选项。
记录地址
658-673 如果ip_dst是某个系统地址(分组已到达目的地),则把接收接口的地址记录在选项
中;否则把i p _ r t a d d r提供的外出接口的地址记录下来。把位移更新为选项中下一个可用地
址位置。如果ip_rtaddr无法找到到目的地的路由,就发送一个 ICMP主机不可达差错报文。
卷1的7.3节举了一些记录路由选项的例子。

ip_rtaddr函数

函数ip_rtaddr查询路由缓存,必要时查询完整的路由表,来找到到给定 IP地址的路由。
它返回一个指向 i n _ i f a d d r结构的指针,该指针与该路由的外出接口有关。图 9 - 11显示了该
函数。

图9-11 函数ip_rtaddr :寻找外出的接口

1. 检查IP转发缓存
735-741 如果路由缓存为空,或者如果 i p _ r t a d d r的唯一参数d e s t与路由缓存中的目的

Linux公社 www.linuxidc.com
bbs.theithome.com
202 计计TCP/IP详解 卷 2:实现

地不匹配,则必须查询路由表选择一个外出的接口。
2. 确定路由
742-750 旧的路由 (如果有的话 )被丢弃,并把新的路由储存在 * s i n (这是转发缓存的
ro_dst成员)。rtalloc搜索路由表,寻找到目的地的路由。
3. 返回路由信息
751-754 如果没有路由可用,就返回一个空指针;否则,就返回一个指针,指向与所选路
由相关联的接口地址结构。

9.6 源站和记录路由选项
通常是在中间路由器所选择的路径上转发分组。源站和记录路由选项允许源站明确指定
一条到目的地的路由,覆盖掉中间路由器的路由选择决定。而且,在分组到达目的地的过程
中,把该路由记录下来。
严格路由包含了源站和目的站之间的每个中间路由器的地址;宽松路由只指定某些中间
路由器的地址。在宽松路由中,路由器可以自由选择两个系统之间的任何路径;而在严格路
由中,则不允许路由器这样做。我们用图 9-12说明源路由处理 .
A、B和C是路由器,而 H S和H D是源和目的主机。因为每个接口都有自己的 I P地址,所以
我们看到路由器 A有三个地址: A 1,A 2和A3。同样,路由器 B和C也有多个地址。图 9 - 1 3显示
了源站和记录路由选项的格式。

图9-12 源路由举例

1 1 1 4 字节 4 字节 4 字节
图9-13 严格和宽松源路由选项

I P首部的源和目的地址以及在选项中列出的位移和地址表,指定了路由以及分组目前在
该路由中所处的位置。图 9-14显示,当分组按照这个宽松源路由从 HS经A、B、C到HD时,信
息是如何改变的。每行代表当分组被第 1列显示的系统发送时的状态。最后一行显示分组被
HD接收。图9-15显示了相关的代码。
符号“•”表示位移与路由中地址的相对位置。注意,每个系统都把出接口的地址放到选
项去。特别地,原来的路由指定 A 3为第一跳目的地,但是外出接口 A 2被记录在路由中。通过
这种方法,分组所采用的路由被记录在选项中。被记录的路由将被目的地系统倒转过来放到
所有应答分组上,让它们沿着原始的路由的逆方向发送。
除了UDP,Net/3在应答时总是把收到的源路由逆转过来。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计203
IP首部 源路由选项
系统
位移 地址

图9-14 当分组通过该路由时,源路由选项被修改。

图9-15 函数ip_dooptions :LSRR和SSRR选项处理

Linux公社 www.linuxidc.com
bbs.theithome.com
204 计计TCP/IP详解 卷 2:实现

图9-15 (续)
583-612 如果选项位移小于 4 (IPOPT_MINOFF),则Net/3发送一个ICMP参数问题差错,并
带上相应的 c o d e值。如果分组的目的地址与本地地址没有一个匹配,且选项是严格源路由
(I P O P T _ S S R R),则发送一个源路由失败差错。如果本地地址不在路由中,则上一个系统把
分组发送到错误的主机上了。对宽松路由 (I P O P T _ L S R R)来说,这不是错误;仅意味着 I P必
须把分组转发到目的地。
1. 源路由的结束
613-620 减小off,把它转换成从选项开始的字节位移。如果 IP首部的ip_dst是某个本地
地址,并且 off所指向的超过了源路由的末尾,源路由中没有地址了,则分组已经到达了目的
地。s a v e _ r t e复制在静态结构 i p _ s r c r t中的路由,并保存在全局 i p _ n h o p s(图9 - 1 8 )里路
由中的地址个数。
i p _ s r c r t被定义成为一个外部静态结构,因为它只能被在 i p _ i n p u t . c中定
义的函数访问。
2. 为下一跳更新分组
621-637 如果ip_dst是一个本地地址,并且 offset指向选项内的一个地址,则该系统是
源路由中指定的一个中间系统,分组也没有到达目的地。在严格路由中,下一个系统必须位
于某个直接相连的网络上。 i f a _ i f w i t h d s t和i f a _ i f w i t h n e t通过在配置的接口中搜索
匹配的目的地址 (一个点到点的接口 )或匹配的网络地址 (广播接口 )来寻找一条到下一个系统的
路由。而在宽松路由中, i p _ r t a d d r(图9 - 11 )通过查询路由表来寻找到下一个系统的路由。
如果没有找到到下一系统的接口或路由,就发送一个 ICMP源路由失败差错报文。
638-644 如果找到一个接口或一条路由,则 ip_dooptions把ip_dst设置成off指向的
I P地址。在源路由选项内,用外出接口的地址代替中间系统的地址,把位移增加,指向路由
中的下一个地址。
3. 多播目的地
6 4 5 - 6 4 6 如果新的目的地址不是多播地址,就将 f o r w a r d设置成1,表明在处理完所有选
项后,应该把分组转发而不是返回给 ipintr。
源路由中的多播地址允许两个多播路由器通过不支持多播的中间路由器进行通信。第 1 4
章详细描述了这一技术。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 205
卷1的8.5节有更多的源路由选项的例子。

9.6.1 save_rte函数

RFC 11 2 2要求,在最终目的地,运输协议必须能够使用分组中被记录下来的路由。运输
协议必须把该路由倒过来并附在所有应答的分组上。图 9 - 1 8中显示的 s a v e _ r t e函数,把源
路由保存在如图 9-16所示的ip_srcrt结构中。

图9-16 结构ip_srcrt

Route的声明是不正确的,尽管这不是个恶性错误。应该是
Struct in_addr route[(MAX_IPOPTLEN - 3)/sizeof(struct in_addr)];
对图9-26和图9-27的讨论详细地说明了这个问题。
57-63 该代码定义了 i p _ s r c r t结构,并声明了静态变量 i p _ s r c r t。只有两个函数访问
i p _ s r c r t:s a v e _ r t e,把到达分组中的源路由复制到 i p _ s r c r t中;i p _ s r c r o u t e,创
建一个与源路由方向相逆的路由。图 9-17说明了源路由处理过程。

传输协议

IP分组

图9-17 对求逆后的源路由的处理

图9-18 函数save_rte

Linux公社 www.linuxidc.com
bbs.theithome.com
206 计计TCP/IP详解 卷 2:实现

759-771 当一个源路由选择的分组到达目的地时, i p _ d o o p t i o n s调用 s a v e _ r t e。


option是一个指向分组的源路由选项的指针, dst是从分组首部来的 ip_src (也就是,返回路
由的目的地,图9-12中的HS)。如果选项的长度超过 ip_srcrt结构,save_rte立即返回。
永远也不可能发生这种情况,因为 ip_srcrt结构比最大选项长度(40字节)要大。
s a v e _ r t e把该选项复制到 i p _ s r c r t,计算并保存 i p _ n h o p s中源路由的跳数,把返回
路由的目的地保存在 dst中。

9.6.2 ip_srcroute函数

当响应某个分组时, I C M P和标准的运输层协议必须把分组带的任意源路由逆转。逆转源
路由是通过 ip_srcroute保存的路由构造的,如图 9-19所示。

图9-19 ip_srcroute 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 207

图9-19 (续)

777-783 i p _ s r c r o u t e把保存在i p _ s r c r t结构中的源路由逆转后,返回与 i p o p t i o n


结构 ( 图 9 - 2 6 ) 格式类似的结果。如果 i p _ n h o p s 是 0 ,则没有保存的路由,所以
ip_srcroute返回一个指针。
记得在图 8 - 1 3中,当一个无效分组到达时, i p i n t r把i p _ n h o p s清零。运输层
协议必须调用i p _ s r c r o u t e,并在下一个分组到达之前自己保存逆转后的路由。正
如以前我们注意到的,这样做是正确的,因为 i p i n t r在处理分组时,在 I P输入队列的
下一个分组被处理之前都会调用运输层 (TCP或UDP)的。
为源路由分配存储器缓存
784-786 如果ip_nhops非0,ip_srcroute就分配一个 mbuf,并把m_len设置成足够大,
以便包含第一跳目的地、选项首部信息 (O P T S I Z)以及逆转后的路由。如果分配失败,则返回
一个空指针,跟没有源路由一样。
p被初始化为指向到达路由的末尾,ip_srcroute把最后记录的地址复制到 mbuf的前面,
在这里它为外出的第一跳目的地开始逆转后的路由。然后该函数把一份 N O P选项(习题9 . 4 )和
源路由信息复制到 mbuf中。
805-818 W h i l e循环把其余的 IP 地址从源路由中以相反的顺序复制到 m b u f中。路由的最
后一个地址被设置成到达分组中被 s a v e _ r t e放在i p _ s r c r t . d s t中的源站地址。返回一个
指向mbuf的指针。图 9-20说明了对图 9-12的路由如何构造逆转的路由。

4 字节

没有使用

没有使用

源路由选项

图9-20 ip_srcroute 逆转ip_srcrt 中的路由

9.7 时间戳选项

当分组穿过一个互联网时,时间戳选项使各个系统把它当前的时间表示记录在分组的选
项内。时间是以从 UTC的午夜开始计算的毫秒计,被记录在一个 32 bit 的字段里。
如果系统没有准确的 UTC(几分钟以内 )或没有每秒更新至少 15次,就不把它作为标准时间。
非标准时间必须把时间戳字段的高比特位置位。

Linux公社 www.linuxidc.com
bbs.theithome.com
208 计计TCP/IP详解 卷 2:实现

有三种时间戳选项类型, Net/3通过如图9-22所示的ip_timestamp结构访问。
114-133 如同ip结构(图8-10)一样,#ifs保证比特字段访问选项中正确的比特位。图 9-21
中列出了由 ipt_flg指定的三种时戳选项类型。

ipt_flg 值 描 述

IPOOPT_TS_TSONLY 0 记录时间戳
IPOPT_TS_TSANDADDR 1 记录地址和时间戳
2 保留
IPOPT_TS_PRESPEC 3 只在预先指定的系统记录时间戳
4-15 保留

图9-21 ipt_flg 可能的值

图9-22 ip_timestamp 结构和常量

初始主机必须构造一个具有足够大的数据区存放可能的时间戳和地址的时间戳选项。对
于i p t _ f l g为3的时间戳选项,初始主机在构造该选项时,填写要记录时间戳的系统的地址。
图9-23显示了三种时间戳选项的结构。

4 字节 4 字节 4 字节 4 字节
图9-23 三种时间戳选项 (省略ipt_ )

因为I P选项只能有4 0个字节,所以时戳选项限制只能有 9个时戳(i p t _ f l g等于0 )或4个地

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计209
址和时间戳对(ipt_flg等于1或3)。图9-24显示了对三种不同的时戳选项类型的处理。
674-684 如果选项长度小于 5个字节 (时戳选项的最小长度 ),则i p _ d o o p t i o n s发出一个
I C M P参数问题差错报文。 o f l w字段统计由于选项数据区满而无法登记时戳的系统个数。如
果数据区满,则 o f l w加1;当它本身超过 1 6 (它是一个 4 bit的字段 )而溢出时,发出一个 I C M P
参数问题差错报文。

图9-24 函数ip_dooptions :时间戳选项处理

Linux公社 www.linuxidc.com
bbs.theithome.com
210 计计TCP/IP详解 卷 2:实现

1. 只有时间戳
685-687 对于ipt_flg为0的时间戳选项(IPOPT_TS_TSONLY),所有的工作都在 switch
语句之后再做。
2. 时间戳和地址
688-700 对于ipt_flg为1的时间戳选项(IPOPT_TS_TSANDADDR),接收接口的地址被记
录下来 (如果数据区还有空间 ),选项的指针前进一步。因为 N e t / 3支持一个接收接口上的多 I P
地址,所以i p _ d o o p t i o n s调用i f a o f _ i f p f o r a d d r选择与分组的初始目的地址 (也就是在
任何源路由选择发生之前的目的地 )最匹配的地址。如果没有匹配,则跳过时间戳选项 (I N A和
SA定义如图9-15所示)。
3. 预定地址上的时间戳
701-710 如果ipt_flg为3 (IPOPT_TS_PRESPEC),ifa_ifwithaddr确定选项中指定
的下一个地址是否与系统的某个地址匹配。如果不匹配,该选项要求在这个系统上不处理;
continue使ip_dooptions继续处理下一个选项。如果下一个地址与系统的某个地址匹配,
则选项的指针前进到下一个位置,控制交给 switch的后面。
4. 插入时间戳
711-713 default截获无效的 ipt_flg值,并把控制传递到 bad。
714-719 时间戳用switch语句后面的代码写入到选项中。 iptime返回自从UTC午夜起到
现在的毫秒数, ip_dooptions记录此时间戳,并增加此选项相对于下一个位置的偏移。

iptime函数

图9-25显示了iptime的实现。

图9-25 函数iptime

458-466 m i c r o t i m e返回从U T C 1 9 7 0年1月1日午夜以来的时间,放在 t i m e v a l结构中。


从午夜以来的毫秒数用 atv计算,并以网络字节序返回。
卷1的7.4节有几个时间戳选项的例子。

9.8 ip_insertoptions函数

我们在 8 . 6节看到, i p _ o u t p u t函数接收一个分组和选项。当 i p _ f o r w a r d调用该函数


时,选项已经是分组的一部分,所以 i p _ f o r w a r d总是把一个空选项指针传给 i p _ o u t p u t。
但是,运输层协议可能会把由 i p _ i n s e r t o p t i o n s(由图8 - 2 2中的i p _ o u t p u t调用)合并到
分组中的选项传递给 ip_forward。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 211
ip_insertoptions希望选项在 ipoption结构中被格式化,如图 9-26所示。

图9-26 结构ipoption

92-95 该结构只有两个成员: ipopt_dst,如果选项表中有源路由,则其中有第一跳目的


地,i p o p t _ l i s t,是一个最多 4 0 (M A X _ I P O P T L E N)字节的选项矩阵,其格式我们在本章中
已做了描述。如果选项表中没有源路由,则 ipopt_dst全为0。
注意, i p _ s r c r t 结构 (图9 - 1 6 )和由 i p _ s r c r o u t e (图9 - 1 9 )返回的 m b u f都符合由
ipoption结构所指定的格式。图 9-27把结构ip_srcrt和ipoption作了比较。

图9-27 结构ip_srcrt 和ipoption

结构ip_srcrt比ipoption多4个字节。路由矩阵的最后一个入口 (route[9])
永远都不会填上,因为这样的话,源路由选项将会有 4 4字节长,比 I P首部所能容纳
的要大(图9-16)。
函数ip_insertoptions如图9-28所示。

图9-28 函数ip_insertoptions

Linux公社 www.linuxidc.com
bbs.theithome.com
212 计计TCP/IP详解 卷 2:实现

图9-28 (续)

352-364 ip_insertoptions有三个参数:m,外出的分组; opt,在结构中格式化的选


项; p h l e n,一个指向整数的指针,在这里返回新首部的长度 (在插入选项之后 )。如果插入
选项分组长度超过最大分组长度 65 535( I P _ M A X P A C K E T ) 字节,则自动将选项丢弃。
i p _ d o o p t i o n s认为i p _ i n s e r t o p t i o n s永远都不会失败,所以无法报告差错。幸好,很
少有应用程序会试图发送最大长度的数据报,更别说选项了。

(20字节)

(8字节)

max_linkhdr 为以太网首部
(16字节) 分配的

IP首部
(20字节)

TCP首部
100字节 (20字节)

44字节

图9-29 函数ip_insertoptions :TCP报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 213
365-366 如果i p o p t _ d s t . s _ a d d r指定了一个非零地址,则选项中包括了源路由,并且
分组首部的 ip_dst被源路由中的第一跳目的地代替。
在26.2节中,我们将看到 TCP调用M G E T H D R为IP和TCP首部分配一个单独的 mbuf。图9-29
显示了在第 367~378行代码执行之前,一个 TCP报文段的mbuf结构。
mbuf{}
m
其他的存储器缓存
20字节 20字节

8字节
max_linkndr
(16字节)
m_data ip
bcopy
IP首部
(20字节)
m_data
TCP首部
IP选项 (20字节)
(optlen字节)

图9-30 函数ip_insertoptions :在选项被复制后的 TCP报文段

mbuf{}

20字节

8字节
m_pktdat

72字节 16字节的以太网首部和
56字节的IP选项的空间

m_data
IP首部
(20字节)

UDP首部(8字节)

图9-31 函数ip_insertoptions :UDP报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
214 计计TCP/IP详解 卷 2:实现

如果被插入的选项占据了多于 1 6的字节数,则第 3 6 7行的测试为真,并调用 M G E T H D R分


配另一个mbuf。图9-30显示了选项被复制到新的 mbuf后,该缓存的结构。
367-378 如果分组首部被存放在一簇,或者第一个缓存中没有多余选项的空间,则
i p _ i n s e r t o p t i o n s分配一个新的分组首部 m b u f,初始化它的长度,从旧的缓存中把该 I P
首部截取下来,并把该首部从旧缓存中移动到新缓存中。
如23.6节中所述, UDP使用M _ P R E P E N D把UDP和IP首部放置到缓存的最后,与数据分离。
如图9 - 3 1所示。因为首部是放在缓存的最后,所以在缓存中总有空间存放选项,对 U D P来说,
第367行的条件总为假。
379-384 如果分组在缓存数据区的开始部分有存放选项的空间,则修改 m _ d a t a和m _ l e n,
以包含 o p t l e n更多的字节。并且当前的 I P首部被o v b c o p y(能够处理源站和目的站的重叠问
题)移走,为选项腾出位置。
385-390 i p _ i n s e r t o p t i o n s现在可以把i p o p t i o n结构的成员 i p o p t _ l i s t直接复制
到紧接在 I P 首部后面的缓存中。把新的首部长度存放在 * p h l e n 中,修改数据报长度
(ip_len),并返回一个指向分组首部缓存的指针。

9.9 ip_pcbopts函数
函数i p _ p c b o p t s把I P选项表及 I P _ O P T I O N S插口选项转换成 i p _ o u t p u t希望的格式:
ipoption结构。如图 9-32所示。

图9-32 函数ip_pcbopts
Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计215

图9-32 (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
216 计计TCP/IP详解 卷 2:实现

559-562 第一个参数, p c b o p t引用指向当前选项表的指针。然后该函数用一个指向新的


选项表的指针来代替该指针,这个新选项表是由第二个参数 m指向的缓存链所指定的选项构造
而来。该过程所准备的选项表,将被包含在 I P _ O P T I O N S插口选项中,除了 L S R R和S S R R选
项的格式外,看起来象一个标准的 I P选项表。对这些选项,第一跳目的地址是作为路由的第
一个地址出现的。图 9 - 1 4显示,在外出的分组中,第一跳目的地址是作为目的地址出现的,
而不是路由的第一个地址。
1. 扔掉以前的选项
563-580 所有被m _ f r e e和* p c b o p t扔掉的选项都被清除。如果该过程传来一个空缓存或
者根本不传递缓存,则该函数不安装任何新的选项,并立即返回。
如果新选项表没有填充到 4 bit 的边界,则 i p _ p c b o p t s跳到 b a d,扔掉该表,并返回
EINVAL。
该函数的其余部分重新安排该表,使其看起来象一个 i p o p t i o n结构。图9-33显示了这个
过程。

松源路由

第1个 第2个 第3个 最后一个 其他选项

第1个 第2个 第3个 最后一个 其他选项

第2个 第3个 最后一个 其他选项

图9-33 ip_pcbopts 选项表处理

2. 为第一跳目的地腾出位置
581-592 如果缓存中没有位置,则把所有的数据都向缓存的末尾移动 4个字节 (是一个
in_addr结构的大小 )。ovbcopy完成复制。 bzero清除缓存开始的 4个字节。
3. 扫描选项表
59 3 - 6 0 6 f o r循环扫描选项表,寻找 L S R R和S S R R选项。对多字节选项,该循环也验证选
项的长度是否合理。
4. 重新安排LSRR和SSRR选项
607-638 当该循环找到一个 L S R R或S S R R选项时,它把缓存的大小、循环变量和选项长度
减去4,因为选项的每一个地址将被移走,并被移到缓存的前面。
b c o p y把第一个地址移走, o v b c o p y把选项的其他部分移动 4个字节,来填补第一个地

Linux公社 www.linuxidc.com
bbs.theithome.com
第 9章 IP选项处理计计 217
址留下的空隙。
5. 清除
639-646 循环结束后,选项表的大小(包括第一跳地址)必须不超过44 (MAX_IPOPTLEN +4)字
节。更长的选项表无法放入 IP分组的首部。该表被保存在 *pcbopt中,函数返回。

9.10 一些限制

除了管理和诊断工具生成的 I P数据报外,很少出现选项。卷 1讨论了两个最常用的工具,


p i n g和t r a c e r o u t e。使用I P选项的应用程序很难写。编程接口的文档很少,也没有很好地
标准化。许多厂商提供的应用程序,比如 Te l n e t和F T P,并没有为用户提供方法,来指定如源
路由等的选项。
在大的互联网上,记录路由、时间戳和源路由选项的用途被 I P首部的最大长度所限制。
许多路由含有的跳数远多于 4 0选项字节所能表示的。当多选项在同一分组中出现时,所能得
到的空间是几乎没有用的。 IPv6用一个更为灵活的选项首部设计强调了这个问题。
在分片过程中, I P只把某些选项复制到非初始片上,因为重组时会扔掉非初始片上的选
项。在目的主机上,运输层协议只能用到初始片上的选项 ( 1 0 . 6节)。但有些选项,如源路由,
即使在目的主机上,被非初始片丢弃,依然必须被复制到每个分片。

9.11 小结

本章中我们显示了 I P选项的格式和处理过程。我们没有讨论安全和流 I D选项,因为 N e t / 3


没有实现它们。
我们看到,多字节选项的大小是由源主机在构造它们时确定的。最大选项首部长度只有
40字节,这严格限制了 IP选项的使用。
源路由选项要求最多的支持。到达的源路由被 s a v e _ r t e保存,并保留在 i p _ s r c r o u t e
中。通常不转发分组的主机可能转发源路由选择的分组,但是 RFC 11 2 2默认要求不允许这种
功能。Net/3没有对这种特性的判断,总是转发源路由选择的分组。
最后,我们看到 ip_insertoptions是如何把选项插入到一个外出的分组中去的。

习题

9.1 如果一个分组中有两个不同的源路由选项会发生什么情况?
9.2 一些商用路由器可以被配置成按照分组的 I P目的地址扔掉它们。通过种方式,可以
把一台或一组主机通过路由器隔离在更大的互联网之外。请描述源路由选择的分组
如何绕过这个机制。假定网络中至少有一个主机,路由器没有阻塞,并转发源路由
选择的数据报。
9.3 某些主机可能没有被配置成默认路由。这样主机就无法路由选择到其他与它直接相
连的网络。请描述源路由如何与这种类型的主机通信。
9.4 为什么NOP采用如图9-16所示的ip_srcrt结构?
9.5 时间戳选项中非标准时间值会和标准时间值混淆吗?
9.6 i p _ d o o p t i o n s在处理其他选项之前要把分组的目的地址保存在 d e s t中(图9 - 8 )。
为什么?

Linux公社 www.linuxidc.com
bbs.theithome.com
第10章 IP的分片与重装
10.1 引言

我们将第8章的IP的分片与重装处理问题推迟到本章来讨论。
I P具有一种重要功能,就是当分组过大而不适合在所选硬件接口上发送时,能够对分组
进行分片。过大的分组被分成两个或多个大小适合在所选定网络上发送的 I P分片。而在去目
的主机的路途中,分片还可能被中间的路由器继续分片。因此,在目的主机上,一个 I P数据
报可能放在一个 I P分组内,或者,如果在发送时被分片,就放在多个 I P分组内。因为各个分
片可能以不同的路径到达目的主机,所以只有目的主机才有机会看到所有分片。因此,也只
有目的主机才能把所有分片重装成一个完整的数据报,提交给合适的运输层协议。
图8 - 5显示在被接收的分组中, 0.3%(72 786/27 881 978) 是分片, 0.12% (264 484/
(29 447 726-796 084)) 的数据报是被分片后发送的。在 w o r l d . s t d . c o m上,被接收分组的
9.5%是被分片的。world有更多的NFS活动,这是 IP分片的主要来源。
I P首部内有三个字段实现分片和重装:标识字段 (i p _ i d)、标志字段 (i p _ o f f的3个高位
比特)和偏移字段 (ip_off的13个低位比特 )。标志字段由三个 1 bit标志组成。比特 0是保留的,
必须为 0;比特 1是“不分片” ( D F )标志;比特 2是“更多分片” ( M F )标志。 N e t / 3中,标志和
偏移字段结合起来,由 ip_off访问,如图 10-1所示。

分片偏移

13比特

图10-1 ip_off 控制IP分组的分片

Net/3通过用IP_DF和 IP_MF掩去i p _ o f f来访问DF和MF。IP实现必须允许应用程序请求


在输出的数据报中设置 DF比特。
当使用UDP或TCP时,Net/3并不提供对 DF比特的应用程序级的控制。
进程可以用原始 I P接口(第3 2章)构造和发送它自己的 I P首部。运输层必须直接设
置DF比特。例如,当 TCP运行“路径 MTU发现(path MTU discovery)”时。
i p _ o f f的其他13 bit指出在原始数据报内分片的位置,以 8字节为单元计算。因而,除最
后一个分片外,其他每个分片都希望是一个 8字节倍数的数据,从而使后面的分片从 8字节边
界开始。图 1 0 - 2显示了在原始数据报内的字节偏移关系,以及在分片的 I P首部内分片的偏移
(ip_off的低位13 bit)。
图1 0 - 2显示了把一个最大的 I P数据报分成 8 1 9 0个分片,除最后一个分片包含 3个字节外,
其他每个分片都包含 8个字节。图中还显示,除最后一个分片外,设置了其余分片的 M F比特。
这是一个不太理想的例子,但它说明了一些实现中存在的问题。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 219
最大数据报

IP首部

20字节 8字节 8字节 8字节 8字节 3


字节
IP首部

20字节 8字节
IP首部

20字节 8字节
IP首部

20字节 8字节

IP首部

20字节 8字节
IP首部

20字节 3字节

图10-2 65535字节的数据报的分片

原始数据报上面的数字是该数据部分在数据报内的字节偏移。分片偏移 (i p _ o f f)是从数
据报的数据部分开始计算的。分片不可能含有偏移超过 65514的字节,因为如果这样的话,重
装的数据报会大于 65535字节—这是ip_len字段的最大值。这就限制了 ip_off的最大值为
8189(8189×8=65512),只为最后一个分片留下 3字节空间。如果有 IP选项,则偏移还要小些。
因为I P互联网是无连接的,所以,在目的主机上,来自一个数据报的分片必然会与来自
其他数据报的分片交错。 i p _ i d唯一地标识某个特定数据报的分片。源系统用相同的源地址
(i p _ s r c)、目的地址 (i p _ d s t)和协议 (i p _ p)值,作为数据报在互联网上生命期的值,把每
个数据报的 ip_id设置成一个唯一的值。
总而言之,ip_id标识了特定数据报的分片,ip_off确定了分片在原始数据报内的位置,
除最后一个分片外, MF标识每个分片。

10.2 代码介绍

重装数据结构出现在一个头文件里。两个 C文件中有重装和分片处理的代码。这三个文件
列在图10-3中。

文 件 描 述

netinet/ip_var.h 重装数据结构
netinet/ip_output.c 分片代码
netinet/ip_input.c 重装代码

图10-3 本章讨论的文件

Linux公社 www.linuxidc.com
bbs.theithome.com
220 计计TCP/IP详解 卷 2:实现

10.2.1 全局变量

本章中只有一个全局变量, ipq。如图10-4所示。

变 量 类 型 描 述

ipq struct ipq * 重装表

图10-4 本章介绍的全局变量

10.2.2 统计量

分片和重装代码修改的统计量如图 1 0 - 5所示。它们是图 8 - 4的i p s t a t结构中所包含统计


量的子集。

ip s t a t成员 描 述

ips_cantfrag 要求分片但被 DF比特禁止而没有发送的数据报数


ips_odropped 因为内存不够而被丢弃的分组数
ips_ofragments 被发送的分片数
ips_fragmented 为输出分片的分组数

图10-5 本章收集的统计量

10.3 分片

我们现在返回到 i p _ o u t p u t,分析分片代码。记得在图 8 - 2 5中,如果分组正好适合选定


出接口的 M T U,就在一个链路级帧中发送它。否则,必须对分组分片,并在多个帧中将其发
送。分组可以是一个完整的数据报或者它自己也是前边系统创建的分片。我们分三个部分讨
论分片代码:
• 确定分片大小(图10-6);
• 构造分片表 (图10-7);以及
• 构造第一个分片并发送分片 (图10-8)。

图10-6 函数ip_output :确定分片大小

253-261 分片算法很简单,但由于对 mbuf结构和链的操作使实现非常复杂。如果 DF比特禁

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 221
止分片,则 i p _ o u t p u t丢弃该分组,并返回 E M S G S I Z E。如果该数据报是在本地生成的,则
运输层协议把该错误传回该进程;但如果分组是被转发的,则 i p _ f o r w a r d生成一个ICMP目
的地不可达差错报文,并指出不分片就无法转发该分组 (图8-21)。
N e t / 3没有实现“路径 M T U发现”算法,该算法用来搜索到目的主机的路径,并发现所有
中间网络支持的最大传送单元。卷 1的11.8节和24.2节讨论了UDP和TCP的路径MTU发现。
每个分片中的数据字节数, l e n的计算是用接口的 M T U减去分组首部的长度后,舍
262-266
去低位的 3个比特 ( & ~7 )。后成为 8字节倍数。如果 M T U太小,使每个分片中无法含有 8字节的
数据,则ip_output返回EMSGSIZE。
每个新的分片中都包含:一个 IP首部、某些原始分组中的选项以及最多 len长度的数据。
图1 0 - 7中的代码,以一个 C的复合语句开始,构造了从第 2个分片开始的分片表。在表生
成后(图10-8),原来的分组被转换成第一个分片。

图10-7 函数ip_output :构造分片表

Linux公社 www.linuxidc.com
bbs.theithome.com
222 计计TCP/IP详解 卷 2:实现

图10-7 (续)

图10-8 函数ip_output :发送分片

267-269 外部块允许在函数中离使用点更近一点的地方定义 mhlen、firstlen和mnext。


这些变量的范围一直到块的末尾,它们隐藏其他在块外定义的有相同名字的变量。
270-276 因为原来的缓存链现在成了第一个分片,所以 f o r循环从第 2个分片的偏移开始:
hlen+len。对每个分片, ip_output采取以下动作:
• 277-284 分配一个新的分组缓存,调整 m _ d a t a指针,为一个 1 6字节链路层首部
(m a x _ l i n k h d r)腾出空间。如果 i p _ o u t p u t不这么做,则网络接口驱动器就必须再分
配一个mbuf来存放链路层首部或移动数据。两种工作都很耗时,在这里就很容易避免。
• 285-290 从原来的分组中把 IP首部和IP选项复制到新的分组中。前者复制在一个结构
中。ip_optcopy只复制那些将被复制到每个分片中的选项 (10.4节)。
• 291-297 设置分片包括 M F比特的偏移字段 (i p _ o f f)。如果原来分组中已设置了 M F
比特,则在所有分片中都把 M F置位。如果原来分组中没有设置 M F比特,则除了最后一
个分片外,其他所有分片中的 MF都置位。
• 298 为分片设置长度,解决首部小一些 (i p _ o p t c o p y可能没有复制所有选项 ),以及
最后一个分片的数据区小一些的问题。以网络字节序存储长度。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 223
• 2 9 9 - 3 0 5 从原始分组中把数据复制到该分片中。如果必要, m _ c o p y会再分配一个
mbuf。如果m_copy失败,则发出ENOBUFS。sendorfree丢弃所有已被分配的缓存。
• 3 0 6 - 3 1 4 调整新创建的分片的 m b u f分组首部,使其具有正确的全长。把新分片的接
口指针清零,把 i p _ o f f转换成网络字节序,计算新分片的检验和。通过 m _ n e x t p k t
把该分片与前面的分片链接起来。
在图10-8中,ip_output构造了第一个分片,并把每个分片传递到接口层。
315-325 把末尾多余的数据截断后,原来的分组就被转换成第一个分片,同时设置 MF比特,
把i p _ l e n和i p _ o f f转换成网络字节序,计算新的检验和。在这个分片中,保留所有的 I P选
项。在目的主机重装时,只保留数据报的第一个分片的 I P选项(图1 0 - 2 8 )。某些选项,如源路
由选项,必须被复制到每个分片中,即使在重装时都被丢弃了。
326-338 此时,ip_output可能有一个完整的分片表,或者已经产生了错误,都必须把生
成的那部分分片表丢弃。 f o r循环遍历该表,发送分片或者由于 e r r o r而丢弃分片。在发送
期间遇到的所有错误都会使后面的分片被丢弃。

10.4 ip_optcopy函数
在分片过程中, i p _ o p t c o p y(图1 0 - 9 )复制到达分组 (如果分组是被转发的 )或者原始数据
报中(如果该数据报是在本地生成的 )中的选项到外出的分片中。

图10-9 函数:确定分片大小

Linux公社 www.linuxidc.com
bbs.theithome.com
224 计计TCP/IP详解 卷 2:实现

395-422 ip_optcopy的参数是: ip,一个指向外出分组的 IP首部的指针;jp,一个指向


新生成的分片的 IP首部的指针; i p _ o p t c o p y初始化c p和d p指向每个分组的第一个选项,并
在处理每个选项时把 c p和d p向前移动。第一个 f o r循环在每次重复时复制一个选项,当它遇
到EOL选项或已经检查完所有选项时。 NOP选项被复制,用来维持后继选项的对齐限制。
Net/2版本废除了 NOP选项。
如果I P O P T _ C O P I E D指示c o p i e d比特被置位,则 i p _ o p t c o p y把选项复制到新片中。图
9 - 5 显示了哪些选项的 c o p i e d比特是被置位的。如果某个选项的长度太大,就被截断;
ip_dooptions应该已经发现这种错误了。
423-426 第2个for循环把选项表填充到 4字节的边界。由于分组首部长度 (ip_hlen)是以4
字节为单位计算的,所以需要这个操作。这也保证了后面跟着的运输层首部与 4字节边界对齐。
这样会提高性能,因为在许多运输层协议的设计中,如果运输层首部从一个 32 bit 边界开始,
那么32 bit首部字段将按照 32 bit边界对齐。在某些机器上, C P U访问32 bit对齐的字有困难,
这时,这种字节安排就提高了 CPU的性能。
图10-10显示了ip_optcopy的运行。

IP首部 时间戳选项 LSRR选项

20字节 12字节 11字节

IP首部 LSRR选项 表尾选项

20字节 11字节

图10-10 在分片中并不复制所有选项

在图1 0 - 1 0中,我们看到 i p _ o p t c o p y不复制时间戳选项 (它的c o p i e d比特为0 ),但却复制


L S R R选项(它的c o p i e d比特为1 )。为了把新选项与 4字节边界对齐, i p _ o p t c o p y也增加了一
个EOL选项。

10.5 重装

到目前为止,我们已经讨论了数据报 (或片)的分片,现在再回到 i p i n t r讨论重装过程。


在图8 - 1 5中,我们把 i p i n t r中的重装代码省略了,并推迟对它的讨论。 i p i n t r可以把数据
报整个地交给运输层处理。 i p i n t r接收的分片被传给 i p _ r e a s s,由它尝试把分片重装成一
个完整的数据报。图 10-11显示了ipintr的代码。
271-279 我们知道 i p _ o f f包含D F比特、M F比特以及分片偏移。如果 M F比特或分片偏移
非零,则 D F就被掩盖掉了,分组就是一个必须被重装的分片。如果两者都为零,则分组就是
一个完整的数据报。跳过重装代码,执行图 1 0 - 11中最后的 e l s e语句,它从全部数据报长度
中排除了首部长度。
280-286 m _ p u l l u p把位于外部簇上的数据移动到 mbuf的数据区。我们记得,如果一个缓
存区无法容纳外部簇上的一个 I P 分组,则 S L I P 接口 ( 5 . 3 节 ) 可能会把该分组整个返回。
m _ d e v g e t 也会全部返回外部簇上的某个 I P分组 ( 2 . 6节)。在 m t o d宏( 2 . 6节)开始工作之前,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计225
m_pullup必须把IP首部从外部簇上移到 mbuf的数据区中去。

图10-11 函数ipintr :分片处理

Linux公社 www.linuxidc.com
bbs.theithome.com
226 计计TCP/IP详解 卷 2:实现

287-297 Net/3在一个全局双向链表 i p q上记录不完整的数据报。这个名字可能容易产生误


解,因为这个数据结构并不是一个队列。也就是说,可以在表的任何地方插入和删除,并不
限制一定要在末尾。我们将用名词“表 (list)”来强调这个事实。
i p i n t r对表进行线性搜索,为当前分片找到合适的数据报。记住分片是由 4元组{ i p _ i d、
i p _ s r c、i p _ d s t和i p _ p }唯一标识的。 i p q的每个入口是一个分片表,如果 i p i n t r找到一个匹
配,则fp指向匹配的表。
N e t / 3采用线性搜索来访问它的许多数据结构。尽管简单,但是当主机支持大量
网络连接时,这种方法就成为瓶颈。
298-303 在found语句,ipintr为方便重装,修改了分组:
• 304 i p i n t r修改i p _ l e n,从中减去标准 I P首部和任何选项。我们必须牢记这一点,
以免混淆对标准 i p _ l e n解释的理解。标准 i p _ l e n中包含了标准首部、选项和数据。
如果跳过重装代码, ip_len也会被改变,因为这个分组不是一个分片。
• 305-307 i p i n t r把MF标志复制到 i p f _ m f f的低位,把 i p _ t o s覆盖掉(&=~1只清除
低位)。注意,在 i p f _ m f f成为一个有效成员之前,必须把 i p指一个 i p a s f r a g结构。
10.6节和图10-14描述了ipasfrag结构。
尽管 RFC 1 1 2 2要求 I P层提供某种机制,允许运输层为每个外出的数据报设置
i p _ t o s比特。但它只推荐,在目的主机, I P层把i p _ t o s值传给运输层。因为 TO S
字段的低位字节必须总是 0,所以当重装算法使用 i p _ o f f(通常在这里找到 M F比特)
时,可以得到MF比特。
现在,可以把 i p _ o f f作为一个 16 bit偏移,而不是 3个标志比特和一个 13 bit偏移来访问
了。
• 308 用8乘ip_off,把它从以 8字节为单元转换成以 1字节为单元。
i p f _ m f f和i p _ o f f决定i p i n t r是否应该重装。图 1 0 - 1 2描述了不同的情况及相应的动
作,其中fp指向的是系统以前为该数据报接收的分片表。许多工作是由 ip_reass做的。
309-322 如果i p _ r e a s s通过把当前分片与以前收到的分片组合在一起,能重装成一个完
整的数据报,它就返回指向该重装好的数据报的指针。如果没有重装好,则 i p _ r e a s s保存
该分片, ipintr跳到next去处理下一个分片 (图8-12)。

ip_off ipf_mff fp 描 述 动 作

0 假 空 完整数据报 没有要求重装
0 假 非空 完整数据报 丢弃前面的分片
任意 真 空 新数据报的分片 用这个分片初始化新的分片表
任意 真 非空 不完整新数据报的分片 插入到已有的分片表中,尝试重装
非零 假 空 新数据报的末尾分片 初始化新的分片表
非零 假 非空 不完整新数据报的末尾分片 插入到已有的分片表中,尝试重装

图10-12 ipintr 和ip_reass 中的IP分片处理

323-324 当到达一个完整的数据报时,就选择这个 e l s e分支,并按照前面的叙述修改


ip_hlen。这是一个普通的流,因为收到的大多数数据报都不是分片。
如果重装处理产生一个完整的数据报, i p i n t r就把这个完整的数据报上传给合适的运输

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计227
层协议(图8-15):
(*inetsw[ip_protox[ip->ip_p]].pr_input)(m,hlen);

10.6 ip_reass函数

i p i n t r把一个要处理的分片和一个指针传给 i p _ r e a s s,其中指针指向的是 i p q中匹


配的重装首部。 i p _ r e a s s可能重装成功并返回一个完整的数据报,可能把该分片链接到数
据报的重装链表上,等待其他分片到达后重装。每个重装链表的表头是一个 i p q结构,如图
10-13所示。

图10-13 ipq 结构

52-60 用来标识一个数据报分片的四个字段, i p _ i d、i p _ s r c、i p _ d s t和i p _ p,被保


存在每个重装链表表头的 i p q结构中。 N e t / 3用n e x t和p r e v构造数据报链表,用 i p q _ n e x t
和ipq_prev构造分片的链表。
到达分组的 IP首部在被放在重装链表之前,首先被转换成一个 ipasfrag结构(图10-14)。

图10-14 ipasfrag 结构

66-86 ip_reass在一个由 ipf_next和ipf_prev链接起来的双向循环链表上,收集某个


数据报的分片。这些指针覆盖了 I P首部的源地址和目的地址。 i p f _ m f f成员覆盖 i p结构中的
Linux公社 www.linuxidc.com
bbs.theithome.com
228 计计TCP/IP详解 卷 2:实现

ip_tos。其他成员是相同的。
图10-15显示了分片首部链表 (ipq)和分片(ipasfrag)之间的关系。

重装表表头;
没有分片链接
到这个结构上

分片表,按分片偏移的顺序

收到的某个数
据报的所有分

收到的某个数
据报的所有分

图10-15 分片首部链表 ipq 和分片

图1 0 - 1 5的左下部是重装首部的链表。表中第一个节点是全局 i p q结构, i p q。它永远不


会有自己的相关分片表。 i p q表是一个双向链表,用于支持快速插入和删除。 n e x t和p r e v
指针指向前一个和后一个 ipq结构,用终止结构的角上的箭头表示。
图1 0 - 1 5仍然没有显示重装结构的所有复杂性。重装代码很难跟踪,因为它完全依靠把指
针指向底层 m b u f上的三个不同的结构。我们已经接触过这个技术了,例如,当一个 i p结构覆
盖某个缓存的数据部分时。
图10-16显示了mbuf、ipq结构、ipasfrag结构和ip结构之间的关系。
图10-16中含有大量信息:
• 所有结构都放在一个 mbuf的数据区内。
• i p q链表由 n e x t和p r e v链接起来的 i p q结构组成。每个 i p q结构保存了唯一标识一个
IP数据报的四个字段 (图10-16中的阴影部分)。
• 当作为分片链表的头访问时,每个 i p q结构被看成是一个 i p a s f r a g结构。这些分片由
ipf_next和ipf_prev链接起来,分别覆盖了ipq 结构的ipq_next和ipq_prev成员。
• 每个i p a s f r a g结构都覆盖了到达分片的 i p结构,与分片一起到达的数据在缓存中跟在
该结构之后。ipasfrag结构的阴影部分的成员的含义与其在 ip结构中不太相同。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 229

图10-16 可通过多种结构访问的一段内存区

图10-15显示了这些重装结构之间的物理连接,图10-16显示了ip_reass使用的覆盖技术。
图 1 0 - 1 7 从逻辑的观点说明重装结构:该图显示了三个数据报的重装,以及 i p q 链表和
ipasfrag结构之间的关系。

图10-17 三个IP数据报的重装

每个重装链表的表头包含原始数据报的标识符、协议、源和目的地址。图中只显示了
i p _ i d字段。分片表通过偏移字段排序,如果 M F比特被置位,则用 M F标志分片,缺少的分
片出现在阴影里。每个分片内的数字显示了该分片的开始和结束字节相对于原始数据报数据
区的相对偏移,而不是相对于原始数据报的 IP首部。
这个例子是用来说明三个没有 IP选项的UDP数据报,其中每个数据报都有 1024字节的数据。
每个数据报的全长是 1052(20+8+1024)字节,正好适合 1500字节以太网 MTU。在到目的主机的
途中,这些数据报会遇到一个 S L I P链路,该链路上的路由器对数据报分片,使其大小适于放
在典型的 2 9 6字节的 SLIP MTU 中。每个数据报分 4个分片到达。第 1个分片中包含一个标准的
20字节IP首部,8字节UDP首部和264字节数据。第 2个和第3个分片中包含一个20字节IP首部和
272字节数据。最后一个分片中有一个 20字节首部和216字节数据(1032=272×3+216)。
在图 1 0 - 1 7中,数据报 5缺少一个包含 2 7 2 ~ 5 4 3字节的分片。数据报 6缺少第一个分片,

Linux公社 www.linuxidc.com
bbs.theithome.com
230 计计TCP/IP详解 卷 2:实现

0~271字节,以及最后一个从偏移 816开始的分片。数据报 7缺少前三个分片, 0~815。


图1 0 - 1 8列出了 i p _ r e a s s。前面讲到,当目的地是本主机的某个 I P分片到达时,在处理
完所有选项后, ipintr会调用ip_reass。

图10-18 函数ip_reass :数据报重装

343-358 当调用ip_reass时,ip指向分片fp或者指向匹配的 ipq结构或者为空。


因为重装只涉及每个分片的数据部分,所以 i p _ r e a s s调整含有该分片的 m b u f的m _ d a t a和
m_len,减去每个分片的 IP首部。
465-469 在重装过程中,如果产生错误,该函数就跳到 d r o p f r a g。d r o p f r a g 增加
ips_fragdropped,丢弃该分片,并返回一个空指针。
在运输层丢弃分片通常会严重降低性能,因为必须重传整个数据报。 T C P谨慎地避免分
片,但是 U D P应用程序必须采取步骤以避免对自己分片。 [ K e n t和Mogul 1987] 解释了为什么
应该避免分片。
所有I P实现必须能够重装最多 5 7 6字节的数据报。没有通用的方法来确定远程主机能重装
的最大数据报的大小。我们将在 27.5节中看到TCP提供了一个机制,可以确定远程主机所能处
理的最大数据报的大小。 UDP没有这样的机制,所以许多基于 UDP的协议(例如,RIP、TFTP、
BOOTP、SNMP和DNS),都限制在 576字节左右。
我们将分7个部分显示重装代码,从图 10-19开始。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 231

图10-19 函数ip_reass :创建重装表

1. 创建重装表
359-366 当f p为空时, i p _ r e a s s用新的数据报的第一个分片创建一个重装表。它分配一
个mbuf来存放新表的头 (一个ipq结构),并调用insque,把该结构插入到重装表的链表中。
图10-20列出了操作数据报和分片链表的函数。
N e t / 3的3 8 6版本是在 m a c h d e p . c文件中定义 i n s q u e和r e m q u e函数的。每台机器都
有自己的文件,在其中定义核心函数,通常是为提高性能。该文件也包括与机器体
系结构有关的函数,包括中断处理支持、 CPU和设备配置以及内存管理函数。
i n s q u e和r e m q u e的存在主要是为了维护内核执行队列。 N e t / 3可把它们用于数
据报重装链表,因为链表具有下一个和前一个两个指针,分别作为各自节点结构的
前两个成员。对任何类型结构的链表,这些函数同样可以用,尽管编译器可能会发
出一些警告。这也是另一个通过两个不同结构访问内存的例子。
在所有内核结构里,下一个指针通常位于前一个指针的前面 (例如,图 1 0 - 1 4 )。
这是因为 i n s q u e和r e m q u e最早是在 VA X上用i n s q u e和r e m q u e硬件指令实现的,
这些指令要求前向和后向指针具有这种顺序。
分片表不是用 i p a s f r a g结构的前两个成员链接起来的 (图1 0 - 1 4 ),所以 N e t / 3调
用ip_deq和ip_enq而不是insque和remque。

函 数 描 述

insque 紧接在prev后面插入node
v o i d i n s q u e(void *node, void* prev) ;
Remque 把node从表中移走
v o i d r e m q u e(void *node) ;
ip_enq 紧接在分片 prev后面插入分片 p
v o i d i p _ e n q( s t r u c t i p a s f r a gp,* S t r u c t i p a s f r a g p*rev) ;
ip_deq 移走分片p
void i p _ d e q( s t r u c t i p a s f r a g p)*;

图10-20 ip_reass 采用的队列函数

Linux公社 www.linuxidc.com
bbs.theithome.com
232 计计TCP/IP详解 卷 2:实现

2. 重装超时
367 RFC 1122要求有生命期字段 (i p q _ t t l),并限制 Net/3等待分片以完成一个数据报的时
间。这与 IP首部的TTL字段是不同的, IP首部的TTL字段是为了限制分组在互联网中循环的时
间。重用 I P首部的 T T L字段作为重装超时的原因在于,一旦分片到达它的最终目的地,就不
再需要首部 TTL。
在Net/3中,重装超时的初始值设为 60 (I P F R A G T T L)。因为每次内核调用 i p _ s l o w t i m o
时,i p q _ t t l就减去1,而内核每 500 ms调用i p _ s l o w t i m o一次。如果系统在接收到数据报
的任一分片 3 0秒后,还没有组装好一个完整的 I P数据报,那么系统就丢弃该 I P重装链表。重
装定时器在链表被创建后的第一次调用 ip_slowtimo时开始计时。
RFC 1 122推荐重装时间在 60~120秒内,并且当收到数据报的第一个分片且定时器超时时,
向源主机发出一个 I C M P超时差错报文。重装后,总是丢弃其他分片的首部和选项,并且在
ICMP差错报文中必须包含出错数据报的前 64 bit(或者,如果该数据报短于 8字节,就可以少一
些)。所以,如果内核还没有接收到分片 0,它就不能发ICMP报文。
N e t / 3的定时器要短一些,并且当丢弃分片时, N e t / 3不发送 I C M P报文。要求返
回数据报的前 64 bit保证含有运输层首部的前端,这样就可以把差错报文返回给发生
错误的应用程序。注意,因为这个原因, TCP和UDP有意把它们的端口号放在首部的
前8个字节。
3. 数据报标识符
368-375 i p _ r e a s s在分配给该数据报的 i p q 结构中保存 i p _ p、i p _ i d、i p _ s r c和
i p _ d s t,让i p q _ n e x t和i p q _ p r e v指针指向该 i p q结构(也就是说,它用一个节点构造一个
循环链表 ),让q指向这个结构,并跳到 i n s e r t(图1 0 - 2 5 ),把第一个分片 i p插入到新的重装
表中去。
i p _ r e a s s的下一个部分 (图1 0 - 2 1 )是当f p不空,并已当前表中为新的分片找到正确位置
后执行的。

图10-21 函数ip_reass :在重装链表中找位置

376-381 因为f p不空,所以 f o r循环搜索数据报的分片链表,找到一个偏移大于 i p _ o f f


的分片。
在目的主机上,分片包含的字节范围可能会相互覆盖。发生这种情况的原因是,当一个
运输层协议重传某个数据报时,采用与原来数据报不同的路由;而且,分片的模式也可能不
同,这就导致在目的主机上的相互覆盖。传输协议必须能强制 I P使用原来的 I D字段,确保目
的主机识别出该数据报是重传的。
N e t / 3并不为运输层协议提供机制保证在重传的数据报中重用 IP ID字段。在准备

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 233
新数据报时, i p _ o u t p u t通过增加全局整数 i p _ i d来赋一个新值 (图8 - 2 2 )。尽管如
此,Net/3系统也能从让运输层用相同 ID字段重传IP数据报的系统上接收重叠的分片。
图1 0 - 2 2说明分片可能会以不同的方式与已经到达的分片重叠。分片是按照它们到达目的
主机的顺序编号的。重装的分片在图 10-22底部显示,分片的阴影部分是被丢弃的多余字节。
在下面的讨论中,早到 (earlier)分片是指先前到达主机的分片。

分片1 分片2 分片3 分片4

分片5 分片7 分片6

重组装的数据报

图10-22 可能会在目的主机重叠的分片的字节范围

图10-23中代码截断或丢弃到达的分片。
382-396 ip_reass把新片中与早到分片末尾重叠的字节丢弃,截断重复的部分(图10-22中分
片5的前部 ),或者,如果新分片的所有字节已经在早先的分片中 (分片4 )出现,就丢弃整个新
分片(分片6)。

图10-23 函数ip_reass :截断到达分组

图10-24中的代码截断或丢弃已有的分片。
397-412 如果当前分片部分地与早到分片的前端部分重叠,就把早到分片中重复的数据截
掉(图10-22中分片2的前部)。丢弃所有与当前分片完全重叠的早到分片 (分片3)。
图10-25中,到达分片被插入到重装链表。
413-426 在截断后, ip_enq把该分片插入链表,并扫描整个链表,确定是否所有分片全
部到达。如果还缺少分片,或链表最后一个分片的 i p f _ m f f被置位,i p _ r e a s s就返回0,并
等待更多的分片。
当目前的分片完成一个数据报后,整个链表被图 10-26所示的代码转换成一个 mbuf链。
427-440 如果某个数据报的所有分片都被接收下来, w h i l e循环用m _ c a t把分片重新构造

Linux公社 www.linuxidc.com
bbs.theithome.com
234 计计TCP/IP详解 卷 2:实现

成数据报。

图10-24 函数ip_reass :截断已有分组

图10-25 函数ip_reass :插入分组

图10-26 函数ip_reass :重装数据报

图10-27显示了一个有三个分片的数据报的 mbuf和ipq结构之间的关系。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 235


分片1


分片2


分片3

图10-27 m_cat 重装缓存内的分片

图中最暗的区域是分组的数据部分,稍淡的阴影部分是 mbuf中未用的部分。有三个分片,
每个分片都被存放在一个有两个 m b u f的链上:一个分组首部和一个簇。每个分片的第一个缓
存上的 m _ d a t a指针指向分组数据,而不是分组的首部。因此,由 m _ c a t构造的缓存链只包
含分片的数据部分。
当一个分片含有多于208字节的数据时,情况通常是这样的 (2.6节)。缓存的“frag”部分是
分片的IP首部。由于图10-18中的代码,各缓存链第一个缓存的 m_data指针指向“opts”之后。
图1 0 - 2 8显示了用所有分片的缓存重装的数据报。注意,分片 2和3的I P部分和选项不在重
装的数据报里。
第一个分片的首部仍然被用作 i p a s f r a g结构。它被图 1 0 - 2 9中的代码恢复成一个有效的
IP数据报首部。
4. 重建数据报首部
441-456 ip_reass把ip指向链表的第一个分片,将 ipasfrag结构恢复成 ip结构:把数
据报长度恢复成 i p _ l e n ,源站地址恢复成 i p _ s r c ,目的地址恢复成 i p _ d s t ;并把
i p f _ m f f 的低位清零 (从图 1 0 - 1 4可以知道, i p a s f r a g 结构的 i p f _ m f f覆盖了 i p结构的
ipf_tos)。
i p _ r e a s s用r e m q u e把整个分组从重装链表中移走,丢弃链表头 i p q结构,调整第一个
缓存中的 m_len和m_data,将前面被隐藏起来的第一个分片的首部和选项包含进来。
5. 计算分组长度

Linux公社 www.linuxidc.com
bbs.theithome.com
236 计计TCP/IP详解 卷 2:实现

分片1的IP首部和选项

图10-28 重装的数据报

图10-29 函数ip_reass :数据报重装

457-464 此处的代码总被执行,因为数据报的第一个缓存总是一个分组首部。 f o r循环计


算缓存链中数据的字节数,并把值保存在 m_pkthdr.len中。
在选项类型字段中, copied比特的意义现在应该很明白了。因为目的主机只保留那些出现
在第一个分片中的选项,而且只有那些在分组去往目的主机的途中控制分组处理的选项才被
复制下来。不复制那些在传送过程中收集信息的选项,因为当分组在目的主机上被重装时,
所有收集的信息都被丢弃了。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 10章 IP的分片与重装计计 237
10.7 ip_slowtimo函数

如7.4节所述, Net/3的各项协议可能指定每 500 ms调用一个函数。对 IP而言,这个函数是


ip_slowtimo,如图10-30所示,为重装链表上的分片计时。

图10-30 ip_slowtimo 函数

515-534 i p _ s l o w t i m o遍历部分数据报的链表,减少重装 T T L字段。当该字段减为 0时,


就调用i p _ f r e e f,把与该数据报相关的分片都丢弃。在 s p l n e t处运行i p _ s l o w t i m o,避
免到达分组修改链表。
ip_freef显示如图10-31。
470-486 ip_freef移走并释放链表上 fp指向的各分片,然后释放链表本身。

图10-31 ip_freef 函数

ip_drain函数

在图7 - 1 4中,我们讲到 I P把i p _ d r a i n定义成一个当内核需要更多内存时才调用的函数。

Linux公社 www.linuxidc.com
bbs.theithome.com
238 计计TCP/IP详解 卷 2:实现

这种情况通常发生在我们讨论过的 (图2-13)分配缓存时。ip_drain显示如图10-32。

图10-32 ip_drain 函数

538-545 I P释放内存的最简单办法就是丢弃重装链表上的所有 I P分片。对属于某个 T C P报


文段的分片, T C P最终会重传该数据。属于 U D P数据报的 I P分片就丢失了,基于 U D P的协议
必须在应用程序层处理这种情况。

10.8 小结

本章展示了当一个外出的数据报过大而不适于在选定网络上传送时, i p _ o u t p u t如何对
数据报分片。由于分片在向目的地传送的途中可能会被继续分片,也有可能走不同的路径,
所以只有目的主机才能组装原来的数据报。
ip_reass接收到达分片,并试图重装数据报。如果重装成功,就把数据报传回 ipintr,
然后提交给恰当的运输层协议。所有 IP实现必须能够重装最多 576字节的数据报。 Net/3的唯一
限制就是可以得到的 mbuf的个数。如果在一段合理的时间内,没有接收完数据报的所有分片,
ip_slowtimo就丢弃不完整的数据报。

习题

10.1 修改i p _ s l o w t i m o,当它丢弃一个不完整数据报时 (图11 - 1 ),发出一个 I C M P超时


差错报文。
10.2 在分片的数据报中,各分片记录的路由可能互不相同。在目的主机上重装某个数据
报时,返回给运输层的哪一个路由?
10.3 画一个图说明图 10-17中ID为7的分片的ipq结构所涉及的mbuf和相关的分片链表。
10.4 [Auerbach 1994] 建议在对数据报分片后,应该首先传送最后的分片。如果接收系
统先收到最后的分片,它就可以利用偏移值为数据报分配一个大小合适的缓冲区。
请修改ip_output,首先发送最后的分片。
[Auerbach 1994] 注意到某些商用 T C P / I P实现如果先收到最后的分片,就会
出现崩溃。
10.5 用图 8 - 5中的统计回答下面的问题。什么是每个重装的数据报的平均分片数?当一
个外出的数据报被分片时,创建的平均分片数是多少?
10.6 如果ip_off的保留比特被置位,分组会发生什么情况?

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议
11.1 引言

ICMP在IP系统间传递差错和管理报文,是任何 IP实现必需和要求的组成部分。 ICMP的规


范见RFC 792 [Postel 1981b]。RFC 950 [Mogul和Postel 1985]和RFC 1256 [Deering 1991a]定义
了更多的ICMP报文类型。 RFC 792 [Braden 1989a]提供了重要的ICMP细节。
I C M P有自己的传输协议号( 1),允许 I C M P报文在 I P数据报内携带。应用程序可以直接从
第32章讨论的原始IP接口发送或接收 ICMP报文。
我们可把 I C M P 报文分成两类:差错和查询。查询报文是用一对请求和回答定义的。
I C M P差错报文通常包含了引起错误的 I P数据报的第一个分片的 I P首部(和选项),加上该分片
数据部分的前 8个字节。标准假定这 8个字节包含了该分组运输层首部的所有分用信息,这样
运输层协议可以向正确的进程提交 ICMP差错报文。
TCP和UDP端口号在它们首部的前 8个字节内出现。
图11-1显示了所有目前定义的 ICMP报文。双线上面的是 ICMP请求和回答报文;双线下面
的是ICMP差错报文。

type和co d e 描 述 PRC_

ICMP_ECHO 回显请求
ICMP_ECHOREPLY 回显回答
ICMP_TSAMP 时间戳请求
ICMP_TSTAMPREPLY 时间戳回答
ICMP_MASKREQ 地址掩码请求
ICMP_MASKREPLY 地址掩码回答
ICMP_IREQ 信息请求(过时的)
ICMP_IREQREPLY 信息回答(过时的)
ICMP_ROUTERADVERT 路由器通告
ICMP_ROUTESOLICIT 路由器请求
ICMP_REDIRECT 有更好的路由
ICMP_REDIRECT_NET 网络有更好的路由 PRC_REDIRECT_HOST
ICMP_REDIRECT_HOST 主机有更好的路由 PRC_REDIRECT_HOST
ICMP_REDIRECT_TOSNET TOS和网络有更好的路由 PRC_REDIRECT_HOST
ICMP_REDIRECT_TOSHOST TOS和主机有更好的路由 PRC_REDIRECT_HOST
其他 不识别码
ICMP_UNREACH 目的主机不可达
ICMP_UNREACH_NET 网络不可达 PRC_UNREACH_NET
ICMP_UNREACH_HOST 主机不可达 PRC_UNREACH_HOST

图11-1 ICMP报文类型和代码

Linux公社 www.linuxidc.com
bbs.theithome.com
240 计计TCP/IP详解 卷 2:实现

type和c o d e 描 述 PRC_

ICMP_UNREACH_PROTOCOL 目的主机上协议不能用 PRC_UNREACH_PROTOCOL


ICMP_UNREACH_PORT 目的主机上端口没有被激活 PRC_UNREACH_PORT
ICMP_UNREACH_SRCFAIL 源路由失败 PRC_UNREACH_SRCFAIL
ICMP_UNREACH_NEEDFRAG 需要分片并设置 DF比特 PRC_MSGSIZE
ICMP_UNREACH_NET_UNKNOWN 目的网络未知 PRC_UNREACH_NET
ICMP_UNREACH_HOST_UNKNOWN 目的主机未知 PRC_UNREACH_HOST
ICMP_UNREACH_ISOLATED 源主机被隔离 PRC_UNREACH_HOST
ICMP_UNREACH_NET_PROHIB 从管理上禁止与目的网络通信 P R C _ U N R E A C H _ N E T
ICMP_UNREACH_HOST_PROHIB 从管理上禁止与目的主机通信 P R C _ U N R E A C H _ H O S T
ICMP_UNREACH_TOSNET 对服务类型,网络不可达 PRC_UNREACH_NET
ICMP_UNREACH_TOSHOST 对服务类型,主机不可达 PRC_UNREACH_HOST
13 用过滤从管理上禁止通信
14 主机优先违规
15 事实上优先切断
其他 不识别码
ICMP_TIMXCEED 超时
ICMP_TIMXCEED_INTRANS 传送过程中 IP生存期到期 PRC_TIMXCEED_INTRANS
ICMP_TIMXCEED_REASS 重装生存期到期 PRC_TIMXCEED_REASS
其他 不识别码
ICMP_PRRAMPROB IP首部的问题
0 未指明首部差错 PRC_PARAMPROB
ICMP_PRRAMPROB_OPTABSENT 丢失需要的选项 PRC_PARAMPROB
其他 无效字节的字节偏移

ICMP_SOURCEQUENCH 要求放慢发送 PRC_QUENCH


其他 不识别类型

图11-1 (续)

图11-1和图11-2中含有大量信息:
• P R C _栏显示了 N e t / 3处理的与协议无关的差错码( 11 . 6节)和 I C M P报文之间的映射。对请
求和回答,这一列是空的。因为在这种情况下不会产生差错。如果对一个 I C M P差错,
这一行为空,说明 Net/3不识别该码,并自动丢弃该差错报文。
• 图11-3显示了我们讨论图 11-2所列函数的位置。
• icmp_input栏是icmp_input为每个ICMP报文调用的函数。
• UDP 栏是为UDP插口处理ICMP报文的函数。
• T C P栏是为T C P插口处理 I C M P报文的函数。注意,是 t c p _ q u e n c h处理I C M P源站抑制
差错,而不是tcp_notify。
• 如果errno栏为空,内核不向进程报告 ICMP报文。
• 表的最后一行显示,在用于接收 I C M P报文的进程的接收点上,不识别的 I C M P报文被提
交给原来的 IP协议。
在Net/3中,ICMP是作为IP之上的一个运输层协议实现的,它不产生差错或请求;它代表

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 241
其他协议格式化并发送报文。 I C M P传递到达的差错,并向适当的传输协议或等待 I C M P报文
的进程发出回答。另一方面, I C M P用一个合适的 I C M P回答响应大多数 I C M P请求。图 11 - 4对
此作了总结。

其他

其他

其他

其他

其他

图11-2 ICMP报文类型和代码 (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
242 计计TCP/IP详解 卷 2:实现

函 数 描 述 引 用

icmp_reflect 为ICMP生成回答 11.12节


in_rtchange 更新IP路由表 图22-34
pfctlinput 向所有协议报告差错 7.7节
pr_ctlinput 向与插口有关的协议报告差错 7.4节
rip_input 进程不识别的 ICMP报文 32.5节
tcp_notify 向进程报告差错或忽略 图27-12
tcp_quench 放慢输出 图27-13
udp_notify 向进程报告差错 图23-31

图11-3 ICMP输入处理时调用的函数

ICMP报文类型 到 达 输 出

请求 向ICMP请求生成回答 由某个进程生成
回答 传给原始 IP 由内核生成
差错 传给传输协议和原始 IP 由IP或传输协议生成
未知 传给原始 IP 由某个进程生成

图11-4 ICMP报文处理

11.2 代码介绍

图11-5的两个文件中有本章讨论的 ICMP数据结构、统计量和处理的程序。

文 件 描 述

netinet/ip_icmp.h ICMP结构定义
netinet/ip_icmp.c ICMP处理

图11-5 本章定义的文件

11.2.1 全局变量

本章介绍的全局变量如图 11-6所示。

变 量 类 型 描 述

icmpmaskrepl int 使ICMP地址掩码回答的返回有效


icmpstat s t r u c t i c m p s t a t ICMP统计量(图 11-7)

图11-6 本章介绍的全局变量

11.2.2 统计量

统计量是由图11-7所示的icmpstat结构的成员收集的。

icmpsta t成员 描 述 SNMP使用的

icps_oldicmp 因为数据报是一个 ICMP报文而丢弃的差错数


icps_oldshort 因为IP数据报太短而丢弃的差错数

图11-7 本章收集的统计信息

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 243
icmpsta t成员 描 述 SNMP使用的

icps_badcode 由于无效码而丢弃的 ICMP报文数


icps_badlen 由于无效的 ICMP体而丢弃的 ICMP报文数
icps_checksum 由于坏的ICMP检验和而丢弃的 ICMP报文数
icps_tooshort 由于ICMP首部太短而丢弃的报文数
icps_outhist[] 输出计数器数组 ;每种ICMP类型对应一个
icps_inhist[] 输入计数器数组 ;每种ICMP类型对应一个
icps_error i c m p _ e r r o r的调用(重定向除外 )数
icps_reflect 内核反映的 ICMP报文数

图11-7 (续)

在分析程序时,我们会看到计数器是递增的。
图11-8显示的是执行n e t s t a t -命
s 令输出的统计信息的例子。

输出 成员

图11-8 ICMP统计信息示例

11.2.3 SNMP变量

图11-9显示了SNMP ICMP组的变量与 Net/3收集的统计量之间的关系。


SNMP变量 成员 描 述
见正文

收到的ICMP报文数
由于错误丢弃的ICMP报文数

计数器 每一类收到的ICMP报文数

图11-9 ICMP组内的简单 SNMP变量


Linux公社 www.linuxidc.com
bbs.theithome.com
244 计计TCP/IP详解 卷 2:实现

见正文
发送的ICMP报文数
由于一个错误而没有发送的ICMP错误数

计数器 每一类发送的ICMP报文数

图11-9 (续)

i c m p I n M s g s是i c p s _ i n h i s t数组和 i c m p I n E r r o r s中的计数之和, i c m p O u t M s g s


是icps_outist数组和icmpOutErrors中的计数之和。

11.3 icmp结构

Net/3通过图11-10中的icmp结构访问某个ICMP报文。
42-45 i c m p _ t y p e标识特定报文, i c m p _ c o d e进一步指定该报文(图 11 - 1的第1栏)。计算
icmp_cksum的算法与IP首部检验和相同,保护整个 ICMP 报文(像IP一样,不仅仅保护首部)。
46-79 联合i c m p _ h u n(首部联合)和 i c m p _ d u n(数据联合)按照 i c m p _ t y p e和i c m p _ c o d e
访问多种 I C M P报文。每种 I C M P报文都使用 i c m p _ h u n;只有一部分报文用 i c m p _ d u n。没
有使用的字段必须设置为 0。
80-86 我们已经看到,利用其他嵌套的结构(例如 m b u f、l e _ s o f t c 和e t h e r _ a r p),
#define宏可以简化对结构成员的访问。
图11 - 11显示了I C M P报文的整体结构,并再次强调 I C M P报文是封装在 I P数据报里的。我
们将在分析程序时,分析所遇报文的特定结构。

图11-10 icmp 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 245

图11-10 (续)

ICMP报文
依赖于type和
code的内容
1 1 2字节

IP首部

IP数据报

图11-11 一个ICMP报文(省略icmp_ )

11.4 ICMP 的protosw结构

i n e t s w [ 4 ](图11 - 1 3)的 p r o t o s w结构描述了 I C M P,并支持内核和进程对协议的访问。


图11 - 1 2显示了该结构。在内核里, i c m p _ i n p u t处理到达的 I C M P报文,进程产生的外出
ICMP报文由rip_output处理。以rip_开头的三个函数将在第 32章中讨论。

成 员 inetsw[4] 描 述

pr_type SOCK_RAW ICMP提供原始分组服务


pr_domain &inetdomain ICMP是Internet域的一部分

图11-12 ICMP 的inetsw 项

Linux公社 www.linuxidc.com
bbs.theithome.com
246 计计TCP/IP详解 卷 2:实现

成 员 inetsw[4] 描 述

pr_protocol IPPROTO_ICMP (1) 出现在IP首部的ip_p字段中


pr_flags PR_ATOMIC|PR_ADDR 插口层标志, ICMP不使用
pr_input icmp_input 从IP层接收ICMP报文
pr_output rip_output 将ICMP报文发送到 IP层
pr_ctlinput 0 ICMP不使用
pr_ctloutput rip_ctloutput 响应来自一个进程的管理请求
pr_usrreq rip_usrreq 响应来自一个进程的通信请求
pr_init 0 ICMP不使用
pr_fasttimo 0 ICMP不使用
pr_slowtimo 0 ICMP不使用
pr_drain 0 ICMP不使用
pr_sysctl 0 ICMP不使用

图11-12 (续)

图11-13 数值为1的ip_p 选择了inetsw[4]

11.5 输入处理: icmp_input函数

回想起ipintr对数据报进行分用是根据 IP首部中的传输协议编号 ip_p。对于ICMP报文,


ip_p是1,并通过ip_protox选择inetsw[4]。
当一个ICMP报文到达时,IP
应用程序
层通过inetsw[4]的pr_input
函数,间接调用 i c m p _ i n p u t
(图10-11)。 传输协议
我们将看到,在 i c m p _
i n p u t中,每一个I C M P报文要 ICMP差错 ICMP差错应答和未知报文

被处理 3次:被 i c m p _ i n p u t
处理一次;被与 I C M P差错报文 ICMP ICMP
应答 输出处理
中的 I P分组相关联的传输协议
ICMP
处理一次;被记录收到 I C M P报 报文
(图11-29)

文的进程处理一次。 I C M P输入
处理过程的总的构成情况如图
11-14所示。 图11-14 ICMP的输入处理过程

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 247
我们将在以下 5节( 11 . 6~11 . 1 0)讨论 i c m p _ i n p u t:(1) 验证收到的报文; (2) ICMP差错
报文;(3) ICMP请求报文; (4) ICMP重定向报文; (5) ICMP回答报文。 i c m p _ i n p u t函数的
第一部分如图11-15所示。

图11-15 icmp_input 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
248 计计TCP/IP详解 卷 2:实现

图11-15 (续)

1. 静态结构
131-134 因为i c m p _ i n p u t是在中断时调用的,此时堆栈的大小是有限的。所以,为了在
每次调用 i c m p _ i n p u t时,避免动态分配造成的延迟,以及使堆栈最小,这 4个结构是动态分
配的。icmp_input把这4个结构用作临时变量。
i c m p s r c 的 命 名 容 易 引 起 误 解 ,因为 i c m p _ i n p u t 把 它用作临时
s o c k a d d r _ i n变量,而它也从未包含过源站地址。在 Net/2版本的i c m p _ i n p u t中,
在报文被 r a w _ i n p u t函数提交给原始 I P之前,报文的源站地址在函数的最后被复制
到i c m p s r c中。而 N e t / 3调用只需要一个指向该分组的指针的 r i p _ i n p u t,而不是
raw_input。虽然有这个改变,但是 icmpsrc仍然保留了在Net/2中的名字。
2. 确认报文
135-139 i c m p _ i n p u t希望收到的 I C M P报文( m)中含有一个指向该数据报的指针,以及该
数据报 I P首部的字节长度( h l e n)。图 11 - 1 6列出了几个在 i c m p _ i n p u t里用于简化检测无效
ICMP报文的常量。

常量/宏 值 描 述

ICMP_MINLEN 8 ICMP报文大小的最小值
ICMP_TSLEN 20 ICMP时间戳报文大小
ICMP_MASKLEN 12 ICMP地址掩码报文大小
ICMP_ADVLENMIN 36 ICMP差错(建议)报文大小的最小值
(IP + ICMP + BADIP = 20 + 8 + 8 = 36)
I CMP_ADVLEN(p) 36 + optsize ICMP差错报文的大小,包含无效分组 p的IP选项的optsize字节

图11-16 ICMP引用的用来验证报文的常量

140-160 icmp_input从ip_len取出ICMP 报文的大小,并把它存放在 icmplen中。第8


章讲过, i p i n t r从i p _ l e n中排除了 I P首部的长度。如果报文长度太短,不是有效报文,就
生成 i c p s _ t o o s h o r t,并丢弃该报文。如果在第一个 m b u f中,ICMP 首部和I P首部不是连
续的,则 由m_pullup保证ICMP 首部以及封闭的 IP分组的 IP首部在同一个mbuf中。
3. 验证检验和
161-170 icmp_input隐藏mbuf中的IP首部,并用 in_cksum验证ICMP 的检验和。如果
报文被破坏,就增加 icps_checksum,并丢弃该报文。
4. 验证类型
171-175 如果报文类型(icmp_type)不在识别范围内, icmp_input就跳过switch执行

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 249
r a w语句(图 11 - 9)。如果在识别范围内, i c m p _ i n p u t复制 i c m p _ c o d e,s w i t c h 按照
icmp_type处理该报文。
在ICMP s w i t c h语句处理完后, i c m p _ i n p u t向r i p _ i n p u t发送ICMP 报文,后者把
ICMP 报文发布给准备接收的进程。只有那些被破坏的报文(长度或检验和出错)以及只由内核
处理的 I C M P请求报文才不传给 r i p _ i n p u t。在这两种情况下, i c m p _ i n p u t都立即返回,
并跳过raw处的源程序。
5. 原始ICMP输入
317-325 i c m p _ i n p u t把到达的报文传给 r i p _ i n p u t,r i p _ i n p u t依据报文里含有的协
议及源站和目的站地址信息( 32章),把报文发布给正在监听的进程。
原始IP机制允许进程直接发送和接收 ICMP报文,这样做有几个原因:
• 新ICMP 报文可由进程处理而无需修改内核(例如,路由器通告,图 11-28)。
• 可以用进程而无需用内核模块来实现发送 ICMP 请求和处理回答的机制( p i n g 和
traceroute)。
• 进程可以增加对报文的内核处理。与此类似,内核在更新完它的路由表后,会把 I C M P
重定向报文传给一个路由守护程序。

11.6 差错处理

我们首先考虑 I C M P差错报文。当主机发出的数据报无法成功地提交给目的主机时,它就
接收这些报文。目的主机或中间的路由器生成这些报文,并将它们返回到原来的系统。图 11-17
显示了多种 ICMP差错报文的格式。
不可达
超时 (必须是0) 被破坏分组的IP首部
源抑制
4字节

需要分片 (必须是0) 被破坏分组的IP首部


2字节 2字节

参数问题 (必须是0)
被破坏分组的IP首部
1 1 2字节 3字节 8字节
图11-17 ICMP差错报文 (省略icmp_ )

图11-18中的源程序来自图 11-15中的switch语句。

图11-18 icmp_input 函数:差错报文

Linux公社 www.linuxidc.com
bbs.theithome.com
250 计计TCP/IP详解 卷 2:实现

图11-18 (续)

176-216 对ICMP差错的处理是最少的,因为这主要是运输层协议的责任。 imcp_input把


i c m p _ t y p e和i c m p _ c o d e映射到一个与协议无关的差错码集上,该差错码是由 P R C _常量

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 251
(图11 - 1 9 )表示的。 P R C _常量有一个隐含的顺序,正好与 I C M P的c o d e相对应。这就解释了为
什么code是按一个PRC_常量递增的。
217-225 如果识别出类型和码, i c m p _ i n p u t就跳到 d e l i v e r。如果没有识别出来,
icmp_input就跳到badcode。
如果对所报告的差错而言,报文长度不正确, icps_badlen的值就加1,并丢弃该报文。
N e t / 3总是丢弃无效的 I C M P报文,也不生成有关该无效报文的 I C M P差错。这样,就避免在两
个有缺陷的实现之间形成无限的差错报文序列。
226-231 i c m p _ i n p u t调用运输层协议的 p r _ c t l i n p u t函数,该函数根据原始数据报的
i p _ p,把到达分组分用到正确的协议,从而构造出原始的 I P数据报。差错码( c o d e)、原始 I P
数据报的目的地址( i c m p s r c )以及一个指向无效数据报的指针( i c m p _ i p )被传给
pr_ctlinput(如果是为该协议定义的)。图 23-31和图27-12讨论这些差错。
232-234 最后,icps_badcode的值增加1,并终止switch语句的执行。

常 量 描 述

PRC_HOSTDEAD 主机似乎已关闭
PRC_IFDOWN 网络接口关闭
PRC_MSGSIZE 无效报文大小
PRC_PRRAMPROB 首部不正确
PRC_QUENCH 某人说要放慢
PRC_QUENCH2 阻塞比特要求放慢
PRC_REDIRECT_HOST 主机路由选择重定向
PRC_REDIRECT_NET 网络路由选择重定向
PRC_REDIRECT_TOSHOST TOS和主机的重定向
P R C _ R E D I R E C T _ T O S N E T TOS和网络的重定向
PRC_ROUTEDEAD 如果可能,选择新的路由
P R C _ T I M X C E E D _ I N T R A N S 传送过程中分组生命期到期
PRC_TIMXCEED_REASS 分片在重装过程中生命期到期
PRC_UNREACH_HOST 没有到主机的路由
PRC_UNREACH_NET 没有到网络的路由
PRC_UNREACH_PORT 目的主机称端口未激活
PRC_UNREACH_PROTOCOL 目的主机称协议不可用
PRC_UNREACH_SRCFAIL 源路由失败

图11-19 与协议无关的差错码

尽管P R C _常量表面上与协议无关,但它们主要还是基于 I n t e r n e t协议族。其结果


是,当某个 Internet协议族以外的协议把自己的差错映射到 P R C _常量时,会失去可指
定性。

11.7 请求处理

Net/3响应具有正确格式的 ICMP请求报文,但把无效 ICMP请求报文传给r i p _ i n p u t。第


32章讨论了应用程序如何生成 ICMP请求报文。
除路由器通告报文外,大多数 Net/3所接收的 ICMP请求报文都生成回答报文。为避免为回
答报文分配新的 m b u f,i c m p _ i n p u t把请求的缓存转换成回答的缓存,并返回给发送方。我

Linux公社 www.linuxidc.com
bbs.theithome.com
252 计计TCP/IP详解 卷 2:实现

们将分别讨论各个请求。

11.7.1 回显询问: ICMP_ECHO和ICMP_ECHOREPLY

尽管I C M P非常简单,但是 I C M P回显请求和回答却是网络管理员最有力的诊断工具。发


出I C M P回显请求称为“ p i n g”一个主机,也就是调用 p i n g程序一次。许多系统提供该程序
来手工发送 ICMP回显请求。卷1的第7章详细讨论了ping。
p i n g程序的名字依照了声纳脉冲 (soar ping),用其他物体对声纳脉冲的反射所产生的回
声确定它们的位置。卷 1把这个名字解释成 Packet InterNet Groper,是不正确的。
图11-20是ICMP回显请求和回答报文的结构。

检验和
8字节

标识符 顺序号

可选数据

图11-20 ICMP回显请求和回答

i c m p _ c o d e总是0。 i c m p _ i d和i c m p _ s e q设置成请求的发送方,回答中也不做修改。


源系统可以用这些字段匹配请求和回答。 i c m p _ d a t a中到达的所有数据也被反射。图 11 - 2 1
是ICMP回显处理和 icmp_input实现反射ICMP请求的源程序。

图11-21 icmp_input 函数:回显请求和回答

235-237 通过把 i c m p _ t y p e 变成 I C M P _ E C H O R E P L Y,并跳转到 r e f l e c t发送回答,


icmp_input把回显请求转换成了回显回答。
277-282 在为每个ICMP 请求构造完回答之后, icmp_input执行reflect处的程序。在
这里,存储数据报正确的长度被恢复,在 i c p s _ r e f l e c t和i c p s _ o u t h i s t [ ]中分别计算
请求的数量和ICMP报文的类型。icps_reflect(11.12节)把回答发回给请求方。

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 253
11.7.2 时间戳询问:ICMP_TSTAMP和ICMP_TSTAMPREPLY

ICMP时间戳报文如图 11-22所示。

检验和

标识符 序号

20字节
32位原始时间戳

32位接收时间戳

32位传送时间戳

图11-22 ICMP时间戳请求和回答

i c m p _ c o d e总是0。i c m p _ i d和i c m p _ s e q的作用与它们在 ICMP回显报文中的一样。请


求的发送方设置 i c m p _ o t i m e ( 发出请求的时间 ) ;i c m p _ r t i m e ( 收到请求的时间 )和
i c m p _ t t i m e(发出回答的时间 )由回答的发送方设置。所有时间都是从 U T C午夜开始的毫
秒数。如果时间值没有以标准单位记录,就把高位置位,与 IP时间戳选项一样。
图11-23是实现时间戳报文的程序。

图11-23 icmp_input 函数:时间戳请求和回答

238-246 icmp_input对ICMP的响应,包括:把icmp_type改成ICMP_TSTAMPREPLY,
记录当前icmp_rtime和icmp_ttime,并跳转到reflect发送回答。
很难精确地设置icmp_rtime和icmp_ttime。当系统执行这段程序时,报文可能已经在IP
输入队列中等待处理,这时设置icmp_rtime已经太晚了。类似地,数据报也可能在要求处理时
在网络接口的传输队列中被延迟,这时设置icmp_ttime又太早了。为了把时间戳设置得更接近
真实的接收和发送时间,必须修改每个网络的接口驱动程序,使其能理解ICMP 报文(习题11.8)。

11.7.3 地址掩码询问: ICMP_MASKREQ和ICMP_MASKREPLY

ICMP 地址掩码请求和回答如图 11-24所示。


RFC 950 [Mogul和Postel 1985]在原来的ICMP规范说明中增加了地址掩码报文,使系统能
发现某个网络上使用的子网掩码。

Linux公社 www.linuxidc.com
bbs.theithome.com
254 计计TCP/IP详解 卷 2:实现

除非系统被明确地配置成地址掩码的授权代理,否则, RFC 1122禁止向其发送掩码回答。


这样,就避免系统与所有向它发出请求的系统共享不正确的地址掩码。如果没有管理员授权
回答,系统也要忽略地址掩码请求。

检验和

12字节
标识符 顺序号

32位子网掩码

图11-24 ICMP地址掩码请求和回答

如果全局整数 i c m p m a s k r e p l非零, N e t / 3会响应地址掩码请求。 i c m p m a s k r e p l的默


认值是0,icmp_sysctl可以通过systctl(8)程序( 11.14节)修改它。
N e t / 2系统中没有控制回答地址掩码请求的机制。其结果是,必须非常正确地配
置Net/2接口的地址掩码;该信息是与网络上所有发出地址掩码请求的系统共享的。
地址掩码报文的处理如图 11-25所示。

图11-25 icmp_input 函数:地址掩码请求和回答

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 255
247-256 如果没有配置响应掩码请求,或者该请求太短,这段程序就中止 s w i t c h的执行,
并把报文传给rip_input(图11-15)。
在这里 N e t / 3无法增加 i c p s _ b a d l e n 。对其他 ICMP 长度差错,它却增加
icps_badlen。
1. 选择子网掩码
257-267 如果地址掩码请求被发到 0.0.0.0或255.255.255.255,源地址被保存在 icmpdst中。
在这里, i f a o f _ o f f o r a d d r把i c m p d s t作为源站地址,在同一网络上查找 i n _ o f a d d r结
构。如果源站地址是 0 . 0 . 0 . 0或2 5 5 . 2 5 5 . 2 5 5 . 2 5 5,i f a o f _ o f f o r a d d r返回一个指针,该指针
指向与接收接口相关的第一个 IP地址。
default情况(针对单播或有向广播 )为ifaof_ifpforaddr保存目的地址。
2. 转换成回答
269-270 通过改变 i c m p _ t y p e,并把所选子网掩码 i a _ s o c k m a s k复制到i c m p _ m a s k,
就完成了把请求转换成回答的工作。
3. 选择目的地址
271-276 如果请求的源站地址全 0(“该网络上的这台主机”,只在引导时用作源站地址,
RFC 11 2 2),并且源站不知道自己的地址, N e t / 3必须广播这个回答,使源站系统接收到这个
报文。在这种情况下,如果接收接口位于某个广播或点到点网络上,该回答的目的地址将分
别是i a _ b r o a d a d d r和i a _ d s t a d d r。i c m p _ i n p u t把回答的目的地址放在 i p _ s r c里,因
为reflect处的程序(图 11-21)会把源站和目的站地址倒过来。不改变单播请求的地址。

11.7.4 信息询问: ICMP_IREQ和ICMP_IREQREPLY

I C M P信息报文已经过时了。它们企图广播一个源和目的站地址字段的网络部分为全 0的
请求,使系统发现连接的 I P 网络的数量。响应该请求的主机将返回一个填好网络号的报文。
主机还需要其他办法找到地址的主机部分。
RFC 11 2 2推荐主机不要实现 I C M P信息报文,因为 R A R P(RFC 903 [Finlayson et al. ,
1984])和BOOTP(RFC 951 [Croft和Gilmore 1985])更适于发现地址。 RFC 1541 [Droms 1993]描
述的一个新协议,动态主机配置协议( Dynamic Host Configuration Protocol,DHCP),可能会
取代或增强 BOOTP的功能。它现在是一个建议的标准。
Net/2响应ICMP信息请求报文。但是, Net/3把它们传给 rip_input。

11.7.5 路由器发现:ICMP_ROUTERADVERT和ICMP_ROUTERSOLICIT

RFC 1256 定义了ICMP路由器发现报文。Net/3内核不直接处理这些报文,而由rip_input


把它们传给一个用户级守护程序,由它发送和响应这种报文。
卷1的9.6节讨论了这种报文的设计和运行。

11.8 重定向处理

图11-26显示了ICMP重定向报文的格式。
i c m p _ i n p u t中要讨论的最后一个 c a s e是I C M P _ R E D I R E C T。如8 . 5节的讨论,当分组

Linux公社 www.linuxidc.com
bbs.theithome.com
256 计计TCP/IP详解 卷 2:实现

被发给错误的路由器时,产生重定向报文。该路由器把分组转发给正确的路由器,并发回一
个ICMP重定向报文,系统把信息记入它自己的路由表。

检验和
8字节

优选路由器的IP地址

IP首部(包括选项)和原始IP数据报中开始的至少8字节

图11-26 ICMP重定向报文

图11-27显示了icmp_input用来处理重定向报文的程序。

图11-27 icmp_input 函数:重定向报文

1. 验证
283-290 如果重定向报文中含有未识别的 ICMP 码,i c m p _ i n p u t就跳到b a d c o d e(图11 -
1 8的2 3 2行);如果报文具有无效长度或封闭的 I P分组具有无效首部长度,则中止 s w i t c h。图
11-16显示了ICMP差错报文的最小长度是 36(I C M P _ A D V L E N M I N)。I C M P _ A D V L E N(i c p)是当
icp所指向的分组有 IP选项时,ICMP差错报文的最小长度。
291-300 i c m p _ i n p u t分别把重定向报文的源站地址(发送该报文的网关)、为原始分组推
荐的路由器(第一跳目的地)和原始分组的最终目的地址分配给 icmpgw、icmpdst和
icmpsrc。

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 257
这里, i c m p s r c并不包含源站地址—这是方便存放目的地址的位置,无需再
定义一个sockaddr结构。
2. 更新路由
301-306 Net/3按照RFC 1122的推荐,等价地对待网络重定向和主机重定向。重定向信息被
传给 r t r e d i r e c t,由这个函数更新路由表。重定向的目的地址(保存在 i c m p s r c)被传给
p f c t l i n p u t,由它通告重定向的所有协议域( 7 . 3节),使协议有机会把缓存的到目的站的路
由作废。
按照RFC 11 2 2,应该把网络重定向作为主机重定向对待,因为当目的网络划分
了子网时,它们会提供不正确的路由信息。事实上, RFC 1009要求,在网络划分子
网的情况下,不发送网络重定向。不幸的是,许多路由器违背了这个要求。 N e t / 3从
不发重定向报文。
I C M P重定向报文是 I P路由选择体系结构的基本组成部分。尽管被划分到差错报文类,但
它却是在任何有多个路由器的网络正常运行时出现的。第 18章更详细讨论了 IP路由选择问题。

11.9 回答处理

内核不处理任何 I C M P回答报文。 I C M P请求由进程产生,内核从不产生请求。所以,内


核把它接收的所有回答传给等待 I C M P 报文的进程。另外, I C M P 路由器发现报文被传给
rip_input。

图11-28 icmp_input 函数:回答报文

307-322 内核无需对ICMP回答报文做出任何反应,所以在raw处的switch语句后继续执行(图
11-15)。注意,switch语句的default情况(未识别的ICMP报文)也把控制传给在raw处的代码。

11.10 输出处理

有几种方法产生外出的 ICMP报文。第 8章讲到IP调用icmp_error来产生和发送ICMP 差


错报文。 i c m p _ r e f l e c t发送ICMP 回答报文,同时,进程也可能通过原始 ICMP 协议生成
ICMP 报文。图11-29显示了这些函数与 ICMP外出处理之间的关系。

Linux公社 www.linuxidc.com
bbs.theithome.com
258 计计TCP/IP详解 卷 2:实现

应用程序

ICMP差错
IP和传输层协议 ICMP

ICMP应答
ICMP输入处理

(图11-15)

图11-29 ICMP外出处理

11.11 icmp_error函数

i c m p _ e r r o r在I P或运输层协议的请求下,构造一个 I C M P差错请求报文,并把它传给


icmp_reflect,在那里该报文被返回无效数据报的源站。我们分三部分分析这个函数:

图11-30 icmp_error 函数:验证

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 259
• 确认该报文(图11-30);
• 构造首部(图11-32);并
• 把原来的数据报包含进来(图 11-33)。
46-57 参数是: n,指向包含无效数据报缓存链的指针; t y p e和c o d e,I C M P差错类型和
代码; d e s t,I C M P重定向报文中的下一跳路由器地址;以及 d e s t i f p,指向原始 I P分组外
出接口的指针。 m t o d把缓存链指针 n转换成 o i p,o i p是指向缓存中 i p结构的指针。原始 I P
分组的字节长度保存在 ioplen中。
58-75 i c p s _ e r r o r统计除重定向报文外的所有 I C M P差错。 N e t / 3不把重定向报文看作错
误,而且icps_error也不是一个 SNMP变量。
icmp_error丢弃无效数据报 oip,并且在以下情况下,不发送差错报文:
• 除I P _ M F和I P _ D F外,i p _ o f f的某些位非零(习题 11 . 1 0)。这表明 o i p不是数据报的第
一个分片,而且 ICMP决不能为跟踪数据报的分片而生成差错报文。
• 无效数据报本身是一个 I C M P差错报文。如果 i c m p _ t y p e是I C M P请求或响应类型,则
ICMP_INFOTYPE返回真;如果icmp_type是一个差错类型,则ICMP_INFOTYPE返回假。
Net/3不考虑ICMP重定向报文差错,尽管 RFC 1 122要求考虑。
• 数据报作为链路层广播或多播到达(由 M_BCAST和M_MCAST标志表明)。
在以下两种其他情况下,不能发送 ICMP差错报文:
• 该数据报是发给 IP广播和IP多播地址的。
• 数据报的源站地址不是单播 I P地址(也即,这个源站地址是一个全零地址、环回地址、
广播地址、多播地址或 E类地址)。
Net/3无法检查第一种情况。 icmp_reflect函数强调了第二种情况( 11.12节)。
有趣的是, Net/2的Deering多播扩展并不丢弃第一种类型的数据报。因为 Net/3的
多播程序来自Deering多播扩展,所以,检测似乎被删去了。
这些限制的目的是为了避免有错的广播数据报触发网络上所有主机都发出 I C M P差错报
文。当网络上所有主机同时要发送差错报文时,产生的广播风暴会使整个网络的通信崩溃。
这些规则适用于 ICMP差错报文,但不适用于 ICMP回答。如RFC 1122和RFC 1127的讨论,
允许响应广播请求,但既不推荐也不鼓励。 N e t / 3只响应具有单播源地址的广播请求,因为
ip_output会把返回到广播地址的 ICMP 报文丢弃(图 11-39)。
图11-31是ICMP差错报文的构造。
图11-32的程序构造差错报文。
76-106 icmp_error以下面的方式构造 ICMP差错报文的首部:
• m _ g e t h d r分配一个新的分组首部缓存。 M H _ A L I G N定位缓存的数据指针,使无效数据
报的ICMP首部、IP首部(和选项)和最多 8字节的数据被放在缓存的最后。
• i c m p _ t y p e、i c m p _ c o d e、i c m p _ g w a d d r(用于重定向)、 i c m p _ p p t r(用于参数问
题)和i c m p _ n e x t m t u(用于要求分片报文)被初始化。 i c m p _ n e x t m t u字段实现了 R F C
11 9 1中描述的要求分片报文的扩展。卷 1的2 4 . 2节描述的“路径 M T U发现算法”依赖于
这个报文。
一旦构造好 ICMP首部,就必须把原始数据报的一部分附到首部上,如图 11-33所示。

Linux公社 www.linuxidc.com
bbs.theithome.com
260 计计TCP/IP详解 卷 2:实现

有差错的数据报 前8个
IP首部 IP选项 数据
字节

ICMP 前8个
ICMP差错报文 IP首部 IP首部 IP选项
首部 字节
无效数据报

图11-31 ICMP差错报文的构造

图11-32 icmp_error 函数:报文首部构造

107-125 无效数据报的IP首部、选项和数据(一共是 icmplen个字节)被复制到 ICMP差错报


文中。同时,首部的长度被加回无效数据报的 ip_len中。
在u d p _ u s r r e q中,UDP也把首部长度加回到无效数据报的 i p _ l e n。其结果是
一个I C M P报文,该报文具有无效分组 I P首部内的不正确的数据报长度。作者发现,
许多基于Net/2程序的系统都有这个错误, Net/1系统没有这个问题。

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 261

图11-33 icmp_error 函数:包含原始数据报

因为M H _ A L I G N把I C M P报文分配在缓存的最后,所以缓存的前面应该有足够的空间存放


IP首部。无效数据报的 IP首部(除选项外)被复制到 ICMP报文的前面。
N e t / 2版本的这部分有一个错误:函数的最后一个 b c o p y移动 o i p l e n个字节,
其中包括无效数据报的选项。应该只复制没有选项的标准首部。
在恢复正确的数据报长度( i p _ l e n)、首部长度( i p _ h l)和协议( i p _ p)后, I P首部就完整
了。TOS字段(ip_tos)被清除。
RFC 792和RFC 1122推荐在ICMP报文中,把 TOS字段设为0。
126-129 完整的报文被传给 i c m p _ r e f l e c t,由i c m p _ r e f l e c t把它发回源主机。丢掉
无效数据报。

11.12 icmp_reflect函数

i c m p _ r e f l e c t 把I C M P 回答或差错发回给请求或无效数据报的源站。必须牢记,
i c m p _ r e f l e c t在发送数据报之前,把它的源站地址和目的地址倒过来。与 ICMP 报文的源
站和目的站地址有关的规则非常复杂,图 11-34对其中几个函数的作用作了小结。
我们分三部分讨论 i c m p _ r e f l e c t函数:源站和目的站地址选择、选项构造及组装和发
送。图11-35显示了该函数的第一部分。
1. 设置目的地址
329-345 i c m p _ r e f l e c t一开始,就复制 i p _ d s t,并把请求或差错报文的源站地址
i p _ s r c移到i p _ d s t。i c m p _ e r r o r和i c m p _ r e f l e c t保证:i p _ s r c对差错报文而言是有
效的目的地址。 ip_output丢掉所有发往广播地址的分组。

Linux公社 www.linuxidc.com
bbs.theithome.com
262 计计TCP/IP详解 卷 2:实现

函 数 小 结

icmp_input 在地址掩码请求中,用接收接口的广播或目的地址代替全 0地址


icmp_error 把作为链路级广播或多播发送的数据报引起的差错报文丢弃。应该丢弃 (但没有)发往
IP广播或多播地址的数据报引起的报文
icmp_reflect 丢弃报文,而不是把它返回给多播或实验地址
把非单播目的地址转换成接收接口的地址,对返回的报文来说,目的地址就是一个有
效的源地址
交换源站和目的站的地址
ip_output 按照ICMP的请求丢弃输出的广播 (也就是说,丢弃由发往广播地址的分组产生的差错
报文)

图11-34 ICMP丢弃和地址小结

2. 选择源站地址
346-371 icmp_reflect在in_ifaddr中找到具有单播或广播地址的接口,该接口地址与
原始数据报的目的地址匹配,这样,icmp_reflect就为报文选好了源地址。在多接口主机上,
匹配的接口可能不是接收该数据报的接口。如果没有匹配,就选择正在接收的接口的
i n _ i f a d d r结构,或者 i n _ i f a d d r中的第一个地址(如果该接口没有被配置成 IP可用的)。该
函数把ip_src设成所选的地址,并把ip_ttl改为255(MAXTTL),因为这是一个新的数据报。
RFC 1700推荐把所有I P分组的T T L字段设成 6 4。但是现在,许多系统把 I C M P报
文的TTL设成255。
T T L的取值有一个拆衷。小的 T T L避免分组在路由回路里面循环,但也有可能使
分组无法到达远一点的节点 (有很多跳 )。大的T T L允许分组到达远距离的主机,但却
让分组在路由回路里循环较长时间。

图11-35 icmp_reflect 函数:地址选择

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计263

图11-35 (续)

RFC 11 2 2提出,对到达的回显请求或时间戳请求,要求把其中的源路由选项及记录路由
和时间戳选项的建议,附到回答报文中。在这个过程期间,源路由必须被逆转过来。 RFC
11 2 2没有涉及在其他 I C M P回答报文中如何处理这些选项。 N e t / 3把这些规则应用于地址掩码
请求,因为它在构造地址掩码回答后调用了 icmp_reflect(图11-21)。
程序的下一部分(图 11-36)为ICMP报文构造选项。

图11-36 icmp_reflect 函数:选项构造

Linux公社 www.linuxidc.com
bbs.theithome.com
264 计计TCP/IP详解 卷 2:实现

图11-36 (续)

3. 取得逆转后的源路由
372-385 如果到达的数据报没有选项,控制被传给 4 3 0行(图 11 - 3 7)。 i c m p _ e r r o r传给
i c m p _ r e f l e c t的差错报文从来没有 I P选项,所以后面的程序只用于那些被转换成回答并直
接传给icmp_reflect的ICMP请求。
c p指向回答的选项的开始。 i p _ s r c r o u t e逆转并返回所有在 i p i n t r处理数据报时保存
下来的源路由选项。如果 i p _ s r c o u t e返回0,即请求中没有源路由选项, i c m p _ r e f l e c t
分配并初始化一个 mbuf,作为空的 ipoption结构。
4. 加上记录路由和时间戳选项
386-416 如果opts指向某个缓存,for循环搜索原始IP首部的选项,在ip_srcroute返回
的源路由后面加上记录路由和时间戳选项。
在ICMP报文发送之前必须移走原始首部里的选项。这由图 11-37中的程序完成。

图11-37 icmp_reflect 函数:最后的组装

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 265
5. 移走原始选项
417-429 i c m p _ r e f l e c t把I C M P报文移到 I P首部的后面,这样就从原始请求中移走了选
项。如图11-38所示。新选项在 opts所指向的mbuf里,被ip_output再次插入。
6. 发送报文和清除
430-435 在报文和选项被传给 icmp_send之前,要明确地清除广播和多播标志位。此后释
放掉存放选项的缓存。
丢弃

mbuf IP选项
bcopy之前
首部 IP首部 (optlen字节) 数据

mbuf
bcopy之后 首部 IP首部 数据

图11-38 icmp_reflect :移走选项

11.13 icmp_send函数

i c m p _ s e n d(图11 - 3 9)处理所有输出的 I C M P报文,并在把它们传给 I P层之前计算I C M P检


验和。

图11-39 icmp_send 函数

440-457 与icmp_input检测ICMP检验和一样, Net/3调整缓存的数据指针和长度,隐藏 IP


首部,让 i n _ c k s u m只看到ICMP报文。计算好的检验和放在首部的 i c m p _ c k s u m,然后把数
据报和所有选项传给 i p _ o u t p u t。ICMP层并不维护路由高速缓存,所以 i c m p _ s e n d只传给
i p _ o u t p u t一个空指针(第 4个参数),而不是控制标志。特别是不传 I P _ A L L O W B R O A D C A S T,
所以i p _ o u t p u t丢弃所有具有广播目的地址的 I C M P报文(也就是说,到达原始数据报的具有
无效的源地址)。

Linux公社 www.linuxidc.com
bbs.theithome.com
266 计计TCP/IP详解 卷 2:实现

11.14 icmp_sysctl函数

IP的i c m p _ s y s c t l函数只支持图11-40列出的选项。系统管理员可以用 s y s c t l程序修改


该选项。

Sysc t l常量 Net/3变量 描 述

ICMPCTL_MASKREPL icmpmaskrepl 系统是否响应 ICMP地址掩码请求

图11-40 icmp_sysctl 参数

图11-41显示了icmp_sysctl函数。

图11-41 icmp_sysctl 函数

467-478 如果缺少所要求的 ICMP sysctl名,就返回 ENOTDIR。


479-486 ICMP级以下没有选项,所以,如果不识别选项,该函数就调用 sysctl_int修改
icmpmaskrepl或返回ENOPROTOOPT。

11.15 小结

I C M P协议是作为 I P上面的运输层实现的,但它与 I P层紧密结合一起。我们看到,内核直


接响应 I C M P请求报文,但把差错与回答传给合适的运输层协议或应用程序处理。当一个
I C M P重定向报文到达时,内核立刻重定向表,并且也把重定向传给所有等待的进程,比如典
型地传给一个路由守护程序。
我们将在 2 3 . 9和2 7 . 6节看到 U D P和T C P协议如何响应 I C M P差错报文,在第 3 2章看到进程
如何产生ICMP请求。

习题

11.1 一个目的地址是 0.0.0.0的请求所产生的 ICMP地址掩码回答报文的源地址是什么?

Linux公社 www.linuxidc.com
bbs.theithome.com
第11章 ICMP:Internet控制报文协议计计 267
11.2 试描述一个具有假的单播源地址的分组在链路级的广播会如何影响网络上另一个主
机的运行。
11.3 RFC 1122建议,如果新的第一跳路由器与旧的第一跳路由器位于不同的子网,或者
如果发送报文的路由器不是报文最终目的地的当前第一跳路由器,那么主机应该丢
弃ICMP重定向报文。为什么要采纳这个建议?
11.4 如果ICMP信息请求是过时的,为什么 i c m p _ i n o u t要把它传给 r i p _ i n p u t而不是
丢弃它呢?
11.5 我们指出, N e t / 3在把I P分组放入一个 I C M P差错报文之前,并不把它的偏移和长度
字段转换成网络字节序。为什么这对 IP位移字段来说是无关紧要的?
11.6 描述某种情况,使图 11-25的ifaof_ifpforaddr返回一个空指针。
11.7 在一次时间戳询问中,时间戳后面的数据会怎么样?
11.8 实现以下改变,改进 ICMP时间戳程序:
在缓存分组首部加上一个时间戳字段,让设备驱动程序把接收分组的确切时间记录
在这个字段内,并用 ICMP 时间戳程序把该值复制到 icmp_rtime字段。
在输出端,让 I C M P时间戳程序保存分组中的某个字节偏移,该位置用于保存时间
戳里的当前时间。修改设备驱动程序,在发送分组之前插入时间戳。
11.9 修改i c m p _ e r r o r,使I C M P差错报文中返回最多 6 4字节(像 Solaris 2.x一样)的原始
数据。
11.10 图11-30中,ip_off的高位被置位的分组会发生什么情况?
11.11 为什么图11-39中丢弃了ip_output返回的值?

Linux公社 www.linuxidc.com
bbs.theithome.com
第12章 IP 多 播
12.1 引言

第8章讲到,D类IP地址(224.0.0.0到239.255.255.255)不识别互联网内的单个接口,但识别
接口组。因为这个原因, D类地址被称为多播组 (multicast group)。具有D类目的地址的数据报
被提交给互联网内所有加入相应多播组的各个接口。
I n t e r n e t上利用多播的实验性应用程序包括:音频和视频会议应用程序、资源发现工具和
共享白板等。
多播组的成员由于接口加入或离开组而动态地变化,这是根据各系统上运行的进程的请
求决定的。因为多播组成员与接口有关,所以多接口主机可能针对每个接口,都有不同的多
播组成员关系表。我们称一个特定接口上的组成员关系为一对 {接口,多播组}。
单个网络上的组成员利用 I G M P协议(第1 3章)在系统之间通信。多播路由器用多播选路协
议(第14章),如DVMRP(Distance Vector Multicast Routing Protocol,距离向量多播路由选择协
议)传播成员信息。标准 IP路由器可能支持多播选路,或者用一专用路由器处理多播选路。
如以太网、令牌环和 FDDI一类的网络直接支持硬件多播。在 Net/3中,如果某个接口支持
多播,那么在接口的 i f n e t结构(图3 - 7 )中的i f _ f l a g s标志的 I F F _ M U L T I C A S T比特就被打
开。因为以太网被广泛使用,并且 N e t / 3有以太网驱动器程序,所以我们将以以太网为例说明
硬件支持的 IP多播。多播业务通常在如 SLIP和环回接口等的点到点网络上实现。
如果本地网络不支持硬件级多播,那么在某个特定接口上就得不到 I P多播业务。 R F C
1122并不反对接口层提供软件级的多播业务,只要它对 IP是透明的。
RFC 1112 [Deering 1989] 描述了多播对主机的要求。分三个级别:
0级:主机不能发送或接收 IP多播。
这种主机应该自动丢弃它收到的具有 D类目的地址的分组。
1级:主机能发送但不能接收 IP多播。
在向某个 I P多播组发送数据报之前,并不要求主机加入该组。多播数据报的发送方
式与单播一样,除了多播数据报的目的地址是 I P多播组之外。网络驱动器必须能够
识别出这个地址,把在本地网络上多播数据报。
2级:主机能发送和接收 IP多播。
为了接收IP多播,主机必须能够加入或离开多播组,而且必须支持IGMP,能够在至少
一个接口上交换组成员信息。多接口主机必须支持在它的接口的一个子网上的多播。
N e t / 3符合2级主机要求,可以完成多播路由器的工作。与单播 I P选路一样,我们假
定所描述的系统是一个多播路由器,并加上了 Net/3多播选路的程序。

知名的IP多播组

和U D P 、T C P的端口号一样,互联网号授权机构 IANA(Internet Assigned Numbers

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 269
A u t h o r i t y )维护着一个注册的 I P多播组表。当前的表可以在 RFC 1700中查到。有关 I A N A的其
他信息可以在RFC 1700中找到。图 12-1只给出了一些知名的多播组。

组 描 述 Net/3常量

224.0.0.0 预留 INADDR_UNSPEC_GROUP
224.0.0.1 这个子网上的所有系统 INADDR_ALLHOSTS_GROUP
224.0.0.2 这个子网上的所有路由器
224.0.0.3 没有分配 INADDR_MAX_LOCAL_GROUP
224.0.0.4 DVMRP路由器
224.0.0.255 没有分配
224.0.1.1 NTP网络时间协议
224.0.1.2 SGI-Dogfight

图12-1 一些注册的 IP多播组

前2 5 6个组( 2 2 4 . 0 . 0 . 0到2 2 4 . 0 . 0 . 2 5 5 )是为实现 I P单播和多播选路机制的协议预留的。不管


发给其中任意一个组的数据报内 I P首部的 T T L值如何变化,多播路由器都不会把它转发出本
地网络。
RFC 1075 只对2 2 4 . 0 . 0 . 0组和2 2 4 . 0 . 0 . 1组有这个要求,但最常见的多播选路实现
mrouted限制这里讨论的其他组。组 224.0.0.0(I N A D D R _ U N S P E C _ G R O U P)被预留,组
224.0.0.255(INADDR_MAX_LOCAL_GROUP)标志着本地最后一个多播组。
对于符合 2 级的系统,要求其在系统初始化时 (图 6 - 1 7 ) ,在所有的多播接口上加入
224.0.0.1组(I N A D D R _ A L L H O S T S _ G R O U P),并且保持为该组成员,直到系统关闭。在一个互
联网上,没有多播组与每个接口都对应。
想像一下,如果你的语音邮件系统有一个选项,可以向公司里的所有语音邮箱
发一个消息。可能你就有这个选项。你发现它有用吗?对更大的公司适用吗?是否
有人能向“所有邮箱”组发邮件,或者是否限制这么做?
单播和多播路由可能会加入 2 2 4 . 0 . 0 . 2组进行互相通信。 I C M P路由器请求报文和路由器通
告报文可能被分别发往 2 2 4 . 0 . 0 . 2 (“所有路由器”组 )和2 2 4 . 0 . 0 . 1 (“所有主机”组 ),而不是受
限的广播地址(255.255.255.255)。
2 2 4 . 0 . 0 . 4组支持在实现 D V M R P的多播路由器之间的通信。本地多播组范围内的其他组被
类似地指派给其他路由选择协议。
除了前256个组外,其他组 (224.0.1.0~239.255.255.255)或者被分配给多个多播应用程序协
议,或者仍然没有被分配。图 1 2 - 1 中有两个例子,网络时间协议 (224.0.1.1)和SGI-
Dogfight(224.0.1.2)。
在本章中,我们注意到,是主机上的运输层发送和接收多播分组。尽管多播程序并不知
道具体是哪个传输协议发送和接收多播数据报,但唯一支持多播的 Internet传输协议是 UDP。

12.2 代码介绍

本章中讨论的基本多播程序与标准 I P程序在相同的文件里。图 1 2 - 2列出了我们研究的文


件。

Linux公社 www.linuxidc.com
bbs.theithome.com
270 计计TCP/IP详解 卷 2:实现

文 件 描 述

net/if_either.h 以太网多播数据结构和宏定义
netinet/in.h 其他Internet多播数据结构
netinet/in_var.h Internet多播数据结构和宏定义
netinet/ip_var.h IP多播数据结构
net/if_ethersubr.c 以太网多播函数
n e t i n e t / i n .c 组成员函数
netinet/ip_input.c 输入多播处理
netinet/ip_output.c 输出多播处理

图12-2 本章讨论的文件

12.2.1 全局变量

本章介绍了三个新的全局变量 (图12-3)。

变 量 数据类型 描 述

ether_ipmulticast_min u_char [] 为IP预留的最小以太网多播地址


ether_ipmulticast_max u_char [] 为IP预留的最大以太网多播地址
ip_mrouter s t r u c t s o c k e t * 多播选路守护程序创建的指向插口的指针

图12-3 本章引入的全局变量

12.2.2 统计量

本章讨论的程序更新全局 ipstat结构中的几个计数器。

ips t a t成员 描 述

ips_forward 被这个系统转发的分组数
ips_cantforward 不能被系统转发的分组数—系统不是一个路由器
ips_noroute 由于无法访问到路由器而无法转发的分组数

图12-4 多播处理统计量

链路级多播统计放在 ifnet结构中(图4-5),还可能统计除 IP以外的其他协议的多播。

12.3 以太网多播地址

I P多播的高效实现要求 I P充分利用硬件级多播,因为如果没有硬件级多播,就不得不在
网络上广播每个多播 I P数据报,而每台主机也不得不检查每个数据报,把那些不是给它的丢
掉。硬件在数据报到达 IP层之前,就把没有用的过滤掉了。
为了保证硬件过滤器能正常工作,网络接口必须把 I P多播组目的地址转换成网络硬件识
别的链路级多播地址。在点到点网络上,如 S L I P和环回接口,必须明确给出地址映射,因为
只能有一个目的地址。在其他网络上,如以太网,也需要有一个明确地完成映射地址的函数。
以太网的标准映射适用于任何使用 802.3寻址方式的网络。
图4 - 1 2显示了以太网单播和多播地址的区别:如果以太网地址的高位字节的最低位是 1,
则它是一个多播地址;否则,它是一个单播地址。单播以太网地址由接口制造商分配,多播

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 271
地址由网络协议动态分配。

IP到以太网地址映射

因为以太网支持多种协议,所以要采取措施分配多播地址,避免冲突。IEEE管理以太网多播
地址分配。IEEE把一块以太网多播地址分给IANA以支持IP多播。块的地址都以01:00:5e开头。
以00:00:5e开头的以太网单播也被分配给 IANA,但为将来使用预留。
图12-5显示了从一个D类IP地址构造出一个以太网多播地址。
标识以太网 必须是0;IANA
多播地址 预留了1

48位以太
网地址

专用

IANA预留的 32位D类IP地址
以太网前缀
图12-5 IP和以太网地址之间的映射

图1 2 - 5显示的映射是一个多到一的映射。在构造以太网地址时,没有使用 D类I P地址的高


位9比特。32个 IP多播组映射到一个以太网多播地址 (习题12.3)。我们将在 12.14节看到这将如
何影响输入的处理。图 12-6显示了Net/3中实现这个映射的宏。

图12-6 ETHER_MAP_IP_MULTICAST 宏

IP到以太网多播映射
61-71 E T H E R _ M A P _ I P _ M U L T I C A S T实现图12-5所示的映射。 i p a d d r指向D类多播地址,
e n a d d r构造匹配的以太网地址,用 6字节的数组表示。该以太网多播地址的前 3个字节是
0x01,0x00和0x5e,后面跟着 0比特,然后是D类IP地址的低23位。

12.4 ether_multi结构

N e t / 3为每个以太网接口维护一个该硬件接收的以太网多播地址范围表。这个表定义了该
设备要实现的多播过滤。因为大多数以太网设备能选择地接收的地址是有限的,所以 IP层必须
要准备丢弃那些通过了硬件过滤的数据报。地址范围被保存在 ether_multi结构中(图12-7):

Linux公社 www.linuxidc.com
bbs.theithome.com
272 计计TCP/IP详解 卷 2:实现

图12-7 ether_multi 结构

1. 以太网多播地址
1 4 7 - 1 5 3 e n m _ a d d r l o和e n m _ a d d r h i指定需要被接收的以太网多播地址的范围。当
e n m _ a d d r l o和e n m _ a d d r h i相同时,就指定一个以太网地址。 e t h e r _ m u l t i的完整列表
附在每个以太网接口的 a r p c o m结构中 (图3 - 2 6 )。以太网多播独立于 A R P—使用a r p c o m结
构只是为了方便,因为该结构已经存在于所有以太网接口结构中。
我们将看到,这个范围的开头和结尾总是相同的,因为在 N e t / 3中,进程无法指
定地址范围。
e n m _ a c指回相关接口的 a r p c o m结构,e n m _ r e f c o u n t跟踪对e t h e r _ m u l t i结构的使
用。当引用计数变成 0时,就释放 a r p c o m结构。 e n m _ n e x t把单个接口的 e t h e r _ m u l t i结
构做成链表。图 1 2 - 8显示出,有三个 e t h e r _ m u l t i结构的链表附在 l e _ s o f t c [ 0 ]上,这是
我们以太网接口示例的 ifnet结构。

图12-8 有三个ether_multi 结构的LANCE接口

在图12-8中,我们看到:
• 接口已经加入了三个组。很有可能是 2 2 4 . 0 . 0 . 1 (所有主机 )、2 2 4 . 0 . 0 . 2 (所有路由器 )和
224.0.1.2(SGI-dogfight)。因为以太网到 IP地址的映射是一到多的,所以只看到以太网多
播地址的结果,无法确定确切的 I P多播地址。比如,接口可能已经加入了 2 2 5 . 0 . 0 . 1、
225.0.0.2和226.0.1.2组。
• 有了e n m _ a c后向指针,就很容易找到链表的开始,释放某个 e h t e r _ m u l t i结构,无
需再实现双向链表。
• ether_multi只适用于以太网设备。其他多播设备可能有其他实现。
图12-9中的ETHER_LOOKUP_MULTI宏,搜索某个ether_multi结构,找到地址范围。
2. 以太网多播查找
166-177 a d d r l o和a d d r h i指定搜索的范围, a c指向包含了要搜索链表的 a r p c o m结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 273
f o r循环完成线性搜索,在表的最后结束,或者当 e n m _ a d d r l o和e n m _ a d d r h i都分别与和
所提供的 a d d r l o 和a d d r h i 匹配时结束。当循环终止时, e n m 为空或者指向某个匹配的
ether_multi结构。

图12-9 ETHER_LOOKUP_MULTI 宏

12.5 以太网多播接收

从本节以后,本章只讨论 I P多播。但是,在 N e t / 3中,也有可能把系统配置成接收所有以


太网多播分组。虽然对 IP 协议族没有用,但内核的其他协议族可能准备接收这些多播分组。
发出图12-10中的ioctl命令,就可以明确地进行多播配置。

命 令 参 数 函 数 描 述

SIOCADDMULTI s t r u c t i f r e q * ifioctl 在接收表里加上多播地址


SIOCDELMULTI s t r u c t i f r e q * ifioctl 从接收表里删去多播地址

图12-10 多播ioctl 命令

这两个命令被 i f i o c t l(图1 2 - 11 )直接传给 i f r e q结构(图6 - 1 2 )中所指定的接口的设备驱


动程序。

图12-11 ifioctl 函数:多播命令

440-446 如果该进程没有超级用户权限,或者如果接口没有 i f _ i o c t l结构,则i f i o c t l


返回一个错误;否则,把请求直接传给该设备驱动程序。

12.6 in_multi结构

12.4节描述的以太网多播数据结构并不专用于 IP;它们必须支持所有内核支持的任意协议
族的多播活动。在网络级, IP维护着一个与接口相关的 IP多播组表。
为了实现方便,把这个 IP多播表附在与该接口有关的 i n _ i f a d d r结构中。6.5节讲到,这
Linux公社 www.linuxidc.com
bbs.theithome.com
274 计计TCP/IP详解 卷 2:实现

个结构中包含了该接口的单播地址。除了它们都与同一个接口相关以外,这个单播地址与所
附的多播组表之间没有任何关系。
这是Net/3实现的产品。也可以在一个不接收IP单播分组的接口上,支持IP多播组。
图12-12中的in_multi结构描述了每个 IP多播{接口,组}对。

图12-12 in_multi 结构

1. IP 多播地址
111-118 inm_addr是一个D类多播地址 (如224.0.0.1,所有主机组)。inm_ifp指回相关接
口的ifnet结构,而inm_ia指回接口的 in_ifaddr结构。
只有当系统中的某个进程通知内核,它要在某个特定的 {接口,组 }对上接收多播数据报
时,才存在一个 i n _ m u l t i结构。由于可能会有多个进程要求接收发往同一个对上的数据报,
所以 i n m _ r e f c o u n t 跟踪对该对的引用次数。当没有进程对某个特定的对感兴趣时,
i n m _ r e f c o u n t 就变成 0,i n _ m u l t i 结构就被释放掉。这个动作可能会引起相关的
ether_multi结构也被释放,如果此时它的引用计数也变成了 0。
i n m _ t i m e r是第1 3章描述的 I G M P协议实现的一部分,最后, i n m _ n e x t指向表中的下
一个in_multi结构。
图1 2 - 1 3用接口示例 l e _ s o f t c [ 0 ]显示了接口,即它的单播地址和它的 I P多播组表之间
的关系。

图12-13 le 接口的一个 IP 多播组表

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 275
为了清楚起见,我们已经省略了对应的 e t h e r _ m u l t i结构(图1 2 - 3 4 )。如果系统有两个
以太网网卡,第二个可能由 l e _ s o f t c[ 1 ]管理,还可能有它自己的附在 a r p c o m结构的多播
组表。IN_LOOKUP_MULTI宏(图12-14)搜索IP多播表寻找某个特定多播组。
2. IP 多播查找
131-146 I N _ L O O K U P _ M U L T I 在与接口 i f p相关的多播组表中查找多播组 a d d r 。
I F P _ T O _ I A搜索Internet地址表i n _ i f a d d r,寻找与接口 i f p相关的i n _ i f a d d r结构。如果
I F P _ T O _ I A找到一个接口,则 f o r循环搜索它的 I P多播表。循环结束后, i n m为空或指向匹
配的in_multi结构。

图12-14 IN_LOOKUP_MULTI 宏

12.7 ip_moptions结构

运输层通过 i p _ m o p t i o n s结构包含的多播选项控制多播输出处理。例如, U D P调用


ip_output是:
error = ip_output(m, inp->inp_options, &inp->inp_route,
inp->inp_socket->so_options & (SO_DONTROUTE|SO_BROADCAST),
inp->inp_moptions);

在第22章中我们将看到, i n p指向某个 Internet协议控制块 (PCB),并且UDP为每个由进程


创建的s o c k e t关联一个PCB。在PCB内,i n p _ m o p t i o n s是指向某个 i p _ m o p t i o n s结构的
指针。这里我们看到,对每个输出的数据报,都可以给 i p _ o u t p u t 传一个不同的
ip_moptions结构。图12-15是ip_moptions结构的定义。

图12-15 ip_moptions 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
276 计计TCP/IP详解 卷 2:实现

多播选项
100-106 i p _ o u t p u t通过i m o _ m u l t i c a s t _ i f p指向的接口对输出的多播数据报进行选
路。如果imo_multicast_ifp为空,就通过目的站多播组的默认接口 (第14章)。
i m o _ m u l t i c a s t _ t t l为外出的多播数据报指定初始的 IP TTL。默认值是1, 把多播数
据报保留在本地网络内。
如果i m o _ m u l t i c a s t _ l o o p是0,就不回送数据报,也不把数据报提交给正在发送的接
口,即使该接口是多播组的成员。如果 i m o _ m u l t i c a s t _ l o o p是1,并且如果正在发送的接
口是多播组的成员,就把多播数据报回送给该接口。
最后,整数 i m o _ n u m _ m e m b e r s h i p s和数组i m o _ m e m b e r s h i p维护与该结构相关的 {接
口,组 }对。所有对该表的改变都转告给 I P,由 I P 在所连到的本地网络上宣布成员的变化。
i m o _ m e m b e r s h i p数组的每个入口都是指向一个 i n _ m u l t i结构的指针,该 i n _ m u l t i结构
附在适当接口的 in_ifaddr结构上。

12.8 多播的插口选项

图12-16显示了几个 IP 级的插口选项,提供对 ip_moptions结构的进程级访问。

命 令 参 数 函 数 描 述

IP_MULTICAST_IF struct in_addr ip_ctloutput 为外出的多播选择默认接口


IP_MULTICAST_TTL u_char ip_ctloutput 为外出的多播选择默认的 TTL
IP_MULTICAST_LOOP u_char ip_ctloutput 允许或使能回送外出的多播
IP_ADD_MEMBERSHIP struct ip_mreq ip_ctloutput 加入一个多播组
IP_DROP_MEMBERSHIP struct ip_mreq ip_ctloutput 离开一个多播组

图12-16 多播插口选项

我们在图8-31中看到 i p _ c t l o u t p u t函数的整体结构。图 12-17显示了与改变和检索多播


选项有关的情况语句。

图12-17 ip_ctloutput 函数:多播选项

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 277

图12-17 (续)

486-491 所有多播选项都由 i p _ s e t m o p t i o n s 和 i p _ g e t m o p t i o n s 函数处理。


ip_moptions结构由引用传给
539-549 ip_getmoptions和ip_setmoptions,该结构与发布 ioctl命令的那个插口
关联。
对于P R C O _ S E T O P T和P R C O _ G E T O P T两种情况,选项不识别时返回的差错码是
不一样的。 ENOPROTOOPT是更合理的选择。

12.9 多播的TTL值

多播的TTL值难以理解,因为它们有两个作用。 TTL值的基本作用,如 IP分组一样,是限


制分组在互联网内的生存期,避免它在网络内部无限地循环。第二个作用是,把分组限制在
管理边界所指定的互联网的某个区域内。管理区域是由一些主观的词语指定的,如“这个结
点”,“这个公司”,“这个州”等,并与分组开始的地方有关。与多播分组有关的区域叫做它
的辖域(scope)。
RFC 1 1 2 2的标准实现把生存期和辖域这两个概念合并在 I P 首部的一个 T T L值里。当 I P
T T L变成0时,除了丢弃该分组外,多播路由器还给每个接口关联了一个 T T L阈值,限制在该
接口上的多播传输。一个要在该接口上传输的分组必须具有大于或等于该接口阈值的 T T L。
由于这个原因,多播分组可能会在它的 TTL到0之前就被丢弃了。
阈值是管理员在配置多播路由器时分配的,这些值确定了多播分组的辖域。管理员使用
的阈值策略以及数据报的源站与多播接口之间的距离定义多播数据报的初始 TTL值的意义。
图12-18显示了多种应用程序的推荐 TTL值和推荐的阈值。
第一栏是 I P首部中的 i p _ t t l初始值。第二栏是应用程序专用阈值 ([Casner 1993])。第三
栏是与该TTL值相关的推荐的辖域。
例如,一个要与本地结点外的网络通信的接口,多播阈值要被配置成 3 2。所有开始时

Linux公社 www.linuxidc.com
bbs.theithome.com
278 计计TCP/IP详解 卷 2:实现

T T L为3 2 (或小于 3 2 )的数据报到达该接口时, T T L都小于 3 2 (假定源站和路由器之间至少有一


跳),所以它们在被转发到外部网络之前,都被丢弃了—即使TTL远大于0。
T T L初始值是 1 2 8的多播数据报可以通过阈值为 3 2的结点接口(只要它以少于 1 2 8-3 2 = 9 6跳
到达接口),但将被阈值为 128的洲际接口丢弃。

ip_ttl 应用程序 辖 域

0 同一接口
1 同一子网
31 本地事件视频
32 同一地点
63 本地事件音频
64 同一区域
95 IETF频道2视频
127 IETF频道1视频
128 同一州
159 IETF频道2音频
191 IETF频道1音频
223 IETF频道2低速率音频
255 IETF频道1低速率音频,辖域不受限

图12-18 IP多播数据报的 TTL值

12.9.1 MBONE

I n t e r n e t上有一个路由器子网支持 I P 多播选路。这个多播骨干网称为 M B O N E , [ C a s n e r
1993] 对其作了描述。它是为了支持用 I P多播的实验——尤其是用音频和视频数据流的实验。
在M B O N E里,阈值限制了多种数据流传播的距离。在图 1 2 - 1 8中,我们看到本地事件视频分
组总是以 TTL 31开始。阈值为 3 2的接口总是阻止本地事件视频。另外, I E T F频道1低速率音
频,只受到 IP TTL固有的最大2 5 5跳的限制。它能传播通过整个 M B O N E。MBONE 内的路由
器的管理员可以选择阈值,有选择地接受或丢弃 MBONE数据流。

12.9.2 扩展环搜索

多播TTL的另一种用处是,只要改变探测数据报的初始 TTL值,就能在互联网上探测资源。
这个技术叫做扩展环搜索 (expanding-ring search,[Boggs 1982])。初始TTL 为0的数据报只能
到达与外出接口相关的本地网络上的一个资源; T T L为1,则到达本地子网 (如果存在 )上的资
源;TTL为2,则到达相距2跳的资源。应用程序指数地增加 TTL的值,迅速地在大的互联网上
探测资源。
RFC 1546 [Partridge 、M e n d e z和Milliken 1993] 描述了一种相关业务的任播
(a n y c a s t i n g)。任播依赖一组显著的 I P地址来表示更像多播的多个主机的组。与多播
地址不同,网络必须传播所有任播的分组,直到它被至少一个主机接收。这样简化
了应用程序的实现,不再进行扩展环搜索。

12.10 ip_setmoptions函数

i p _ s e t m o p t i o n s 函数块包括一个用来处理各选项的 s w i t c h 语句。图 1 2 - 1 9是
Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 279
ip_setmoptions的开始和结束。下面几节讨论 switch的语句体。

图12-19 ip_setmoptions 函数

650-664 第一个参数, o p t n a m e,指明正在改变哪个多播参数。第二个参数, i m o p,是


指向某个 i p _ m o t i o n s结构的指针。如果 * i m o p不空, i p _ s e t m o p t i o n s修改它所指向的

Linux公社 www.linuxidc.com
bbs.theithome.com
280 计计TCP/IP详解 卷 2:实现

结构。否则, i p _ s e t m o p t i o n s分配一个新的 i p _ m o p t i o n s结构,并把它的地址保存在


* i m o p里。如果没有内存了, i p _ s e t m o p t i o n s立即返回 E N O B U F S。后面的所有错误都通
告e r r o r,e r r o r在函数的最后被返回给调用方。第三个参数, m,指向存放要改变选项数
据的mbuf(图12-16的第二栏)。
1. 构造默认值
665-679 当分配一个新的 ip_moptions结构时,ip_setmoptions把默认的多播接口指
针初始化为空,把默认 T T L初始化为 1 (I P _ D E F A U L T _ M U L T I C A S T _ T T L),使能多播数据报
的回送,并清除组成员表。有了这些默认值后,ip_output查询路由表选择一个输出的接口,
多播被限制在本地网络中,并且,如果输出的接口是目的多播组的成员,则系统将接收它自
己的多播发送。
2. 进程选项
680-860 i p _ s e t m o p t i o n s体由一个 s w i t c h语句组成,其中对每种选项都有一个 c a s e
语句。default情况(对未知选项 )把error设成EOPNOTSUPP。
3. 如果默认值是OK,丢弃结构
861-872 s w i t c h语句之后,i p _ s e t m o p t i o n s检查i p _ m o p t i o n s结构。如果所有多播
选项与它们对应的默认值匹配,就不再需要该结构,将其释放。 i p _ s e t m o p t i o n s返回0或
公布的差错码。

12.10.1 选择一个明确的多播接口: IP_MULTICAST_IF

当o p t n a m e是I P _ M U L T I C A S T _ I F时,传给i p _ s e t m o p t i o n s的m b u f中就包含了多播


接口的单播地址,该地址指定了在这个插口上发送的多播所使用的特定接口。图 1 2 - 2 0是这个
选项的程序。

图12-20 ip_setmoptions 函数:选择多播输出接口

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 281

图12-20 (续)

1. 验证
681-698 如果没有提供 m b u f,或者 m b u f 中的数不是一个 i n _ a d d r 结构的大小,则
i p _ s e t m o p t i o n s 通告一个 E I N V A L差错;否则把数据复制到 a d d r。如果接口地址是
I N A D D R _ A N Y,则丢弃所有前面选定的接口。对后面用这个 i p _ m o p t i o n s结构的多播,将
根据它们的目的多播组进行选路,而不再通过一个明确命名的接口 (图12-40)。
2. 选择默认接口
699-710 如果addr中有地址,就由 INADDR_TO_IFP找到匹配接口的位置。如果找不到匹
配或接口不支持多播,就发布 E A D D R N O T A V A I L 。否则,匹配接口 i f p 成为与这个
ip_moptions结构相关的输出请求的多播接口。

12.10.2 选择明确的多播 TTL:IP_MULTICAST_TTL

当o p t n a m e是I P _ M U L T I C A S T _ T T L时,缓存中有一个字节指定输出多播的 IP TTL。这


个TTL是ip_output在每个发往相关插口的多播数据报中插入的。图 12-21是该选项的程序。

图12-21 ip_setmoptions 函数:选择明确的多播 TTL

验证和选项默认的 TTL
711-720 如果缓存中有一个字节的数据,就把它复制到 imo_multicast_ttl。否则,发
布EINVAL。

12.10.3 选择多播环回: IP_MULTICAST_LOOP

通常,多播应用程序有两种形式:
• 一个系统内一个发送方和多个远程接收方的应用程序。这种配置中,只有一个本地进程
向多播组发送数据报,所以无需回送输出的多播。这样的例子有多播选路守护进程和会
议系统。
• 一个系统内的多个发送方和接收方。必须回送数据报,确保每个进程接收到系统其他发

Linux公社 www.linuxidc.com
bbs.theithome.com
282 计计TCP/IP详解 卷 2:实现

送方的传送。
IP_MULTICAST_LOOP选项(图12-22)为i p _ m o p t i o n s结构选择回送策略。

图12-22 ip_setmoptions 函数:选择多播环回

验证和选择环回策略
721-732 如果m为空,或者没有 1字节数据,或者该字节不是 0或1,就发布EINVAL。否则,
把该字节复制到 imo_multicast_loop。0指明不要把数据报回送, 1允许环回机制。
图1 2 - 2 3 显示了多播数据报的最大辖域值之间的关系: imo_multicast_ttl和
imo_multicast_loop。

图12-23 环回和TTL对多播辖域的影响

图1 2 - 2 3显示了根据发送的环回策略,指定的 T T L值接收多播分组的接口的设置。如果硬
件接收自己的发送,则不管采用什么环回策略,都接收分组。数据报可能通过选路穿过该网
络,并到达与系统相连的其他接口 (习题1 2 . 6 )。如果发送系统本身是一个多播路由器,输出的
分组可能被转发到其他接口,但是,只有一个接口接受它们进行输入处理 (第14章)。

12.11 加入一个IP多播组

除了内核自动加入 (图6-17)的IP所有主机组外,其他组成员是由进程明确发出请求产生的。
加入(或离开)多播组选项比其他选项更多使用。必须修改接口的 i n _ m u l t i表以及其他链路层
多播结构,如我们在以太网中讨论的 ether_multi。
当o p t n a m e 是I P _ A D D M E M B E R S H I P 时, m b u f 中的数据是一个如图 1 2 - 2 4所示的
ip_mreq结构。

图12-24 ip_mreq 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 283
148-151 imr_multiaddr指定多播组,imr_interface用相关的单播IP地址指定接口。
ip_mreq结构指定{接口,组}对表示成员的变化。
图12-25显示了加入和离开与我们的以太网接口例子相关的多播组时,所调用的函数。

图12-25 加入和离开一个多播组

我们从 i p _ s e t m o p t i o n s(图1 2 - 2 6 )的I P _ A D D _ M E M B E R S H I P情况开始,在这里修改


i p _ m o p t i o n s结构。然后我们跟踪请求通过 IP层、以太网驱动程序,一直到物理设备—在
这里,是LANCE以太网网卡。

图12-26 ip_setmoptions 函数:加入一个多播组

Linux公社 www.linuxidc.com
bbs.theithome.com
284 计计TCP/IP详解 卷 2:实现

图12-26 (续)

1. 验证
733-746 ip_setm o p t i o n s从验证该请求开始。如果没有传给 mbuf,或缓存的大小不对,
或结构的地址 (i m r _ m u l t i a d d r)不是一个多播组地址,则 i p _ s e t m o p t i o n s发布E N I V A L。
Mreq指向有效ip_mreq地址。
2. 找到接口
747-774 如果接口的单播地址 (imr_interface)是INADDR_ANY,则ip_setmoptions

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 285
必须找到指定组的默认接口。该多播组构造一个 r o u t e 结构,作为目的地址,并传给
r t a l l o c,由r t a l l o c为多播组找到一个路由器。如果没有路由器可用,则请求失败,产生
错误E A D D R N O T A V A I L。如果找到路由器,则在 i f p中保存指向路由器外出接口的指针,而
不再需要路由器入口,将其释放。
如果i m r _ i n t e r f a c e不是I N A D D R _ A N Y,则请求一个明确的接口。 I N A D D R _ T O _ I F P
宏用请求的单播地址搜索接口。如果没有找到接口或者它支持多播,则请求失败,产生错误
EADDRNOTAVAIL。
8 . 5节描述了 r o u t e结构, 1 9 . 2节描述了 r t a l l o c函数,第 1 4章描述了用路由选
择表选择多播接口。
3. 已经是成员了?
775-792 对请求做的最后检查是检查 imo_membership数组,看看所选接口是否已经是请
求组的成员。如果 f o r循环找到一个匹配,或者成员数组为空,则发布 E A D D R I N U S E或
ETOOMANYREFS,并终止对这个选项的处理。
4. 加入多播组
793-803 此时,请求似乎是合理的了。 i n _ a d d m u l t i安排IP开始接收该组的多播数据报。
i n _ a d d m u l t i返回的指针指向一个新的或已存在的 i n _ m u l t i结构(图1 2 - 1 2 ),该结构位于
接口的多播组表中。这个结构被保存在成员数组中,并且把数组的大小加 1。

12.11.1 in_addmulti函数

i n _ a d d m u l t i和相应的 i n _ d e l m u l t i(图1 2 - 2 7和图1 2 - 3 6 )维护接口已加入多播组的表。


加入请求或者在接口表中增加一个新的 i n _ m u l t i结构,或者增加对某个已有结构的引用次
数。

图12-27 in_addmulti 函数:前半部分

1. 已经是一个成员了
469-487 ip_setmoptions已经证实ap指向一个D类多播地址,ifp指向一个能够多播的

Linux公社 www.linuxidc.com
bbs.theithome.com
286 计计TCP/IP详解 卷 2:实现

接口。 I N _ L O O K U P _ M U L T I(图1 2 - 1 4 )确定接口是否已经是该组的一个成员。如果是,则


in_addmulti更新引用计数后返回。
如果接口还不是该组的成员,则执行图 12-28中的程序。

图12-28 in_addmulti 函数:后半部分

2. 更新in_multi表
487-509 如果接口还不是成员,则 i n _ a d d m u l t i分配并初始化一个新的 i n _ m u l t i结构,
把该结构插到接口的 in_ifaddr(图12-13)结构中ia_multiaddrs表的前端。
3. 更新接口,通告变化
510-530 如果接口驱动程序已经定义了一个 if_ioctl函数,则i n _ a d d m u l t i构造一个

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 287
包含了该组地址的 i f r e q结构(图4 - 2 3 ),并把 S I O C A D D M U L T I请求传给接口。如果接口拒绝
该请求,则把 i n _ m u l t i 结构从链表中断开,释放掉。最后, i n _ a d d m u l t i 调用
igmp_joingroup,把成员变化信息传播给其他主机和路由器。
in_addmulti返回一个指针,该指针指向 in_multi结构,或者如果出错,则为空。

12.11.2 slioctl和loioctl函数:SIOCADDMULTI和SIOCDELMULTI

S L I P和环回接口的多播组处理很简单:除了检查差错外,不做其他事情。图 1 2 - 2 9显示了
SLIP处理。

图12-29 slioctl 函数:多播处理

673-687 不管请求为空还是不适用于 AF_INET协议族,都返回 EAFNOSUPPORT。


图12-30显示了环回处理。

图12-30 lioctl 函数:多播处理

152-166 环回接口的处理等价于图 1 2 - 2 9中S L I P 的程序。不管请求为空还是不适用于


AF_INET协议族,都返回 EAFNOSUPPORT。

Linux公社 www.linuxidc.com
bbs.theithome.com
288 计计TCP/IP详解 卷 2:实现

12.11.3 leioctl函数:SIOCADDMULTI和SIOCDELMULTI

在图4 - 2中,我们讲到 L A N C E以太网驱动程序的 l e i o c t l和i f _ i o c t l函数。图 1 2 - 3 1是


处理SIOCADDMULTI和SIOCDELMULTI的程序。

图12-31 leioctl 函数:多播处理

657-671 leioctl把增加和删除请求直接传给 ether_addmulti或ether_delmulti函


数。如果请求改变了该物理硬件必须接收的 I P多播地址集,则两个函数都返回 E N E T R E S E T。
如果发生了这种情况,则 leioctl调用lereset,用新的多播接收表重新初始化该硬件。
我们没有显示 l e r e s e t ,因为它是 L A N C E以太网硬件专用的。对多播来说,
l e r e s e t安排硬件接收所有寻址到 e t h e r _ m u l t i中与该接口相关的多播地址的帧。
如果多播表中的每个入口是一个地址,则 L A N C E驱动程序采用散列机制。散列程序
使硬件可以有选择地接收分组。如果驱动程序发现某个入口是一个地址范围,它废
除散列策略,配置硬件接收所有多播分组。如果驱动程序必须回到接收所有以太网
多播地址的状态, lereset就在返回时把IFP_ALLMULTI标志位置位。

12.11.4 ether_addmulti函数

所有以太网驱动程序都调用 e t h e r _ a d d m u l t i函数处理 S I O C A D D M U L T I请求。这个函


数把IP D类地址映射到合适的以太网多播地址 (图12-5)上,并更新 ether_multi表。图12-32
是ether_multi函数的前半部。
1. 初始化地址范围
366-399 首先,e t h e r _ a d d m u l t i初始化a d d r l o和a d d r h i (两者都是六个无符号字符 )
中的多播地址范围。如果所请求的地址来自 A F _ U N S P E C族,e t h e r _ a d d m u l t i假定该地址
是一个明确的以太网多播地址,并把它复制到 a d d r l o和a d d r h i中。如果地址属于 A F _ I N E T
族 , 并 且 是 INADDR_ANY (0.0.0.0), ether_addmulti把 addrlo初 始 化 成
e t h e r _ i p m u l t i c a s t _ m i n,把a d d r h i初始化成 e t h e r _ i p m u l t i c a s t _ m a x。这两个以
太网地址常量定义为:
u_char ether_ipmulticast_min[6] = { 0x01, 0x00, 0x5e, 0x00, 0x00, 0x00 };
u_char ether_ipmulticast_max[6] = { 0x01, 0x00, 0x5e, 0x7f, 0xff, 0xff };

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 289

图12-32 ether_addmulti 函数:前一半

与e t h e r b r o a d c a s t a d d r( 4 . 3节)一样,这是一个很方便地定义一个 48 bit常量
的方法。
I P多播路由器必须监听所有 I P多播。把组指定为 I N A D D R _ A N Y,被认为是请求加入所有
I P多播组。在这种情况下,所选择的以太网地址范围跨越了分配给 I A N A的整个 I P多播地址
块。
当 m r o u t e d ( 8 )守 护 程 序 开 始 对 到 多 播 接 口 的 分组 进 行 路 选 时 , 它用
INADDR_ANY发布一个 SIOCADDMULTI请求。
E T H E R _ M A P _ I P _ M U L T I C A S T把其他特定的 I P多播组映射到合适的以太网多播地址。当
发生EAFNOSUPPORT错误时,将拒绝对其他地址族的请求。
尽管以太网多播表支持地址范围,但是除了列举出所有地址外,进程或内核无法对某个
特定范围提出请求,因为总是把 addrlo和addrhi设成同一值。
e t h e r _ a d d m u l t i的第二部分,显示如图 1 2 - 3 3,证实地址范围,并且,如果该地址是
Linux公社 www.linuxidc.com
bbs.theithome.com
290 计计TCP/IP详解 卷 2:实现

新的,就把它加入表中。

图12-33 ether_addmulti 函数:后一半

2. 已经在接收
400-418 e t h e r _ a d d m u l t i检查高地址和低地址的多播比特位 (图4-12),保证它们是真正
的以太网多播地址。 E T H E R _ L O O K U P _ M U L T I(图12-9)确定硬件是否已经对指定的地址开始监
听。如果是,则增加匹配的 e t h e r _ m u l t i 结构中的引用计数 (e n m _ r e f c o u n t ),并且
ether_addmulti返回0。
3. 更新ether_multi表
419-441 如果这是一个新的地址范围,则分配并初始化一个新的 e t h e r _ m u l t i结构,把
它链到接口 a r p c o m结构(图1 2 - 8 )中的a c _ m u l t i a d d r s表上。如果 e t h e r _ a d d m u l t i返回

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 291
ENETRESET,则调用它的设备驱动程序就知道多播表被改变了,必须更新硬件接收过滤器。
图1 2 - 3 4显示在 L A N C E以太网接口加入所有主机组后, i p _ m o p t i o n s、i n _ m u l t i和
ether_multi结构之间的关系。

图12-34 多播数据结构的整体图

12.12 离开一个 IP多播组

通常 情 况 下 ,离 开一 个多 播组 的步 骤 是 加 入一 个多 播组 的步 骤的 反 序 。 更新
i p _ m o p t i o n s结构中的成员表、 I P接口的 i n _ m u l t i表和设备的 e t h e r _ m u l t i表。首先,
我们回到 ip_setmoptions中的IP_DROP_MEMBERSHIP情况语句,如图 12-35所示。

图12-35 ip_setmoptions 函数:离开一个多播组

Linux公社 www.linuxidc.com
bbs.theithome.com
292 计计TCP/IP详解 卷 2:实现

图12-35 (续)

1. 验证
804-830 存储器缓存中必然包含一个 ip_mreq结构,其中的imr_multiaddr必须是一个
多播组,而且必须有一个接口与单播地址 i m r _ i n t e r f a c e相关。如果这些条件不满足,则
发布EINVAL和EADDRNOTAVAIL错误信息,继续到该 switch语句的最后进行处理。
2. 删除成员引用
831-856 f o r循环用请求的 {接口,组 }对在组成员表中寻找一个 i n _ m u l t i结构。如果没
有找到,则发布 E A D D R N O T A V A I L错误信息。如果找到了,则 i n _ d e l m u l t i更新i n _ m u l t i
表,并且第二个 f o r循环把成员数组中不用的入口删去,把后面的入口向前移动。数组的大
小也被相应更新。

12.12.1 in_delmulti函数

因为可能会有多个进程接收多播数据报,所以调用 i n _ d e l m u l t i(图1 2 - 3 6 )的结果是,


当对in_multi结构没有引用时,只离开指定的多播组。
更新in_multi结构
534-567 i n _ d e l m u l t i一开始就减少 i n _ m u l t i结构的引用计数,如果该计数非零,则
返回。如果该计数减为 0,则表明在指定的 {接口,组 }对上,没有其他进程等待多播数据报。
调用igmp_leavegroup,但该函数不做任何事情,我们将在 13.8节中看到。
for循环遍历in_multi结构的链表,找到匹配的结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 293

图12-36 in_delmulti 函数

for循环体只包含一个 continue语句。但所有工作都由循环上面的表达式做了,
并不需要continue语句,只是因为它比只有一个分号更清楚一些。
图12-9中的宏E T H E R _ L O O K U P _ M U L T I不用c o n t i n u e语句,仅有一个分号几乎
是不可检测的。
循环结束后,把匹配的 i n _ m u l t i 结构从链表上断开, i n _ d e l m u l t i向接口发布
S I O C D E L M U L T I请求,以便更新所有设备专用的数据结构。对以太网接口来说,这意味着更
新e t h e r _ m u l t i表。最后释放in_multi结构。
L A N C E驱动程序的 S I O C D E L M U L T I情况语句包括在图 1 2 - 3 1中,这里我们也讨
论了SIOCADDRMULTI情况。

12.12.2 ether_delmulti函数

当 I P 释放与某个以太网设备相关的 i n _ m u l t i 结构时,该设备也可能释放匹配的
e t h e r _ m u l t i 结构。我们说“可能”是因为 I P忽略其他监听 I P多播的软件。当 e t h e r _

Linux公社 www.linuxidc.com
bbs.theithome.com
294 计计TCP/IP详解 卷 2:实现

multi结构的引用计数变成 0时,就释放该结构。图 12-37是 ether_delmulti函数。

图12-37 ether_delmulti 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 295

图12-37 (续)

445-479 e t h e r _ d e l m u l t i函数用 e t h e r _ a d d r m u l t i 函数采用的同一方法初始化


addrlo和addrhi数组。
1. 寻找ether_multi结构
480-494 E T H E R _ L O O K U P _ M U L T I寻找匹配的 e t h e r _ m u l t i结构。如果没有找到,则返
回 E N X I O 。如果找到匹配的结构,则把引用计数减去 1 。如果此时引用计数非零,
e t h e r _ d e l m u l t i立即返回。在这种情况下,可能会由于其他协议也要接收相同的多播分组
而释放该结构。
2. 删除ether_multi结构
495-511 for循环搜索ether_multi表,寻找匹配的地址范围,并从链表中断开匹配的结
构,将它释放掉。最后,更新链表的长度,返回 E N E T R E S E T,使设备驱动程序可以更新它的
硬件接收过滤器。

12.13 ip_getmoptions函数

取得当前的选项设置比设置它们要容易。 ip_getmoptions完成所有的工作,如图 12-38


所示。
复制选项数据和返回
876-914 i p _ g e t m o p t i o n s 的三个参数是: o p t n a m e ,要取得的选项; i m o,
i p _ m o p t i o n s结构;m p,一个指向 m b u f的指针。 m _ g e t分配一个 m b u f存放该选项数据。这
三个选项的指针 (分别是 a d d r、t t l和l o o p)被初始化为指向 m b u f的数据域,而 m b u f的长度
被设成选项数据的长度。
对I P _ M U L T I C A S T _ I F,返回I F P _ T O _ I A发现的单播地址,或者如果没有选择明确的多
播接口,则返回 INADDR_ANY。
对I P _ M U L T I C A S T _ T T L,返回i m o _ m u l t i c a s t _ t t l,或者如果没有选择明确的 TTL,
则返回1(IP_DEFAULT_MULTICAST_TTL)。
对I P _ M U L T I C A S T _ L O O P,返回i m o _ m u l t i c a s t _ l o o p,或者如果没有选择明确的多
播环回策略,则返回 1(IP_DEFAULT_MULTICAST_LOOP)。
最后,如果不识别该选项,则返回 EOPNOTSUPP。

Linux公社 www.linuxidc.com
bbs.theithome.com
296 计计TCP/IP详解 卷 2:实现

图12-38 ip_getmoptions 函数

12.14 多播输入处理: ipintr函数

到目前为止,我们已经讨论了多播选路,组成员关系,以及多种与 I P和以太网多播有关
的数据结构,现在转入讨论对多播数据报的处理。
在图4 - 1 3中,我们看到 e t h e r _ i n p u t检测到达的以太网多播分组,在把一个 I P分组放到
I P输入队列之前 (i p i n t r q),把m b u f首部的 M _ M C A S T标志位置位。 i p i n t r函数按顺序处理
每个分组。我们在 ipintr中省略的多播处理程序如图 12-39所示。
该段代码来自于 i p i n t r程序,用来确定分组是寻址到本地网络还是应该被转发。此时,
已经检测到分组中的错误,并且已经处理完分组的所有选项。 ip指向分组内的IP首部。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 297
如果被配置成多播路由器,就转发分组
214-245 如果目的地址不是一个 I P多播组,则跳过整个这部分代码。如果地址是一个多播
组,并且系统被配置成 I P 多播路由器 (i p _ m r o u t e r ) ,就把 i p _ i d 转换成网络字节序
(i p _ m f o r w a r d希望的格式 ),并把分组传给 i p _ m f o r w a r d。如果出现错误或者分组是通过
一个多播隧道 (multicast tunnel)到达的,则 i p _ m f o r w a r d返回一个非零值。分组被丢弃,且
ips_cantforward的值加1。
我们在第14章中描述了多播隧道。它们在两个被标准 IP路由器隔开的多播路由器
之间传递分组。通过隧道到达的分组必须由 i p _ m f o r w a r d处理,而不是由 i p i n t r
处理。
如果ip_mforward返回0,则把ip_id转换回主机字节序,由 ipintr继续处理分组。
如果i p指向一个 I G M P分组,则接受该分组,并在 o u r s处(图1 0 - 11的i p i n t r)继续执行。
不管到达接口的每个目的组或组成员是什么,多播路由器必须接受所有 I G M P分组。 I G M P分
组中有组成员变化的信息。
246-257 根据系统是否被配置成多播路由器来确定是否执行图 1 2 - 3 9中的其余程序。
I N _ L O O K U P _ M U L T I搜索接口加入的多播组表。如果没有找到匹配,则丢弃该分组。当硬件
过滤器接受不需要的分组时,或者当与接口相关的多播组与分组中的目的多播地址映射到同
一个以太网地址时,才出现这种情况。
如果接受了该分组,就继续执行 ipintr(图10-11)的ours标号处的语句。

图12-39 ipintr 函数:多播输入处理

Linux公社 www.linuxidc.com
bbs.theithome.com
298 计计TCP/IP详解 卷 2:实现

图12-39 (续)

12.15 多播输出处理: ip_output函数

当我们在第 8章讨论i p _ o u t p u t时,推迟了对i p _ o u t p u t的m p参数和多播处理程序的讨


论。在 i p _ o u t p u t中,如果m p指向一个 i p _ m o p t i o n s结构,它就覆盖多播输出处理的默认
值。i p _ o u t p u t中省略的程序在图 1 2 - 4 0和图1 2 - 4 1中显示。 i p指向输出的分组, m指向包含
该分组的mbuf,ifp指向路由表为目的多播组选择的接口。

图12-40 ip_output 函数:默认和源地址

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计299

图12-40 (续)

图12-41 ip_output 函数:环回、转发和发送

Linux公社 www.linuxidc.com
bbs.theithome.com
300 计计TCP/IP详解 卷 2:实现

1. 建立默认值
129-155 只有分组是到一个多播组时,才执行图 1 2 - 4 0中的程序。此时, i p _ o u t p u t把
m b u f中的M _ M C A S T置位,并把 d s t重设成最终目的地址,因为 i p _ o u t p u t可能曾把它设成
下一跳路由器(图8-24)。
如果传递了一个 i p _ m o p t i o n s结构,则相应地改变 i p _ t t l和i f p。否则,把 i p _ t t l
设成1 (I P _ D E F A U L T _ M U L T I C A S T _ T T L),避免多播分组到达某个远程网络。查询路由表或
i p _ m o p t i o n s结构所得到的接口必须支持多播。如果不支持,则 i p _ o u t p u t丢弃该分组,
并返回ENETUNREACH。
2. 选择源地址
156-167 如果没有指定源地址,则由 f o r循环找到与输出接口相关的单播地址,并填入 I P
首部的ip_src。
与单播分组不同,如果系统被配置成一个多播路由器,则必须在一个以上的接口上发送
输出的多播分组。即使系统不是一个多播路由器,输出的接口也可能是目的多播组的一个成
员,也会需要接收该分组。最后,我们需要考虑一下多播环回策略和环回接口本身。把所有
这些都考虑进去,共有三个问题:
• 是否要在输出的接口上接收该分组?
• 是否向其他接口转发该分组?
• 是否在出去的接口发送该分组?
图12-41显示了ip_output中解决这三个问题的程序。
3. 是否环回?
168-176 如果 I N _ L O O K U P _ M U L T I 确定输出的接口是目的多播组的成员,而且
i m o _ m u l t i c a s t _ l o o p非零,则分组被 i p _ m l o o p b a c k放到输出接口上排队,等待输入。
在这种情况下,不考虑转发原始分组,因为在输入过程中如果需要,分组的复制会被转发
的。
4. 是否转发?
178-197 如果分组不是环回的,但系统被配置成一个多播路由器,并且分组符合转发的条
件,则 i p _ m f o r w a r d向其他多播接口分发该分组的备份。如果 i p _ m f o r w a r d没有返回 0,
则ip_output丢弃该分组,不发送它。这表明分组中有错误。
为了避免 i p _ m f o r w a r d 和i p _ o u t p u t 之间的无限循环, i p _ m f o r w a r d 在调用
i p _ o u t p u t之前,总是把 I P _ F O R W A R D I N G打开。在本系统上产生的数据报是符合转发条件
的,因为运输层不打开 IF_FORWARDING。
5. 是否发送?
198-209 T T L是0的分组可能被环回,但从不转发它们 (i p _ m f o r w a r d丢弃它们 ),也从不
被发送。如果 T T L是0或者如果输出接口是环回接口,则 i p _ o u t p u t丢弃该分组,因为 T T L
超时,或者分组已经被 ip_mloopback环回了。
6. 发送分组
210-211 到这个时候,分组应该已经从物理上在输出接口上被发送了。
s e n d i t(i p _ o u t p u t,图8 - 2 5 )处的程序在把分组传给接口的 i f _ o u t p u t函数之前可能已经
把它分片了。我们将在 2 1 . 1 0节中看到,以太网输出函数 e t h e r _ o u t p u t调用a r p r e s o l v e,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 12章 IP 多 播计计 301
a r p r e s o l v e又调用E T H E R _ M A P _ M U L T I C A S T,由E T H E R _ M A P _ M U L T I C A S T根据I P多播目
的地址构造一个以太网多播目的地址。

ip_mloopback函数

i p _ m l o o p b a c k 依靠 l o o u t p u t(图5 - 2 7 )完成它的工作。 i p _ m l o o p b a c k 传递的


l o o u t p u t 不是指向环回接口的指针,而是指向输出多播接口的指针。图 1 2 - 4 2 显示了
ip_mloopback函数。

图12-42 ip_mloopback 函数

复制并把分组放到队列中
929-956 仅仅复制分组是不够的;必须看起来分组已经被输出接口接收了,所以
i p _ m l o o p b a c k把i p _ l e n和i p _ o f f转换成网络字节序,并计算分组的检验和。 l o o u t p u t
把分组放到 IP输入队列。

12.16 性能的考虑

N e t / 3的多播实现有几个潜在的性能瓶颈。因为许多以太网网卡并不能完美地实现对多播
地址的过滤,所以操作系统必须能够丢弃那些通过硬件过滤器的分组。在最坏的情况下,以
太网网卡可能会接收所有分组,而其中大部分可能会被 i p i n t r发现不具有合法的 IP多播组地
址。
I P用简单的线性表和线性搜索过滤到达的 I P数据报。如果表增长到一定长度后,某些高
速缓存技术,如移动最近接收地址到表的最前面,将有助于提高性能。

12.17 小结

本章我们讨论了一个主机如何处理 I P多播数据报。我们看到,在 I P的D类地址和以太网多


Linux公社 www.linuxidc.com
bbs.theithome.com
302 计计TCP/IP详解 卷 2:实现

播地址的格式及它们之间的映射关系。
我们讨论了i n - m u l t i和e t h e r _ m u l t i结构,每个IP多播接口都维护一个它自己的组成
员表,而每个以太网接口都维护一个以太网多播地址。
在输入处理中,只有到达接口是目的多播组的成员时,该 I P多播才被接受下来。尽管如
果系统被配置成多播路由器,它们也可能被继续转发到其他接口。
被配置成多播路由器的系统必须接受所有接口上的所有多播分组。只要为 I N A D D R _ A N Y
地址发布SIOCADDMULTI命令,就可以迅速做到这一点。
i p _ m o p t i o n s结构是多播输出处理的基础。它控制对输出接口的选择、多播数据报 TTL
辖域值的设置以及环回策略。它也控制对 i n _ m u l t i结构的引用计数,从而决定接口加入或
离开某个IP多播组的时机。
我们也讨论了多播 TTL值实现的两个概念:分组生存期和分组辖域。

习题

12.1 发送I P广播分组到 2 5 5 . 2 5 5 . 2 5 5 . 2 5 5和发送I P多播给所有主机组 2 2 4 . 0 . 0 . 1的区别是什


么?
12.2 为什么用多播代码中的 I P单播地址标识接口?如果接口能发送和接收多播地址,但
没有一个单播IP地址,必须做什么改动?
12.3 在12.3节中,我们讲到 32个IP组地址被映射到同一个以太网地址上。因为 32 bit地址
中的9 bit 不在映射中。为什么我们不说 512(29)个IP 组被映射到一个以太网地址上?
12.4 你认为为什么把 I P _ M A X _ M E M B E R S H I P S设成20?能被设得更大一些吗?提示:考
虑ip_moptions结构(图12-15)的大小。
12.5 当一个多播数据报被 I P环回并且被发送它的硬件接口接收 (即一个非单工接口 )时,
会发生什么情况?
12.6 画一个有一个多接口主机的网络图,即使该主机没有被配置成多播路由器,其他接
口也能接收到在某个接口上发送的多播分组。
12.7 通过SLIP和环回接口而不是以太网接口跟踪成员增加请求。
12.8 进程如何请求内核加入多于 IP_MAX_MEMBERSHIPS个组?
12.9 计算环回分组的检验和是多余的。设计一个方法,避免计算环回分组的检验和。
12.10 接口在不重用以太网地址的情况下,最多可加入多少个 IP多播组中?
12.11 细心的读者可能已经注意到 i n _ d e l m u l t i在发布 S I O C D E L M U L T I请求时,假定
接口已经定义了 ioctl函数。为什么这样不会出错?
12.12 如果请求一个未识别的选项,则 i p _ g e t m o p t i o n s中分配的 m b u f将会发生什么
情况?
12.13 为什么把组成员机制与用于接收单播和广播数据报的绑定机制分离开来?

Linux公社 www.linuxidc.com
bbs.theithome.com
第13章 IGMP:Internet组管理协议
13.1 引言

I G M P在本地网络上的主机和路由器之间传达组成员信息。路由器定时向“所有主机组”
多播I G M P查询。主机多播 I G M P报告报文以响应查询。 I G M P规范在RFC 111 2中。卷1的第1 3
章讨论了IGMP的规范,并给出了一些例子。
从体系结构的观点来看, IGMP是位于IP上面的运输层协议。它有一个协议号 (2),它的报
文是由 I P数据报运载的 (与I C M P一样)。与I C M P一样,进程通常不直接访问 I G M P,但进程可
以通过 I G M P插口发送或接收 I G M P报文。这个特性使得能够把多播选路守护程序作为用户级
进程实现。
图13-1显示了Net/3中IGMP协议的整体结构。

图13-1 IGMP处理概要

I G M P处理的关键是一组在图 1 3 - 1中心显示的 i n _ m u l t i 结构。到达的 I G M P 查询使


i g m p _ i n p u t为每个 i n _ m u l t i结构初始化一个递减定时器。该定时器由 i g m p _ f a s t t i m o
更新,当每个定时器超时时, igmp_fasttimo调用igmp_sendreport。
我们在第 1 2章中看到,当创建一个新的 i n _ m u l t i结构时, i p _ s e t m o p t i o n s调用
i g m p _ j o i n g r o u p。i g m p _ j o i n g r o u p调用i g m p _ s e n d r e p o r t来发布新的组成员信息,
使组的定时器能够在短时间内安排第二次通告。 igmp_sendreport完成对IGMP报文的格式

Linux公社 www.linuxidc.com
bbs.theithome.com
304 计计TCP/IP详解 卷 2:实现

化,并把它传给 ip_output。
在图13-1的左边和右边,我们看到一个原始插口可以直接发送和接收 IGMP报文。

13.2 代码介绍

图13-2中列出了实现IGMP协议的4个文件。

文 件 描 述

netinet/igmp.h IGMP协议定义
netinet/igmp_var.h IGMP实现定义
netinet/in_var.h IP多播数据结构
netinet/igmp.c IGMP协议实现

图13-2 本章讨论的文件

13.2.1 全局变量

本章中介绍的新的全局变量显示在图 13-3中。

变 量 数据类型 描 述

igmp_all_hosts_group u_long 网络字节序的“所有主机组”地址


igmp_timer_are_running int 如果所有 IGMP定时器都有效,则为真;否则为假
igmp_stat s t r u c t i g m p s t a t IGMP统计(图13-4)

图13-3 本章介绍的全局变量

13.2.2 统计量

IGMP统计信息是在图 13-4的igmpstat变量中维护的。

I g m p s t a t成员 描 述

igps_rcv_badqueries 作为无效查询接收的报文数
igps_rcv_badreports 作为无效报告接收的报文数
igps_rcv_badsum 接收的报文检验和错误数
igps_rcv_ourreports 作为逻辑组的报告接收的报文数
igps_rcv_queries 作为成员关系查询接收的报文数
igps_rcv_reports 作为成员关系报告接收的报文数
igps_rcv_tooshort 字节数太少的报文数
igps_rcv_total 接收的全部报文数
igps snd reports 作为成员关系报告发送的报文数

图13-4 IGMP统计

图13-5是在vangogh.cs.berkeley.edu上执行netstat -p igmp命令后,输出的
统计信息。
在图1 3 - 5中,我们看到 v a n g o g h是连到一个使用 I G M P的网络上的,但是 v a n g o g h没有
加入任何多播组,因为 igps_snd_reports是0。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 13章 IGMP:Internet组管理协议计计 305
输出 成员

图13-5 IGMP统计示例

13.2.3 SNMP变量

I G M P没有标准的 SNMP MIB,但 [McCloghrie Farinacci 1994a]描述了一个 I G M P的实验


MIB。

13.3 igmp结构

IGMP报文只有8字节长。图 13-6显示了Net/3使用的igmp结构。

图13-6 igmp结构

igmp_type包括一个4 bit 的版本码和一个 4 bit 的类型码。图13-7显示了标准值。

版本 类型 igmp_type 描 述

1 1 0x11(IGMP_HOST_MEMBERSHIP_QUERY) 成员关系查询
1 2 0 x 1 1 ( I G M P _ H O S T _ M E M B E R S H I P _ R E P O R 成员关系报告
T)
1 3 0x13 DVMRP报文(第14章)

图13-7 IGMP报文类型

IGMP报文

1 1 2 字节 4 字节

IP首部

IP数据报

图13-8 IGMP 报文(省略igmp_ )

Linux公社 www.linuxidc.com
bbs.theithome.com
306 计计TCP/IP详解 卷 2:实现

43-44 N e t / 3只使用版本 1的报文。多播路由器发送 1类报文 (I G M P _ H O S T _ M E M B E R S H I P _


Q U E R Y)向本地网络上所有主机请求成员关系报告。对 1类I G M P报文的响应是主机的一个 2类
报文(I G M P _ H O S T _ M E M B E R S H I P _ R E P O R T),报告它们的多播成员信息。 3类报文在路由器
之间传输多播选路信息 (第14章)。主机不处理3类报文。本章后面部分只讨论 1类和2类报文。
45-46 在I G M P版本1中没有使用i g m p _ c o d e。i g m p _ c k s u m与I P类似,计算I G M P报文的
所有8个字节。
47-48 对查询,igmp_group是0。对回答,它包括报告的多播组。
图13-8是相对于IP数据报的IGMP报文结构。

13.4 IGMP的protosw的结构

图13-9是IGMP的protosw结构。

成 员 I n e t s w[5] 描 述

pr_type SOCK_RAW IGMP提供原始分组服务


pr_domain &inetdomain IGMP是Internet域的一部分
pr_protocol IPROTO_IGMP (2) 显示在IP首部的i p _ p字段
pr_flags PR_ATOMIC|PR_ADDR 插口层标志,协议处理不使用
pr_input igmp_input 从IP层接收报文
pr_output rip_output 向IP层发送IGMP报文
pr_ctlinput 0 IGMP没有使用
pr_ctloutput rip_ctloutput 响应来自进程的管理请求
pr_usrreq rip_usrreq 响应来自进程的通信请求
pr_init igmp_init 为IGMP初始化
pr_fasttimo igmp_fasttino 进程挂起成员关系报告
pr_slowtimo 0 IGMP没有使用
pr_drain 0 IGMP没有使用
pr_sysctl 0 IGMP没有使用

图13-9 IGMP protosw 的结构

尽管进程有可能通过 IGMP p r o t o s w入口发送原始 I P分组,但在本章,我们只考虑内核


如何处理IGMP报文。第32章讨论进程如何用原始插口访问 IGMP。
三种事件触发IGMP处理:
• 一个本地接口加入一个新的多播组 (13.5节);
• 某个IGMP定时器超时 (13.6节);和
• 收到一个IGMP查询(13.7节)。
还有两种事件也触发本地 IGMP处理,但结果不发送任何报文:
• 收到一个IGMP报告(13.7节);和
• 某个本地接口离开一个多播组 (13.8节)。
下一节将讨论这五种事件。

13.5 加入一个组:igmp_joingroup函数

在第 1 2 章中我们看到,当一个新的 i n _ m u l t i 结构被创建时, i n _ a d d m u l t i 调用
i g m p _ j o i n g r o u p。后面加入同一多播组的请求只增加 i n _ m u l t i结构里的引用计数;不调

Linux公社 www.linuxidc.com
bbs.theithome.com
第 13章 IGMP:Internet组管理协议计计 307
用igmp_joingroup。igmp_joingroup如图13-10所示。

图13-10 igmp_joingroup 函数

164-178 inm指向组的新 i n _ m u l t i结构。如果新的组是“所有主机组”,或成员关系请求


是环回接口的,则 i n m _ t i m e r被禁止, i g m p _ j o i n g r o u p返回。不报告“所有主机组”的
成员关系,因为假定每个多播主机都是该组的成员。没必要向环回接口发送组成员报告,因
为本地主机是在回路网络上的唯一系统,它已经知道它的成员状态了。
在其他情况下,新组的报告被立即发送,并根据组的情况为组定时器选择一个随机的值。
全局标志位 i g m p _ t i m e r s _ a r e _ r u n n i n g 被设置,表明至少使能一个定时器。
igmp_fasttimo (13.6节)检查这个变量,避免不必要的处理。
59-73 当新组的定时器超时,就发布第 2次成员关系报告。复制报告是无害的,当第一次报
告丢失或被破坏时,有了它就保险了。 IGMP_RANDOM_DELAY (13-11图)计算报告时延。

图13-11 IGMP_RANDOM_DELAY 函数

根据 R F C 1 1 2 2,报告定时器必须设成 0到 1 0之间的随机秒数 (I G M P _ M A X _ H O S T _
R E P O R T _ D E L A Y)。因为 IGMP 定时器每秒被减去 5次(P R _ F A S T H Z),所以 I G M P _ R A N D O M _
D E L A Y必须选择一个在 1 ~ 5 0之间的随机数。如果 r是把接到的所有 I P分组数、主机的原始地址
和多播组相加后得到的随机数,则

Linux公社 www.linuxidc.com
bbs.theithome.com
308 计计TCP/IP详解 卷 2:实现

0≤(rmod50)≤49

1≤(rmod50)+1≤50
要避免为0,因为这会禁止定时器,并且不发送任何报告。

13.6 igmp_fasttimo函数

在讨论igmp_fasttino之前,我们需要描述一下遍历 in_multi结构的机制。
为找到各个 i n _ m u l t i结构, N e t / 3必须遍历每个接口的 i n _ m u l t i表。在遍历过程中,
in_multistep结构(如图13-12所示)记录位置。

图13-12 in_multistep 函数

123-126 i_ia指向下一个 in_ifaddr接口结构, i_inm指向当前接口的 in_multi结构。


IN_FIRST_MULTI和IN_NEXT_MULTI宏(显示如图13-13)遍历该表。

图13-13 IN_FIRST_MULTI 和IN_NEXT_MULTI 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 13章 IGMP:Internet组管理协议计计 309
154-169 如果 i n _ m u l t i表有多个入口, i _ i n m就前进到下一个入口。当 I N _ N E X T _
M U L T I到达多播表的最后时, i _ i a就指向下一个接口, i _ i n m指向与该接口相关的第一个
i n _ m u l t i结构。如果该接口没有多播结构, w h i l e循环继续遍历整个接口表,直到搜索完
所有接口。
170-177 i n _ m u l t i s t e p数组初始化时,指向 i n _ i f a d d r表的第一个 i n _ i f a d d r结构,
i_inm设成空。 IN_NEXT_MULTI找到第一个 in_multi结构。
从图 1 3 - 9我们知道, i g m p _ f a s t t i m o 是I G M P 的快速超时函数,每秒被调用 5次。
igmp_fasttimo(如图13-14)递减多播报告定时器,并在定时器超时时发送一个报告。

图13-14 igmp_fasttino 结构

187-198 如果i g m p _ t i m e r s _ a r e _ r u n n i n g为假,i g m p _ f a s t t i m o立即返回,不再浪


费时间检查各个定时器。
199-213 i g m p _ f a s t t i m o重新设置运行标志位,用 I N _ F I R S T _ M U L T I初始化 s t e p和
i n m。i g m p _ f a s t t i m o函数用w h i l e循环找到各个 i n _ m u l t i结构和I N _ N E X T _ M U L T I宏。
对每个结构:
• 如果定时器是0,什么都不做。
• 如果定时器不是 0,则将其递减。如果到达 0,则发送一个IGMP组成员关系报告。
• 如果定时器还不是 0,则至少还有一个定时器在运行,所以把 i g m p _ t i m e r s _ a r e _
running设成1。

Linux公社 www.linuxidc.com
bbs.theithome.com
310 计计TCP/IP详解 卷 2:实现

igmp_sendreport函数

igmp_sendreport函数(图13-15)为一个多播组构造和发送 IGMP报告报文。

图13-15 igmp_sentreport 函数

214-232 唯一的参数 i n m指向被报告组的 i n _ m u l t i结构。i g m p _ s e n d r e p o r t分配一个


新的m b u f,准备存放一个 I G M P报文。i g m p _ s e n d r e p o r t为链路层首部留下空间,把 m b u f
Linux公社 www.linuxidc.com
bbs.theithome.com
第 13章 IGMP:Internet组管理协议计计 311
的长度和分组的长度设成 IGMP报文的长度。
233-245 每次构造IP首部和IGMP报文的一个字段。数据报的源地址设成 INADDR_ANY,目
的地址是被报告的多播组。 i p _ o u t p u t用输出接口的单播地址替换 I N A D D R _ A N Y。每个组成
员和所有多播路由器都接收报告 (因为路由器接收所有 IP多播)。
2 4 6 - 2 6 0 最后,i g m p _ s e n t r e p o r t构造一个 i p _ m o p t i o n s结构,并把它与报文一起传
给i p _ o u t p u t。与i n _ m u l t i结构相关的接口被选做输出的接口; T T L被设成 1,使报告只
在本地网络上;如果本地系统被配置成路由器,则允许这个请求的多播环回。
进程级的多播路由器必须监听成员关系报告。在 1 2 . 1 4节中我们看到,当系统被
配置成多播路由器时,总是接收 I G M P数据报。通过普通的运输层分用程序把报文传
给IGMP的igmp_input和pr_input函数(图13-9)。

13.7 输入处理:igmp_input函数
在1 2 . 1 4节中,我们描述了 i p i n t r的多播处理部分。我们看到,多播路由器接受所有
I G M P报文,但多播主机只接受那些到达接口是目的多播组成员的 I G M P报文(也即,那些接收
它们的接口是组成员的查询和成员关系报告 )。
标准协议分用机制把接受的报文传给igmp_input。igmp_input的开始和结束如图13-16
所示。下面几节描述每种 IGMP报文类型码。

图13-16 igmp_input 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
312 计计TCP/IP详解 卷 2:实现

图13-16 (续)

1. 验证IGMP报文
52-96 函数i p i n t r传递一个指向接受分组 (存放在一个m b u f中)的指针m,和数据报 I P首部
的大小iphlen。
数据报的长度必须足够容纳一个 I G M P报文(I G M P _ M I N _ L E N),并能被放在一个标准的
m b u f首部中(m _ p u l l u p),而且还必须有正确的 I G M P检验和。如果发现有任何错误,统计错
误的个数,并自动丢弃该数据报, igmp_input返回。
i g m p _ i n p u t 进程体根据 i g m p _ t y p e 内的代码处理无效报文。记得在图 1 3 - 6 中,
i g m p _ t y p e包含一个版本码和一个类型码。 s w i t c h语句基于 i g m p _ t y p e(图1 3 - 7 )中两个值
的结合。下面儿节分别讨论几种情况。
2. 把IGMP报文传给原始IP
1 5 7 - 1 6 3 这个s w i t c h语句没有 d e f a u l t情况。所有有效报文 (也就是,格式正确的报文 )
被传给 r i p _ i n p u t,在r i p _ i n p u t里被提交给所有监听 IGMP报文的进程。监听进程可以自
由处理或丢弃那些具有内核不识别的版本或类型的 IGMP报文。
mrouted依靠对rip_input的调用接收成员关系查询和报告。

13.7.1 成员关系查询: IGMP_HOST_MEMBERSHIP_QUERY

RFC 1075 推荐多播路由器每 1 2 0秒至少发布一次 I G M P 成员关系查询。把查询发到


224.0.0.1组(“所有主机组” )。图13-17显示了主机如何处理报文。
97-122 到达环回接口上的查询报文被自动丢弃 (习题1 3 . 1 )。查询报文被定义成发给“所有

Linux公社 www.linuxidc.com
bbs.theithome.com
第 13章 IGMP:Internet组管理协议计计313
主机组”,到达其他地址的查询报文由 igps_rcv_badqueries统计数量,并被丢弃。

图13-17 IGMP查询报文的输入处理

接受查询报文并不会立即引起IGMP成员报告。相反,igmp_input为与接收查询的接口相
关的各个组定时器设置一个随机的值 I G M P _ R A N D O M _ D E L A Y。当某组的定时器超时,则
igmp_fasttimo发送一个成员关系报告,与此同时,其他所有收到查询的主机也进行同一动作。
一旦某个主机上的某个特定组的随机定时器超时,就向该组多播一个报告。这个报告将取消其
他主机上的定时器,保证只有一个报告在网络上多播。路由器与其他组成员一样,接收该报告。
这个情况的一个例外就是“所有主机组”。这个组不设定时器,也不发送报告。

13.7.2 成员关系报告: IGMP_HOST_MEMBERSHIP_REPORT

接收一个 I G M P成员关系报告是我们在 1 3 . 1节中提到的不会产生 I G M P报文的两种事件之


一。该报文的效果限于接收它的接口本地。图 13-18显示了报文处理。
123-146 和发送到不正确多播组的成员关系报告一样,发到环回接口上的报告被丢弃。也
就是说,报文必须寻址到报文内标识的组。
不完整地初始化的主机的源地址中可能没有网络号或主机号 ( 或两者都没有 ) 。
i g m p _ r e p o r t查看地址的 A类网络部分,如果地址的网络或子网部分是 0,这部分一定为 0。
如果是这种情况,则把源地址设成子网地址,其中包含正在接收接口的网络标识符和子网标
识符。这样做的唯一原因是为了通知子网号所标识的正在接收接口上的某个进程级守护程序。
如果接收接口属于被报告的组,就把相关的报告定时器重新设成 0。从而使发给该组的第
一个报告能够制止其他主机发布报告。路由器只需知道网络上至少有一个接口是组的成员,

Linux公社 www.linuxidc.com
bbs.theithome.com
314 计计TCP/IP详解 卷 2:实现

就无需维护一个明确的组成员表或计数器。

图13-18 IGMP报告报文的输入处理

13.8 离开一个组:igmp_leavegroup函数

我们在 1 2章中看到,当 i n _ m u l t i结构中的引用计数器跳到 0时, i n _ d e l m u l t i调用


igmp_leavegroup。如图13-19所示。

图13-19 igmp_leavegroup 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 13章 IGMP:Internet组管理协议计计 315
179-186 当一个接口离开一个组时, IGMP没有采取任何动作。不发明确的通知—下一次
多播路由器发布 I G M P查询时,接口不为该组生成 I G M P报告。如果没有为某个组生成报告,
则多播路由器就假定所有接口已经离开该组,并停止把到该组的分组在网络上多播。
如果当一个报告被挂起时,接口离开了该组 (就是说,此时组的报告定时器正在计时 ),就
不再发送该报告,因为当 i c m p _ l e a v e g r o u p返回时, i n _ d e l m u l t i(图1 2 - 3 6 )已经把组的
定时器及其相关的 in_multi结构丢掉了。

13.9 小结

本章我们讲述了 I G M P,I G M P在一个网络上的主机和路由器之间传递 I P多播成员信息。


当一个接口加入一个组时,或按照多播路由器发布的 I G M P报告查询报文的要求,生成 I G M P
成员关系报告。
设计IGMP使交换成员信息所需要的报文数最少:
• 当主机加入一个组时,宣布它们的成员关系;
• 对成员关系查询的响应被推迟一个随机的时间,而且第一个响应抑制了其他的响应;
• 当主机离开一个组时,不发通知报文;
• 每分钟发的成员查询不超过一次。
多播路由器与其他路由器共享自己收集的 I G M P信息(第1 4章),以便于把多播数据报传给
多播目的组的远程成员。

习题

13.1 为什么不需要响应在环回接口上到达的 IGMP查询?


13.2 验证图13-15中226到229行的假设。
13.3 对在点到点网络接口上到达的成员关系查询,是否有必要设置随机的延迟时间?

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路
14.1 引言

前面两章讨论了在一个网络上的多播。本章我们讨论在整个互联网上的多播。我们将
讨论 m r o u t e d程序的执行,该程序计算多播路由表,以及在网络之间转发多播数据报的内
核函数。
从技术上说,多播分组 ( p a c k e t )被转发。本章我们假定每个多播分组中都包含一
个完整数据报 (也就是说,没有分片 ),所以我们只用名词数据报 ( d a t a g r a m )。N e t / 3转
发IP分片,也转发IP数据报。
m r o u t e d版本 描 述
图1 4 - 1是m r o u t e d的几个版本及它们和 B S D
版本的对应关系。 m r o u t e d版本包括用户级守护 1.2 修改4.3 BSD Tahoe版本
2.0 包括在4.4 BSD和Net/3中
程序和内核级多播程序。 3.3 修改SunOS 4.1.3
I P多播技术是一个活跃的研究和开发领域。
图14-1 mrouted 和IP多播版本
本章讨论包括在 N e t / 3中的多播软件的 2 . 0版,但
被认为已经过时了。 3 . 3版的发行还有一段时间,因此无法在本书中完整地讨论,但我们在整
个过程中将指出 3.3版本的一些特点。
因为还没有广泛安装商用多播路由器,所以常用多播隧道连接标准 I P单播互联网上的两
个多播路由器,构造多播网络。 Net/3支持多播隧道,并采用宽松源站记录路由 (LSRR,Loose
Source Record Route)选项( 9 . 6节)构造多播隧道。一种更好的隧道技术把 I P多播数据报封装在
一个单播数据报里, 3.3版的多播程序支持这一技术,但 Net/3不支持。
与第1 2章一样,我们用通常名称运输层协议代指发送和接收多播数据报的协议,但 U D P
是唯一支持多播的 Internet协议。

14.2 代码介绍

本章讨论的三个文件显示在图 14-2中。

文 件 描 述

netinet/ip_mroute.h 多播结构定义
n e t i n e t / i p _ m r o u t e . c多播选路函数
netinet/raw_ip.c 多播选路选项

图14-2 本章讨论的文件

14.2.1 全局变量

多播选路程序所使用的全局变量显示在图 14-3中。
Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 317
变 量 数据类型 描 述

cached_mrt struct mrt 多播选路的“后面一个”高速缓存


cached_origin u_long “后面一个” 高速缓存的多播组
cached_originmask u_long “后面一个” 高速缓存的多播组的掩码
mrtstat struct m r t s t a t 多播选路统计
mrttable struct m r t * [ ] 指向多播路由器的指针的散列表
numvifs vifi_t 允许的多播接口数
viftable struct vif[] 虚拟多播接口的数组

图14-3 本章介绍的全局变量

14.2.2 统计量

多播选路程序收集的所有统计信息都放在图 1 4 - 4的m r t s t a t结构中。图 1 4 - 5是在执行


n e t s t a t - g命令后,输出的统计信息。
s

mrts t a t成员 描 述 S N M P使用的

mrts_mrt_lookups 查找的多播路由数
mrts_mrt_misses 高速缓存丢失的多播路由数
mrts_grp_lookups 查找的组地址数
mrts_grp_misses 高速缓存丢失的组地址数
mrts_no_route 查找失败的多播路由数
mrts_bad_tunnel 有错误的隧道选项的分组数
mrts_cant_tunnel 没有空间存放隧道选项的分组数

图14-4 本章收集的统计量

输出 成员

图14-5 IP多播路由选择统计的例子

这些统计信息来自一个有两个物理接口和一个隧道接口的系统。它们说明, 9 8 %的时间,
在高速缓存中发现多播路由。组地址高速缓存的效率稍低一些,最高只有 3 4 %。图1 4 - 3 4描述
了路由缓存,图 14-21描述了组地址高速缓存。

14.2.3 SNMP变量

多播选路没有标准的 SNMP MIB,但 [ M c C l o g h r i e和Farinacci 1994a] 和 [ M c C l o g h r i e和


Farinacci 1994b] 描述一些多播路由器的实验 MIB。

14.3 多播输出处理(续)

1 2 . 1 5节讲到如何为输出的多播数据报选择接口。我们看到在 i p _ m o p t i o n s 结构中
Linux公社 www.linuxidc.com
bbs.theithome.com
318 计计TCP/IP详解 卷 2:实现

i p _ o u t p u t被传给一个明确的接口,或者 i p _ o u t p u t在路由表中查找目的组,并使用在路
由入口中返回的接口。
如果在选择了输出的接口后, i p _ o u t p u t回送该数据报,就把它放在所选输出接口等待
输入处理,当ipintr处理它时,把它当作是要转发的数据报。图 14-6显示了这个过程。

传输协议 传输协议

隧道 以太网

图14-6 有环回的多播输出处理

在图 1 4 - 6 中,虚线箭头代表原始输出的数据报,本例是本地以太网上的多播。
i p _ m l o o p b a c k 创建的备份由带箭头的细线表示;并作为输入被传给运输层协议。当
i p _ m f o r w a r d决定通过系统上的另一个接口转发该数据报时,就产生第三个备份。图 14-6中
最粗的箭头代表第三个备份,在多播隧道上发送。
如果数据报不是回送的,则 ip_output把
它直接传给 ip_mforward,ip_mforward复 传输协议

制并处理该数据报,就像它是从 i p _ o u t p u t
选定的接口上收到的一样。图 1 4 - 7显示了这个
过程。
一旦i p _ m f o r w a r d调用i p _ o u t p u t发送
多播数据报,它就把 I P _ F O R W A R D I N G置位,
这样, i p _ o u t p u t 就不再把数据报传回给 隧道 以太网
ip_mforward,以免导致无限循环。
图14-7 没有环回的多播输出处理
图1 2 - 4 2显示了 i p _ m l o o p b a c k。1 4 . 8节
描述了ip_mforward。

14.4 mrouted守护程序

用户级进程 m r o u t e d守护程序允许和管理多播路由选择。 m r o u t e d实现I G M P协议的路


由部分,并与其他多播路由器通信,实现网络间的多播路由选择。路由算法在 m r o u t e d上实
现,但内核维护多播路由选择表,并转发数据报。
本书中我们只讨论支持 m r o u t e d的内核数据结构和函数—不讨论m r o u t e d本身。我们
讨论用于为数据报选择路由的截断逆向路径广播 TRPB(Truncated Reverse Path Broadcast)算法
[ D e e r i n g和Cheriton 1990] ,以及用于在多播路由器之间传递信息的距离向量多播选路协议
DVMRP。我们力求使读者了解内核多播程序的工作原理。

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 319
RFC 1075 [Waitzman、Partidge 和Deering1988] 是DVMRP的一个老版本。 mrouted实现
了一个新的 D V M R P ,还没有用 R F C 文档写出来。目前,该算法和协议的最好的文档是
mrouted发布的源代码。附录 B指出在哪里能找到到源代码。
m r o u t e d守护程序通过在一个 I G M P插口上设置选项与内核通信 (第3 2章)。这些选项总结
在图14-8中。

optname o p t v a l类型 函 数 描 述

DVMRP_INIT ip_mrouter_init m r o u t e d开始


DVMRP_DONE ip_mrouter_done m r o u t e d被关闭
DVMRP_ADD_VIF struct vifctl add_vif 增加虚拟接口
DVMRP_DEL_VIF vifi_t del_vif 删除虚拟接口
DVMRP_ADD_LGRP s t r u c t l g r p l c t l add_lgrp 为某个接口增加多播组入口
DVMRP_DEL_LGRP s t r u c t l g r p l c t l del_lgrp 为某个接口删除多播组入口
DVMRP_ADD_MRT struct mrtctl add_mrt 增加多播路由
DVMRP_DEL_MRT s t r u c t i n a d d r del_mrt 删除多播路由

图14-8 多播路由插口选项

图1 4 - 8显示的插口选项被 s e t s o c k o p t系统调用传给 r i p _ c t l o u t p u t( 3 2 . 8节)。图1 4 - 9


显示了处理 DVMRP_xxx 选项的rip_ctloutput部分。

图14-9 rip_ctloutput 函数:DVMRP_ xxx 插口选项

173-187 当调用 s e t s o c k o p t 时, o p等于 P R C O _ S E T O P T ,而且所有选项都被传给


ip_mrouter_cmd函数。对于 g e t s o c k o p t系统调用,o p等于P R C O _ G E T O P T;对所有选项
都返回EINVAL。
图14-10显示了ip_mrouter_cmd函数。

图14-10 ip_mrouter_cmd 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
320计计TCP/IP详解 卷 2:实现

图14-10 (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 321
这些“选项”更像命令,因为它们引起内核更新多个数据结构。本章后面我们
将使用命令 (command)一词强调这个事实。
84-92 m r o u t e d 发布的第一个命令必须是 D V M R P _ I N I T 。后续命令必须来自发布
DVMRP_INIT的同一插口。当在其他插口上发布其他命令时,返回 EACCES。
94-142 switch语句的每个 case语句检查每条命令中的数据量是否正确,然后调用匹配函
数。如果不能识别该命令,则返回 E O P N O T S U P P。任何从匹配函数返回的错误都在 e r r o r中
发布,并在函数的最后返回。
初始化时, mrouted发布DVMRP_INIT命令,调用图14-11显示的ip_mrouter_init。

图14-11 ip_mrouter_init 函数:DVMRP_INIT 命令

146-157 如果不是在某个原始 IGMP插口上发布命令,或者如果 D V M R P _ I N I T已经被置位,


则分别返回 EOPNOTSUPP和EADDRINUSE。全局变量ip_mrouter保存指向某个插口的指针,
初始化命令就是在该插口上发布的。必须在该插口上发布后续命令。以避免多个 m r o u t e d进
程的并行操作。
下面几节讨论其他 DVMRP_xxx命令。

14.5 虚拟接口

当作为多播路由器运行时, N e t / 3接收到达的多播数据报,复制它们,并在一个或多个接
口上转发备份。通过这种方式,数据报被转发给互联网上的其他多播路由器。
Ts 隧道 Te
HS A B HG

网络A 任意实现LSRR的单播IP路由器集 网络B

src=HS src=HS src=HS


dst=G dst=Te dst=G
IP数据报
没有LSRR的 IP单播 没有LSRR的
硬件多播 LSRR={TS,G} 硬件多播

图14-12 多播隧道

Linux公社 www.linuxidc.com
bbs.theithome.com
322 计计TCP/IP详解 卷 2:实现

输出的接口可以是一个物理接口,也可以是一个多播隧道。多播隧道的两端都与一个多
播路由器上的某个物理接口相关。多播隧道使两个多播路由器,即使被不能转发多播数据报
的路由器分隔,也能够交换多播数据报。图 14-12是一个多播隧道连接的两个多播路由器。
图1 4 - 1 2中,网络 A上的源主机H S正在向组 G多播数据报。组 G的唯一成员在网络 B上,并
通过一个多播隧道连接到网络 A。路由器 A接收多播 (因为多播路由器接收所有多播 ),查询它
的多播路由选择表,并通过多播隧道转发该数据报。
隧道的开始是路由器 A上的一个物理接口,以 I P单播地址 T s标识。隧道的结束是网络 B上
的一个物理接口,以 I P单播地址 Te 标识。隧道本身是一个任意复杂的网络,由实现 L S R R选项
的IP单播路由器连接起来。图 14-13显示IP LSRR选项如何实现多播隧道。

系统 IP首部 源路由选项 描 述
ip_src ip_dst 偏移 地 址

HS HS G 在网络A上
Ts HS Te 8 Ts • G 在隧道上
Te HS G 12 T s 见正文 在路由器 B上i p _ d o o p t i o n s之后
Te HS G 在路由器 B上i p _ m f o r w a r d之后

图14-13 LSRR多播隧道选项

图1 4 - 1 3的第一行是 H S在网络 A上发送的多播数据报。路由器 A全部接收,因为多播路由


器接收本地连接的网络上的所有数据报。
为通过隧道发送数据报,路由器 A在I P首部插入一个 L S R R选项。第二行是在隧道上离开
A时的数据报。 L S R R选项的第一个地址是隧道的源地址,第二个地址是目的多播组地址。数
据报的目的地址是 T e—隧道的另一端。 LSRR偏移指向目的组。
经过隧道的数据报被转发,通过互联网,直到它到达路由器 B上的隧道的另一端。
该图中的第三行是被路由器 B上的ip_dooptions处理之后的数据报。记得第9章中讲到,
i p _ d o o p t i o n s在i p i n t r检查数据报的目的地址之前处理 LSRR选项。因为数据报的目的地
址(T e)和路由器 B上的一个接口匹配,所以 i p _ d o o p t i o n s把由选项偏移 (本例中是 G )标识的
地址复制到 I P 首部的目的地址字段。在选项内, G 被 i p _ r t a d d r 返回的地址取代,
i p _ r t a d d r通常根据 I P目的地址 (本例中是 G )为数据报选择输出的接口。这个地址是不相关
的,因为 ip_mforward将丢弃整个选项。最后, ip_dooptions把选项偏移向前移动。
图1 4 - 1 3的第四行是 i p i n t r调用i p _ m f o r w a r d之后的数据报。在那里, L S R R选项被识
别,并从数据报首部中移走。得到的数据报看起来就象原始多播数据报,由 i p _ m f o r w a r d
处理它,把它作为多播数据报在网络 B上转发,并被HG收到。
用L S R R构造的多播隧道已经过时了。因为 1 9 9 3年3月发布了 m r o u t e d程序,该程序通过
在I P多播数据报的首部前面加上另一个 I P首部来构造隧道。新 I P首部的协议设置为 4,表明分
组的内容是另一个 I P分组。有关这个值的文档在 RFC 1700—“I P中的I P”协议中。新版本
的mrouted程序为了向后兼容,也支持 LSRR隧道。

14.5.1 虚拟接口表

无论物理接口还是隧道接口,内核都为其在虚拟接口 (virtual interface)表中维护一个入口,


其中包含了只有多播使用的信息。每个虚拟接口都用一个 v i f结构表示 (图1 4 - 1 4 )。全局变量

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 323
viftable是一个这种结构的数组。数组的下标保存在无符号短整数 vifi_t变量中。

图14-14 vif 结构

105-110 为v _ f l a g s定义的唯一的标志位是 V I F F _ T U N N E L。被置位时,该接口是一个到


远程多播路由器的隧道。没有置位时,接口是在本地系统上的一个物理接口。 v_threshold
是我们在 1 2 . 9节描述的多播阈值。 v _ l c l _ a d d r是与这个虚拟接口相关的本地接口的 I P地址。
v_rmt_addr是一个IP多播隧道远端的单播 IP地址。v_lcl_addr或者v_rmt_addr为非零,
但不会两者都为非零。对物理接口, v _ i f p非空,并指向本地接口的 i f n e t结构。对隧道,
v_ifp是空的。

图14-15 viftable 数组

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
324 计计TCP/IP详解 卷 2:实现

111-116 v_lcl_grps指向一个IP 多播组地址数组,这个数组记录了在连到的接口上的成


员组列表。对隧道来说, v _ l c l _ g r p s总是空的。数组的大小保存在 v _ l c l _ g r p s _ m a x中,
被使用的入口数保存在 v _ l c l _ g r p s _ n 中。数组随着组成员关系表的增大而增长。
v _ c a c h e d _ g r o u p和v _ c a c h e d _ r e s u l t实现“一个入口”高速缓存,其中记录的是最近
一次查找得到的组。
图1 4 - 1 5说明了v i f t a b l e,它最多有 3 2个(M A X V I F S)入口。v i f t a b le [ 2 ]是正在使用的
最后一个入口,所以 n u m v i f s是3。编译内核时固定了表的大小。图中还显示了表的第一个
入口的vif结构的几个成员。 v_ifp指向一个ifnet结构,v_lcl_grps指向in_addr结构中
的一个数组。数组有 32(v_lcl_grps_max)个入口,其中只用了 4个(v_lcl_grps_n)。
m r o u t e d通过 D V M R P _ A D D _ V I F和D V M R P _ D E L _ V I F命令维护 v i f t a b l e。通常,当
m r o u t e d开始运行时,会把本地系统上有多播能力的接口加入表中。当 m r o u t e d阅读自己的
配置文件,通常是 / e t c / m r o u t e d . c o n f时,会把多播隧道加入表中。这个文件中的命令也
可能从虚拟接口表中删除物理接口,或者改变与接口有关的多播信息。
m r o u t e d用D V M R P _ A D D _ V I F命令把c t l结构(图1 4 - 1 6 )传给内核。它指示内核在虚拟接
口表中加入一个接口项。

图14-16 vifctl 结构

78-82 v i f c _ v i f i识别v i f t a b l e 中虚拟接口的下标。其他 4个成员, v i f c _ f l a g s、


vifc_threshold、vifc_lcl_addr和vifc_rmt_addr,被add_vif函数复制到vif函数中。

14.5.2 add_vif函数

图14-17是add_vif函数。

图14-17 add_vif 函数:DVMRP_ADD_VIF 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 325

图14-17 (续)

1. 验证下标
202-216 如果m r o u t e d指定的v i f c _ v i f i中的下标太大,或者该表入口已经被使用,则
分别返回 EINVAL或EADDRINUSE。
2. 本地物理接口
217-221 ifa_ifwithaddr取得vifc_lcl_addr中的单播IP地址,并返回一个指向相关
i f n e t结构的指针。这就标识出这个虚拟接口要用的物理接口。如果没有匹配的接口,返回
EADDRNOTAVAIL。
3. 配置隧道接口
222-224 对于隧道,它的远端地址被从 vifctl结构中复制到接口表的 vif结构中。
4. 配置物理接口
225-243 对于物理接口,链路级驱动程序必须支持多播。 S I O C A D D M U L T I 命令与

Linux公社 www.linuxidc.com
bbs.theithome.com
326 计计TCP/IP详解 卷 2:实现

I N A D D R _ A N Y一起配置接口,开始接收所有 I P多播数据报 (图1 2 - 3 2 ),因为它是一个多播路由


器。当ipintr把到达数据报传给 ip_mforward时,被ip_mforward转发。
5. 保存多播信息
244-253 其他接口信息被从 v i f c t l结构复制到 v i f结构。如果需要,更新 n u m v i f s,记
录正在使用的虚拟接口数。

14.5.3 del_vif函数

图1 4 - 1 8显示的 d e l _ v i f 函数从虚拟接口表中删除表项。当 m r o u t e d设置 D V M R P _


DEL_VIF命令时,调用该函数。
1. 验证下标
257-268 如果传给del_vif的下标大于正在使用的最大下标,或者指向一个没有使用的入
口,则分别返回 EINVAL和EADDRNOTAVAIL。

图14-18 del_vif 函数:DVMRP_DEL_VIF 命令

2. 删除接口
269-278 对于物理接口,释放本地多播组表, S I O C A D D M U L T I禁止接收所有多播数据报,
bzero对viftable的入口清零。

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 327
3. 调整接口计数
279-286 f o r循环从以前活动的最大入口开始向后直到第一个入口为止,搜索出第一个活
动的入口。对没有使用的入口, v _ l c l _ a d d r(一个i n _ a d d r结构)的成员 s _ a d d r是0。相应
地更新numvifs,函数返回。

14.6 IGMP(续)

第13章侧重于IGMP协议的主机部分,mrouted实现了这个协议的路由器部分。 mrouted
必须为每个物理接口记录哪个多播组有成员在连到的网络上。 m r o u t e d每1 2 0秒多播一个
IGMP_HOST_MEMBERSHIP_QUERY数据报,并把IGMP_HOST_MEMBERSHIP_REPORT的结
果汇编到与每个网络相关的成员关系数组中。这个数组不是我们在第 13章讲的成员关系表。
m r outed根据收集到的信息构造多播路由选择表。多播组表也提供信息,用来抑制向没有
目的组成员的多播互联网区进行多播。
只为物理接口维护这样的成员关系数组。对其他多播路由器来说,隧道是点到点接口,
所以无需组成员关系信息。
我们在图 1 4 - 1 4中看到, v _ l c l _ g r p s指向一个 I P多播组数组。 m r o u t e d 用D V M R P _
A D D _ L G R P和D V M R P _ D E L _ L G R P命令维护这个表。两个命令都带了一个 l g r p c t l 结构
(图14-19)。

图14-19 lgrpctl 结构

进程
内核

选项

数据报

图14-20 IGMP报告处理

Linux公社 www.linuxidc.com
bbs.theithome.com
328 计计TCP/IP详解 卷 2:实现

87-90 l g c _ v i f i和l g c _ g a d d r标识{接口,组 }对。接口下标 (无符号短整数 l g c _ v i f i)


标识一个虚拟接口,而不是物理接口。
当收到一个 IGMP_HOST_MEMBERSHIP_REPORT时,调用图 14-20所示的函数。

14.6.1 add_lgrp函数

m r o u t e d检查到达 I G M P报告的源地址,确定是哪个子网,从而确定报告是哪个接口接

图14-21 add_lgrp 函数:DVMRP_ADD_GLRP 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 329
收的。根据这个信息, m r o u t e d为该接口设置 D V M R P _ A D D _ L G R P命令,更新内核中的成员
关系表。这个信息也被送到多播路由选择算法,更新路由选择表。图 1 4 - 2 1显示了 a d d _ l g r p
函数。
1. 验证增加请求
291-301 如果该请求标识了一个无效接口,就返回 E I N V A L。如果没有使用该接口或它是
一个隧道,则返回 EADDRNOTAVAIL。
2. 如果需要,扩展组数组
302-326 如果新组无法放在当前的组数组中,就分配一个新的数组。第一次为接口调用
add_lgrp函数时,分配一个能装 32个组的数组。
每次数组被填满后, a d d _ l g r p就分配一个两倍于前面数组大小的新数组。 M a l l o c负责
分配, b z e r o 负责清零, b c o p y 把旧数组中的内容复制到新数组中。更新最大入口数
v_lcl_grps_max,释放旧数组(如果有的话 ),把新数组和v_lcl_grps连接到vif入口。

“偏执狂(paranoid)”评论指出,无法保证 malloc分配的内存全部是 0。

3. 增加新的组
327-332 新组被复制到下一个可用的入口,如果高速缓存中已经存放了新组,就把高速缓
存标记为有效。
查找高速缓存中包含一个地址 v _ c a c h e d _ g r o u p ,以及一个高速缓存的查找结果
v _ c a c h e d _ r e s u l t。g r p l s t _ m e m b e r函数在搜索成员关系数组之前,总是先查一下这个
高速缓存。如果给定的组与 v _ c a c h e d _ g r o u p匹配,就返回高速缓存的查找结果;否则,搜
索成员关系数组。

14.6.2 del_lgrp函数

如果在 2 7 0秒内,没有收到该组任何成员关系的报告,则每个接口的组信息超时。
m r o u t e d维护适当的定时器,并当信息超时后,发布 D V M R P _ D E L _ L G R P命令。图 1 4 - 2 2显示
了del_lgrp。
1. 验证接口下标
337-347 如果请求标识无效接口,叫返回 E I N V A L。如果该接口没有使用或是一个隧道,
则返回EADDRNOTAVAIL。
2. 更新查找高速缓存
348-350 如果要删除的组在高速缓存里,就把查找结果设成 0(假)。
3. 删除组
351-364 如果在成员关系表中没有找到该组,则在 e r r o r中发布E A D D R N O T A V A I L。f o r
循环搜索与该接口相关的成员关系数组。如果 s a m e(是一个宏,用 b c m p比较两个地址 )为真,
则清除 e r r o r,把组计数器加 1。b c o p y移动后续的数组入口,删除该组, d e l _ l g r p跳出该
循环。
如果循环结束,没有找到匹配,则返回 EADDRNOTAVAIL;否则返回 0。

Linux公社 www.linuxidc.com
bbs.theithome.com
330 计计TCP/IP详解 卷 2:实现

图14-22 del_lgrp 函数:DVMRP_ DEL_LGRP命令

14.6.3 grplst_member函数

在转发多播时,查询成员关系数组,以免把数据报发到没有目的组成员的网络上。图 1 4 -
23显示的grplst_member函数,搜索整个表,寻找给定组地址。

图14-23 grplst_member 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计331

图14-23 (续)

1. 检查高速缓存
368-379 如果请求的组在高速缓存中,则返回高速缓存的结果,不搜索成员关系数组。
2. 搜索成员关系数组
380-390 对数组进行线性搜索,确定组是否在其中。如果找到,就更新高速缓存以记录匹
配的值,并返回 1;如果没有找到,就更新高速缓存记录丢失的,并返回 0。

14.7 多播选路

正如在本章开始提到的,我们不给出 m r o u t e d实现的 T R P B算法,但给出一个有关该机


制的综述,描述内核的多播路由选择表和多播路由选择函数。图 1 4 - 2 4显示了一个我们用于解
释该算法的示例多播网络。

网络 A

隧道
C E 网络E

网络 C

D
网络 B
网络 D

图14-24 多播网络示例

图1 4 - 2 4中,方框代表路由器,椭圆代表连接到路由器的多播网络。例如,路由器 D可以
在网络 D和网络 C上多播。路由器 C可以向网络 C多播,通过点到点接口向路由器 A和B多播,
并可以通过一个多播隧道向路由器 E多播。
最简单的路由选择办法是,从互联网拓扑中选出一个子网,形成一个生成树。如果每个
路由器都沿着生成树转发多播,则各路由器最终会收到数据报。图 1 4 - 2 5显示了示例网络的一
个生成树。其中,网络 A上的主机S是多播数据报的源。
有关生成树的讨论,参见 [Tanenbaum 1989] 或 [Perlman 1992]。

Linux公社 www.linuxidc.com
bbs.theithome.com
332 计计TCP/IP详解 卷 2:实现

隧道

图14-25 网络A的生成树

这个生成树是根据从各网络回到网络 A上的源站的最短逆向路径 (reverse path)构造的。图


1 4 - 2 5的生成树中,省略了路由器 B和C之间的线路。源站和路由器 A之间的箭头,以及路由器
B和C之间的箭头,强调了多播网络是生成树的一部分。
如果用同一生成树转发来自网络 C的数据报,为了在网络 B上收到,数据报经过的转发路
径将大于需要的长度。 RFC 1075提出的算法为每个潜在的源站计算了一个单独的生成树,以
避免这种情况。路由选择表为每条路由记录了一个网络号和子网掩码,所以一条路由可以应
用到源子网内的任意主机。
因为构造生成树是为了给源站的数据报提供最短逆向路径,而每个网络都接收所有多播
数据报,所以这个过程称为逆向路径广播 (reverse path broadcast)即RPB。
R P B没有任何多播组成员信息,使许多数据报被不必要地转发到没有目的组成员的网络
上。如果,除了计算生成树外,该路由选择算法还能记录哪些网络是叶子,注意到每个网络
上的组成员关系,那么,连到叶子网络的路由器就可以避免把数据报转发到没有目的组成员
的网络上去。这称为截断逆向路径广播 ( T R P B ),2 . 0版的m r o u t e d在I G M P帮助下记录叶子网
络上的成员关系,从而实现这一算法。
图1 4 - 2 6显示了 T R P B算法的应用。多播来自网络 C上的源站,并在网络 B上有一个目的组
成员。
我们用图14-26说明Net/3多播路由选择表中使用的名词。在这个例子中,有阴影的网络和

隧道

图14-26 网络C的TRPB路由选择

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 333
路由器收到来自网络 C上源站的数据报。A和B之间的线路不属于生成树,C与D之间没有连接,
因为C和D直接收到源站发送的多播。
在这个图中,网络 A、B、D和E是叶子网络。路由器 C接收多播,并通过连到路由器 A、
B和E的接口将其转发—尽管把它发给A和E都是浪费。这是 TRPB算法的缺点。
路由器 C上与网络 C相关的接口叫做父亲,因为路由器 C期望用它接收来自网络 C的多播。
从路由器 C到路由器 A、B和E的接口叫做儿子接口。对路由器 A来说,点到点接口是来自 C的
源分组的父亲,到网络 A的接口是儿子。接口相对于数据报的源站,被标识为父亲和儿子。只
在相关的儿子接口上转发多播数据报,不在父亲接口上转发多播。
继续我们的例子,因为网络 A、D和E是叶子网络,并且没有目的组成员,所以它们没有
阴影。在路由器处截断生成树,也不把数据报转发到这些网络上去。路由器 B把数据报转发到
网络B上,因为 B上有一个目的组成员。为实现截断算法,接收数据报的所有路由器都在自己
的viftable中查询与每个虚拟接口相关的组表。
对该多播路由选择算法的最后一个改进叫做逆向路径多播 (reverse path multicasting ,
R P M )。R P M的目的是修剪 (p r u n e)各生成树,避免在没有目的组成员的分支上发送数据报。
在图14-26中,RPM可以避免路由器 C向A和E发送数据报,因为在这两个分支上没有目的多播
组的成员。 3.3版的mrouted实现了RPM。
图1 4 - 2 7是我们的示例网络,但这一次,只有那些 R P M算法选路数据报能到达的路由器和
网络才有阴影。

图14-27 网络C的RMP路由选择

为了计算生成树对应的路由表,多播路由器和邻近的多播路由器通信,发现多播互联网
拓扑和多播组成员的位置。在 N e t / 3中,用 D V M R P进行这种通信。 D V M R P作为I G M P数据报
传送,发给 224.0.0.4组,该组是给DVMRP通信保留的 (图12-1)。
在图 1 2 - 3 9 中,我们看到,多播路由器总是接受到达的 I G M P 分组,把它们传给
i g m p _ i n p u t和r i p _ i n p u t,然后 m r o u t e d在一个原始 I G M P插口上读它们。 m r o u t e d把
DVMRP报文发送到同一原始 IGMP插口上的其他多播路由器。
实现这些算法需要的有关 R P B、T R P B、R P M以及D V M R P报文的其他细节参见 [ D e e r i n g
和Cheriton 1990] 和mrouted的源代码。
I n t e r n e t上还使用了其他多播路由选择协议。 P r o t e o n路由器实现了 RFC 1584 [Moy 1994]
提出的 M O S P F 协议。 C i s c o从操作软件的 1 0 . 2版开始实现了 PIM(Protocol Independent

Linux公社 www.linuxidc.com
bbs.theithome.com
334 计计TCP/IP详解 卷 2:实现

Multicasting)。[Deering et al1994]描述了PIM。

14.7.1 多播选路表

现在我们描述 N e t / 3中实现的多播路由选择。内核的多播路由选择表是作为一个有 6 4个入


口的散列表实现的 (M R T H A S H I Z)。该表保存在全局数组 m r t t a b l e中,每个入口指向一个
mrt结构的链表,如图 14-28所示。

图14-28 mrt 结构

120-127 m r t c _ o r i g i n和m r t c _ o r i g i n m a s k标识表中的一个入口。 m r t c _ p a r e n t是


虚拟接口的下标,该虚拟接口上预期有来自起点的所有多播数据报。 m r t c _ c h i l d r e n是一
个位图,标识外出的接口。 m r t c _ l e a v e s也是一个位图,里面标识多播路由选择树中也是
叶子的外出接口。当多条路由散列到同一个数组入口时,最后一个成员 m r t _ n e x t实现该入
口的一个链表。
图 1 4 - 2 9 是多播选路表的整体结构。各 m r t 结构都放在一个散列链上,该散列链与
nethash(图14-31)函数返回的值对应。

mrttable[5]

mrttable[62]

图14-29 多播选路表

内核维护的多播选路表是 m r o u t e d维护的多播选路表的一个子集,其中的信息足够内核
支持多播转发。发送内核表更新和 D V M R P _ A D D _ M R T命令,其中包含图 1 4 - 3 0显示的m r t c t l
结构。
95-101 mrtctl结构的5个成员携带了我们谈到的mrouted和内核之间的信息(图14-28)。
多播选路表的键值是多播数据报的源 I P地址。 n e t h a s h(图1 4 - 3 1 )实现该用于该表的散列

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 335
算法。它接受源 IP地址,并返回0~63之间的一个值(M R T H A S H S I Z-1)。

图14-30 mrtctl 结构

图14-31 nethash 结构

398-407 in_netof返回in,主机部分设置为全 0,在n中仅留下发送主机的 A、B和C类网


络。右移结果,直到低 8位非零为止。MRTHASHMOD是
#define MRTHASHMOD(h) ((h) & (MRTHASHSIZ - 1))

把低8位与63进行逻辑与运算,留下低 6位,这是0~63之间的一个整数。
用两个函数调用 (n e t h a s h和i n _ n e t o f)计算散列值,作为散列 32 bit地址值太
过昂贵了。

14.7.2 del_mrt函数

m r o u t e d守护程序通过 D V M R P _ A D D _ M R T和D V M R P _ D E L _ M R T命令在内核的多播选路表


中增加或删除表项。图 14-32显示了del_mrt函数。

图14-32 del_mrt 函数:DVMRP_DEL_MRT 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
336 计计TCP/IP详解 卷 2:实现

图14-32 (续)

1. 找到路由入口
451-462 for循环从hash标识的入口开始 (在nethash中定义时初始化 )。如果没有找到入
口,则返回 ESRCH。
2. 删除路由入口
463-473 如果该入口在高速缓存中,则高速缓存也无效了。从散列链上把该入口断开,并
且释放。当匹配入口在表的最前面时,需要 if语句处理这一特殊情况。

14.7.3 add_mrt函数

add_mrt函数如图14-33所示。

图14-33 add_mrt 函数:处理 DVMRP_ADD_MRT 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 337

图14-33 (续)

1. 更新存在的路由
411-427 如果请求的路由已经在路由表中,则把新的信息复制到该路由中, add_mrt返回。
2. 分配新路由
4 2 8 - 4 4 7 在新分配的m b u f中,根据增加请求传递的 m r t c t l结构,构造一个 m r t结构。从
mrtc_origin计算出散列下标,并把新路由插入散列链的第一个入口。

14.7.4 mrtfind函数

mrtfind函数负责搜索多播选路表。如图14-34所示。把数据报的源站地址传给mrtfind,
mrtfind返回一个指向匹配mrt结构的指针;如果没有匹配,则返回一个空指针。
1. 检查路由查询高速缓存
4 7 7 - 4 8 8 把给定的源 I P地址(o r g i n)与高速缓存中的原始掩码做逻辑与运算。如果结果与
cached_origin匹配,则返回高速缓存的入口。

图14-34 mrtfind 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
338 计计TCP/IP详解 卷 2:实现

2. 检查散列表
489-501 n e t h a s h返回该路由入口的散列下标。 f o r循环搜索散列链找到匹配的路由。当
找到一个匹配时,更新高速缓存,返回一个指向该路由的指针。如果没有找到匹配,则返回
一个空指针。

14.8 多播转发:ip_mforward函数

内核实现了整个多播转发。我们在图 1 2 - 3 9中看到,当 i p _ m r o u t e r非空时,也就是


mrouted在运行时, ipintr把到达数据报传给 ip_mforward。
我们在图 1 2 - 4 0 中看到, i p _ o u t p u t 可以把本地主机产生的多播数据报传给 ip_
m f o r w a r d,由i p _ m f o r w a r d为这些数据报选路到除 i p _ o u t p u t选定的接口以外的其他接
口上去。
与单播转发不同,每当多播数据报被转发到某个接口上时,就为该数据报产生一个备份。
例如,如果本地主机是一个多播路由器,并且连接到三个不同的网络,则系统产生的多播数
据报被分别复制三份,在三个接口上等待输出。另外,如果应用程序设置了多播环回标志位,
或者任何输出的接口也接收它自己的传送,则数据报也将被复制,等待输入。
图14-35显示了一个到达某个物理接口的多播数据报。

运输层协议

接收的分组

丢弃数据报
(图14-39)

到达多播 隧道 以太网

图14-35 到达某个物理接口的多播数据报

在图1 4 - 3 5中,数据报到达的接口是目的多播组的一个成员,所以数据报被传给运输层协
议等待输入处理。该数据报也被传给 i p _ m f o r w a r d,在这里它被复制和转发到一个物理接
口和一个隧道上 (带粗线的箭头),这两个必须都不和接收接口相同。
图14-36显示了一个到达某隧道的多播数据报。
在图1 4 - 3 6中,用带虚线的箭头表示与该隧道的本地端有关的物理接口,数据报就在这一
接口上到达。数据报被传给 i p _ m f o r w a r d,我们将在图 14-37看到,因为分组到达一个隧道,
所以ip_mforward返回一个非零值。这导致 ipintr不再把该分组传给运输层协议。
i p _ m f o r w a r d从分组中取出隧道选项,查询多播选路表,并且,在本例中,还把分组转
发到另一个隧道以及到达的物理接口上去,用带细线的箭头表示。这是可行的,因为多播选

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 339
路表是根据虚拟接口,而不是物理接口。
在图1 4 - 3 6中,我们假定物理接口是目的多播组的成员,所以 i p _ o u t p u t把该数据报传
给i p _ m l o o p b a c k,i p _ m l o o p b a c k把它送到队列中等待 i p i n t r的处理 (带粗线的箭头 )。
然后,分组又被传给 i p _ m f o r w a r d ,并被这个函数丢弃 ( 练习 1 4 . 4 ) 。这一次,
i p _ m f o r w a r d返回0(因为分组是在物理接口上到达的 ),所以i p i n t r接受该数据报,并进行
输入处理。

运输层协议

只有当它在物理接口上
到达时,才接收的分组

丢弃的数据报
(图14-39)

在隧道上到达的分组现在在物
理接口上排队等待输入

到达多播 隧道 以太网

图14-36 到达某个多播隧道的多播数据报

我们分三部分说明多播转发程序:
• 隧道输入处理(图14-37);
• 转发条件合格(图14-39);和
• 转发到出去的接口上 (图14-40)。

图14-37 ip_mforward 函数:到达隧道

Linux公社 www.linuxidc.com
bbs.theithome.com
340 计计TCP/IP详解 卷 2:实现

图14-37 (续)

516-526 i p _ m f o r w a r d的两个参数是:一个指向包含该数据报的 m b u f链的指针;另一个


是指向接收接口 ifnet结构的指针。
1. 到达物理接口
527-530 为了区分在同一物理接口上到达的多播数据报是否经过隧道,要检查 I P首部的特
征L S R R选项。如果首部太小,无法包含该选项;或者该选项不是以一个后面跟着一个 L S R R
选项的NOP开始,就假定该数据报是在一个物理接口上到达的,并把 tunnel_src设为0。
2. 到达隧道
531-558 如果数据报看起来像是从隧道上到达的,就检查选项,验证格式是否正确。如果选
项的格式不符合多播隧道,则 i p _ m f o r w a r d返回1,指示应该把该数据报丢弃。图 14-38是隧
道选项的结构。

在图1 4 - 3 8中,我们假定数据报里没有其他选项,但不是必须这样的。任何其他
I P选项都可能出现在 L S R R选项的后面,因为隧道开始端的多播路由器总是把 L S R R
选项插在所有其他选项之前。

3. 删除隧道选项
559-565 如果选项正确,就把后面的选项和数据向前移动,调整 mbuf首部的m_len和IP 首
部的ip_len和ip_hl的值,然后删除隧道选项 (图14-38)。

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 341
NOP
LSRR
11(length)
12(offset)

IP首部 隧道源 目的组 数据

20字节 1 1 1 1 4字节 4字节

隧道选项

IP首部 数据

20字节 10字节
图14-38 多播隧道选项

i p _ m f o r w a r d经常把t u n n e l _ s o u r c e作为返回值。当数据报从隧道上到达时,这个值
只能是非零的。当 i p _ m f o r w a r d返回非零值时,它的调用方就丢弃该数据报。对 i p i n t r来
说,这意味着在隧道上到达的一个数据报被传给 i p _ m f o r w a r d,并且被 i p i n t r丢弃。转发
程序取出隧道信息,复制数据报,用 i p _ o u t p u t将其发送出去;如果接口是目的多播组的成
员,则ip_output调用ip_mloopback。
i p _ m f o r w a r d的下一部分显示在图 1 4 - 3 9中,在这部分程序中,如果数据报不符合转发
的条件,就丢弃它。

图14-39 ip_mforward 函数:转发可行性检查

Linux公社 www.linuxidc.com
bbs.theithome.com
342 计计TCP/IP详解 卷 2:实现

4. 超时的TTL或本地多播
566-572 如果i p _ t t l是0或1,那么数据报已经到了生存期的最后,不再转发它。如果目
的组小于或等于 I N A D D R _ M A X _ L O C A L _ G R O U P(几个2 2 4 . 0 . 0 . x组,图 1 2 - 1 ),则不允许数据报
离开本地网络,也不转发它。在两种情况下,都把 tunnel_src返回给调用方。
3 . 3版的m r o u t e d支持对某些目的多播组的管理辖域。可把接口配置成丢弃所有寻
址到这些组的数据报,与 224.0.0.x组的自动辖域类似。
5. 没有路由可用
573-579 如果mrtfind无法根据数据报中的源地址找到一条路由,则函数返回。没有路由,
多播路由器无法确定把数据报转发到哪个接口上去。这种情况可能发生在,比如,多播数据
报在mrouted更新多播选路表之前到达。
6. 在没有想到的接口上到达
580-592 如果数据报到达某个物理接口,但系统本来预想它应该到达某个隧道或其他物理
接口,则 i p _ m f o r w a r d返回;如果数据报到达某个隧道,但系统本来预想它应该在某个物
理接口或其他隧道上到达,则 i p _ m f o r w a r d也返回。产生这些情况的原因是,当组成员关
系或网络的物理拓扑发生变化后,正在更新选路表时,数据报到达。
i p _ m f o r w a r d的最后一部分 (图1 4 - 4 0 )把该数据报在多播路由入口所指定的每个输出接
口上发送。

图14-40 ip_mforward 函数:转发

593-615 对viftable中的每个接口,如果以下条件满足,则在该接口上发送数据报:
• 数据报的TTL大于接口的多播阈值;
• 接口是该路由的子接口;以及
• 接口没有和某个叶子网络相连。
Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 343
如 果 该 接口 是 一 个 叶子 ,那 么 只 有 当网 络 上 有 目的 多 播 组 成员 时 ( 也就 是说,
grplst_member返回一个非零值 ),才输出该数据报。
tunnel_send在隧道接口上转发该数据报;用 phyint_send在物理接口上转发。

14.8.1 phyint_send函数

为在物理接口上发送多播数据报, p h y i n t _ s e n d(图1 4 - 4 1 )在它传给 i p _ o u t p u t的


ip_moptions结构中,明确指定了输出接口。

图14-41 phyint_send 函数

616-634 m_copy复制输出的数据报。 ip_moptions结构设置为强制在选定的接口上传送


该数据报。递减 TTL,允许多播环回。
数据报被传给 i p _ o u t p u t 。 I P _ F O R W A R D I N G 标志位避免产生无限回路,使
ip_output再次调用ip_mforward。
mbuf 通道
分组首部 IP首部 选项
28字节 20字节 12字节

IP首部 IP选项 数据

20字节

mbuf首部 选项和数据

20字节
图14-42 插入隧道选项

Linux公社 www.linuxidc.com
bbs.theithome.com
344 计计TCP/IP详解 卷 2:实现

14.8.2 tunnel_send函数

为了在隧道上发送数据报, t u n n e l _ s e n d(图1 4 - 4 3 )必须构造合适的隧道选项 ,并将其


插到输出数据报的首部。图 14-42显示了tunnel_send如何为隧道准备分组。

图14-43 tunnel_send 函数:验证和分配新首部

1. 隧道选项合适吗
635-652 如果I P首部内没有隧道选项的空间, t u n n e l _ s e n d立即返回,不再在隧道上转
发该数据报。可能在其他接口上转发。
2. 复制数据报,为新首部和隧道选项分配 mbuf
653-672 在调用 m _ c o p y时,复制的开始偏移是 2 0 (I P _ H D R _ L E N)。产生的 m b u f链中包含
了数据报的选项和数据报,但没有 I P首部。m b _ o p t s指向M G E T H D R分配的一个新的数据报首
部,这个新的数据报首部被放在 m b _ c o p y的前面。然后调整 m _ l e n和m _ d a t a的值,以容纳
IP首部和隧道选项。

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 345
tunnel_send的第二部分,如图 14-44所示,修改输出分组的首部,并发送该分组。

图14-44 tunnel_send 函数:构造首部和发送

3. 修改IP首部
673-679 从原始mbuf链中把原始 IP 首部复制到新分配的 mbuf首部中。减少该首部的 TTL,
把目的地址改成隧道另一端的接口地址。
4. 构造隧道选项
680-664 调整i p _ h l和i p _ l e n的值以容纳隧道选项。隧道选项紧跟在 IP 首部的后面:一
个NOP,后面是LSRR码,LSRR选项的长度 (11字节),以及一个指向选项第二个地址的指针 (8
字节)。源路由包括了本地隧道端点和后面的目的多播组地址 (图14-13)。
5. 发送经过隧道处理的数据报
665-697 现在,这个数据报看起来像一个有 LSRR选项的单播数据报,因为它的目的地址是
隧道另一端的单播地址。 i p _ o u t p u t发送该数据报。当数据报到达隧道的另一端时,隧道选
项被剥离,另一端可能会通过其他隧道将数据报继续转发。

14.9 清理:ip_mrouter_done函数

当m r o u t e d结束时,它发布 D V M R P _ D O N E命令,i p _ m r o u t e r _ d o n e函数(图14-45)处理


这个命令。
161-186 这个函数在 s p l n e t上运行,避免与多播转发代码的任何交互。对每个物理多播
接口,释放本地组表,并发布 S I O C D E L M U L T I 命令,阻止接收多播数据报 (练习 14.3) 。
bzero清零整个 viftable数组,并把 numvifs设置成0。

Linux公社 www.linuxidc.com
bbs.theithome.com
346 计计TCP/IP详解 卷 2:实现

187-198 释放多播选路表中的所有活动入口, b z e r o 清零整个表,清零缓存,置位


ip_mrouter。
多播选路表中的每个入口都可能是入口链表的第一个。这段代码只释放表的第
一个入口,引起内存泄露。

图14-45 ip_mrouter_done 函数:DVMRP_DONE 命令

14.10 小结

本章我们描述了网际多播的一般概念和支持它的 N e t / 3内核中心专用函数。我们没有讨论
mrouted的实现,有兴趣的读者可以得到源代码。
我们描述了虚拟接口表,讨论了物理接口和隧道之间的区别,以及 N e t / 3中用于实现隧道
的LSRR选项。
我们说明了 R P B、T R P B和R P M算法,描述了根据 T R P B转发多播数据报的内核表,还讨
论了父网络和叶子网络。

Linux公社 www.linuxidc.com
bbs.theithome.com
第14章 IP多播选路计计 347
习题

14.1 在图14-25中,需要多少多播路由?
14.2 为什么splnet和splx保护对图14-23中组成员关系高速缓存的更新?
14.3 当某个接口用 I P _ A D D _ M E M B E R S H I P选项明确加入一个多播组后,如果向它发布
SIOCDELMULTI,会发生什么?
14.4 当某个上隧道上到达一个数据报,并被 i p _ m f o r w a r d接收后,可能会在转发到某
个物理接口时,被 i p _ o u t p u t 环回。为什么当环回分组到达该物理接口时,
ip_mforward会丢弃它呢?
14.5 重新设计组地址高速缓存,提高它的效率。

Linux公社 www.linuxidc.com
bbs.theithome.com
第15章 插 口 层
15.1 引言

本书共有三章介绍 N e t / 3的插口层代码,本章是第一章。插口概念最早出现于 1 9 8 3年的


4 . 2 B S D版本中,它的主要目的是提供一个统一的访问网络和进程间通信协议的接口。这里讨
论的N e t / 3版基于4.3BSD Reno版,该版本与大多数 U n i x供应商使用的早期的 4 . 2版有些细小的
差别。
如第1 . 7节所介绍的,插口层的主要功能是将进程发送的与协议有关的请求映射到产生插
口时指定的与协议有关的实现。
为了允许标准的 Unix I/O系统调用,如 r e a d和w r i t e,也能读写网络连接,在 B S D版本
中将文件系统和网络功能集成在系统调用级。与通过一个描述符访问一个打开的文件一样,
进程也是通过一个描述符 (一个小整数)来访问插口上的网络连接。这个特点使得标准的文件系
统调用,如 r e a d和w r i t e,以及与网络有关的系统调用,如 s e n d m s g和r e c v m s g,都能通
过描述符来处理插口。
我们的重点是插口及相关的系统调用的实现而不是讨论如何使用插口层来实现网络应用。
关于进程级的插口接口和如何编写网络应用的详细讨论,请参考 [Stevens 1990]和[Rago 1990]。
图15-1说明了进程中的插口接口与内核中的协议实现之间的层次关系。

应用程序

函数调用

插口系统调用

进程 系统调用
内核

插口系统
调用实现

函数调用

插口层函数

TCP TP 4
通过pr_usrreq或pr_ctloutput调用

UDP SPP

图15-1 插口层将一般的请求转换为指定的协议操作

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 349
splnet处理

插口包含很多对 s p l n e t和s p l x的成对调用。正如第 1 . 1 2节中介绍的,这些调用保护访


问在插口层和协议处理层间共享的数据结构的代码。如果不使用 s p l n e t,初始化协议处理和
改变共亨的数据结构的软件中断将使得插口层代码恢复执行时出现混乱。
我们假定读者理解了这些调用,因而在以后讨论中一般不再特别说明它们。

15.2 代码介绍

本章讨论涉及的三个文件在图 15-2中列出。

文 件 描 述

sys/socketvar.h s o c k et结构定义
kern/uipc_syscalls.c 系统调用实现
kern/uipc_socket.c 插口层函数

图15-2 本章讨论涉及的源文件

全局变量

本章讨论涉及到的两个全局变量如图 15-3所示。

变 量 数据类型 描 述

socketps s t r u c t f i l e o p s I/O系统调用的 s o c k e t实现


sysent s t r u c t s y s e n[]t 系统调用入口数组

图15-3 本章介绍的全局变量

15.3 socket结构

插口代表一条通信链路的一端,存储或指向与链路有关的所有信息。这些信息包括:使
用的协议、协议的状态信息 (包括源和目的地址 )、到达的连接队列、数据缓存和可选标志。图
15-5中给出了插口和与插口相关的缓存的定义。
41-42 s o _ t y p e由产生插口的进程来指定,它指明插口和相关协议支持的通信语义。
s o _ t y p e的值等于图7 - 8所示的p r _ t y p e。对于U D P,s o _ t y p e等于S O C K _ D G R A M,而对于
TCP,so_type则等于SOCK_STREAM。
43 so_options是一组改变插口行为的标志。图 15-4列出了这些标志。
通过g e t s o c k o p t和s e t s o c k o p t系统调用进程能修改除 S O _ A C C E P T C O N N外
所有的插口选项。当在插口上发送 l i s t e n系统调用时, S O _ A C C E P T C O N N被内核设
置。
44 s o _ l i n g e r等于当关闭一条连接时插口继续发送数据的时间间隔 (单位为一个时钟滴
答)(第15.15节)。
45 s o _ s t a t e表示插口的内部状态和一些其他的特点。图 1 5 - 6列出了s o _ s t a t e可能的取
值。

Linux公社 www.linuxidc.com
bbs.theithome.com
350计计TCP/IP详解 卷 2:实现

so_options 仅用于内核 描 述

SO_ACCEPTCONN • 插口接受进入的连接
SO_BROADCAST 插口能够发送广播报文
SO_DEBUG 插口记录排错信息
SO_DONTROUTE 输出操作旁路选路表
SO_KEEPALIVE 插口查询空闲的连接
SO_OOBINLINE 插口将带外数据同正常数据存放在一起
SO_REUSEADDR 插口能重新使用一个本地地址
SO_REUSEPORT 插口能重新使用一个本地地址和端口
SO_USELOOPBACK 仅针对选路域插口;发送进程收到它自己的选路请求

图15-4 so_options 的值

图15-5 struct socket


定义

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 351

图15-5 (续)

so_state 仅用于内核 描 述

SS_ASYNC 插口应该I/O事件的异步通知
SS_NBIO 插口操作不能阻塞进程
SS_CANTRCVMORE • 插口不能再从对方接收数据
SS_CANTSENDMORE • 插口不能再发送数据给对方
SS_ISCONFIRMING • 插口正在协商一个连接请求
SS_ISCONNECTED • 插口被连接到外部插口
SS_ISCONNECTING • 插口正在连接一个外部插口
SS_ISDISCONNECTING • 插口正在同对方断连
SS_NOFDREF • 插口没有同任何描述符相连
SS_PRIV • 插口由拥有超级用户权限的进程所产生
SS_RCVATMARK • 在最近的带外数据到达之前,插口已处理完所有收到的数据

图15-6 so_state 的值

从图 1 5 - 6 的第二列中可以看出,进程可以通过 f c n t l 和i o c t l 系统调用直接修改
S S _ A S Y N C和S S _ N B I O。对于其他的标志,进程只能在系统调用的执行过程中间接修改。例
如,如果进程调用 connect,当连接被建立时, SS_ISCONNECTED标志就会被内核设置。

SS_NBIO和SS_ASYNC标志

在默认情况下,进程在发出 I / O请求后会等待资源。例如,对一个插口发 r e a d系统调用,


如果当前没有网络上来的数据,则 r e a d系统调用就会被阻塞。同样,当一个进程调用 w r i t e
系统调用时,如果内核中没有缓存来存储发送的数据,则内核将阻塞进程。如果设置了
S S _ N B I O,在对插口执行 I / O操作且请求的资源不能得到时,内核并不阻塞进程,而是返回
EWOULDBLOCK。
如果设置了 S S _ A S Y N C,当因为下列情况之一而使插口状态发生变化时,内核发送
SIGIO信号给so_pgid标识的进程或进程组:
• 连接请求已完成;
• 断连请求已被启动;
• 断连请求已完成;
• 连接的一个通道已被关闭;
• 插口上有数据到达;
• 数据已被发送(即,输出缓存中有闲置空间 );或
• UDP或TCP插口上出现了一个异步差错。
46 so_pcb指向协议控制块,协议控制块包含与协议有关的状态信息和插口参数。每一种协
议都定义了自己的控制块结构,所以 s o _ p c b被定义成一个通用的指针。图 15-7列出了我们讨
论的控制块结构。
so_pcb从来不直接指向 tcpcb结构;参考图22-1。

Linux公社 www.linuxidc.com
bbs.theithome.com
352 计计TCP/IP详解 卷 2:实现

协 议 控 制 块 参考章节

UDP struct inpcb 第22.3节


TCP s t r u c t i n p c b 第22.3节
s t r u c t t c p c b 第24.5节
ICMP、IGMP和原始IP s t r u c t i n p c b 第22.3节
路由 struct rawcb 第20.3节

图15-7 协议控制块

47 so_proto指向进程在 socket系统调用(第7.4节)中选择的协议的 protosw结构。


48-64 设置了S O _ A C C E P T C O N N标志的插口维护两个连接队列。还没有完全建立的连接 (如
T C P的三次握手还没完成 )被放在队列 s o _ q 0中。已经建立的连接或将被接受的连接 (例如,
T C P的三次握手已完成 )被放入队列 s o _ q中。队列的长度分别为 s o _ q 0 l e n和s o _ q l e n。每
一个被排队的连接由它自己的插口来表示。在每一个被排队的插口中, s o _ h e a d指向设置了
SO_ACCEPTCONN的源插口。
插口上可排队的连接数通过 s o _ q l i m i t来控制,进程可以通过 l i s t e n系统调用来设置
s o _ q l i m i t。内核隐含设置的上限为 5 ( S O M A X C O N N,图1 5 - 2 4 )和下限为 0。图1 5 - 2 9中显示
的有点晦涩的公式使用 so_qlimit来控制排队的连接数。
图15-8说明了有三个连接将被接受、一个连接已被建立的情况下的队列内容。

当TCP SYN 到达时,插口


进入此队列

当TCP SYN被应答时将插口
移入此队列, a c c e p t将插口
从此队列中删除

图15-8 插口连接队列

65 so_timeo用作accept、connet和close处理期间的等待通道 (wait channel,第15.10


节)。
66 s o _ e r r o r保存差错代码,直到在引用该插口的下一个系统调用期间差错代码能送给进
程。
67 如果插口的 SS_ASYNC被设置,则 SIGIO信号被发送给进程 (如果so_pgid大于0)或进程
组(如果 s o _ p g i d小于0 )。可以通过 i o c t l的S I O C S P G R P和S I O C G P G R P命令来修改或检查
so_pgid的值。关于进程组的更详细信息请参考 [Stevens 1992]。
68 so_oobmark标识在输入数据流中最近收到的带外数据的开始点。第 16.11节将讨论插口
对带外数据的支持,第 29.7节将讨论TCP中的带外数据的语义。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 353
69-82 每一个插口包括两个数据缓存, s o _ r c v和s o _ s n d,分别用来缓存接收或发送的数
据。s o _ r c v和s o _ s n d是包含在插口结构中的结构而不是指向结构的指针。我们将在第 1 6章
中描述插口缓存的结构和使用。
83-86 在Net/3中不使用so_tpcb。so_upcall和so_upcallarg也仅用于Net/3中的NFS
软件。
NFS与通常的软件不太一样。在很大程度上它是一个进程级的应用但却在内核中
运行。当数据到达接收缓存时,通过 s o _ u p c a l l来触发 N F S的输入处理。在这种情
况下,t s l e e p和w a k e u p机制是不合适的,因为 N F S协议是在内核中运行而不是作
为一个普通进程。
文件socketvar.h和uipc_socket2.c定义了几个简化插口层代码的宏和函数。图 15-9
对它们进行了描述。

名 称 描 述

sosendallatonce s o中指定的协议要求每一个发送系统调用产生一个协议请求吗?
int s o s e n d a l l a t o n c e( s t r u c t s o c k e t so)
*;
soisconnecting 将插口状态设置为 S O _ I S C O N N E C T I N G
int s o i s c o n n e c t i n g( s t r u c t s o c k e t so)
*;
soisconnected 参考图1 5 - 3 0
soreadable 插口so上的读调用不阻塞就返回信息吗?
int s o r e a d a b l e( s t r u c t s o c k e t so)
* ;
sowriteable 插口so上的写调用不阻塞就返回吗?
int s o w r i t e a b l e( s t r u c t s o c k e t so)
*;
socantsendmore 设置插口标志 S O _ C A N T S E N D M O R E。唤醒所有等待在发送缓存上的进程
int s o c a n t s e n d m o r e( s t r u c t s o c k e t so)
*;
socantrcvmore 设置插口标志 S O _ C A N T R C V M O R E。唤醒所有等待在接收缓存上的进程
int s o c a n t r c v m o r e( s t r u c t s o c k e t so)
*;
soisdisconnecting 清除SS_ISCONNECTING标志。设置SS_ISDISCONG、SS_CANTRCVMORE
和S S _ C A N T S E N D M O R E标志。唤醒所有等待在插口上的进程
int s o i s d i s c o n n e c t i n g( s t r u c t s o c k e t so)
*;
soisdisconnected 清除S S _ I S C O N N E C T I N G、S S _ I S C O N N E C T E D和S S _ I S D I S C O N N E C T I N G
标志。设置 S S _ C A N T R C V M O R E和S S _ C A N T S E N D M O R E标志。唤醒所有等待在
插口上的进程或等待 c l o s e完成的进程
i n t s o i s d i s c o n n e c t e d( s t r u c t s o c k e t so)
* ;
soqinsque 将s o插入head指向的队列中。如果 q等于0,插口被插到存放未完成的连接的
s o _ q 0队列的后面。否则,插口被插到存放准备接受的连接的队列 s o _ q的后面。
N e t / 1错误地将插口插到队列的前面
int soqinsque(struct socket * head, struct socket * so, int q);
soqremque 从队列q中删除so。通过so- >so_head来定位插口队列
int s o q r e m q u e( s t r u c t s o c k e t so,
* i n t q) ;

图15-9 插口的宏和函数

Linux公社 www.linuxidc.com
bbs.theithome.com
354 计计TCP/IP详解 卷 2:实现

15.4 系统调用

进程同内核交互是通过一组定义好的函数来进行的,这些函数称为系统调用。在讨论支
持网络的系统调用之前,我们先来看看系统调用机制的本身。
从进程到内核中的受保护的环境的转换是与机器和实现相关的。在下面的讨论中,我们
使用Net/3在386上的实现来说明如何实现有关的操作。
在B S D内核中,每一个系统调用均被编号,当进程执行一个系统调用时,硬件被配置成
仅传送控制给一个内核函数。将标识系统调用的整数作为参数传给该内核函数。在 386实现中,
这个内核函数为 s y s c a l l。利用系统调用的编号, s y s c a l l在表中找到请求的系统调用的
sysent结构。表中的每一个单元均为一个 sysent结构。
struct sysent {
int sy_narg; /* number of arguments */
int (*sy_call) (); /* implementing function */
}; /* system call table entry */

表中有几个项是从 sysent数组中来的,该数组是在 kern/init_sysent.c中定义的。


struct sysent sysent[] = {
/* . . . */
{ 3, recvmsg }, /* 27 = recvmsg */
{ 3, sendmsg }, /* 28 = sendmsg */
{ 6, recvfrom }, /* 29 = recvfrom */
{3, accept }, /* 30 = accept */
{3, getpeername }, /* 31=getpeername */
{3, getsockname }, /* 32 = getsockname */
/* . . . */
}

例如, r e c v m s g系统调用在系统调用表中的第 2 7个项,它有两个参数,利用内核中的


recvmsg函数实现。
s y s c a l l将参数从调用进程复制到内核中,并且分配一个数组来保存系统调用的结果。
然后,当系统调用执行完成后, s y s c a l l将结果返回给进程。 s y s c a l l将控制交给与系统调
用相对应的内核函数。在 386实现中,调用有点像:
struct sysent *callp;
error = (*callp->sy_call) (p, args, rval);

这里指针 c a l l p指向相关的 s y s e n t结构;指针 p则指向调用系统调用的进程的进程表项;


a rg s作为参数传给系统调用,它是一个 32 bit长的字数组;而 r v a l则是一个用来保存系统调用
的返回结果的数组,数组有两个元素,每个元素是一个 32 bit长的字。当我们用 “系统调用”
这个词时,我们指的是被 syscall调用的内核中的函数,而不是应用调用的进程中的函数。
s y s c a l l期望系统调用函数 (即s y _ c a l l指向的函数 )在没有差错时返回 0,否则返回非 0
的差错代码。如果没有差错出现,内核将 r v a l中的值作为系统调用 (应用调用的 )的返回值传
送给进程。如果有差错, s y s c a l l忽略r v a l中的值,并以与机器相关的方式返回差错代码
给进程,使得进程能从外部变量 e r r n o中得到差错代码。应用调用的函数则返回 - 1或一个空
指针表示应用应该查看 errno获得差错信息。
在3 8 6上的实现,设置进位比特 (carry bit)来表示 s y s c a l l的返回值是一个差错代码。进
程中的系统调用残桩将差错代码赋给 e r r n o,并返回- 1或空指针给应用。如果没有设置进位

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 355
比特,则将 syscall返回的值返回给进程中的系统调用的残桩。
总之,实现系统调用的函数“返回”两个值:一个给 s y s c a l l函数;在没有差错的情况
下,syscall将另一个(在rval中)返回给调用进程。

15.4.1 举例

socket系统调用的原型是:
int socket(int domain, int type, int protocol);

实现socket系统调用的内核函数的原型是:
struct socket_args {
int domain;
int type;
int protocol;
};
socket(struct proc *p, struct socket_args *uap, int *retval);

当一个应用调用socket时,进程用系统调用机制将三个独立的整数传给内核。 syscall
将参数复制到 3 2 b i t值的数组中,并将数组指针作为第二个参数传给 s o c k e t的内核版。内核
版的socket将第二个参数作为指向 socket_args结构的指针。图 15-10显示了上述过程。

进程

从用户空间复制到
内核空间的参数

内核

图15-10 socket 参数处理

同s o c k e t类似,每一个实现系统调用的内核函数将 a r g s说明成一个与系统调用有关的
结构指针,而不是一个指向 32 bit的字的数组的指针。
当原型无效时,隐式的类型转换仅在传统的 K&R C中或ANSI C中是合法的。如
果原型是有效的,则编译器将产生一个警告。
syscall在执行内核系统调用函数之前将返回值置为 0。如果没有差错出现,系统调用函
数直接返回而不需清除 *retval,syscall返回0给进程。

15.4.2 系统调用小结

图15-11对与网络有关的系统调用进行了小结。
我们将在本章中讨论建立、服务器、客户和终止类系统调用。输入、输出类系统调用将
在第16章中介绍,管理类系统调用将在第 17章中介绍。

Linux公社 www.linuxidc.com
bbs.theithome.com
356 计计TCP/IP详解 卷 2:实现

类 别 名 称 功 能

socket 在指明的通信域内产生一个未命名的插口
建 立
bind 分配一个本地地址给插口
listen 使插口准备接收连接请求
服务器
accept 等待并接受连接
客 户 connect 同外部插口建立连接
read 接收数据到一个缓存中
readv 接收数据到多个缓存中
输 入 recv 指明选项接收数据
recvfrom 接收数据和发送者的地址
recvmsg 接收数据到多个缓存中,接收控制信息和发送者地址;指明接收选项
write 发送一个缓存中的数据
writev 发送多个缓存中的数据
输 出 send 指明选项发送数据
sendto 发送数据到指明的地址
sendmsg 从多个缓存发送数据和控制信息到指明的地址;指明发送选项
I/O select 等待I/O事件
shutdown 终止一个或两个方向上的连接
终 止
close 终止连接并释放插口
fcntl 修改I/O语义
ioctl 各类插口操作
setsockopt 设置插口或协议选项
管 理
getsockopt 得到插口或协议选项
getsockname 得到分配给插口的本地地址
getpeername 得到分配给插口的远端地址

图15-11 Net/3中的网络系统调用

图15-12 网络系统调用流程图

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 357
图15-12画出了应用使用这些系统调用的顺序。大方块中的 I/O系统调用可以在任何时候调
用。该图不是一个完整的状态流程图,因为一些正确的转换在本图中没有画出;仅显示了一
些常见的转换。

15.5 进程、描述符和插口

在描述插口系统调用之前,我们需要介绍将进程、描述符和插口联系在一起的数据结构。
图1 5 - 1 3给出了这些结构以及与我们的讨论有关的结构成员。关于文件结构的更复杂的解释请
参考[Leffer ea al. 1989]。

图15-13 进程、文件和插口结构

实现系统调用的函数的第一个参数总为 p,即指向调用进程的 proc结构的指针。内核利用


p r o c结构记录进程的有关信息。在 p r o c结构中, p _ f d指向f i l e d e s c结构,该结构的主要
功能是管理 fd_ofiles指向的描述符表。描述符表的大小是动态变化的,由一个指向 file结
构的指针数组组成。每一个 file结构描述一个打开的文件,该结构可被多个进程共亨。
图1 5 - 1 3仅显示了一个 f i l e结构。通过 p - > p _ f d - > f d _ o f i l e s [ f d ]访问到该结构。在
f i l e结构中,有两个结构成员是我们感兴趣的: f _ o p s和f _ d a t a。I / O系统调用 (如r e a d和
w r i t e)的实现因描述符中的 I / O对象类型的不同而不同。 f _ o p s指向f i l e o p s结构,该结构
包含一张实现 r e a d、w r i t e、i o c t l、s e l e c t和c l o s e系统调用的函数指针表。图 1 5 - 1 3
显示f _ o p s指向一个全局的 f i l e o p s结构,即 s o c k e t o p s,该结构包含指向插口用的函数
的指针。
f _ d a t a指向相关 I / O对象的专用数据。对于插口而言, f _ d a t a指向与描述符相关的
s o c k e t结构。最后, s o c k e t结构中的 s o _ p r o t o指向产生插口时选中的协议的 p r o t o s w结
构。回想一下,每一个 protosw结构是由与该协议关联的所有插口共亨的。
下面我们开始讨论系统调用。

Linux公社 www.linuxidc.com
bbs.theithome.com
358 计计TCP/IP详解 卷 2:实现

15.6 socket系统调用

s o c k e t 系统调用产生一个新的插口,并将插口同进程在参数 domain 、type和


p r o t o c o l中指定的协议联系起来。该函数 (如图1 5 - 1 4所示)分配一个新的描述符,用来在后
续的系统调用中标识插口,并将描述符返回给进程。

图15-14 socket 系统调用

42-55 在每一个系统调用的前面,都定义了一个描述进程传递给内核的参数的结构。在这
种情况下,参数是通过 s o c k e t _ a r g s传入的。所有插口层系统调用都有三个参数: p,指向
调用进程的 p r o c结构; u a p,指向包含进程传送给系统调用的参数的结构; r e t v a l,用来
接收系统调用的返回值。在通常情况下,忽略参数 p和r e t v a l,引用 u a p所指的结构中的内
容。
56-60 f a l l o c分配一个新的 f i l e结构和f d _ o f i l e s数组(图1 5 - 1 3 )中的一个元素。 f p指
向新分配的结构, f d则为结构在数组 f d _ o f i l e s中的索引。
成 员 值
s o c k e t将f i l e结构设置成可读、可写,并且作为一个插口。
将所有插口共亨的全局 f i l e o p s 结构 s o c k e t o p s 连接到
f _ o p s指向的f i l e结构中。 s o c k e t o p s变量在编译时被初始
化,如图15-15所示。
60-69 s o c r e a t e分配并初始化一个 s o c k e t结构。如果 图15-15 socketops :插口
s o c r e a t e执行失败,将差错代码赋给 e r r o r,释放 f i l e结 用全局fileops 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 359
构,清除存放描述符的数组元素。如果 s o c r e a t e执行成功,将 f _ d a t a指向s o c k e t结构,
建立插口和描述符之间的联系。通过 * r e t v a l将f d返回给进程。 s o c k e t返回 0或返回由
socreate返回的差错代码。

15.6.1 socreate函数

大多数插口系统调用至少被分成两个函数,与 s o c k e t和s o c r e a t e类似。第一个函数从


进程那里获取需要的数据,调用第二个函数 s oxxx来完成功能处理,然后返回结果给进程。这
种分成多个函数的做法是为了第二个函数能直接被基于内核的网络协议调用,如 NFS。
socreate的代码如图 15-16所示。

图15-16 socreate 函数

43-52 socreate共有四个参数: dom,请求的协议域 (如,PF_INET);aso,保存指向一


个新的s o c k e t结构的指针;t y p e,请求的插口类型 (如,S O C K _ S T R E A M);p r o t o,请求的
协议。
1. 发现协议交换表

Linux公社 www.linuxidc.com
bbs.theithome.com
360 计计TCP/IP详解 卷 2:实现

53-60 如果p r o t o等于非 0值, p f f i n d p r o t o查找进程请求的协议。如果 p r o t o等于0,


p f f i n d t y p e用由t y p e指定的语义在指定域中查找一种协议。这两个函数均返回一个指向匹
配协议的 protosw结构的指针或空指针 (参考第7.6节)。
2. 分配并初始化socket结构
61-66 socreate分配一个新的socket结构,并将结构内容全清成 0,记录下type。如果
调用进程有超级用户权限,则设置插口结构中的 SS_PRIV标志。
3. PRU_ATTACH请求
67-69 在与协议无关的插口层中发送与协议有关的请求的第一个例子出现在 s o c r e a t e中。
回想在第 7 . 4节和图1 5 - 1 3中,s o - > s o _ p r o t o - > p r _ u s r r e q是一个指向与插口 s o相关联的
协议的用户请求函数指针。每一个协议均提供了一个这样的函数来处理从插口层来的通信请
求。函数原型是:
int pr_usrreq(struct socket *so, int req, struct mbuf *mo, *m1, *m2);

第一个参数是一个指向相关插口的指针, re q是一个标识请求的常数。后三个参数 (m0,


m1,m2)因请求不同而异。它们总是被作为一个 mbuf结构指针传递,即使它们是其他的类型。
在必要的时候,进行类似转换以避免编译器的警告。
图1 5 - 1 7列出了 p r _ u s r r e q函数提供的通信请求。每一个请求的语义起决于服务请求的
协议。

参 数
请 求 描 述
m0 m1 m2

PRU_ABORT 异常终止每一个存在的连接
PRU_ACCEPT address 等待并接受连接
PRU_ATTACH protocol 产生了一个新的插口
PRU_BIND address 绑定地址到插口
PRU_CONNECT address 同地址建立关联或连接
PRU_CONNECT2 socket2 将两个插口连在一起
PRU_DETACH 插口被关闭
PRU_DISCONNECT 切断插口和另一地址间的关联
PRU_LISTEN 开始监听连接请求
PRU_PEERADDR buffer 返回与插口关联的对方地址
PRU_RCVD flags 进程已收到一些数据
PRU_RCVOOB buffer flags 接收OOB数据
PRU_SEND data address control 发送正常数据
PRU_SENDOOB data address control 发送OOB数据
PRU_SHUTDOWN 结束同另一地址的通信
PRU_SOCKADDR buffer 返回与插口相关联的本地地址

图15-17 pr_usrreq 函数

P R U _ C O N N E C T 2请求只用于 U n i x域,它的功能是将两个本地插口连接起来。
Unix的管道(pipe)就是通过这种方式来实现的。
4. 退出处理
70-77 回到s o c r e a t e,函数将协议交换表连接到插口,发送 P R U _ A T T A C H请求通知协议

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 361
已建立一个新的连接端点。该请求引起大多数协议,如 T C P和U D P,分配并初始化所有支持
新的连接端点的数据结构。

15.6.2 超级用户特权

图15-18列出了要求超级用户权限的网络操作。

超级用户
函 数 描 述 参考图
进程 插口

in_control • 分配接口地址、网络掩码、目的地址 图6-14


in_control • 分配广播地址 图6-22
in_pcbbind • 绑定到一个小于 1024的Internet端口 图22-22
ifioctl • 改变接口配置 图4-29
ifioctl • 配置多播地址 (见下面的说明 ) 图12-11
rip_usrreq • 产生一个ICMP、IGMP或原始 IP插口 图32-10
slopen • 将一个SLIP设备与一个 tty设备联系起来 图5-9

图15-18 Net/3中的超级用户特权

当多播 i o c t l 命令 ( S I O C A D D M U L T I 和S I O C D E L M U L T I ) 是被 I P _ A D D _
M E M B E R S H I P和I P _ D R O P _ M E M B E R S H I P插口选项间接激活时,它可以被非超级用
户访问。
在图1 5 - 1 8中,“进程”栏表示请求必须由超级用户进程来发起,“插口”栏表示请求必须
是针对由超级用户产生的插口 (也就是说,进程不需要超级用户权限,而只需有访问插口的权
限,习题 1 5 . 1 )。在 N e t / 3中, s u s e r函数用来判断调用进程是否有超级用户权限,通过
SS_PRIV标志来判断一个插口是否由超级用户进程产生。
因为r i p _ u s r r e q在用s o c r e a t e产生插口后立即检查 S S _ P R I V标志,所以我们认为只
有超级用户进程才能访问这个函数。

15.7 getsock和sockargs函数

这两个函数重复出现在插口系统调用中。 g e t s o c k的功能是将描述符映射到一个文件表
项中, s o c k a r g s将进程传入的参数复制到内核中的一个新分配的 m b u f中。这两个函数都要
检查参数的正确性,如果参数不合法,则返回相应的非 0差错代码。
图15-19列出了getsock函数的代码。
754-767 getsock函数利用fdp查找描述符 fdes指定的文件表项, fdp是指向filedesc
结构的指针。 g e t s o c k将打开的文件结构指针赋给 f p p,并返回,或者当出现下列情况时返
回差错代码:描述符的值超过了范围而不是指向一个打开的文件;描述符没有同插口建立联
系。
图15-20列出了sockargs函数的代码。
768-783 如图1 5 - 4中所描述的, s o c k a r g s将进程传给系统调用的参数的指针从进程复制
到内核而不是复制指针指向的数据,这样做是因为每一个参数的语义只有相对应的系统调用
才知道,而不是针对所有的系统调用。多个系统调用在调用 s o c k a r g s复制参数指针后,将
指针指向的数据从进程复制到内核中新分配的 mbuf中。例如,s o c k a r g s将b i n d的第二个参
Linux公社 www.linuxidc.com
bbs.theithome.com
362 计计TCP/IP详解 卷 2:实现

数指向的本地插口地址从进程复制到一个 mbuf中。

图15-19 getsock 函数

图15-20 sockargs 函数

如果数据不能存入一个 mbuf中或无法分配 mbuf,则s o c k a r g s返回E I N V A L或E N O B U F S。


注意,这里使用的是标准的 m b u f而不是分组首部的 m b u f。c o p y i n的功能是将数据从进程复
制到mbuf中。copyin返回的最常见的差错是 EACCES,它表示进程提供的地址不正确。
784-785 当出现差错时,丢弃 m b u f,并返回差错代码。如果没有差错,通过 m p返回指向
mbuf的指针,sockargs返回0。
786-794 如果t y p e等于M T _ S O N A M E,则进程传入的是一个 s o c k a d d r结构。s o c k a r g s

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 363
将刚复制的参数的长度赋给内部长度变量 s a _ l e n。这一点确保即使进程没有正确地初始化结
构,结构内的大小也是正确的。
Net/3确实包含了一段代码来支持在 pre-4.3BSD Reno系统上编译的应用,这些应
用的sockaddr结构中并没有sa_len字段,但是图15-20中没有显示这段代码。

15.8 bind系统调用

b i n d系统调用将一个本地的网络运输层地址和插口联系起来。一般来说,作为客户的进
程并不关心它的本地地址是什么。在这种情况下,进程在进行通信之前没有必要调用 b i n d;
内核会自动为其选择一个本地地址。
服务器进程则总是需要绑定到一个已知的地址上。所以,进程在接受连接 ( T C P )或接收数
据报( U D P )之前必须调用 b i n d,因为客户进程需要同已知的地址建立连接或发送数据报到已
知的地址。
插口的外部地址由 c o n n e c t指定或由允许指定外部地址的写调用 (s e n d t o或s e n d m s g)
指定。
图15-21列出了bind调用的代码。

图15-21 bind 函数

70-82 bind调用的参数有(在bind_args结构中):s,插口描述符; name,包含传输地址


(如,sockaddr_in结构)的缓存指针;和 namelen,缓存大小。
83-90 getsock返回描述符的 file结构,sockargs将本地地址复制到 mbuf中,sobind
将进程指定的地址同插口联系起来。在 bind返回sobind的结果之前,释放保存地址的 mbuf。
从技术上讲,一个描述符,如 s,标识一个同 s o c k e t结构相关联的 f i l e结构,而它
本身并不是一个 socket结构。将这种描述符看作插口是为了简化我们的讨论。
我们将在下面的讨论中经常看到这种模式:进程指定的参数被复制到 m b u f,必要时还要

Linux公社 www.linuxidc.com
bbs.theithome.com
364 计计TCP/IP详解 卷 2:实现

进行处理,然后在系统调用返回之前释放 m b u f。虽然m b u f是为方便处理网络数据分组而设计


的,但是将它们用作一般的动态内存分配机制也是有效的。
b i n d说明的另一种模式是:许多系统调用不使用 r e t v a l。在第1 5 . 4节中我们已提到过,
在s y s c a l l将控制交给相应的系统调用之前总是将 r e t v a l清0。如果 0不是合适的返回值,
系统调用并不需要修改 retval。

sobind函数

如图15-22所示,sobind是一个封装器,它给与插口相关联的协议发送 PRU_BIND请求。

图15-22 sobind 函数

78-89 s o b i n d发送P R U _ B I N D请求。如果请求成功,将本地地址 n a m同插口联系起来;否


则,返回差错代码。

15.9 listen系统调用

l i s t e n系统调用的功能是通知协议进程准备接收插口上的连接请求,如图 1 5 - 2 3所示。
它同时也指定插口上可以排队等待的连接数的门限值。超过门限值时,插口层将拒绝进入的
连接请求排队等待。当这种情况出现时, T C P 将忽略进入的连接请求。进程可以通过调用
accept (第15.11节)来得到队列中的连接。

图15-23 listen 系统调用

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计365
91-98 listen系统调用有两个参数:一个指定插口描述符;另一个指定连接队列门限值。
99-105 getsock返回描述符 s的file结构,solisten将请求传递给协议层。

solisten函数

solisten函数发送PRU_LISTEN请求,并使插口准备接收连接,如图15-24所示。
90-109 在solisten发送PRU_LISTEN请求且pr_usrreq返回后,标识插口处于准备接收连
接状态。如果当pr_usrreq返回时有连接正在连接队列中,则不设置SS_ACCEPTCONN标志。
计算存放进入连接的队列的最大值,并赋给 s o _ q l i m i t。N e t / 3默认设置下限为 0,上限
为5(SOMAXCONN)条连接。

图15-24 solisten 函数

15.10 tsleep和wakeup函数

当一个在内核中执行的进程因为得不到内核资源而不能继续执行时,它就调用 t s l e e p等
待。tsleep的原型是:
int tsleep(caddr_t chan, int pri, char *mesg, int timeo);

t s l e e p的第一个参数 chan,被称之为等待通道。它标志进程等待的特定资源或事件。许
多进程能同时在同一个等待通道上睡眠。当资源可用或事件出现时,内核调用 w a k e u p,并将
等待通道作为唯一的参数传入。 wakeup的原型是:
void wakeup(caddr_t chan);

所有等待在该通道上的的进程均被唤醒,并被设置成运行状态。当每一个进程均恢复执
行时,内核安排 tsleep返回。
当进程被唤醒时, t s l e e p的第二个参数 p r i指定被唤醒进程的优先级。 p r i中还包括几个
用于 t s l e e p 的可选的控制标志。通过设置 p r i 中的 P C A T C H 标志,当一个信号出现时,
tsleep也返回。mesg是一个说明调用 tsleep的字符串,它将被放在调用报文或ps的输出中。

Linux公社 www.linuxidc.com
bbs.theithome.com
366 计计TCP/IP详解 卷 2:实现

timeo设置睡眠间隔的上限值,其单位是时钟滴答。
图15-25列出了tsleep的返回值。
因为所有等待在同一等待通道上的进程均被wakeup唤醒,所以我们总是看到在一个循环中
调用tsleep。每一个被唤醒的进程在继续执行之前必须检查等待的资源是否可得到,因为另一
个被唤醒的进程可能已经先一步得到了资源。如果仍然得不到资源,进程再调用tsleep等待。

tsleep() 描 述

0 进程被一个匹配的 wakeup唤醒
EWOULDBLOCK 进程在睡眠 timeo个时钟滴答后,在匹配的 w a k e u p调用之前被唤醒
ERESTART 在睡眠期间信号被进程处理,应重新启动挂起的系统调用
EINTR 在睡眠期间信号被进程处理,挂起的系统调用失败

图15-25 tsleep 的返回值

多个进程在一个插口上睡眠等待的情况是不多见的,所以,通常情况下,一次调用
wakeup只有一个进程被内核唤醒。
关于睡眠和唤醒机制的详细讨论请参考 [Leffler et al. 1989]。

举例

多个进程在同一个等待通道上睡眠的一个例子是:让多个服务器进程读同一个 U D P插口。
每一个服务器都调用recvfrom,并且只要没有数据可读就在 tsleep中等待。当一个数据报到
达插口时,插口层调用wakeup,所有等待进程均被放入运行队列。第一个运行的服务器读取了
数据报而其他的服务器则再次调用tsleep。在这种情况下,不需要每一个数据报启动一个新的
进程,就可将进入的数据报分发到多个服务器。这种技术同样可以用来处理 T C P的连接请求,
只需让多个进程在同一个插口上调用accept。这种技术在[Comer and Stevens 1993]中描述。

15.11 accept系统调用

调用l i s t e n后,进程调用 a c c e p t等待连接请求。 a c c e p t返回一个新的描述符,指向


一个连接到客户的新的插口。原来的插口 s仍然是未连接的,并准备接收下一个连接。如果
name指向一个正确的缓存, accept就会返回对方的地址。
处理连接的细节由与插口相关联的协议来完成。对于TCP而言,当一条连接已经被建立(即,
三次握手已经完成 )时,就通知插口层。对于其他的协议,如 OSI的TP4,只要一个连接请求到
达,tsleep就返回。当进程通过在插口上发送或接收数据来显式证实连接后,连接则算完成。
图15-26说明accept的实现。

图15-26 accept 系统调用

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计367

图15-26 (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
368 计计TCP/IP详解 卷 2:实现

图15-26 (续)

106-114 accept有三个参数:s为插口描述符; name为缓存指针,accept将把外部主机


的运输地址填入该缓存; anamelen是一个保存缓存大小的指针。
1. 验证参数
1 1 6 - 1 34 a c c e p t将缓存大小(* a n a m e l e n)赋给n a m e l e n,g e t s o c k返回插口的f i l e结
构。如果插口还没有准备好接收连接 (即,还没有调用 l i s t e n),或已经请求了非阻塞的 I / O,
且没有连接被送入队列,则分别返回 EINVAL或EWOULDBLOCK。
2. 等待连接
1 3 5 - 1 4 5 当出现下列情况时, w h i l e循环退出:有一条连接到达;出现差错;或插口不能
再接收数据。当信号被捕获之后 (t s l e e p返回E I N T R),a c c e p t并不自动重新启动。当协议
层通过sonewconn将一条连接插入队列后,唤醒进程。
在循环内,进程在 t s l e e p中等待,当有连接到达时, t s l e e p返回0。如果t s l e e p被信
号中断或插口被设置成非阻塞,则 accept返回EINTR或EWOULDBLOCK(图15-25)。
3. 异步差错
1 4 6 - 1 51 如果进程在睡眠期间出现差错,则将插口中的差错代码赋给 a c c e p t中的返回码,
清除插口中的差错码后, accept返回。
异步事件改变插口状态是比较常见的。协议处理层通过设置 s o _ e r r o r或唤醒在插口上
等待的所有进程来通知插口层插口状态的改变。因为这一点,插口层必须在每次被唤醒后检
查so_error,查看是否在进程睡眠期间有差错出现。
4. 将插口同描述符相关联
1 5 2 - 1 6 4 f a l l o c为新的连接分配一个描述符;调用 s o q r e m q u e将插口从接收队列中删
除,放到描述符的 file结构中。习题15.4讨论调用panic。
5. 协议处理
167-179 accept分配一个新的mbuf来保存外部地址,并调用 s o a c c e p t来完成协议处理。
在连接处理期间产生的新的插口的分配和排队在第 1 5 . 1 2中描述。如果进程提供了一个缓存来
接收外部地址, c o p y o u t将地址和地址长度分别从 n a m和n a m e l e n中复制给进程。如果有必
要,c o p y o u t还可能将地址截掉,如果进程提供的缓存不够大。最后,释放 m b u f,使能协议
处理,accept返回。
因为仅仅分配了一个 m b u f来存放外部地址,运输地址必须能放入一个 m b u f中。因为 U n i x
域地址是文件系统中的路径名 (最长可达 1 0 2 3个字节 ),所以要受到这个限制,但这对 I n t e r n e t
域中的 1 6字节长的 s o c k a d d r _ i n地址没有影响。第 1 7 0行的注释说明可以通过分配和复制一
个mbuf链的方式来去掉这个限制。

soaccept函数

soaccept函数通过协议层获得新的连接的客户地址,如图 15-27所示。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计369

图15-27 soaccept 函数

184-197 s o a c c e p t 确保插口与一个描述符相连,并发送 P R U _ A C C E P T请求给协议。


pr_usrreq返回后,nam中包含外部插口的名字。

15.12 sonewconn和soisconnected函数

从图1 5 - 2 6中可以看出, a c c e p t等待协议层处理进入的连接请求,并且将它们放入 s o _ q


中。图15-28利用TCP来说明这个过程。

等待进入的连接请求

未完成的连接 已完成的连接

发送SYN和ACK
等待ACK
进入的TCPSYN TCP握手协议的最后一个ACK
图15-28 处理进入的 TCP连接

Linux公社 www.linuxidc.com
bbs.theithome.com
370 计计TCP/IP详解 卷 2:实现

在图1 5 - 2 8的左上角, a c c e p t调用t s l e e p等待进入的连接。在左下角, t c p _ i n p u t调


用s o n e w c o n n为新的连接产生一个插口来处理进入的 TCP SYN(图2 8 - 7 )。s o n e w c o n n将产
生的插口放入so_q0排队,因为三次握手还没有完成。
当TCP握手协议的最后一个ACK到达时, tcp_input调用soisconnected(图29-2)来更
新产生的插口,并将它从so_q0中移到so_q中,唤醒所有调用accept等待进入的连接的进程。
图的右上角说明我们在图 1 5 - 2 6中描述的函数。当 t s l e e p返回时, a c c e p t从s o _ q中得
到连接,发送 P R U _ A T T A C H请求。插口同一个新的文件描述符建立了联系, a c c e p t也返回
到调用进程。
图15-29显示了sonewconn函数。

图15-29 sonewconn 函数

123-129 协议层将 h e a d(指向正在接收连接的插口的指针 )和c o n n s t a t u s(指示新连接的


状态的标志 )传给sonewconn。对于TCP而言,connstatus总是等于0。
对于T P 4,c o n n s t a t u s总是等于 S S _ I S C O N F I R M I N G。当一个进程开始从插
口上接收或发送数据时隐式证实连接。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 371
1. 限制进入的连接
130-131 当下面的不等式成立时, sonewconn不再接收任何连接:
3× so_qlimit
so_qlen + so_q0len >
2
这个不等式为一直没有完成的连接提供了一个令人费解的因子,且该不等式确保
listen(fd, 0)允许一条连接。有关这个不等式的详细情况请参考卷 1的图18-23。
2. 分配一个新的插口
132-143 一个新的 s o c k e t结构被分配和初始化。如果进程对处理接收连接状态的插口调
用了 s e t s o c k o p t ,则新产生的 s o c k e t 继承好几个插口选项,因为 s o _ o p t i o n s 、
so_linger、so_pgid和sb_hiwat的值被复制到新的 socket结构中。
3. 排队连接
144 在第1 2 9行的代码中,根据 c o n n s t a t u s的值设置 s o q u e u e。如果s o q u e u e为0 (如,
T C P连接),则将新的插口插入到 s o _ q 0中;若c o n n s t a t u s等于非0值,则将其插入到 s o _ q
中(如,TP4连接)。
4. 协议处理
145-150 发送P R U _ A T T A C H请求,启动协议层对新的连接的处理。如果处理失败,则将插
口从队列中删除并丢弃,然后 sonewconn返回一个空指针。
5. 唤醒进程
151-157 如果connstatus等于非0值,所有在 accept中睡眠或查询插口的可读性的进程
均被唤醒。将 c o n n s t a t u s对s o _ s t a t e执行或操作。 T C P协议从来不会执行这段代码,因
为对TCP而言,connstatus总是等于0。
某些将进入的连接首先插入 s o _ q 0 队列中的协议在连接建立阶段完成时调用
soisconnected,如TCP。对于TCP,当第二个 SYN被应答时,就出现这种情况。
图15-30显示了soisconnected的代码。

图15-30 soisconnected 函数

6. 排队未完成的连接
78-87 通过修改插口的状态来表明连接已经完成。当对进入的连接调用 s o i s c o n n e c t e d

Linux公社 www.linuxidc.com
bbs.theithome.com
372 计计TCP/IP详解 卷 2:实现

(即,本地进程正在调用 accept)时,head为非空。
如果s o q r e m q u e返回1,就将插口放入 s o _ q排队,s o r w a k e u p唤醒通过调用 s e l e c t测
试插口的可读性来监控插口上连接到达的进程。如果进程在 a c c e p t中因等待连接而阻塞,则
wakeup使得相应的 tsleep返回。
7. 唤醒等待新连接的进程
8 8 - 9 3 如果h e a d为空,就不需要调用 s o q r e m q u e,因为进程用 c o n n e c t系统调用初始化
连接,且插口不在队列中。如果 h e a d非空,且 s o q r e m q u e返回0,则插口已经在 s o _ q队列
中。在某些协议中,如 T P 4,就出现这种情况,因为在 T P 4中,连接完成之前就已插入到
s o _ q队列中。 w a k e u p唤醒所有阻塞在 c o n n e c t中的进程,s o r w a k e u p和s o w w a k e u p负责
唤醒那些调用select等待连接完成的进程。

15.13 connect系统调用
服务器进程调用 l i s t e n和a c c e p t系统调用等待远程进程初始化连接。如果进程想自己
初始化一条连接 (即客户端),则调用connect。
对于面向连接的协议如 T C P,c o n n e c t建立一条与指定的外部地址的连接。如果进程没
有调用bind来绑定地址,则内核选择并且隐式地绑定一个地址到插口。
对于无连接协议如 UDP或ICMP,c o n n e c t记录外部地址,以便发送数据报时使用。任何
以前的外部地址均被新的地址所代替。
图15-31显示了UDP或TCP调用connect时涉及到的函数。

请求
请求

TCP开始
三次握手

TCP三次
握手完成

TCP连接建立
图15-31 connect 处理过程

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 373
图的左边说明 c o n n e c t 如何处理无连接协议,如 U D P。在这种情况下,协议层调用
soisconnected后connect系统调用立即返回。
图的右边说明 c o n n e c t如何处理面向连接的协议,如 T C P。在这种情况下,协议层开始
建立连接,调用 s o i s c o n n e c t i n g指示连接将在某个时候完成。如果插口是非阻塞的,
s o c o n n e c t 调用 t s l e e p 等待连接完成。对于 T C P ,当三次握手完成时,协议层调用
s o i s c o n n e c t e d 将插口标识为已连接,然后调用 w a k e u p唤醒等待的进程,从而完成
connect系统调用。
图15-32列出了connect系统调用的代码。

图15-32 connect 系统调用

Linux公社 www.linuxidc.com
bbs.theithome.com
374 计计TCP/IP详解 卷 2:实现

180-188 connect的三个参数 (在connect_args结构中)是:s为插口描述符; name是一


个指针,指向存放外部地址的缓存; namelen为缓存的长度。
189-200 g e t s o c k获取插口描述符对应的 f i l e结构。可能已有连接请求在非阻塞的插口
上,若出现这种情况,则返回 EALREADY。函数sockargs将外部地址从进程复制到内核。
1. 开始连接处理
201-208 连接是从调用soconnect开始的。如果soconnect报告差错出现, connect跳
转到 b a d。如果 s o c o n n e c t 返回时连接还没有完成且使能了非阻塞的 I / O ,则立即返回
E I N P R O G R E S S以免等待连接完成。因为通常情况下,建立连接要涉及同远程系统交换几个
分组,因而这个过程可能需要一些时间才能完成。如果连接没完成,则下次对 c o n n e c t调用
就返回EALREADY。当连接完成时, soconnect返回EISCONN。
2. 等待连接建立
208-217 w h i l e循环直到连接已建立或出现差错时才退出。 s p l n e t防止c o n n e c t在测试
插口状态和调用 t s l e e p之间错过w a k e u p。循环完成后, e r r o r包含0、t s l e e p中的差错代
码或插口中的差错代码。
218-224 清除S S _ I S C O N N E C T I N G标志,因为连接已完成或连接请求已失败。释放存储外
部地址的mbuf,返回差错代码。

15.13.1 soconnect函数

s o c o n n e c t函数确保插口处于正确的连接状态。如果插口没有连接或连接没有被挂起,
则连接请求总是正确的。如果插口已经连接或连接正等待处理,则新的连接请求将被面向连
接的协议 (如TCP)拒绝。对于无连接协议,如 UDP,多个连接是允许的,但是每一个新的请求

图15-33 soconnect 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 375
中的外部地址会取代原来的外部地址。
图15-33列出了soconnect函数的代码。
198-222 如果插口被标识准备接收连接,则 s o c o n n e c t返回E O P N O T S U P P,因为如果已
经对插口调用了 l i s t e n,则进程不能再初始化连接。如果协议是面向连接的,且一条连接已
经被初始化,则返回 E I S C O N N 。对于无连接协议,任何已有的同外部地址的联系都被
sodisconnect切断。
PRU_CONNECT请求启动相应的协议处理来建立连接或关联。

15.13.2 切断无连接插口和外部地址的关联

对于无连接协议,可以通过调用 c o n n e c t,并传入一个不正确的 n a m e参数,如指向内容


为全0的结构指针或大小不对的结构,来丢弃同插口相关联的外部地址。 s o d i s c o n n e c t删
除同插口相关联的外部地址, P R U _ C O N N E C T 返回差错代码,如 E A F N O S U P P O R T 或
E A D D R N O T A V A I L,留下没有外部地址的插口。这种方式虽然有点晦涩,但却是一种比较有
用的断连方式,在无连接插口和外部地址之间断连,而不是替换。

15.14 shutdown系统调用

s h u t d o w n系统调用关闭连接的读通道、写通道或读写通道,如图 1 5 - 3 4所示。对于读通
道,s h u t d o w n丢弃所有进程还没有读走的数据以及调用 s h u t d o w n之后到达的数据。对于
写通道, s h u t d o w n使协议作相应的处理。对于 T C P,所有剩余的数据将被发送,发送完成
后发送FIN。这就是TCP的半关闭特点(参考卷1的第18.5节)。
为了删除插口和释放描述符,必须调用 c l o s e。可以在没有调用 s h u t d o w n 的情况下,
直接调用 c l o s e。同所有描述符一样,当进程结束时,内核将调用 c l o s e,关闭所有还没有
被关闭的插口。

图15-34 shutdown 系统调用

550-557 在s h u t d o w n _ a r g s结构中, s为插口描述符, h o w指明关闭连接的方式。图 1 5 -


35列出了how和how++(在图15-36中用到的)的期望值。

Linux公社 www.linuxidc.com
bbs.theithome.com
376 计计TCP/IP详解 卷 2:实现

how how++ 描 述

0 FREAD 关闭连接的读通道
1 FWRITE 关闭连接的写通道
2 FREAD|FWRITE 关闭连接的读写通道

图15-35 shutdown 系统调用选项

注意,在how和常数FREAD和FWRITE之间有一种隐含的数值关系。
558-564 shutdow n是函数s o s h u t d o w n的包装函数 (wrapper function)。由g e t s o c k返回
与描述符相关联的插口,调用 soshutdown,并返回其值。

soshutdown和sorflush函数

关闭连接的读通道是由插口层调用 s o r f l u s h 处理的,写通道的关闭是由协议层的
PRU_SHUTDOWN请求处理的。soshutdown函数如图15-36所示。

图15-36 soshutdown 函数

720-732 如果是关闭插口的读通道,则 s o r f l u s h丢弃插口接收缓存中的数据,禁止读连


接(如图15-37所示)。如果是关闭插口的写通道,则给协议发送 PRU_SHUTDOWN请求。
733-747 进程等待给接收缓存加锁。因为 S B _ N O I N T R 被设置,所以当中断出现时,
s b l o c k并不返回。在修改插口状态时, s p l i m p阻塞网络中断和协议处理,因为协议层在接
收到进入的分组时可能要访问接收缓存。
s o c a n t r c v m o r e标识插口拒绝接收进入的分组。将 s o c k b u f结构保存在 a s b中,当
splx恢复中断后,要使用 asb。调用bzero清除原始的 sockbuf结构,使得接收队列为空。
释放控制mbuf
748-751 当shutdown被调用时,存储在接收队列中的控制信息可能引用了一些内核资源。
通过sockbuf结构的副本中的 sb_mb仍然可以访问mbuf链。
如果协议支持访问权限,且注册了一个dom_dispose函数,则调用该函数来释放这些资源。
在U n i x域中,用控制报文在进程间传递描述符是可能的。这些报文包含一些引
用计数的数据结构的指针。 d o m _ d i s p o s e函数负责去掉这些引用,如果必要,还释
放相关的数据缓存以避免产生一些未引用的结构和导致内存漏洞。有关在 U n i x域内
传递文件描述符的细节请参考 [Stevens 1990]和[Leffler et al. 1989]。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计 377

图15-37 sorflush 函数

当s b r e l e a s e释放接收队列中的所有 mbuf时,丢弃所有调用 s h u t d o w n时还没有被处理


的数据。
注意,连接的读通道的关闭完全由插口层来处理 (习题1 5 . 6 ),连接的写通道的关闭通过发
送P R U _ S H U T D O W N请求交由协议处理。 TCP协议收到P R U _ S H U T D O W N请求后,发送所有排队
的数据,然后发送一个 FIN来关闭TCP连接的写通道。

15.15 close系统调用

c l o s e系统调用能用来关闭各类描述符。当 f d是引用对象的最后的描述符时,与对象有
关的close函数被调用:
error = (*fp->f_ops->fo_close)(fp,p);

如图15-13所示,插口的fp->f_ops->fo_close是soo_close函数。

15.15.1 soo_close函数

soo_close函数是soclose函数的封装器,如图 15-38所示。

图15-38 soo_close 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
378 计计TCP/IP详解 卷 2:实现

152-161 如果s o c k e t结构与f i l e相关联,则调用 s o c l o s e,清除f _ d a t a,返回已出现


的差错。

15.15.2 soclose函数

s o c l o s e函数取消插口上所有未完成的连接 (即,还没有完全被进程接受的连接 ),等待


数据被传输到外部系统,释放不需要的数据结构。
soclose函数的代码如图 15-39所示。

图15-39 soclose 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 15章 插 口 层计计379
1. 丢弃未完成的连接
129-141 如果插口正在接收连接, soclose遍历两个连接队列,并且调用 soabort取消每
一个挂起的连接。如果协议控制块为空,则协议已同插口分离, s o c l o s e跳转到d i s c a r d进
行退出处理。
s o a b o r t 发送 P R U _ A B O R T 请求给协议,并返回结果。本书中没有介绍
soabort的代码。图 23-38和图30-7讨论了UDP和TCP如何处理PRU_ABORT请求。
2. 断开已建立的连接或关联
142-157 如果插口没有同任何外部地址相连接,则跳转到 drop处继续执行。否则,必须断
开插口与对等地址之间的连接。如果断连没有开始,则 s o d i s c o n n e c t启动断连进程。如果
设置了SO_LINGER插口选项, soclose可能要等到断连完成后才返回。对于一个非阻塞的插
口,从来不需要等待断连完成,所以在这种情况下, soclose立即跳转到 drop。否则,连接
终止正在进行且 SO_LINGER选项指示soclose必须等待一段时间才能完成操作。直到出现下
列情况时while才退出:断连完成;拖延时间 (so_linger)到;或进程收到了一个信号。
如果滞留时间被设为 0,t s l e e p仅当断连完成 (也许因为一个差错 )或收到一个信号
时才返回。
3. 释放数据结构
158-173 如果插口仍然同协议相连,则发送 P R U _ D E T A C H请求断开插口与协议的联系。最
后,插口被标记为同任何描述符没有关联,这意味着可以调用 sofree释放插口。
sofree函数代码如图15-40所示。

图15-40 sofree 函数

4. 如果插口仍在用则返回
110-114 如果仍然有协议同插口相连,或如果插口仍然同描述符相连,则 s o f r e e立即返
回。
5. 从连接队列中删除插口
115-119 如果插口仍在连接队列上 (s o _ h e a d非空),则插口的队列应该为空。如果不空,
则插口代码和内核 panic中有差错。如果队列为空,清除 so_head。
6. 释放发送和接收队列中的缓存

Linux公社 www.linuxidc.com
bbs.theithome.com
380 计计TCP/IP详解 卷 2:实现

120-123 sorelease释放发送队列中的所有缓存,sorflush释放接收队列中的所有缓存。
最后,释放插口本身。

15.16 小结

本章中我们讨论了所有与网络操作有关的系统调用。描述了系统调用机制,并且跟踪系
统调用直到它们通过 pr_usrreq函数进入协议处理层。
在讨论插口层时,我们避免涉及地址格式、协议语义或协议实现等问题。在接下来的章
节中,我们将通过协议处理层中的 Internet协议的实现将链路层处理和插口层处理联系在一起。

习题

15.1 一个没有超级用户权限的进程怎样才能获取对超级用户进程产生的插口的访问权 ?
15.2 一个进程怎样才能判断它提供给 a c c e p t的s o c k a d d r缓存是不是太小以至不能存
放调用返回的外部地址 ?
15.3 I P v 6的插口有一个特点:使 a c c e p t和r e c v f r o m返回一个 128 bit的I P v 6地址的数
组作为源路由,而不是仅返回一个对等地址。因为数组不能存放在一个 mbuf中,所
以修改 a c c e p t和r e c v f r o m,使得它们能够处理协议层来的 m b u f链而不是仅仅一
个m b u f。如果协议在 m b u f簇中返回一个数组而不是一个 m b u f链,已有的代码仍然
能正常工作吗?
15.4 为什么在图 15-26中当soqremque返回一个空指针时要调用 panic?
15.5 为什么sorflush要复制接收缓存 ?
15.6 在s o r f l u s h将插口的接收缓存清 0后,如果还有数据到达会出现什么现象 ?在做这
个习题之前请阅读第 16章的内容。

Linux公社 www.linuxidc.com
bbs.theithome.com
第16章 插 口 I/O
16.1 引言

本章讨论有关从网络连接上读写数据的系统调用,分三部分介绍。
第一部分介绍四个用来发送数据的系统调用:write、writev、sendto和sendmsg。第
二部分介绍四个用来接收数据的系统调用:read、readv、recvfrom和recvmsg。第三部分
介绍select系统调用,select调用的作用是监控通用描述符和特殊描述符(插口)的状态。
插口层的核心是两个函数: s o s e n d和s o r e c e i v e。这两个函数负责处理所有插口层和
协议层之间的 I / O操作。在后续的章节中我们将看到,因为这两个函数要处理插口层和各种类
型的协议之间的 I/O操作,使得这两个函数特别长和复杂。

16.2 代码介绍

图16-1中列出了本章后续章节要用到的三个头文件和四个 C源文件。

文 件 名 说 明

sys/socket.h 插口API中的结构和宏定义
sys/socketvar.h s o c k e t结构和宏定义
sys/uio.h u i o结构定义
kern/uipc_syscalls.c s o c k e t系统调用
kern/uipc_socket.c 插口层处理
kern/sys_generic.c s e l e c t系统调用
kern/sys_socket.c s e l e c t对插口的处理

图16-1 本章涉及的头文件和 C源文件

全局变量

图1 6 - 2列出了三个全局变量。前两个变量由 s e l e c t系统调用使用,第三个变量控制分配
给插口的存储器大小。

变 量 数据类型 说 明

selwait int s e l e c t调用的等待通道


nselcoll int 避免s e l e c t调用中出现竞争的标志
sb_max u_long 插口发送或接收缓存的最大字节数

图16-2 本章涉及的全局变量

16.3 插口缓存

从第15.3节我们已经知道,每一个插口都有一个发送缓存和一个接收缓存。缓存的类型为

Linux公社 www.linuxidc.com
bbs.theithome.com
382 计计TCP/IP详解 卷 2:实现

sockbuf。图16-3中列出了sockbuf结构的定义 (重复图15-5)。

图16-3 sockbuf 结构

72-78 每一个缓存均包含控制信息和指向存储数据的 mbuf链的指针。 sb_mb指向mbuf链的


第一个 m b u f,s b _ c c的值等于存储在 m b u f链中的数据字节数。 s b _ h i w a t和s b _ l o w a t用来
调整插口的流控算法。 sb_mbcnt等于分配给缓存中的所有 mbuf的存储器数量。
在前面的章节中提到过每一个 m b u f可存储 0 ~ 2 0 4 8个字节的数据 (如果使用了外部簇 )。
s b _ m b m a x是分配给插口 mbuf缓存的存储器数量的上限。默认的上限在 socket系统调用中发送
PRU_ATTACH请求时由协议设置。只要内核要求的每个插口缓存的大小不超过 262,144个字节
的限制(s b _ m a x),进程就可以修改缓存的上限和下限。流控算法将在 1 6 . 4节和1 6 . 8节中讨论。
图16-4显示了Internet协议的默认设置。

协 议

(忽略)

原始
(忽略)

图16-4 Internet协议的默认的插口缓存限制

因为每一个进入的 U D P报文的源地址同数据一起排队,所以 U D P协议的 s b _ h i w a t的默


认值设置为能容纳 40个1K字节长的数据报和相应的 sockaddr_in结构(每个16字节)。
79 sb_sel是一个用来实现 select系统调用的 selinfo结构(16.13节)。
80 图16-5列出了sb_flags的所有可能的值。

sb-flags 说 明

SB_LOCK 一个进程已经锁定了插口缓存
SB_WANT 一个进程正在等待给插口缓存加锁
SB_WAIT 一个进程正在等待接收数据或发送数据所需的缓存
SB_SEL 一个或多个进程正在选择这个缓存
SB_ASYNC 为这个缓存产生异步 I/O信号
SB_NOINTR 信号不取消加锁请求
SB_NOTIFY (SB_WAIT|SB_AEL|SB_ASYNC)
一个进程正在等待缓存的变化,如果缓存发生任何改变,用 w a k e u p通知该进程

图16-5 sb_flags 的值

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 383
81-82 s b _ t i m e o用来限制一个进程在读写调用中被阻塞的时间,单位为时钟滴答 ( t i c k )。
默认值为 0,表示进程无限期的等待。 S O _ S N D T I M E O和S O _ R C V T I M E O插口选项可以改变或
读取sb_timeo的值。

插口宏和函数

有许多宏和函数用来管理插口的发送和接收缓存。图 1 6 - 6中列出了与缓存加锁和同步有
关的宏和函数。

名 称 说 明

sblock 申请给sb加锁,如果 wf等于M _ W A I T O K,则进程睡眠等待加锁;否则,如果不能立即给


缓存加锁,就返回 E W O U L D B L O C K 。如果进程睡眠被一个信号中断,则返回 E I N T R或
EREST A R T;否则返回 0
wt
int s b l o c k( s t r u c t s o c k b u f * s b , i n f) ;
sbunlock 释放加在sb上的锁。所有等待给 sb加锁的进程被唤醒
v o i d s b u n l o c k( s t r u c t s o c k b u f sb);
*
sbwait 调用t s l e e p等待sb上的协议动作。返回 t s l e e p返回的结果
int s b w a i t( s t r u c t s o c k b u f sb);
*
sowakeup 通知插口有协议动作出现。唤醒所有匹配的调用 s b w a i t的进程或在 sb上调用t s l e e p的
进程
v o i d s o w a k e u p( s t r u c t s o c k e t sb,
* s t r u c t s o c k b u f sb);
*
sorwakeup 唤醒等待sb上的读事件的进程,如果进程请求了 I/O事件的异步通知,则还应给该进程发
送SIGI O信号
void s o r w a k e u p( s t r u c t s o c k e t sb);
*
sowwakeup 唤醒等待sb上的写事件的进程,如果进程请求了 I/O事件的异步通知,则还应给该进程发
送SIGI O信号
v o i d s o w w a k e u p( s t r u c t s o c k e t sb);
*

图16-6 与缓存加锁和同步有关的宏和函数

图1 6 - 7显示了设置插口资源限制、往缓存中写数据和从缓存中删除数据的宏和函数。在
该表中,m、m0、n和control都是指向mbuf链的指针。 sb指向插口的发送或接收缓存。

名 称 说 明

sbspace sb中可用的空间 (字节数) :


min(sb_hiwat - sb_cc), (sb_mbmax - sb_mbcont)
long s b s p a c e( s t r u c t s o c k b u fsb); *
sballoc 将m加到sb中,同时修改 sb中的s b _ c c和s b _ m b c n t
v o i d s b a l l o c( s t r u c t s o c k b u f sb,
* s t r u c t m b u f m);
*
sbfree 从sb中删除m,同时修改 sb中的s b _ c c和s b _ m b c n t
int s b f r e e( s t r u c t s o c k b u f sb, * s t r u c t m b u f m);*
sbappend 将m中的mbuf加到sb的最后面
int s b a p p e n d( s t r u c t s o c k b u f sb, * s t r u c t m b u f m*);
sbappendrecord 将m0中的记录加到 sb的最后面。调用 s b c o m p r e s s
int s b a p p e n d r e c o r d( s t r u c t s o c k b u f sb,
* s t r u c t m b u f m*0);

图16-7 与插口缓存分配与操作有关的宏和函数

Linux公社 www.linuxidc.com
bbs.theithome.com
384 计计TCP/IP详解 卷 2:实现

名 称 说 明

sbappendaddr 将asa的地址放入一个 mbuf。将地址、 control和m0连接成一个 mbuf链,并将该


链放在sb的最后面
int a b a p p e n d a d d r(struct *sb, s t r u c t s o c k a d d ras* a,
s t r u c t m b u f m*0, s t r u c t m b u f co*ntrol);
sbappendcontrol 将control和m0连接成一个 mbuf链,并将该链放在 sb的最后面
int a b a p p e n d c o n t r o l( s t r u c t s*b, s t r u c t m b u f m*0,
s t r u c t m b u f c*
ontrol);
sbinsertoob 将m0插在没有带外数据的 sb的第一个记录的前面
int a b i n s e r t o o b( s t r u c t s o c k b u f sb, * s t r u c t m b u f m*0);
sbcompress 将m合并到n中并压缩没用的空间
v o i d a b c o m p r e s s( s t r u c t s o c k b u f sb,
* s t r u c t m b u f m,*
s t r u c t m b u f n);
*
sbdrop 删除sb的前len个字节
v o i d s b d r o p( s t r u c t s o c k b u f sb, * i n t len);
sbdroprecord 删除sb的第一个记录,将下一个记录移作第一个记录
v o i d s b d r o p r e c o r d( s t r u c t s o c k b u f sb);
*
sbrelease 调用s b f l u s h释放sb中所有的mbuf。并将s b _ h i w a t和s b _ m b m a x清0
v o i d s b r e l e a s e( s t r u c t s o c k b u f sb);
*
sbflush 释放sb中的所有mbuf
v o i d s b f l u s h( s t r u c t s o c k b u f sb);
*
soreserve 设置插口缓存高、低水位标记 ( h i g h - w a t e r a n d l o w - w a t e r m a。对 rk)
于发送缓存,调用 s b r e s e r v e 并传入参数 s n d c c 。对于接收缓存,调用
s b r e s e r v e并传入参数 r c v c c。将发送缓存和接收缓存的 s b _ l o w a t初始化成默
认值(图16-4)。如果超过系统限制,则返回 E N O B U F S
int s o r e s e r v e( s t r u c t s o c k e t so, * i n t sndcc, i n t rcvcc);
sbreserve 将sb的高水位标记设置成cc。同时将低水位标记降到cc。本函数不分配存储器
int s b r e s e r v e( s t r u c t s o c k b u f sb, * i n t cc);

图16-7 (续)

16.4 write、writev、sendto和send m s g系统调用

我们将w r i t e、w r i t e v、s e n d t o和s e n d m s g四个系统调用统称为写系统调用,它们的


作用是往网络连接上发送数据。相对于最一般的调用 s e n d m s g而言,前三个系统调用是比较
简单的接口。
所有的写系统调用都要直接或间接地调用 s o s e n d。s o s e n d的功能是将进程来的数据复
制到内核,并将数据传递给与插口相关的协议。图 16-8给出了sosend的工作流程。
在下面的章节中,我们将讨论图 1 6 - 8 中带阴影的函数。其余的四个系统调用和
soo_write 留给读者自己去了解。
图16-9说明了这四个系统调用和一个相关的库函数 (send)的特点。
在N e t / 3中,s e n d被实现成一个调用 s e n d t o的库函数。为了与以前编译的程序
二进制兼容,内核将旧的 send调用映射成函数 osend,该函数不在本书中讨论。
从图16-9的第二栏中可以看出, w r i t e和w r i t e v系统调用适用于任何描述符,而其他的
系统调用只适用于插口描述符。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 385
库函数
进程
内核

库函数

TCP TP 4
通过pr_usrreq发送的
PRU_SEND或PRU_SENDOOB
UDP ICMP

图16-8 所有的插口输出均由 sosend 处理

函 数 描述符类型 缓存数量 是否指明 标 志? 控制信息?


目的地址

write 任何类型 1
writev 任何类型 [1..UIO_MAXIOV]
send 插口 1 •
sendto 插口 1 • •
sendmsg 插口 [1..UIO-MAXIOV] • • •

图16-9 写系统调用

从图1 6 - 9的第三栏中可以看出, w r i t e v和s e n d m s g系统调用可以接收从多个缓存中来


的数据。从多个缓存中写数据称为收集 ( g a t h e r i n g ),同它相对应的读操作称为分散
(s c a t t e r i n g)。执行收集操作时,内核按序接收类型为 i o v e c的数组中指定的缓存中的数
据。数组最多有 UIO_MAXIOV个单元。图 16-10显示了类型 iovec的结构。

图16-10 iovec 结构

41-44 在图16-10中,iov_base指向长度为 iov_len个字节的缓存的开始。


如果没有这种接口,一个进程将不得不将多个缓存复制到一个大的缓存中,或调用多个

Linux公社 www.linuxidc.com
bbs.theithome.com
386 计计TCP/IP详解 卷 2:实现

写系统调用来发送从多个缓存来的数据。相对于用一个系统调用传送类型为 i o v e c的数组,
这两种方法的效率更低。对于数据报协议而言,调用一次 w r i t e v就是发送一个数据报,数据
报的发送不能用多个写动作来实现。
图1 6 - 11说明了i o v e c结构在w r i t e v系统调用中的应用,图中 i o v p指向数组的第一个元
素,iovcnt等于数组的大小。

图16-11 writev 系统调用中的 iovec 参数

数据报协议要求每一个写调用必须指定一个目的地址。因为 w r i t e、w r i t e v和s e n d调


用接口不支持对目的地址的指定,因此这些调用只能在调用 c o n n e c t将目的地址同一个无连
接的插口联系起来后才能被调用。调用 s e n d t o或s e n d m s g时必须提供目的地址,或在调用
它们之前调用connect来指定目的地址。
图1 6 - 9的第五栏显示 s e n d x x x系统调用接收一个可选的控制标志,这些标志在图 1 6 - 1 2中
定义。

flags 描 述 参 考

MSG_DONTROUTE 发送本报文时,不查路由表 图16-23


MSG_DONTWAIT 发送本报文时,不等待资源 图16-22
MSG_EOR 标志一个逻辑记录的结束 图16-25
MSG_OOB 发送带外数据 图16-26

图16-12 send xxx系统调用: flags 值

如图1 6 - 9的最后一栏所示,只有 s e n d m s g系统调用支持控制信息。控制信息和另外几个


参数是通过结构 msghdr(图16-13)一次传递给 sendmsg,而不是分别传递。

图16-13 msghdr 结构

msg_name应该被说明成一个指向sockaddr结构的指针,因为它包含网络地址。
228-236 msghdr结构包含一个目的地址 (msg_name和msg_namelen)、一个分散 /收集数
组(m s g _ i o v和m s g _ i o v l e n)、控制信息(m s g _ c o n t r o l和m s g _ c o n t r o l l e n)和接收标志

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 387
(msg_flags)。控制信息的类型为 cmsghdr结构,如图 16-14所示。

图16-14 cmsghdr 结构

251-256 插口层并不解释控制信息,但是报文的类型被置为 c m s g _ t y p e,且报文长度为


cmsg_len。多个控制报文可能出现在控制信息缓存中。

举例

图16-15说明了在调用sendmsg时msghdr的结构。

图16-15 sendmsg 系统调用的 msghdr 结构

16.5 sendmsg系统调用

只有通过 s e n d m s g系统调用才能访问到与插口 A P I的输出有关的所有功能。 s e n d m s g和


s e n d i t函数准备 s o s e n d系统调用所需的数据结构,然后由 s o s e n d调用将报文发送给相应
的协议。对 S O C K _ D G R A M协议而言,报文就是数据报。对 S O C K _ S T R E A M协议而言,报文是
一串字节流。对于 S O C K _ S E Q P A C K E T协议而言,报文可能是一个完整的记录 (隐含的记录边
界)或一个大的记录的一部分 (显式的记录边界 )。对于 S O C K _ P D M协议而言,报文总是一个完
整的记录(隐含的记录边界 )。
即使一般的 s o s e n d代码处理 S O C K _ S E Q P A C K E T和S O C K _ P D K协议,但是在
Internet域中没有这样的协议。
图16-16显示了sendmsg系统调用的源代码。
307-319 s e n d m s g有三个参数:插口描述符;指向 m s g h d r结构的指针;几个控制标志。
函数copyin将msghdr结构从用户空间复制到内核。

Linux公社 www.linuxidc.com
bbs.theithome.com
388 计计TCP/IP详解 卷 2:实现

图16-16 sendmsg 系统调用

1. 复制iov数组
320-334 一个有8个元素(U I O _ S M A L L I O V)的i o v e c数组从栈中自动分配。如果分配的数
组不够大, s e n d m s g 将调用 M A L L O C 分配更大的数组。如果进程指定的数组单元大于
1024(U I O _ M A X I O V),则返回 E M S G S I Z E。c o p y i n将i o v e c数组从用户空间复制到栈中的数
组或一个更大的动态分配的数组中。
这种技术避免了调用 m a l l o c带来的高代价,因为大多数情况下,数组的单元数
小于等于8。
2. sendit和cleanup
335-340 如果s e n d i t返回,则表明数据已经发送给相应的协议或出现差错。 s e n d m s g释
放iovec数组(如果它是动态分配的 ),并且返回sendit调用返回的结果。

16.6 sendit函数

s e n d i t函数是被 s e n d t o和s e n d m s g调用的公共函数。 s e n d i t初始化一个 u i o结构,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计389
将控制和地址信息从进程空间复制到内核。在讨论 s o s e n d之前,我们必须先解释 u i o m o v e
函数和uio结构。

16.6.1 uiomove函数

uiomove函数的原型为:
int uiomove(caddr_t cp, int n, struct uio *uio);

u i o mo v e函数的功能是在由 c p指向的缓存与 u i o指向的类型为 i o v e c的数组中的多个缓存之


间传送n个字节。图 16-7说明了uio结构的定义,该结构控制和记录 uiomove的行为。

图16-17 uio 结构

45-61 在u i o 结构中, u i o _ i o v 指向类型为 i o v e c 结构的数组, u i o _ o f f s e t记录


u i o m o v e 传送的字节数, u i o _ r e s i d 记录剩余的字节数。每次调用 uiomove,
uio_offset增加n, u i o _ r e s i减去n。同时,u
d iomove根据传送的字节数调整 uio_iov
数组中的基指针和缓存长度,从而从缓存中删除每次调用时传送的字节。最后,每当从
uio_iov中传送一块缓存, u i o _ i o v数组的每个单元就向前进一个数组单元。 u i o _ s e g f l g
指向u i o _ i o v数组的基指针指向的缓存的位置。 u i o _ r w指定数据传送的方向。缓存可能在
用户数据空间,用户指令空间或内核数据空间。图 1 6 - 1 8对u i o m o v e函数的操作进行了小结。
图中对操作的描述用到了 uiomove函数原型中的参数名。

uio_segflg uio_rw 描 述

UIO_USERSPACE UIO-READ 从内核缓存 cp中分散n个字节到进程缓存


UIO_USERISPACE
UIO_USERSPACE UIO-WRITE 从进程缓存中收集 n个字节到内核缓存 c p
UIO_USERISPACE
UIO_SYSSPACE UIO-READ 从内核缓存 cp中分散n个字节到多个内核缓存
UIO-WRITE 从多个内核缓存中收集 n个字节到内核缓存 cp中

图16-18 uiomove 操作

Linux公社 www.linuxidc.com
bbs.theithome.com
390 计计TCP/IP详解 卷 2:实现

16.6.2 举例

图16-19显示了一个调用 uiomove之前的uio结构。

进程
内核

进程

图16-19 调用uiomove 前的uio 结构

u i o _ i o v指向i o v e c数组的第一个单元。 i o v _ b a s e指针数组的每一个单元分别指向它


们在进程地址空间中的缓存的起始地址。 u i o _ o f f s e t等于0,u i o _ r e s i d等于三块缓存的
总的大小。 c p指向内核中的一块缓存,一般来说,这块缓存是一个 m b u f的数据区。图 1 6 - 2 0
显示了调用 uiomove之后同一个 uio结构的内容。
uiomove(cp, n, uio);

进程
内核

进程

图16-20 调用uiomove 后的uio 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 391
在上述调用中, n 包括第一块缓存中的所有字节和第二块缓存中的部分字节 ( 即,
n0<n<n0+n1)。
调用u i o m o v e后,第一块缓存的长度变为 0,且它的基指针指向缓存的末端。 u i o _ i o v
现在指向 i o v e c数组的下一个单元。单元指针也前进了一个单元,长度也减少了,减少的字
节数等于缓存中被传送的字节数。同时, u i o _ o f f s e t增加了n,u i o _ r e s i d减少了n。数据
已经从进程中的缓存传送到内核缓存,因为 uio_rw等于UIO_WRITE.。

16.6.3 sendit代码

现在开始讨论sendit的代码,如图16-21所示。
1. 初始化auio
341-368 sendit调用getsock函数获取描述符对应的 file结构,初始化uio结构,并将
进程指定的输出缓存中的数据收集到内核缓存中。传送的数据的长度通过一个 f o r循环来计
算,并将结果保存在 u i o _ r e s i d。循环内的第一个 i f保证缓存的长度非负。第二个 i f保证
uio_resid不溢出,因为uio_resid是一个有符号的整数,且 iov_len要求非负。
2. 从进程复制地址和控制信息
369-385 如果进程提供了地址和控制信息 ,则s o c k a r g s将地址和控制信息复制到内核缓存
中。

图16-21 sendit 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
392 计计TCP/IP详解 卷 2:实现

图16-21 (续)

3. 发送数据和清除缓存
386-401 为了防止s o s e n d不接受所有数据而无法计算传送的字节数,将 u i o _ r e s i d的值
保存在 l e n中。将插口、目的地址、 u i o结构、控制信息和标志全部传给函数 s o s e n d。当
sosend返回后,sendit响应如下:
• 如果s o s e n d传送了部分数据后,传送被信号或阻塞条件所中断,差错被丢弃,报告传
送了部分数据。
• 如果s o s e n d返回E P I P E,则发送信号 S I G P I P E给进程。 e r r o r设置成非 0,所以如果
进程捕捉到了该信号,并且从信号处理程序中返回,或进程忽略信号,写调用返回
EPIPE。
• 如果没有差错出现 (或差错被丢弃 ),则计算传送的字节数,并将其保存在 * r e t s i z e中。
如果sendit返回0,syscall(第15.4节)返回*retsize给进程而不是返回差错代码。
• 如果任何其他类型的差错出现,返回相应差错代码给进程。
在返回之前,sendit释放包含目的地址的缓存。 sosend负责释放control缓存。

16.7 sosend函数

s o s e n d是插口层中最复杂的函数之一。在图 1 6 - 8中已提到过所有五个写系统调用最终都
要调用s o s e n d。s o s e n d的功能就是:根据插口指明的协议支持的语义和缓存的限制,将数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 393
据和控制信息传递给插口指明的协议的 p r _ u s r r e q函数。s o s e n d从不将数据放在发送缓存
中;存储和移走数据应由协议来完成。
s o s e n d对发送缓存的 s b _ h i w a t和s b _ l o w a t值的解释,取决于对应的协议是否实现可
靠或不可靠的数据传送功能。

16.7.1 可靠的协议缓存

对于提供可靠的数据传送协议,发送缓存保存了还没有发送的数据和已经发送但还没有
被确认的数据。 sb_cc等于发送缓存的数据的字节数,且 0≤sb_cc≤sb_hiwat。
如果有带外数据发送,则 sb_cc有可能暂时超过 sb_hiwat。
s o s e n d应该确保在通过 p r _ u s r r e q函数将数据传递给协议层之前有足够的发送缓存。
协议层将数据放到发送缓存中。 sosend通过下面两种方式之一将数据传送给协议层:
• 如果设置了 P R _ A T O M I C,s o s e n d就必须保护进程和协议层之间的边界。在这种情况
下,s o s e n d等待得到足够的缓存来存储整个报文。当获取到足够的缓存后,构造存储
整个报文的 m b u f链,并用 p r _ u s r r e q函数一次性传送给协议层。 R D P和S P P就是这种
类型的协议。
• 如果没有设置 P R _ A T O M I C,s o s e n d每次传送一个存有报文的 m b u f给协议,可能传送
部分 m b u f给协议层以防止超过上限。这种方法在 S O C K _ S T R E A M类协议如 T C P中和
S O C K _ S E Q P A C K E T类协议如 T P 4中被采用。在 T P 4中,记录边界通过 M S G _ E O R标志(图
16-12)来显式指定,所以 sosend没有必要保护报文边界。
T C P应用程序对外出的 T C P报文段的大小没有控制。例如,在 T C P插口上发送一个长度为
4 0 9 6字节的报文,假定发送缓存中有足够的缓存,则插口层将该报文分成两部分,每一部分
长度为 2 0 4 8个字节,分别存放在一个带外部簇的 m b u f中。然后,在协议处理时, T C P将根据
连接上的最大报文段大小将数据分段,通常情况下,最大报文段大小为 2048个字节。
当一个报文因为太大而没有足够的缓存时,协议允许报文被分成多段,但 s o s e n d仍然不
将数据传送给协议层直到缓存中的闲置空间大小大于 s b _ l o w a t。对于TCP而言,s b _ l o w a t
的默认值为 2048 (图16-4),从而阻止插口层在发送缓存快满时用小块数据干扰 TCP。

16.7.2 不可靠的协议缓存

对于提供不可靠的数据传输的协议 (如UDP)而言,发送缓存不需保存任何数据,也不等待
任何确认。每一个报文一旦被排队等待发送到相应的网络设备,插口层立即将它传送到协议。
在这种情况下, s b _ c c总是等于 0,s b _ h i w a t指定每一次写的最大长度,间接指明数据报的
最大长度。
图1 6 - 4显示了 U D P协议的 s b _ h i w a t的默认值为 9 2 1 6 ( 9×1 0 2 4 )。如果进程没有通过
SO_SNDBUF插口选项改变sb_hiwat的值,则发送长度大于 9216个字节的数据报将导致差错。
不仅如此,其他的协议限制也可能不允许一个进程发送大的数据报报文。卷 1的第11 . 1 0节中
已讨论了在其他的 TCP/IP实现中的这些选项和限制。
对于NFS写而言, 9216已足够大,NFS写的数据加上协议首部的长度一般默认为
8192个字节。

Linux公社 www.linuxidc.com
bbs.theithome.com
394 计计TCP/IP详解 卷 2:实现

图16-22显示了sosend函数的概况。下面分别讨论图中四个带阴影的部分。

图16-22 sosend 函数:概述

271-278 sosend的参数有如下几个: so,指向相应插口的指针; addr,指向目的地址的


指针; u i o,指向描述用户空间的 I / O缓存的 u i o结构; t o p,保存将要发送的数据的 m b u f
链;control,保存将要发送的控制信息的 mbuf链;flags,包含本次写调用的一些选项。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 395
正常情况下,进程通过 u i o机制将数据提供给插口层, t o p为空。当内核本身正在使用插
口层时(如NFS),数据将作为一个 mbuf链传送给 sosend,top指向该mbuf链,而uio为空。
279-304 初始化代码分别如下所述。
1. 给发送缓存加锁
305-308 s o s e n d的主循环从 r e s t a r t开始,在循环的开始调用 s b l o c k给发送缓存加锁。
通过加锁确保多个进程按序互斥访问插口缓存。
如果在 f l a g s 中 M S G _ D O N T W A I T 被设置,则 S B L O C K W A I T 将返回 M _ N O W A I T 。
M_NOWAIT告知sblock,如果不能立即加锁,则返回 EWOULDBLOCK。
MSG_DONTWAIT仅用于Net/3中的NFS。
主循环直到将所有数据都传送给协议 (即resid=0)后才退出。
2. 检查空间
309-341 在传送数据给协议之前,需要对各种差错情况进行检查,并且 s o s e n d实现前面
讨论的流控和资源控制算法。如果 s o s e n d阻塞等待输出缓存中的更多的空间,则它跳回
restart等待。
3. 使用top中的数据
342-350 一旦有了足够的空间并且 s o s e n d也获得了发送缓存上的锁,则准备传送给协议
的数据。如果 u i o等于空(即数据在 t o p指向的mbuf链中),则s o s e n d检查M S G _ E O R,并且在
链中设置M_EOR来标志逻辑记录的结束。 mbuf链是准备发送给协议层的。
4. 从进程复制数据
351-396 如果uio不空,则sosend必须从进程间复制数据。当PR_ATOMIC被设置时(例如,
U D P ),循环继续,直到所有数据都被复制到一个 m b u f链中。当 s o s e n d从进程得到所有数据
后,通过循环中的 b r e a k(图1 6 - 2 2中没有显示这个 b r e a k)跳出循环。跳出循环后, s o s e n d
将整个数据链一次传送给相应协议。
5. 传送数据给协议
395-414 对于P R _ A T O M I C协议,当整个数据链被传送给协议后, r e s i d总是等于 0,并且
控制跳出两个循环后至 r e l e a s e处。如果 P R _ A T O M I C没有被置位,且当还有数据要发送并
有缓存空间时,则 s o s e n d继续往 m b u f中写数据。如果缓存中没有闲置空间,但仍然有数据
要发送,则 sosend回到循环开始,等待闲置空间来写下一个 mbuf。如果所有数据都发送完,
则两个循环结束。
6. 释放缓存
414-422 当所有数据都传送给协议后,给插口缓存解锁,释放多余的 mbuf缓存,然后返回。
sosend的详细情况将分四个部分来描述:
• 初始化(图16-23)
• 差错和资源检查 (图16-24)
• 数据传送(图16-25)
• 协议处理(图16-26)
sosend的第一部分初始化变量,如图 16-23所示。
7. 计算传送大小和语义
279-284 如果s o s e n d a l l a t o n c e等于t r u e(任何设置了P R _ A T O M I C的协议)或数据已经

Linux公社 www.linuxidc.com
bbs.theithome.com
396 计计TCP/IP详解 卷 2:实现

通过t o p中的mbuf链传送给 s o s e n d,则将设置 a t o m i c。这个标志控制数据是作为一个 mbuf


链还是作为多个独立的 mbuf传送给协议。
285-297 r e s i d等于i o v e c缓存中的数据字节数或 t o p中的mbuf链中的数据字节数。习题
16.1讨论为什么 resid可能等于负数的问题。

图16-23 sosend 函数:初始化

8. 关闭路由
298-303 如果仅仅要求对这个报文不通过路由表进行路由选择,则设置 dontroute。
clen等于在可选的控制缓存中的字节数。
304 宏snderr传送差错代码,重新使能协议处理,控制跳转到 out执行解锁和释放缓存的工
作。这个宏简化函数内的差错处理工作。
图16-24显示的sosend代码功能是检查差错条件和等待发送缓存中的闲置空间。
309 当检查差错情况时,为防止缓存发生改变,协议处理被挂起。在每一次数据传送之前,
sosend要检查以下几种差错情况:
310-311 • 如果插口输出被禁止 (即,TCP连接的写道通已经被关闭 ),则返回EPIPE。
312-313 • 如果插口正处于差错状态 (例如,前一个数据报可能已经产生了一个 ICMP不可达
的差错 ),则返回 s o _ e r r o r。如果差错出现之前数据已经被收到,则 s e n d i t忽略这个差错
(图16-21的第389行)。
314-318 • 如果协议请求连接且连接还没有建立或连接请求还没有启动,则返回ENOTCONN。
sosend允许只有控制信息但没有数据的写操作,即使连接还没有建立。
Internet协议并不使用这个特点,但 TP4用它在连接请求中发送数据,证实连接请

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 397
求,在断连请求中发送数据。
319-321 • 如果在无连接协议中没有指定目的地址 ( 例如,进程调用 s e n d但并没有用
connect建立目的地址),则返回EDESTADDREQ。

图16-24 sosend 函数:差错和资源检查

9. 计算可用空间
322-324 sbspace函数计算发送缓存中剩余的闲置空间字节数。这是一个基于缓存高水位
标记的管理上的限制,但也是 s b _ m b m a x对它的限制,其目的是为了防止太多的小报文消耗
太多的m b u f缓存(图1 6 - 6 )。s o s e n d通过放宽缓存限制到 1 0 2 4个字节来给予带外数据更高的优
先级。
10. 强制实施报文大小限制
325-327 如果a t o m i c被置位,并且报文大于高水位标记 (h i g h - w a t e r mark ),则返回
E M S G S I Z E;报文因为太大而不被协议接受,即使缓存是空的。如果控制信息的长度大于高
水位标记,同样返回 EMSGSIZE。这是限制数据或记录大小的测试代码。
11. 等待更多的空间吗 ?
328-329 如果发送缓存中的空间不够,数据来源于进程 (而不是来源于内核中的 top),并且
下列条件之一成立,则 sosend必须等待更多的空间:
Linux公社 www.linuxidc.com
bbs.theithome.com
398 计计TCP/IP详解 卷 2:实现

• 报文必须一次传送给协议 (atomic为真);或
• 报文可以分段传送,但闲置空间大小低于低水位标记;或
• 报文可以分段传送,但可用空间存放不下控制信息。
当数据通过 t o p传送给 s o s e n d (即, u i o为空 )时,数据已经在 m b u f缓存中。因此,
sosend忽略缓存高、低水位标记限制,因为不需要附加的缓存来保存数据。
如果在测试中,忽略发送缓存的低水位标记,在插口层和运输层之间将出现一种有趣的
交互过程,它将导致性能下降。 [Crowcroft et al. 1992]提供了有关这个问题的详细情况。
12. 等待空间
330-338 如果sosend必须等待缓存且插口是非阻塞的,则返回 EWOULDBLOCK。同时,缓
存锁被释放, s o s e n d 调用 s b w a i t 等待,直到缓存状态发生变化。当 s b w a i t 返回后,
s o s e n d重新使能协议处理,并且跳转到 r e s t a r t获取缓存锁,检查差错和缓存空间。如果
条件满足,则继续执行。
默认情况下, s b w a i t阻塞直到可以发送数据。通过 S O _ S N D T I M E O插口选项改变缓存中
的s b _ t i m e o,进程可以设置等待时间的上限。如果定时器超时,则返回 E W O U L D B L O C K。回
想一下图 1 6 - 2 1,如果数据已经被成功发送给协议,则 s e n d i t忽略这个差错。这个定时器并
不限制整个调用的时间,而仅仅是限制写两个 mbuf缓存之间的不活动时间。
339-341 在这点上, s o s e n d 已经知道一些数据已传送给协议。 s p l x使能中断,因为
s o s e n d从进程复制数据到内核相对较长的时间间隔内不应该被阻塞。 m p包含一个指针,用
来构造 m b u f链。在 s o s e n d从进程复制任何数据之前,可用缓存的数量需减去控制信息的大
小(clen)。
图16-25显示了sosend从进程复制数据到一个或多个内核中的 mbuf中的代码段。
13. 分配分组首部或标准 mbuf
351-360 当a t o m i c被置位时,这段代码在第一次循环时分配一个分组首部,随后分配标
准的m b u f缓存。如果 a t o m i c没有被置位,则这段代码总是分配一个分组首部,因为进入循
环之前,top总是被清除。
14. 尽可能用簇
361-371 如果报文足够大使得为其分配一个簇是值得的,并且 s p a c e 大于或等于
MCLBYTES,则调用MCLGET分配一个簇同mbuf连在一起。当space小于MCLBYTES时,额外
的2048个字节将超过缓存分配限制,因为即使 resid小于MCLBYTES,整个簇也将被分配。
如果调用MCLGET失败,sosend跳转到nopages,用一个标准的 mbuf代替一个外部簇。
对M I N C L S I Z E的测试应该用>,而不是≥,因为208(M I N C L S I Z E)个字节的写操
作只适合小于两个 mbuf的情况。
如果a t o m i c被设置(例如,U D P ),则m b u f链表示一个数据报或记录,并且在第一个簇的
前面为协议首部保留 m a x _ h d r个字节。而后续的簇因为是同一条链的一部分,所以不需要再
为协议首部保留空间。
如果a t o m i c没有被置位 (如,T C P ),则不需要保留空间,因为 s o s e n d不知道协议如何
将发送的数据进行分段。
需要注意的是, s p a c e由簇大小 ( 2 0 4 8个字节)而不是l e n来决定, l e n等于放在簇中的数
据的字节数 (习题16-2)。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计399

图16-25 sosend 函数:数据传送

15. 准备mbuf
372-382 如果不用簇,存储在 m b u f中的字节数受下面三个量中最小一个量的限制: ( 1 )
mbuf中的可用空间; (2) 报文的字节数; (3) 缓存的空间。
如果a t o m i c被置位,则利用 M H _ A L I G N可知数据在链中的第一个缓存的尾部。如果数据
占居整个 m b u f,则忽略 M H _ A L I G N。这一点可能导致没有足够的空间来存放协议首部,主要
取决于有多少数据存放在 mbuf中。如果atomic没有被置位,则没有为协议首部保留空间。

Linux公社 www.linuxidc.com
bbs.theithome.com
400 计计TCP/IP详解 卷 2:实现

16. 从进程复制数据
383-395 u i o m o v e从进程复制 l e n个字节的数据到 m b u f。传送完成后,更新 m b u f的长度,
前面的 m b u f连接到新的 m b u f (或t o p指向第一个 m b u f ),更新 m b u f链的长度。如果在传送过程
中发生差错,则 sosend跳转到release。
一旦最后一个字节传送完毕,如果进程设置了 M S G _ E O R,则设置分组中的 M _ E O R,然后
sosend跳出循环。
M S G _ E O R仅用于有显式的记录边界的协议,如 O S I协议簇中的 T P 4。T C P不支持逻辑记录
因而忽略 MSG_EOR标志。
17. 写另一个缓存吗 ?
396 如果设置了 atomic,sosend回到循环开始,写另一个 mbuf。
对s p a c e > 0的测试好像无关紧要。当 a t o m i c没有被设置时, s p a c e也是无关
紧要的,因为一次只传送一个 mbuf给协议。如果设置了 a t o m i c,只有当有足够的缓
存空间来存放整个报文时才进入这个循环。参考习题 16-2。
s o s e n d的最后一段代码的功能是传送数据和控制 m b u f给插口指定的协议,如图 1 6 - 2 6所
示。

图16-26 sosend 函数:协议分散

397-405 在传送数据到协议层的前后,可能通过 SO_DONTROUTE选项选择是否利用路由表


为这个报文选择路由。这是唯一的一个针对单个报文的选项,如图 1 6 - 2 3所示,在写期间通过
MSG_DONTROUTE标志来控制路由选择。
为了防止协议在处理报文期间 p r _ u s r r e q阻塞中断, p r _ u s r r e q被放在s p l n e t函数和
s p l x函数之间执行。一些协议 (如U D P )可能在进行输出处理期间并不阻塞中断,但插口层得
不到这些信息。
如果进程传送的是带外数据,则 s o s e n d 发送 P R U _ S E N D O O B 请求;否则,它发送
PRU_SEND请求。同时将地址和控制 mbuf传送给协议。
406-413 因为控制信息只需传送给协议一次,所以将 clen、control、top和mp初始化,
然后为传送报文的下一部分构造新的 m b u f链。只有 a t o m i c没有被设置时 (如T C P ),r e s i d才

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 401
可能等于非 0。在这种情况下,如果缓存中仍然有空间,则 s o s e n d回到循环开始,继续写另
一个mbuf。如果没有可用空间,则 sosend回到循环开始,等待可用空间 (图16-24)。
在第2 3章我们将了解到不可靠的协议,如 U D P,立即将数据排队等待发送。第 2 6章描述
可靠的协议,如 TCP,将数据放到插口发送缓存直到数据被发送和确认。

16.7.3 sosend函数小结

s o s e n d是一个比较复杂的函数。它共有 1 4 2行,包含 3个嵌套的循环,一个利用 g o t o实


现的循环,两个基于是否设置 P R _ A T O M I C的代码分支,两个并行锁。像许多其他软件一样,
复杂性是多年积累的结果。 N F S加入M S G _ D O N T W A I T功能以及从m b u f链接收数据而不是从进
程那里接收数据。 S S _ I S C O N F I R M I N G状态和M S G _ E O R标志是为处理 O S I协议连接和记录功
能而加入的。
比较好的做法是为每一种协议实现一个独立的 s o s e n d函数,通过分散指针 p r _ s e n d给
protosw入口来实现。[Partridge and Pink 1993]中提出并实现了这种方法。

16.7.4 性能问题

如图1 6 - 2 5所描述的, s o s e n d尽可能地以 m b u f为单位将报文传送到协议层。与将一个报文


用一个 m b u f链的形式一次建立并传送给协议层的方法相比,这种做法导致了更多的调用,但
是[Jacobson 1998a]说明了这种做法增加了并行性,因而获得了较好的性能。
一次传送一个 m b u f ( 2 0 4 8个字节 )允许C P U在网络硬件传输数据的同时准备一个分组。同
发送一个大的 m b u f链相比:构造一个大的 m b u f链的同时,网络和接收系统是空闲的。在
[Jacobson 1998a]描述的系统中,这种改变导致了网络吞吐量增加 20%。
有一点非常重要,即确保发送缓存的大小总是大于连接的带宽和时延的乘积 (卷1的第20.7
节)。例如,如果 T C P认为一条连接在收到确认之前能保留 2 0个报文段,那么发送缓存必须大
到足够存储 2 0个未被确认的报文段。如果发送缓存太小, T C P在收到第一个确认之前将用完
数据,连接将在一段时间内是空闲的。

16.8 read、readv、recvfrom和recv m s g系统调用

我们将r e a d、r e a d v、r e c v f r o m和r e c v m s g系统调用统称为读系统调用,从网络连接


上接收数据。同 r e c v m s g相比,前三个系统调用比较简单。 r e c v m s g因为比较通用而复杂得
多。图16-27给出了这四个系统调用和一个库函数 (recv)的特点。

函 数 描述符类型 缓存数量 返回发送者的地址吗 ? 标 志? 返回控制信息 ?

read 任何类型 1
readv 任何类型 [1..UIO_MAXIOV]
recv 插口 1 •
recvfrom 插口 1 • •
recvmsg 插口 [1..UIO-MAXIOV] • • •

图16-27 读系统调用

在N e t / 3中,r e c v是一个库函数,通过调用 r e c v f r o m来实现的。为了同以前编

Linux公社 www.linuxidc.com
bbs.theithome.com
402 计计TCP/IP详解 卷 2:实现

译的程序二进制兼容,内核将旧的 r e c v系统调用映射到函数 o r e c v。我们仅仅讨论


recvfrom的内核实现。
只有read和readv系统调用适用于各类描述符,其他的调用只适用于插口描述符。
同写调用一样,通过 i o v e c结构数组来指定多个缓存。对数据报协议, r e c v f r o m和
r e c v m s g返回每一个收到的数据报的源地址。对于面向连接的协议, g e t p e e r n a m e返回连
接对方的地址。与接收调用相关的标志参考第 16.11节。
同写调用一样,读调用利用一个公共函数 s o r e c e i v e来做所有工作。图 1 6 - 2 8说明读系
统调用的流程。

库函数
进程
内核

通过pr_usrreg发送的PRU_RCVD
或PRU_RCVOOB

图16-28 所有插口输入都由 soreceive 处理

我们仅仅讨论图 16-28中的带阴影的函数。其余的函数读者可以自己查阅有关资料。

16.9 recvmsg系统调用

r e c v m s g函数是最通用的读系统调用。如果一个进程使用任何一个其他的读系统调用,
且地址、控制信息和接收标志的值还未定,则系统可能在没有任何通知的情况下丢弃它们。
图16-29显示了recvmsg函数。
433-445 r e c v m s g的三个参数是:插口描述符;类型为 m s g h d r的结构指针,几个控制标
志。
1. 复制iov数组
446-461 同s e n d m s g一样, r e c v m s g将m s g h d r结构复制到内核,如果自动分配的数组

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 403
a i o v太小,则分配一个更大的 i o v e c数组,并且将数组单元从进程复制到由 i o v指向的内核
数组(第16.4节)。将第三个参数复制到 msghdr结构中。

图16-29 recvmsg 系统调用

2. recvit和释放缓存
462-470 r e c v i t收完数据后,将更新过的缓存长度和标志的 m s g h d r结构再复制到进程。
如果分配了一个更大的 iovec结构,则返回之前释放它。

16.10 recvit函数

r e c v i t函数被 r e c v、r e c v f r o m和r e c v m s g调用,如图 1 6 - 3 0所示。基于 r e c v x x x调


用提供的msghdr结构,recvit函数为soreceive的处理准备了一个 uio结构。
471-500 g e t s o c k为描述符 s返回一个 f i l e结构,然后r e c v i t初始化u i o结构,该结构
描述从内核到进程之间的一次数据传送。通过对 i o v e c数组中的 m s g _ i o v l e n字段求和得到

Linux公社 www.linuxidc.com
bbs.theithome.com
404 计计TCP/IP详解 卷 2:实现

传送的字节数。结果保留在 uio_resid中的len中。

图16-30 recvit 函数:初始化 uio 结构

recvit的第二部分调用 soreceive,并且将结果复制到进程,如图 16-31所示。

图16-31 recvit 函数:返回结果

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 405

图16-31 (续)

1. 调用soreceive
501-510 s o r e c e i v e实现从插口缓存中接收数据的最复杂的功能。传送的字节数保存在
* r e t s i z e 中,并且返回给进程。如果有些数据已经被复制到进程后信号出现或阻塞出现
(len不等于uio_resid),则忽略差错,并返回已经传送的字节。
2. 将地址和控制信息复制到进程
511-542 如果进程传入了一个存放地址或控制信息或两者都有的缓存,则 r e c v i t将结果
写入该缓存,并且根据 s o r e c e i v e返回的结果调整它们的长度。如果缓存太小,则地址信息
可能被截掉。如果进程在发送读调用之前保留缓存的长度,将该长度同内核返回的
n a m e l e n p 变量 ( 或 s o c k a d d r 结构的长度域 ) 相比较就可以发现这个差错。通过设置
msg_flags中的MSG_CTRUNC标志来报告这种差错,参考习题 16-7。
3. 释放缓存
543-549 从out开始,释放存储源地址和控制信息的 mbuf缓存。

16.11 soreceive函数

s o r e c e i v e函数将数据从插口的接收缓存传送到进程指定的缓存。某些协议还提供发送
者的地址,地址可以同可能的附加控制信息一起返回。在讨论它的代码之前,先来讨论接收
操作,带外数据和插口接收缓存的组织的含义。

Linux公社 www.linuxidc.com
bbs.theithome.com
406 计计TCP/IP详解 卷 2:实现

图16-32 列出了在执行soreceive期间内核知道的一些标志。

flags 描 述 参 考

MSG_DONTWAIT 在调用期间不等待资源 图16-38


MSG_OOB 接收带外数据而不是正常的数据 图16-39
MSG_PEEK 接收数据的副本而不取走数据 图16-43
MSG_WAITALL 在返回之前等待数据写缓存 图16-50

图16-32 recv xxx系统调用:传递给内核的标志值

r e c v m s g是唯一返回标志字段给进程的读系统调用。在其他的系统调用中,控制返回给
进程之前,这些信息被内核丢弃。图 16-33列出了在msghdr中recvmsg能设置的标志。

ms g_f l a g s 描 述 参 考

MSG_CTRUNC 控制信息的长度大于提供的缓存长度 图16-31


MSG_EOR 收到的数据标志一个逻辑记录的结束 图16-48
MSG_OOB 缓存中包含带外数据 图16-45
MSG_TRUNC 收到的报文的长度大于提供的缓存长度 图16-51

图16-33 recvmsg 系统调用:内核返回的 msg_flag 值

16.11.1 带外数据

带外数据 ( O O B )在不同的协议中有不同的含义。一般来说,协议利用已建立的通信连接
来发送 O O B数据。O O B数据可能与已发送的正常数据同序。插口层支持两种与协议无关的机
制来实现对 O O B数据的处理:标记和同步。本章讨论插口层实现的抽象的 O O B机制。U D P不
支持OOB数据。TCP的紧急数据机制与插口层的 OOB数据之间的关系在 TCP一章中描述。
发送进程通过在 s e n dx x x调用中设置 M S G _ O O B标志将数据标记为 O O B数据。 s o s e n d将
这个信息传递给插口协议,插口层收到这个信息后,对数据进行特殊处理,如加快发送数据
或使用另一种排队策略。
当一个协议收到OOB数据后,并不将它放进插口的接收缓存而是放在其他地方。进程通过
设置 r e c vx x x调用中的 M S G _ O O B标志来接收到达的 O O B数据。另一种方法是,通过设置
SO_OOBINLINE插口选项(见第17.3节),接收进程可以要求协议将 OOB数据放在正常的数据之
内。当S O _ O O B I N L I N E被设置时,协议将收到的 O O B数据放进正常数据的接收缓存。在这种
情况下, M S G _ O O B不用来接收 O O B数据。读调用要么返回所有的正常数据,要么返回所有的
O O B数据。两种类型的数据从来不会在一个输入调用的输入缓存中混淆。进程使用 r e c v m s g
来接收数据时,可以通过检查 MSG_OOB标志来决定返回的数据是正常数据还是 OOB数据。
插口层支持 O O B数据和正常数据的同步接收,采用的方法是允许协议在正常数据流中标
记O O B数据起始点。接收者可以在每一个读系统调用的后面,通过 S I O C A T M A R Ki o c t l命
令来检查是否已经达到 O O B数据的起始点。当接收正常的数据时,插口层确保在一个报文中
只有在标记前的正常数据才会收到,使得接收者接收的数据不会超过标记。如果在接收者到
达标记之前收到一些附加的 OOB数据,标记就自动向前移。

16.11.2 举例

图1 6 - 3 4说明两种接收带外数据的方法。在两个例了中,字节 A ~ I作为正常数据接收,字

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 407
节J作为带外数据接收,字节 K ~ L作为正常数据接收。接收进程已经接收了 A之前(不包括A )的
所有数据。
处理 接收缓存

带外数据
标记

已处理 接收缓存

标记和带外数据
图16-34 接收带外数据

在第一个例子中,进程能够正确读出字节 A~I,或者如果设置 MSG_OOB,也能读出字节 J。


即使读请求的长度大于 9个字节( A ~ I ),插口层也只返回 9个字节,以免超过带外数据的同步标
记。当读出字节 I后,SIOCATMARK为真;对于到达带外数据标记的进程,不必读出字节 J。
在第二个例了中,在 SIOCATMARK为真时只能读字节 A~I。第二次调用读字节 J~L。
在图16-34中,字节 J不是TCP的紧急数据指针指示的字节。在本例中,紧急指针指向的是
字节K。有关细节请参考第 29.7节。

16.11.3 其他的接收操作选项

进程能够通过设置标志 M S G _ P E E K来查看是否有数据到达。而数据仍然留在接收队列中,
被下一个不设置 MSG_PEEK的读调用读出。
标志M S G _ W A I T A L L指示读调用只有在读到指定数量的数据后才返回。即使 s o r e c e i v e
中有一些数据可以返回给进程,但它仍然要等到收到剩余的数据后才返回。
当标志M S G _ W A I T A L L被设置后, s o r e c e i v e只有在下列情况下可以在没有读完指定长
度的数据时返回:
• 连接的读通道被关闭;
• 插口的接收缓存小于所读数据的大小;
• 在进程等待剩余的数据时差错出现;
• 带外数据到达;或
• 在读缓存被写满之前,一个逻辑记录的结尾出现。
N F S是N e t / 3中唯一使用 M S G _ W A I T A L L和M S G _ D O N T W A I T标志的软件。进程可
以不通过 i o c t l或f c n t l来选择非阻塞的 I / O操作而是设置 M S G _ D O N T W A I T标志来
实现非阻塞的读系统调用。

16.11.4 接收缓存的组织:报文边界

对于支持报文边界的协议,每一个报文存放在一个 m b u f链中。接收缓存中的多个报文通

Linux公社 www.linuxidc.com
bbs.theithome.com
408 计计TCP/IP详解 卷 2:实现

过m _ n e x t p k t指针链接成一个 m b u f队列(图2 - 2 1 )。协议处理层加数据到接收队列,插口层从


接收队列中移走数据。接收缓存的高水位标记限制了存储在缓存中的数据量。
如果P R _ A T O M I C没有被置位,协议层尽可能多地在缓存中存放数据,丢弃输入数据中的
不合要求的部分。对于 T C P,这就意味着到达的任何数据如果在接收窗口之外都将被丢弃。
如果 P R _ A T O M I C被置位,缓存必须能够容纳整个报文,否则协议层将丢弃整个报文。对于
U D P而言,如果接收缓存已满,则进入的数据报都将被丢弃,缓存满的原因可能是进程读数
据报的速度不够快。
P R _ A D D R被置位的协议使用 s b a p p e n d a d d r构造一个 m b u f链,并将其加入到接收队列。
缓存链包含一个存放报文源地址的 m b u f,0个或更多的控制 m b u f,后面跟着 0个或更多的包含
数据的mbuf。
对于S O C K _ S E Q P A C K E T和S O C K _ R D M协议,它们为每一个记录建立一个 m b u f链。如果
P R _ A T O M I C 被置位,则调用 s b a p p e n d r e c o r d ,将记录加到接收缓存的尾部。如果
P R _ A T O M I C没有被置位 ( O S I的T P 4 ),则用 s b a p p e n d r e c o r d产生一个新的记录,其余的数
据用sbappend加到这个记录中。
假定 P R _ A T O M I C就是表示缓存的组织结构是不正确的。例如, T P 4中并没有
PR_ATOMIC,而是用M_EOR标志来支持记录边界。
图1 6 - 3 5说明了由三个 m b u f链(即三个数据报 )组成的U D P接收缓存的结构。每一个 m b u f中
都标有m_type的值。
在图1 6 - 3 5中,第三个数据报中有一些控制信息。三个 U D P插口选项能够导致控制信息被
存入接收缓存。详细情况参考图 22-5和图23-7。

数据报 1

数据报 2

数据报 3

图16-35 包含三个数据报的 UDP接收缓存

对于P R _ A T O M I C协议,当收到数据时, s b _ l o w a t被忽略。当没有设置 P R _ A T O M I C时,


sb_lowat的值等于读系统调用返回的最小的字节数。但也有一些例外,如图 16-41所示。

16.11.5 接收缓存的组织:没有报文边界

当协议不需维护报文边界 (即S O C K _ S T R E A M协议,如 T C P )时,通过 s b a p p e n d将进入的

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 409
数据加到缓存中的最后一个 m b u f链的尾部。如果进入的数据长度大于缓存的长度,则数据将
被截掉, sb_lowat为一个读系统调用返回的字节数设置了一个下限。
图16-36说明了仅仅包含正常数据的 TCP接收缓存的结构。

图16-36 TCP的so_rcv 缓存

16.11.6 控制信息和带外数据

不像T C P,一些流协议支持控制信息,并且调用 s b a p p e n d c o n t r o l将控制信息和相关


数 据 作 为 一 个 新 的 m b u f 链加 入接 收 缓 存 。 如 果 协议 支 持 内 含 O O B 数 据 , 则调用
s b i n s e r t o o b插入一个新的 m b u f链到任何包含 O O B数据的m b u f链之后,但在任何包含正常
数据的mbuf链之前。这一点确保进入的 OOB数据总是排在正常数据之前。
图16-37说明包含控制信息和 OOB数据的接收缓存的结构。

图16-37 带有控制信息和 OOB数据的so_rcv 缓存

U n i x域流协议支持控制信息, OSI TP4协议支持 M T _ O O B D A T A m b u f。T C P既不支持控制


信息,也不支持 M T _ O O B D A T A形式的带外数据。如果 T C P的紧急指针指向的字节存储在数据
内(S O _ O O B I N L I N E被设置 ),那么该字节是正常数据而不是 O O B数据。 T C P对紧急指针和相

Linux公社 www.linuxidc.com
bbs.theithome.com
410 计计TCP/IP详解 卷 2:实现

关数据的处理在第 29.7节中讨论。

16.12 soreceive代码

我们现在有足够的背景信息来详细讨论 s o r e c e i v e函数。在接收数据时, s o r e c e i v e
必须检查报文边界,处理地址和控制信息以及读标志所指定的任何特殊操作 (图1 6 - 3 2 )。一般
来说, s o r e c e i v e的一次调用只处理一个记录,并且尽可能返回要求读的字节数。图 1 6 - 3 8
显示了soreceive函数的大概情况。

图16-38 soreceive 函数:概述

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 411

图16-38 (续)

439-446 s o r e c e i v e有六个参数。指向插口的 s o指针。指向存放接收地址信息的 m b u f缓


存的指针 * p a d d r。如果 m p 0指向一个 m b u f链,则 s o r e c e i v e将接收缓存中的数据传送到
* m p 0指向的 m b u f缓存链。在这种情况下, u i o结构中只有用来记数的 u i o _ r e s i d字段是有
意义的。如果mp0为空,则 soreceive将数据传送到uio结构中指定的缓存。 *controlp指
向包含控制信息的 mbuf缓存。soreceive将图16-33中描述的标志存放在 *flagsp。
447-453 soreceiv e一开始将pr指向插口协议的交换结构,并将 u i o _ r e s i d(接收请求的
大小)保存在o r i g _ r e s i d。如果将控制或地址信息从内核复制到进程,则将 o r i g _ r e s i d清
0。如果复制的是数据,则更新 u i o _ r e s i d。不管哪一种情况, o r i g _ r e s i d都不可能等于
uio_resid。soreceive函数的最后处理要利用这一事实 (图16-51)。
454-461 在这一段代码中,首先将 *paddr和*controlp置空。在将 MSG_EOR标志清0后,
将传给 s o r e c e i v e的* f l a g s p的值保存在 f l a g s中(习题1 6 . 8 )。f l a g s p是一个用来返回结
果的参数,但是只有 recvmsg系统调用才能收到结果。如果 flagsp为空,则将 flags清0。
483-487 在访问接收缓存之前,调用 s b l o c k 给缓存加锁。如果 f l a g s 中没有设置
MSG_DONTWAIT标志,则 soreceive必须等待加锁成功。
支持在内核中从 NFS发调用到插口层带来了另一个负作用。
挂起协议处理,使得在检查缓存过程中 s o r e c e i v e不被中断。 m是接收缓存中的第一个
mbuf链上的第一个mbuf。
1. 如果需要,等待数据
488-541 soreceive要检查几种情况,并且如果需要,它可能要等待接收更多的数据才继
续往下执行。如果 s o r e c e i v e在这里进入睡眠状态,则在它醒来后跳转到 r e s t a r t查看是
否有足够的数据到达。这个过程一直继续,直到收到足够的数据为止。
542-545 当s o r e c e i v e已收到足够的数据来满足读请求所要求的数据量时,就跳转到
dontblock。并将指向接收缓存中的第二个 mbuf链的指针保存在 nextrecord中。

Linux公社 www.linuxidc.com
bbs.theithome.com
412 计计TCP/IP详解 卷 2:实现

2. 处理地址和控制信息
542-545 在传送数据之前,首先处理地址信息和控制信息。
3. 建立数据传送
591-597 因为只有 O O B数据或正常数据是在一次 s o r e c e i v e调用中传送,这段代码的功
能就是记住队列前端的数据的类型,这样在类型改变时, soreceive能够停止传送。
4. 传送数据循环
598-692 只要缓存中还有 mbuf (m不空),请求的数据还没有传送完毕 (uio_resid>0),且
没有差错出现,本循环就不会退出。
退出处理
693-719 剩余的代码主要是更新指针、标志和偏移;释放插口缓存锁;使能协议处理并返
回。
图16-39说明soreceive对OOB数据的处理。

图16-39 soreceive 函数:带外数据

5. 接收OOB数据
462-477 因为OOB数据不存放在接收缓存中,所以 soreceive为其分配一块标准的 mbuf,
并给协议发送 P R U _ R C V O O B请求。w h i l e循环将协议返回的数据复制到 uio指定的缓存中。复
制完成后, soreceive返回0或差错代码。
对于P R U _ R C V O O B请求, U D P协议总是返回 E O P N O T S U P P。关于 T C P的紧急数据的处理
的详细情况参考第 30.2节。图16-40说明soreceive对连接信息的处理。

图16-40 soreceive 函数:连接信息

6. 连接证实
478-482 如果返回的数据存放在 m b u f链中,则将 * m p初始化成空。如果插口处于

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 413
SO_ISCONFIRMING状态,PRU_RCVD请求告知协议进程想要接收数据。
S O _ I S C O N F I R M I N G状态仅用于 O S I的流协议, T P 4。在T P 4中,直到一个用户
级进程通过发送或接收数据的方式来证实连接,该连接才被认为已完全建立。在通
过调用g e t p e e r n a m e来获取对方的身份后,进程可能调用 s h u t d o w n或c l o s e来拒
绝连接。
图1 6 - 3 8显示了图 1 6 - 4 1中的代码在检查接收缓存时,接收缓存被加锁。 s o r e c e i v e的这
部分代码的功能是查看接收缓存中的数据是否能满足读系统调用的要求。

图16-41 soreceive 函数:数据够吗 ?

7. 读调用的请求能满足吗 ?
488-504 一般情况下,soreceive要等待直到接收缓存中有足够的数据来满足整个读请求。
但是,有几种情况可能导致差错或返回比读请求要求少的数据。
• 接收缓存没有数据 (m等于0)。
• 缓存中的数据不能满足读请求要求的数量 (s b _ c c < u i o _ r e s i d ) 并且没有设置
M S G _ D O N T W A I T标志,最少的数据也得不到 (s b _ c c < s b _ l o w a t),且当该链到达时更
多的数据能够加到链的后面 (m_nextpkt等于0,且没有设置PR_ATOMIC)。
• 缓存中的数据不能满足读请求要求的数量,能得到最少的数据量,数据能够加到链中来,
但是MSG_WAITALL指示soreceive必须等待直到缓存中的数据能满足读请求。
如果最后一种情况的条件能够满足,但是因为读请求的数据太大以至如果不阻塞等待就
不能满足(uio_resid≤sb_hiwat),soreceive就不等待而是继续往下执行。
如果接收缓存有数据,并且设置了 MSG_DONTWAIT,则sorecevie不等待更多的数据。
有几种原因使得等待更多的数据是不合适的。在图 1 6 - 4 2中,s o r e c e i v e要么检查三种
情况,然后返回;要么等待更多的数据到达。
8. 等待更多的数据吗 ?
505-534 在此处,soreceive已经决定等待更多的数据来满足读请求。在等待之前,它需
要检查以下几种情况:
505-512 • 如果插口处于差错状态,且缓存为空 (m为空),则soreceive返回差错代码。如

Linux公社 www.linuxidc.com
bbs.theithome.com
414 计计TCP/IP详解 卷 2:实现

果有差错,但是接收缓存中有数据 (m非空),则返回缓存的数据;当下一个读调用来时,如果
没有数据,就返回差错。如果设置了 M S G _ P E E K,就不清除差错,因为设置了 M S G _ P E E K的
读调用不能改变插口的状态。
513-518 • 如果连接的读通道已经被关闭并且数据仍在接收缓存中,则 sosend不等待而是
将数据返回给进程 (在d o n t b l o c k 的情况下 )。如果接收缓存为空,则 s o r e c e i v e跳转到
release,读系统调用返回 0,表示连接的读通道已经被关闭。
519-523 • 如果接收缓存中包含带外数据或出现一个逻辑记录的结尾,则 soreceive不等
待,而是跳转到 dontblock。
524-528 • 如果协议请求中的连接不存在,则设置差错代码为 E N O T C O N N,函数跳转到
release。
529-534 • 如果读请求读 0字节或插口是非阻塞的,则函数跳转到 r e l e a s e,并返回 0或
EWOULDBLOCK(后一种情况 )。

图16-42 soreceive 函数:等待更多的数据吗 ?

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 415
9. 是,等待更多的数据
535-541 此处soreceive已决定等待更多的数据,并且有理由这么做 (即,将有数据到达)。
在进程调用 sbwait进入睡眠期间,缓存被解锁。如果因为差错或信号出现使得 sbwait返回,
则s o r e c e i v e返回相应的差错;否则 s o r e c e i v e跳转到r e s t a r t,查看接收缓存中的数据
是否能够满足读请求。
同s o s e n d中一样,进程能够利用 S O _ R C V T I M E O插口选项为 s b w a i t设置一个接收定时
器。如果在数据到达之前定时器超时,则 sbwait返回EWOULDBLOCK。
定时器并不能总令人满意。因为当插口上有活动时,定时器每次都被重置。如
果在一个超时间隔内至少有一个字节到达,则定时器从来不会超时,一直到设置了
更长的超时值的读系统调用返回。 s b _ t i m e o是一个不活动定时器,并不要求超时
值上限,但为了满足读系统调用,超时值的上限可能是必要的。
在此处,soreceive准备从接收缓存中传送数据。图 16-43说明了地址信息的传送。

图16-43 soreceive 函数:返回地址信息

10. dontblock
542-545 n e x t r e c o r d指向接收缓存中的下一条记录。在 s o r e c e i v e的后面,当第一个
链被丢弃后,该指针被用来将剩余的 mbuf放入插口缓存。
11. 返回地址信息
546-564 如果协议提供地址信息,如 UDP,则将从mbuf链中删除包含地址的 mbuf,并通过
*paddr返回。如果 paddr为空,则地址被丢弃。
在soreceive中,如果设置了 MSG_PEEK,则数据仍留在缓存中。
图16-44中的代码处理缓存中的控制 mbuf。
12. 返回控制信息

Linux公社 www.linuxidc.com
bbs.theithome.com
416 计计TCP/IP详解 卷 2:实现

565-590 每一个包含控制信息的 mbuf都将从缓存中删除 (如果设置了 MSG_PEEK,则不删除


而是复制),并连到 *controlp。如果controlp为空,则丢弃控制信息。

图16-44 soreceive 函数:处理控制信息

如果进程准备接收控制信息,则协议定义了一个 d o m _ e x t e r n a l i z e函数,一旦控制信
息m b u f中包含 S C M _ R I G H T S(访问权限 ),就调用 d o m _ e x t e r n a l i z e函数。该函数执行内核
中所有接收访问权限的操作。只有 U n i x域协议支持访问权限,有关细节在第 7 . 3节已讨论过。
如果进程不准备接收控制信息 (controlp为空),则丢弃控制mbuf。
直到处理完所有包含控制信息的 mbuf或出现差错时,循环才退出。
对于U n i x协议域, d o m _ e x t e r n a l i z e函数通过修改接收进程的文件描述符表
来实现文件描述符的传送。
处理完所有的控制 m b u f后, m指向链中的下一个 m b u f。如果在地址或控制信息的后面,
链中没有其他的 m b u f,则m为空。例如,当一个长度为 0的数据报进入接收缓存时就会出现这
种情况。图 16-45说明了soreceive准备从mbuf链中传送数据。

图16-45 soreceive 函数:准备传送 mbuf

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 417
13. 准备传送数据
591-597 处理完控制m b u f后,链中应该只剩下正常数据、带外数据 m b u f或没有任何m b u f。
如果 m为空,则 s o r e c e i v e完成处理,控制跳到 w h i l e循环的底部。如果 m不空,所有剩余
的m b u f链(n e x t r e c o r d)都将重新连接到 m,并将下一个 m b u f的类型赋给 t y p e。如果下一个
mbuf包含OOB数据,则设置 f l a g s中的M S G _ O O B标志,并在最后返回给进程。因为 TCP不支
持MT_OOBDATA形式的带外数据,所以 MSG_OOB不会返回给 TCP插口上的读调用。
图16-47显示了传送 mbuf循环的第一部分。图 16-46列出了循环中更新的变量。

变 量 描 述

moff 当M S G _ P E E K被置位时,将被传送的下一个字节的偏移位置
offset 当M S G _ P E E K被置位时, OOB标记的偏移位置
uio_resid 还未传送的字节数
len 从本mbuf中将要传送的字节数;如果 u i o _ r e s i d比较小或靠 OOB标记比较近,则 l e n
可能小于m _ l e n。

图16-46 soreceive 函数:循环内的变量

图16-47 soreceive 函数:uiomove

598-600 w h i l e循环的每一次循环中,一个 m b u f中的数据被传送到输出链或 u i o缓存中。


一旦链中没有mbuf或进程的缓存已满或出现差错,就退出循环。
14. 检查OOB和正常数据之前的变换
600-605 如果在处理 mbuf链的过程中,mbuf的类型发生变化,则立即停止传送,以确保正
常数据和带外数据不会混合在一个返回的报文中。但是,这种检查不适用于 TCP。

Linux公社 www.linuxidc.com
bbs.theithome.com
418 计计TCP/IP详解 卷 2:实现

15. 更新OOB标记
606-611 计算当前字节到 oobmark之间的长度来限制传送的大小,所以 oobmark的前一个
字节为传送的最后一个字节。传送的大小同时还要受 m b u f大小的限制。这段代码同样适用于
TCP。
612-625 如果将数据传送到 uio缓存,则调用uiomove。如果数据是作为一个 mbuf链返回
的,则更新 uio_resid的值,使其等于传送的字节数。
为了避免在传送数据过程中协议处理挂起的时间太长,在调用 u i o m o v e过程中使能协议
处理。所以,在 uiomove运行的过程中,接收缓存中可能会出现新的数据。
图16-48中描述的代码说明调整指针和偏移准备传送下一个 mbuf。

图16-48 soreceive 函数:更新缓存

16. mbuf处理完毕了吗
626-646 如果m b u f中的所有字节都已传送完毕,则必须丢弃 m b u f或将指针向前移。如果
m b u f 中包含了一个逻辑记录的结尾,还应设置 M S G _ E O R。如果将 M S G _ P E E K 置位,则
s o _ r e c e i v e 跳到下一个缓存。在没有将 M S G _ P E E K 置位的情况下,如果数据已通过
u i o m o v e复制完成,则丢弃这块缓存;或者如果数据是作为一个 m b u f链返回,则将缓存添加
到mp中。
图16-49包含处理OOB偏移和MSG_EOR的代码段。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 419

图16-49 soreceive 函数:带外数据标记

17. 更新OOB标记
658-670 如果带外数据标志等于非 0,则将其减去已传送的字节数。如果已到达标记处,则
将S S _ R C V A T M A R K置位, s o r e c e i v e跳出w h i l e循环。如果没有将 M S G _ P E E K置位,则更
新offset,而不是so_oobmark。
18. 逻辑记录结束
671-672 如果已到达一个逻辑记录的结尾,则 s o r e c e i v e跳出m b u f处理循环,因而不会
将下一个逻辑记录也作为这个报文的一部分返回。
在图1 6 - 5 0中,当设置了 M S G _ W A I T A L L标志 ,并且读请求还没有完成,则循环将等待更
多的数据到达。

图16-50 soreceive 函数:MSG_WAITALL 处理

19. MSG_WAITALL

Linux公社 www.linuxidc.com
bbs.theithome.com
420 计计TCP/IP详解 卷 2:实现

673-681 如果将MSG_WAITALL置位,而缓存中没有数据(m等于0),调用者需要更多的数据,
s o s e n d a l l a t o n c e为假,并且这是接收缓存中的最后一个记录 (n e x t r e c o r d为空 ),则
soreceive必须等待新的数据。
20. 差错或没有数据到达
682-683 如果差错出现或连接被关闭,则退出循环。
21. 等待数据到达
684-689 当接收缓存被协议层改变时 s b w a i t返回。如果 s b w a i t是被信号中断 (e r r o r非
0),则soreceive立即返回。
22. 用接收缓存同步 m和nextrecord
690-692 更新m和nextrecord,因为接收缓存被协议层修改了。如果数据到达 mbuf,则m
等于非0,while循环结束。
23. 处理下一个 mbuf
693 本行是mbuf处理循环的结尾。控制返回到循环开始的第 600行(图16-47)。一旦接收缓存
中有数据,有新的缓存空间,没有差错出现,则循环继续。
如果soreceive停止复制数据,则执行图 16-51所示的代码段。

图16-51 soreceive 函数:退出处理

24. 被截断的报文
694-698 如果因为进程的接收缓存太小而收到一个被截断的报文 (数据报或记录 ),则插口
层将这种情况通过设置 M S G _ T R U N C来通知进程,报文的被截断部分被丢弃。同其他接收标志
一样,进程只有通过 r e c v m s g系统调用才能获得 M S G _ T R U N C,即使s o r e c e i v e总是设置这
个标志。
25. 记录结尾的处理
699-706 如果没有将M S G _ P E E K置位,则下一个 mbuf链将被连接到接收缓存,并且如果发
送了 P R U _ R C V D协议请求,则通知协议接收操作已经完成。 T C P通过这种机制来完成对连接

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 421
接收窗口的更新。
26. 没有传送数据
707-712 如果soreceive运行完成,没有传送任何数据,没有到达记录的结尾,且连接的
读通道是活动的,则将接收缓存解锁, soreceive跳回到restart继续等待数据。
713-714 soreceive中设置的任何标志都在*flagsp中返回,缓存被解锁,soreceive返回。

讨论

s o r e c e i v e是一个复杂的函数。导致其复杂性的主要原因是繁锁的指针操作及对多种类
型的数据(带外数据、地址、控制信息和正常数据 )和多目标(进程缓存, mbuf链)的处理。
同s o s e n d类似, s o r e c e i v e的复杂性是多年积累的结果。为每一种协议编写一个特殊
的接收函数将会模糊插口层和协议层之间的边界,但是可以大大简化代码。
[Partridge and Pink 1993]描述了一个专门为 UDP编写的s o r e c e i v e函数,其功能是将数
据报从接收缓存复制到进程缓存中时给数据报求检验和。他们给出的结论是:修改通用的
soreceive函数来支持这一功能将“使本来已经很复杂的插口子程序变得更加复杂。”

16.13 select系统调用
在下面的讨论中,我们假定读者熟悉 select调用的基本操作和含义。关于 select的应用接口
的详细描述参考 [Stevens 1992]。
图16-52列出了select能够监控的插口状态。

select监控的操作
描 述
读 写 例外

有数据可读 •
连接的读通道被关闭 •
listen插口已经将连接排队 •
插口差错未处理 •
缓存可供写操作用,且一个连接存在或还没有连接请求 •
连接的写通道被关闭 •
插口差错未处理 •
OOB同步标记未处理 •

图16-52 select 系统调用:插口事件

我们从select系统调用的第一部分开始讨论,如图 16-53所示。
1. 验证和初始化
390-410 在堆栈中分配两个数组:ibits和obits,每个数组有三个单元,每个单元为一个
描述符集合。用 b z e r o将它们清 0。第一个参数, n d,必须不大于进程的描述符的最大数量。
如果nd大于当前分配给进程的描述符个数,将其减少到当前分配给进程的描述符的个数。 ni等
于用来存放nd个比特(1个描述符占1个比特)的比特掩码所需的字节数。例如,假设最多有256个
描述符(FD_SETSIZE),falset表示一个32 bit的整型(NFDBITS)数组,且nd等于65,那么:
ni=howmany(65,32)×4=3×4=12
在上面的公式中, howmany (x,y)返回存储x比特所需要的长度为 y比特的对象的数量。

Linux公社 www.linuxidc.com
bbs.theithome.com
422 计计TCP/IP详解 卷 2:实现

图16-53 Select 函数:初始化

2. 从进程复制文件描述符集
411-418 g e t b i t s宏用c o p y i n从进程那里将文件描述符集合传送到 i b i t s中的三个描述
符集合。如果描述符集合指针为空,则不需复制。
3. 设置超时值

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 423
419-438 如果tv为空,则将 timeo置成0,select将无限期等待。如果 tv非空,则将超时
值复制到内核,并调用 i t i m e r f i x将超时值按硬件时钟的分辨率取整。调用 t i m e v a l a d d将
当前时间加到超时值中。调用 h z t o计算从启动到超时之间的时钟滴答数,并保存在 t i m o中。
如果计算出来的结果为 0,将t i m e o置1,从而防止 s e l e c t阻塞,实现利用全 0的t i m e v a l结
构来实现非阻塞操作。
s e l e c t的第二部分代码,如图 1 6 - 5 4所示。其作用是扫描进程指示的文件描述符,当一
个或多个描述符处于就绪状态或定时器超时或信号出现时返回。

图16-54 select 函数:第二部分

4. 扫描文件描述符
439-442 从r e t r y开始的循环直到 s e l e c t能够返回时退出。在调用进程的控制块中保存

Linux公社 www.linuxidc.com
bbs.theithome.com
424 计计TCP/IP详解 卷 2:实现

全局整数 n s e l c o l l的当前值和 P _ S E L E C T标志。如果在 s e l s c a n(图1 6 - 5 5 )扫描文件描述符


期间出现任何一种变化,则这种变化表明描述符的状态因为中断处理而发生改变, s e l e c t必
须重新扫描文件描述符。 s e l s c a n查看三个输入的描述符集合中的每一个描述符集合,如果
描述符处于就绪状态,则在输出的描述符集合中设置匹配的描述符。

图16-55 selscan 函数

5. 差错或一些描述符准备就绪
443-444 如果差错出现或描述符处于就绪状态,就立即返回。
6. 超时了吗
445-451 如果进程提供的时间限制和当前时间已经超过了超时值,则立即返回。
7. 在执行selscan期间状态发生变化
452-455 s e l s c a n可以被协议处理中断。如果在中断期间插口状态改变,则将 P _ S E L E C T
和nselcoll置位,且selscan必须重新扫描所有描述符。
8. 等待缓存发生变化
456-460 所有调用 s e l e c t的进程均在调用 t s l e e p时用s e l w a i t作为等待通道。如图 16-
60所示,这种做法在多个进程等待同一个插口缓存的情况下将导致效率降低。如果 t s l e e p正
确返回,则 select跳转到retry,重新扫描所有描述符。
9. 准备返回

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 425
461-480 在done处清除P_SELECT,如果差错代码为 ERESTART,则修改为 EINTR;如果
差错代码为 E W O U L D B L O C K,则将差错代码置成 0。这些改变确保在 s e l e c t调用期间若信号
出现时能返回EINTR;若超时,则返回 0。

16.13.1 selscan函数

s e l e c t函数的核心是图 1 6 - 5 5所示的s e l s c a n函数。对于任意一个描述符集合中设置的


每一个比特, s e l s c a n 找出同它相关联的描述符,并且将控制分散给与描述符相关联的
so_select函数。对于插口而言,就是 soo_select函数。
1. 定位被监视的描述符
481-496 第一个 f o r循环依次查看三个描述符集合:读,写和例外。第二个 f o r循环在每
个描述符集合内部循环,这个循环在集合中每隔 32 bit(NFDBITS)循环一次。
最里面的 while循环检查所有被 32 bit 的掩码标记的描述符,该掩码从当前描述符集合中
获取并保存在 b i t s中。函数 f f s返回b i t s中的第一个被设置的比特的位置,从最低位开始。
例如,如果 bits等于1000 (省略了前面的28个0),则ffs (bits)等于4。
2. 轮询描述符
497-500 从i到f f s函数的返回值,计算与比特相关的描述符,并保存在 f d中。在b i t s中
( 而不是在输入描述符集合中 ) 清除比特,找到与描述符相对应的 f i l e 结构,调用
fo_select。
fo_select的第二个参数是 flag数组中的一个元素。msk是外层的for循环的循环变量。
所以,第一次循环时,第二个参数等于 F R E A D,第二次循环时等于 F W R I T E,第三次循环时
等于0。如果描述符不正确,则返回 EBADF。
3. 描述符准备就绪
501-504 当发现某个描述符的状态为准备就绪时,设置输出描述符集合中相对应的比特位。
并将n (状态就绪的描述符的个数 )加1。
505-510 循环继续直到轮询完所有描述符。状态就绪的描述符的个数通过 *retval返回。

16.13.2 soo_select函数

对于s e l s c a n在输入描述符集合中发现的每一个状态就绪的描述符, s e l s c a n调用与描


述符相关的 f i l e o p s结构(参考第1 5 . 5节)中的f o _ s e l e c t指针引用的函数。在本书中,我们
只对插口描述符和图 16-56所示的soo_select函数感兴趣。

图16-56 soo_select 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
426 计计TCP/IP详解 卷 2:实现

图16-56 (续)

105-112 soo_select每次被调用时,它只检查一个描述符的状态。如果相对于which中指定
的条件,描述符处于就绪状态,则立即返回1。如果描述符没有处于就绪状态,就用selrecord
标记插口的接收缓存或发送缓存,指示进程正在选择该缓存,然后soo_select返回0。
图16-52显示了插口的读、写和例外情况。我们将看到 s o o _ s e l e c t使用了s o r e a d a b l e
和sowriteable宏,这些宏在sys/socketvar.h中定义。
1. 插口可读吗
113-120 soreadable宏的定义如下:
#define soreadable(so) \
((so)->so_rcv.sb_cc >= (so)->so_rcv.sb_lowat || \
((so)->so_state & SS_CANTRCVMORE) || \
(so)->so_qlen || (so)->so_error)

因为U D P和T C P的接收下限默认值为 1 (图1 6 - 4 ),下列情况表示插口是可读的:接收缓存


中有数据,连接的读通道被关闭,可以接受任何连接或有挂起的差错。
2. 插口可写吗
121-128 sowriteable宏的定义如下:
#define sowriteable(so) \
(sbspace(&(so)->so_snd) >= (so)->so_snd.sb_lowat && \
(((so)->so_state&SS_ISCONNECTED) || \
((so)->so_proto->pr_flags&PR_CONNREQUIRED)==0) || \
((so)->so_state & SS_CANTSENDMORE) || \
(so)->so_error)

TCP和UDP默认的发送低水位标记是 2048。对于UDP而言,s o w r i t e a b l e总是为真,因


Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计 427
为sbspace总是等于sb_hiwat,当然也总是大于或等于 so_lowat,且不要求连接。
对于T C P而言,当发送缓存中的可用空间小于 2 0 4 8个字节时,插口不可写。其他的情况
在图16-52中讨论。
3. 还有挂起的例外情况吗
129-140 对于例外情况,需检查标志 s o _ o o b m a r k和S S _ R E C V A T M A R K。直到进程读完数
据流中的同步标记后,例外情况才可能存在。

16.13.3 selrecord函数

图16-57显示同发送和接收缓存存储在一起的 s e l i n f o结构的定义(图16-3中的s b _ s e l成
员)。

图16-57 selinfo 结构

41-44 当只有一个进程对某一给定的插口缓存调用 s e l e c t时,s l _ p i d等于等待进程的进


程标志符。当其他的进程对同一缓存调用 s e l e c t时,设置 s l _ f l a g s中的S I _ C O L L标志。
将这种情况称为冲突。这个标志是目前 sl_flags中唯一已定义的标志。
当s o o _ s e l e c t发现描述符不在就绪状态时就调用 s e l r e c o r d函数,如图 1 6 - 5 8所示。
该函数记录了足够的信息,使得缓存内容发生变化时协议处理层能够唤醒进程。

图16-58 selrecord 函数

1. 重复选择描述符
522-531 s e l r e c o r d的第一个参数指向调用 s e l e c t进程的p r o c结构。第二个参数指向
s e l i n f o 记录,该记录的 s o _ s n d . s b _ s e l 和s o _ r c v . s b _ s e l 可能会被修改。如果
s e l i n f o中已记录了该进程的信息,则立即返回。例如,进程对同一个描述符调用 s e l e c t
查询读和例外情况时,函数就立即返回。

Linux公社 www.linuxidc.com
bbs.theithome.com
428 计计TCP/IP详解 卷 2:实现

2. 同另一个进程的操作冲突 ?
532-534 如果另一个进程已经对同一插口缓存执行 select操作,则设置SI_COLL。
3. 没有冲突
535-537 如果调用没有发生冲突,则 s i _ p i d 等于 0,将当前进程的进程标志符赋给
si_pid。

16.13.4 selwakeup函数

当协议处理改变插口缓存的状态 ,并且只有一个进程选择了该缓存时, N e t / 3就能根据


selinfo结构中记录的信息立即将该进程放入运行队列。
当插口缓存发生变化但是有多个进程选择同一插口缓存时 (设置了S I _ C O L L),N e t / 3就无
法确定哪些进程对这种缓存变化感兴趣。我们在讨论图 1 6 - 5 4中的代码段时就已经指出,每一
个调用 s e l e c t 的进程在调用 t s l e e p 时使用 s e l w a i t作为等待通道。这意味着对应的
wakeup将唤醒所有阻塞在 select上的进程—甚至是对缓存的变化不关心的进程。
图16-59说明如何调用selwakeup。

OOB数据已到达 接收缓存已改变 发送缓存已改变

图16-59 selwakeup 处理

当改变插口状态的事件出现时,协议处理层负责调用图 1 6 - 5 9的底部列出的一个函数来通
知插口层。图 1 6 - 5 9底部显示的三个函数都将导致 s e l w a k e u p被调用,在插口上选择的任何
进程将被调度运行。
selwakeup函数如图16-60所示。
541-548 如果si_pid等于0,表明没有进程对该缓存执行 select操作,函数立即返回。
在冲突中唤醒所有进程
549-553 如果多个进程对同一插口执行 s e l e c t操作,将 n s e l c o l l加1,清除冲突标志,
唤醒所有阻塞在 s e l e c t上的进程。正如图 1 6 - 5 4中讨论的,进程在 t s l e e p中阻塞之前若缓
存发生改变,nselcoll能使select重新扫描描述符 (习题16.9)。
554-567 如果si_pid标识的进程正在selwait中等待,则调度该进程运行。如果进程是在
其他等待通道中,则清除P_SELECT标志。如果对一个正确的描述符执行selrecord,则调用
进程可能正在其他的等待通道中等待,然后, selscan在描述符集合中发现一个差错的文件描
述符,并返回EBADF,不清除以前修改的 selinfo记录。到selwakeup运行时,selwakup

Linux公社 www.linuxidc.com
bbs.theithome.com
第 16章 插 口 I/O计计429
可能会发现sel_pid标识的进程不再在插口缓存等待,从而忽略selinfo中的信息。
如果没有出现多个进程共享同一个描述符的情况 (也就是同一块插口缓存 ),当然这种情况
很少,则只有一个进程被 sel w a k e u p唤醒。在作者使用的机器上, n s e l c o l l总是等于0,这
说明select冲突是很少发生的。

图16-60 selwakeup 函数

16.14 小结

本章介绍了插口的读、写和选择系统调用。
我们了解到 s o s e n d处理插口层与协议处理层之间的所有输出,而 s o r e c e i v e处理所有
输入。
本章还介绍了发送缓存和接收缓存的组织结构,以及缓存的高、低水位标记的默认值和
含义。
本章的最后一部分介绍了 s e l e c t系统调用。从这部分内容中我们了解到,当只有一个进
程对描述符执行 s e l e c t调用时,协议处理层仅仅唤醒 s e l i n f o结构中标识的那个进程。当
有多个进程对同一个描述符执行 s e l e c t操作而发生冲突时,协议层只能唤醒所有等待在该描
述符上的进程。

习题

16.1 当将一个大于最大的正的有符号整数的无符号整数传给 w r i t e 系统调用时,

Linux公社 www.linuxidc.com
bbs.theithome.com
430 计计TCP/IP详解 卷 2:实现

sosend中的resid如何变化?
16.2 当s o s e n d将小于M C L B Y T E S个字节的数据放入簇中时, s p a c e被减去M C L B Y T E S,
可能会成为一个负数,这会导致为 a t o m i c协议填写m b u f的循环结束。这种结果是
正常的吗?
16.3 数据报和流协议有着不同的语义。将 s o s e n d和s o r e c e i v e函数分别分成两个函
数,一个用来处理报文,另一个用来处理流。除了使得代码清晰外,这样做还有什
么好处?
16.4 对于 P R _ A T O M I C协议,每一个写调用都指定了一个隐含的报文边界。插口层将这
个报文作为一个整体交给协议。 M S G _ E O R标志允许进程显式指定报文边界。为什
么仅有隐含的报文边界是不够的 ?
16.5 如果插口描述符没有标记为非阻塞,且进程也没有指定 M S G _ D O N T W A I T ,当
sosend不能立即获取发送缓存上的锁时,结果如何 ?
16.6 在什么情况下,虽然 s b _ c c < s b _ h i w a t,但s b s p a c e仍然报告没有闲置空间 ?为
什么在这种情况下进程应该被阻塞 ?
16.7 为什么recvit不将控制报文的长度而是将名字长度返回给进程 ?
16.8 为什么soreceive要清除MSG_EOR?
16.9 如果将nselcoll代码从select和selwakeup中删除,会有什么问题 ?
16.10 修改select系统调用,使得 select返回时返回定时器的剩余时间。

Linux公社 www.linuxidc.com
bbs.theithome.com
第17章 插 口 选 项
17.1 引言

本章讨论修改插口行为的几个系统调用,以此来结束插口层的介绍。
s e t s o c k o p t和g e t s o c k o p t系统调用已在第 8 . 8节中介绍过,主要描述访问 I P特点的选
项。在本章中,我们将介绍这两个系统调用的实现以及通过它们来控制的插口级选项。
i o c t l函数在第4.4节中已介绍过,在第 4.4节中,我们描述了用于网络接口配置的与协议
无关的i o c t l命令。在第6 . 7节中,我们描述了用来分配网络掩码以及单播、多播和目的地址
的IP专用的ioctl命令。本章我们将介绍 ioctl的实现和fcntl函数的相关特点。
最后,我们介绍 g e t s o c k n a m e和g e t p e e r n a m e系统调用,它们用来返回插口和连接的
地址信息。
图17-1列出了实现插口选项系统调用的函数。本章描述带阴影的函数。

图17-1 setsockopt 和getsockopt 系统调用

17.2 代码介绍
本章中涉及的源代码来自于图 17-2中列出的四个文件。

文 件 名 说 明

kern/kern_descrip.c f c n t l系统调用
kern/uipc_syscalls.c setsockopt、getsockopt、getsockname和getpeername系统调用
kern/uipc_socket.c 插口层对s e t s o c k o p t和g e t s o c k o p t的处理
kern/sys_socket.c i o c t l系统调用对插口的处理

图17-2 本章讨论的源文件

Linux公社 www.linuxidc.com
bbs.theithome.com
432 计计TCP/IP详解 卷 2:实现

全局变量和统计量

本章中描述的系统调用没有定义新的全局变量,也没有收集任何统计量。

17.3 setsockopt系统调用
图8 - 2 9列出了函数 s e t s o c k o p t (和g e t s o c k o p t)能够访问的各种不同的协议层。本章
主要集中在 SOL_SOCKET级的选项,这些选项在图 17-3中列出。

optname optval 类型 变 量 说 明

SO_SNDBUF int so_snd.sb_hiwat 发送缓存高水位标记


SO_RCVBUF int so_rcv.sb_hiwat 接收缓存高水位标记
SO_SNDLOWAT int so_snd.sb_lowat 发送缓存低水位标记
SO_RCVLOWAT int so_rcv.sb_lowat 接收缓存低水位标记
SO_SNDTIMEO s t r u c t t i m e v a lso_snd.sb_timeo 发送超时值
SO_RCVTIMEO s t r u c t t i m e v a lso_snd.sb_timeo 接收超时值
SO_DEBUG int so_options 记录插口调试信息
SO_REUSEADDR int so_options 插口能重新使用一个本地地址
SO_REUSEPORT int so_options 插口能重新使用一个本地端口
SO_KEEPALIVE int so_options 协议查询空闲的连接
SO_DONTROUTE int so_options 旁路路由表
SO_BROADCAST int so_options 插口支持广播报文
SO_USELOOPBACK int so_options 仅用于选路域插口;发送进程接收自己的选
路报文
SO_OOBINLINE int so_options 协议排队内联的带外数据
SO_LINGER struct linger so_linger 插口关闭但仍发送剩余数据
SO_ERROR int so_error 获取差错状态并清除;只用于 getsockopt
SO_TYPE int so_type 获取插口类型;只用于 g e t s o c k o p t
其他 返回E N O P R O T O O P T

图17-3 setsockopt 和getsockopt 选项

setsockopt函数原型为:
int setsockopt(int s, int level, int optname, void *optval, int optlen);

图17-4显示了setsockopt调用的源代码。
565-597 g e t s o c k返回插口描述符的 f i l e结构。如果 v a l非空,则将 v a l s i z e个字节的
数据从进程复制到用 m _ g e t分配的mbuf中。与选项对应的数据长度不能超过 M L E N个字节,所
以,如果valsize大于MLEN,则返回EINVAL。调用sosetopt,并返回其值。

图17-4 setsockopt 系统调用

Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计433

图17-4 (续)

sosetopt函数

s o s e t o p t 函数处理所有插口级的选项,并将其他的选项传给与插口关联的协议的
pr_ctloutput函数。图17-5列出了sosetopt函数的部分代码。

图17-5 sosetopt 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
434 计计TCP/IP详解 卷 2:实现

图17-5 (续)

752-764 如果选项不是插口级的 (SOL_SOCKET)选项,则给底层协议发送 PRCO_SETOPT请


求。注意:调用的是协议的 p r _ c t l o u t p u t函数,而不是它的 p r _ u s r r e q函数。图 1 7 - 6说
明了Internet协议调用的 pr_ctloutput函数。

协 议 p r _ c t l o u t p u t函数 参 考

UDP ip_ctloutput 第8.8节


TCP tcp_ctloutput 第30.6节
ICMP
IGMP r i p _ c t l o u t p u t和i p _ c t l o u t p u t 第8.8节和第32.8节
原始IP

图17-6 pr_ctloutput 函数

765 switch语句处理插口级的选项。
841-844 对于不认识的选项,在保存它的 mbuf被释放后返回ENOPROTOOPT。
845-855 如果没有出现差错,则控制总是会执行到 switch。在switch语句中,如果协议
层需要响应请求或插口层,则将选项传送给相应的协议。 I n t e r n e t协议中没有一个预期处理插
口级的选项。
注意,如果协议收到不预期的选项,则直接将其 p r _ c t l o u t p u t函数的返回值丢弃。并
将m置空,以免调用 m_free,因为协议负责释放缓存。
图17-7说明了linger选项和在插口结构中设置单一标志的选项。
766-772 linger选项要求进程传入 linger结构:
struct linger {
int l_onoff; /* option on/off */
int l_linger; /* linger time in seconds */
};

确保进程已传入长度为 l i n g e r 结构大小的数据后,将结构成员 l _ l i n g e r 复制到


s o _ l i n g e r中。在下一组 c a s e语句后决定是使能还是关闭该选项。 s o _ l i n g e r和c l o s e系
统调用在第 15.15节中已介绍过。
773-789 当进程传入一个非 0值时,设置选项对应的布尔标志;当进程传入的是 0时,将对
应标志清除。第一次检查确保一个整数大小 (或更大 )的对象在 m b u f中,然后设置或清除对应
的选项。
图17-8显示了插口缓存选项的处理。
790-815 这组选项改变插口的发送和接收缓存的大小。第一个 i f语句确保提供给四个选项
的变量是整型。对于 S O _ S N D B U F和S O _ R C V B U F,s b r e s e r v e只调整缓存的高水位标记而不

Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计435
分配缓存。对于 SO_SNDLOWAT和SO_RCVLOWAT,调整缓存的低水位标记。

图17-7 sosetopt 函数:linger 和标志选项

图17-8 sosetopt 函数:插口缓存选项

Linux公社 www.linuxidc.com
bbs.theithome.com
436 计计TCP/IP详解 卷 2:实现

图17-9说明超时选项。

图17-9 sosetopt 函数:超时选项

816-824 进程在t i m e v a l结构中设置S O _ S N D T I M E O和S O _ R C V T I M E O选项的超时值。如


果传入的数值不正确,则返回 EINVAL。
825-830 存储在 t i m e v a l结构中的时间间隔值不能太大,因为 s b _ t i m e o是一个短整数,
当时间间隔值的单位为一个时钟滴答时,时间间隔值的大小就不能超过一个短整数的最大值。
第826行代码是不正确的。在下列条件下,时间间隔不能表示为一个短整数:
tv_usec
tv_sec × hz + > SHRT_MAX
tick
其中,t i c c k = 1 0 0 0 0 0 0 /和S
h z HRT_MAX=32767
所以,如果下列不等式成立,则返回。
SHRT_MAX tv_usec SHRT_MAX tv_usec
tv_sec > − = −
hz tick × hz hz 1000000
等式的最后一项不是代码指明的 hz。正确的测试代码应该是:
if (tv->tv_sec * hz+tv->tv_usec/tick>SHRT_MAX)
error=EDOM;
习题17.3中有更详细的讨论。
831-840 将转换后的时间, v a l,保存在请求的发送或接收缓存中。 s b _ t i m e o限制了进
程等待接收缓存中的数据或发送缓存中的闲置空间的时间。详细讨论参考第 16.7和16.11节。
超时值是传给tsleep的最后一个参数,因为tsleep要求超时值为一个整数,所以
进程最多只能等待65535个时钟滴答。假设时钟频率为100 Hz,则等待时间小于11分钟。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计 437
17.4 getsockopt系统调用

getsockopt返回进程请求的插口和协议选项。函数原型是:
int getsockopt(int s, int level, int name, caddr_t val, int *valsize);

该调用的源代码如图 17-10所示。

图17-10 getsockopt 系统调用

598-633 这段代码现在看上去应该很熟悉了。 g e t s o c k获取插口的 f i l e结构,将选项缓


存的大小复制到内核,调用 s o g e t o p t来获取选项的值。将 s o g e t o p t返回的数据复制到进
程提供的缓存,可能还需修改缓存长度。如果进程提供的缓存不够大,则返回的数据可能会
被截掉。通常情况下,存储选项数据的 mbuf在函数返回后被释放。

sogetopt函数

同s o s e t o p t一样, s o g e t o p t函数处理所有插口级的选项,并将其他的选项传给与插
口关联的协议。图 17-11列出了sogetopt函数的开始和结束部分的代码。

Linux公社 www.linuxidc.com
bbs.theithome.com
438 计计TCP/IP详解 卷 2:实现

图17-11 sogetopt 函数:概述

856-871 同 sosetopt一 样 , 函 数 将 那 些 与 插 口 级 选 项 无 关 的 选 项 立 即 通 过
PRCO_GETOPT协议请求传递给相应的协议级。协议将被请求的选项保存在 mp指向的mbuf中。
对于插口级的选项,分配一块标准的 m b u f缓存来保存选项值,选项值通常是一个整数,
所以将m_len设成整数大小。相应的选项值通过 switch语句复制到 mbuf中。
918-925 如果执行的是 s w i t c h中的 d e f a u l t 情况下的语句,则释放 m b u f,并返回
E N O P R O T O O P T。否则, s w i t c h语句执行完成后,将指向 m b u f的指针赋给* m p。当函数返回
后,getsockopt从该mbuf中将数据复制到进程提供的缓存,并释放 mbuf。
图17-12说明对SO_LINGER选项和作为布尔型标志实现的选项的处理。
872-877 S O _ L I N G E R选项请求返回两个值:一个是标志值,赋给 l _ o n o f f;另一个是拖
延时间,赋给l_linger。
878-887 其余的选项作为布尔标志实现。将 so_options和optname执行逻辑与操作,如
果选项被打开,则与操作的结果为非 0值;反之则结果为 0。注意:标志被打开并不表示返回
值等于1。
sogetopt的下一部分代码 (图17-13)将整型值选项的值复制到 mbuf中。
888-906 将每一个选项作为一个整数复制到 mbuf中。注意:有些选项在内核中是作为一个
短整数存储的 (如缓存高低水位标记 ),但是作为整数返回。一旦将 s o _ e r r o r复制到 m b u f中
后,即清除 so_error,这是唯一的一次 getsockopt调用修改插口状态。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计439

图17-12 sogetopt 选项:SO_LINGER 选项和布尔选项

图17-13 sogetopt 函数:整型值选项

图17-14 sogetopt 函数:超时选项

Linux公社 www.linuxidc.com
bbs.theithome.com
440 计计TCP/IP详解 卷 2:实现

图17-14列出了sogetopt的第三和第四部分代码,它们的作用分别是处理 SO_SNDTIMEO
和SO_RCVTIMEO选项。
907-917 将发送或接收缓存中的 s b _ t i m e o值赋给 v a r。基于 v a l中的时钟滴答数,在
mbuf中构造一个 timeval结构。
计算tv_usec的代码有一个差错。表达式应该为: "(v a l % h z
) * tick"。

17.5 fcntl和ioctl系统调用

因为历史的原因而非有意这么做,插口 API的几个特点既能通过 i o c t l也能通过f c n t l来


访问。关于 ioctl命令,我们已经讨论了很多。我们也几次提到 fcntl。
图17-15显示了本章描述的函数。

系统调用

默认

得到配置

图17-15 fcntl 和ioctl 函数

ioctl和fcntl的原型分别为:
int ioctl(int fd, unsigned long result, char *argp);
int fcntl(int fd, int cmd,... /* int arg */);

Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计 441
图17-16总结了这两个系统调用与插口有关的特点。我们在图 17-16中还列出了一些传统的
常数,因为它们出现在代码中。考虑与 P o s i x 的兼容性,可以用 O _ N O N B L O C K 来代替
FNONBLOCK,用O_ASYNC来代替FASYNC。

描 述 fcntl ioctl

通过打开或关闭 so_st a t e中的S S _ N B I O F N O N B L O C K文件状态标志 F I O N B I O命令


来使能或禁止非阻塞功能
通过打开或关闭 sb_fl a g s中的 SB_ASYNC F A S Y N C文件状态标志 F I O A S Y N C命令
来使能或禁止异步通知功能
设置或得到 so_pgid,它是S I G I O G和 F _ S E T O W N或F _ G E T O W N S I O C S P G R P或S I O C G P G R P命令
S I G U R G信号的目标进程或进程组
得到接收缓存中的字节数;返回 FIONREAD
so_rcv.sb_cc
返回OOB同步标记;即 s o _ s t a t e中的 SIOCATMARK
S S _ R C VATMARK标志

图17-16 fcntl 和ioctl 命令

17.5.1 fcntl代码

图17-17列出了fcntl函数的部分代码。

图17-17 fcntl 系统调用:概况

Linux公社 www.linuxidc.com
bbs.theithome.com
442 计计TCP/IP详解 卷 2:实现

133-153 验证完指向打开文件的描述符的正确性后, switch语句处理请求的命令。


253-257 对于不认识的命令, fcntl返回EINVAL。
图17-18仅显示fcntl中与插口有关的代码。

图17-18 fcntl 系统调用:插口处理

168-185 F_GETFL返回与描述符相关的当前文件状态标志, F_SETFL设置状态标志。通过


调用f o _ i o c t l将F N O N B L O C K和F A S Y N C的新设置传递给对应的插口,而插口的新设置是通
过图1 7 - 2 0中描述的 s o o _ i o c t l函数来传递的。只有在第二个 f o _ i o c t l调用失败后,才第
三次调用 f o _ i o c t l。该调用的功能是清除 F N O N B L O C K标志,但是应该改为将这个标志恢复

Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计 443
到原来的值。
186-194 F _ G E T O W N返回与插口相关联的进程或进程组的标识符, s o _ p g i d。对于非插口
描述符,将 T I O C G P G R Pi o c t l
命令传给对应的 f o _ i o c t l函数。 F _ S E T O W N的功能是给
so_pgid赋一个新值。

17.5.2 ioctl代码

我们跳过 i o c t l 系统调用本身而先从 s o o _ i o c t l 开始讨论,如图 1 7 - 2 0所示,因为


i o c t l的代码中的大部分是从图 1 7 - 1 7所示的代码中复制的。我们已经说过, s o o _ i o c t l函
数将选路命令发送给 r t i o c t l,接口命令发送给 i f i o c t l,任何其他的命令发送给底层协议
的pr_usrreq函数。
55-68 有几个命令是由 s o o _ i o c t l直接处理的。如果 * d a t a非空,则 F I O N B I O打开非阻
塞方式,否则关闭非阻塞方式。正于我们已经了解的,这个标志将影响到 accept、
connect和close系统调用,也包括其他的读和写系统调用。
69-79 F I O A S Y N C使能或禁止异步 I / O通知功能。如果设置了 S S _ A S Y N C,则无论什么时候
插口上有活动,就调用 sowakeup,将信号SIGIO发送给相应的进程或进程组。
80-88 F I O N R E A D返回接收缓存中的可读字节数。 S I O C S P G R P设置与插口相关的进程组,
S I O C G P G R P则是得到它。 s o _ p g i d作为我们刚讨论过的 S I G I O信号的目标进程或进程组,
当有带外数据到达插口时,则作为 SIGURG信号的目标进程或进程组。
89-92 如果插口正处于带外数据的同步标记,则 SIOCATMARK返回真;否则返回假。
ioctl命令,FIOxxx和SIOxxx常量,有一个内部结构,如图 17-19所示。
输入
输出

图17-19 ioctl 命令的内部结构

图17-20 soo_ioctl 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
444 计计TCP/IP详解 卷 2:实现

图17-20 (续)

如果将 i o c t l的第三个参数作为输入,则
宏 描 述
设置i n p u t。如果该参数作为输出,则 o u t p u t被
IOCPARM_LEN(cmd) 返回cmd中的length
置位。如果不用该参数,则 v o i d 被置位。
IOCBASECMD(cmd) length设为0的命令
length是参数的大小 (字节)。相关的命令在同一 IOCGROUP(cmd) 返回cmd中的group
个 g ro u p 中但每一个命令在组中都有各自的
图17-21 ioctl 命令宏
n u m b e r。图1 7 - 2 1中的宏用来解析 i o c t l命令
中的元素。
93-104 宏I O C G R O U P从命令中得到 8 bit的group。接口命令由 i f i o c t l处理。选路命令由
rtioctl处理。通过 PRU_CONTROL请求将所有其他的命令传递给插口协议。
正如我们在第19章中描述的,Net/2定义了一个新的访问路由选择表接口,在该接
口中,报文是通过一个在 PF_ROUTE域中产生的插口传递给路由选择子系统。用这种
方法来代替这里讨论的ioctl。在不兼容的内核中,rtioctl总是返回ENOTSUPP。

17.6 getsockname系统调用

getsockname系统调用的原型是:
Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计445
int getsockname(int fd, caddr_t asa, int * alen);

g e t s o c k n a m e得到绑定在插口 f d上的本地地址,并将它存入 a s a指向的缓存中。当在一


个隐式的绑定中内核选择了一个地址,或在一个显式的 b i n d调用中进程指定了一个通配符地
址(2.2.5节)时,该函数就很有用。 getsockname系统调用如图17-22所示。

图17-22 getsockname 系统调用

682-715 getsock返回描述符的file结构。将进程指定的缓存的长度赋给 len。这是我们第


一次看到对 m _ g e t c l r的调用:该函数分配一个标准的 m b u f,并调用 b z e r o清零。当协议收
到PRU_SOCKADDR请求时,协议处理层负责将本地地址存入 m。
如果地址长度大于进程提供的缓存的长度,则返回的地址将被截掉。 * a l e n等于实际返
回的字节数。最后,释放 mbuf,并返回。

17.7 getpeername系统调用

getpeername系统调用的原型是:
int getpeername(int fd, caddr_t asa, int * alen);

Linux公社 www.linuxidc.com
bbs.theithome.com
446 计计TCP/IP详解 卷 2:实现

g e t p e e r n a m e系统调用返回指定插口上连接的远端地址。当一个调用 a c c e p t的进程通
过f o r k和e x e c启动一个服务器时 (即,任何被 i n e t d启动的服务器 ),经常要调用这个函数。
服务器不能得到 a c c e p t返回的远端地址,而只能调用 g e t p e e r n a m e。通常,要在应用的访
问地址表查找返回地址,如果返回地址不在访问表中,则连接将被关闭。
某些协议,如 T P 4,利用这个函数来确定是否拒绝或证实一个进入的连接。在 T P 4中,
a c c e p t 返回的插口上的连接是不完整的,必须经证实之后才算连接成功。基于
g e t p e e r n a m e返回的地址,服务器能够关闭连接或通过发送或接收数据来间接证实连接。
这一特点与 TCP 无关,因为 T C P必须在三次握手完成之后, a c c e p t才能建立连接。图 1 7 - 2 3
列出了getpeername函数的代码。

图17-23 getpeername 系统调用

719-753 图中列出的代码与 getsockname的代码是一样的。 getsock获取插口对应的 file


结构,如果插口还没有同对方建立连接或连接还没有证实 (如,T P 4 ),则返回 E N O T C O N N。如
果已建立连接,则从进程那里得到缓存的大小,并分配一块 m b u f 来存储地址。发送
P R U _ P E E R A D D R请求给协议层来获取远端地址。将地址和地址的长度从内核的 mbuf中复制到
Linux公社 www.linuxidc.com
bbs.theithome.com
第 17 章 插 口 选 项计计447
进程提供的缓存中。释放 mbuf,并返回。

17.8 小结

本章中,我们讨论了六个修改插口功能的函数。插口选项由 setsockopt和getsockopt
函数处理。其他的选项,其中有些不仅仅用于插口,由 f c n t l和i o c t l处理。最后,通过
getsockname和getpeername来获取连接信息。

习题

17.1 为什么选项受标准 mbuf大小(MHLEN, 128 个字节)的限制?


17.2 为什么图17-7中的最后一段代码能处理 SO_LINGER选项?
17.3 图17-9中用来测试 timeval结构的代码有些问题,因为 tv-> t v _ s e c * hz可能会
溢出。请对这段代码作些修改来解决这个问题。

Linux公社 www.linuxidc.com
bbs.theithome.com
第18章 Radix树路由表
18.1 引言

由I P完成的路由选择是一种选路机制,它通过搜索路由表来确定从哪个接口把分组发送
出去。它与选路策略 (routing policy)不一样,选路策略是一组规则的集合,这些规则用来确定
哪些路由可以编入到路由表中。 N e t / 3 内核实现选路机制,而选路守护进程,典型地如
r o u t e d或g a t e d,实现选路策略。由于分组转发是频繁发生的 (一个繁忙的系统每秒要转发
成百上千个分组 ),相对而言,选路策略的变化要少些,因此路由表的结构必须能够适应这种
情况。
关于路由选择的详细情况,我们分三章进行讨论:
• 本章将讨论 Net/3分组转发代码所使用的 Radix树路由表的结构。每次发送或转发分组时,
IP都将查看该表 (发送时分组需要查看该表,是因为 IP必须决定哪个本地接口将接收该分
组)。
• 第1 9章着重讨论内核与 R a d i x树之间的接口函数以及内核与选路进程 (通常指实现选路策
略的选路守护进程 )之间交换的选路消息。进程可以通过这些消息来修改内核的路由表
(添加路由、删除路由等 ),并且当发生了一个异步事件,可能影响到路由策略 (如收到重
定向,接口断开等 )时,内核也通过这些消息来通知守护进程。
• 第20章给出了内核与进程之间交换选路消息时使用的选路插口。

18.2 路由表结构

在讨论Net/3路由表的内部结构之前,我们需要了解一下路由表中包含的信息类型。图 18-1
是图1-17(作者以太网中的四个系统 )的下半部分。

以太网,140.252.13.0

图18-1 路由表例子中使用子网

图18-2给出了图18-1中bsdi上的路由表。
为了能够更容易地看出每个表项中所设置的标志,我们已经对 n e t s t a t输出的“ F l a g s”
列进行了修改。
该表中的路由是按照下列过程添加的。其中,第 1、3、5、8和第9步是在系统的初始化阶
段执行/ e t c / n e t s t a r t s h e脚本时完成的。
ll

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 449

图18-2 主机bsdi 上的路由表

1) 默认路由是由route命令添加的。该路由通往主机 sun(140 252 13 33),主机sun拥有


一条到Internet的PPP链路。
2) 到网络 1 2 7的路由表项通常是由选路守护进程 ( 如 g a t e d )创建的,也可以通过
/ e t c / n e t s t a r t文件中的 r o u t e命令将其添加到路由表中。该表项使得所有发往该网络的
分组都将被环回驱动器 (图5 - 2 7 )拒绝,但发往主机 1 2 7 . 0 . 0 . 1的分组除外,因为对于该类分组,
在下一步中添加的一条更特殊的路由将屏蔽本路由表项的作用。
3) 到环回接口 (127.0.0.1)的表项是由 ifconfig命令配置的。
4) 到v a n g o g h . c s . b e r k e l e y . e d u( 1 2 8 . 3 2 . 3 3 . 5 )的表项是用 r o u t e命令手工创建的。
该路由指定的路由器与默认路由所指定的相同 (都是140.252.13.33)。但是在拥有一条替代默认
路由的通往特定主机的路由之后,我们就能把路由度量存储在该路由表项中。这些度量能够
可以由管理者选择设置。每次 T C P建立一条到达目的主机的连接时都使用该度量,并且在连
接关闭时,由TCP对其进行更新。我们将在图 27-3中详细描述这些度量。
5) 接口 l e 0的初始化是由 i f c o n f i g命令完成的。该命令会在路由表中增加一条到
140.252.13.32网络的表项。
6) 到以太网上另两台主机 s u n(140.252.13.33)和s v r 4(140.252.13.34)的路由表项是由 ARP
创建的,见第 2 1章。它们都是临时路由,经过一段时间后,如果还未被使用,它们就会被自
动删除。
7) 到本机(140.252.13.35)的表项是在第一次引用本机 IP地址时创建的。该接口是一个环回,
也就是说,任何发往本机 I P地址的数据报将从内部反送回来。 4 . 4 B S D中包含了自动创建该路
由的新功能,见第 21.13节。
8) 到主机140.252.13.65的表项是在 ifconfig配置SLIP接口时创建的。
9) 通过以太网接口到达网络 244的路由是由 route命令添加的。
10) 到多播组224.0.0.1(所有主机的组, all-host group)的表项是Ping程序在连接 224.0.0.1即
“Ping 224.0.0.1”时创建的。它也是一条临时路由,如果在一段时间内未被使用,就会被自动
删除。
图18-2中的“Flags”列需要简单地说明一下。图 18-25列出了所有可能的标志。
U 该路由存在。

Linux公社 www.linuxidc.com
bbs.theithome.com
450 计计TCP/IP详解 卷 2:实现

G 该路由通向一个网关 (路由器 )。这种路由被称为间接路由。如果没有设置本标志,则


表明路由的目的地与本机直接相连,称为直接路由。
H 该路由通往一台主机,也就是说,目的地址是一个完整的主机地址。如果没有设置本
标志,则路由通往一个网络,目的地址是一个网络地址:一个网络号,或一个网络号
与子网号的组合。 n e t s t a t命令并不区分这一点,但每一条网络路由中都包含一个网
络掩码,而主机路由中则隐含了一个全 1的掩码。
S 该路由是静态的。图 18-2中route命令创建的三个路由表项是静态的。
C 该路由可被克隆 ( c l o n e )以产生新的路由。在本路由表中有两条路由设置了这个标志:
一条是到本地以太网 ( 1 4 0 . 2 5 2 . 1 3 . 3 2 )的路由, A R P通过克隆该路由创建到以太网中其
他特定主机的路由;另一条是到多播组 2 2 4的路由,克隆该路由可以创建到特定多播
组(如224.0.0.1)的路由。
L 该路由含有链路层地址。本标志应用于单播地址和多播地址。由 A R P从以太网路由克
隆而得到的所有主机路由都设置了本标志。
R 环回驱动器 (为设有本标志的路由而设计的普通接口 )将拒绝所有使用该路由的数据报。
添加带有拒绝标志的路由的功能由 N E T / 2提供。它提供了一种简单的方法,来防
止主机向外发送以网络 127为目的地的数据报。参见习题 6.6。
在4.3BSD Reno之前,内核将为 IP地址维护两个不同的路由表:一个针对主机路由,另一
个针对网络路由。对于给定的路由,将根据它的类型添加到相应的路由表中。默认路由被存
储在网络路由表中,其目的地址是 0 . 0 . 0 . 0。查找过程隐含了这样一种层次关系:首先查看主
机路由表;如果找不到,则查找网络路由表;如果仍找不到,则查找默认路由。仅当三次查
找都失败时,才认为目的地不可达。 [ L e ffler et al. 1998] 的第11 . 5节描述了一种带链表结构的
hash表,该hash表同时用于 Net/1中的主机路由表和网络路由表。
4.3BSD Reno [Sklower 1991]的变化主要与路由表的内部表示有关。这些变化允许相同的
路由表函数访问不同协议栈的路由表,如 OSI协议,它的地址是变长的,这一点与长度固定为
32位的IP地址不同。为了提高查询速度,路由表的内部结构也做了变动。
Net/3路由表采用 Patricia树结构[Sedgewick 1990]来表示主机地址和网络地址 (Patricia 支持
“从文字数字的编码中提取信息的 P a t r i c i a算法” )。待查找的地址和树中的地址都被看成比特
序列。这样就可以用相同的函数来查找和维护不同类型的树,如:含有 32 bit定长IP地址的树、
含有48 bit定长XNS地址的树以及一棵含有变长 OSI地址的树。
使用Patricia树构造路由表的思想应归功于 Van Jacobson 的[Sklower 1991]。
举个例子就可以很容易地描述出这个算法。查找路由表的目标就是为了找到一个最能匹
配给定目标的特定地址。我们称这个给定的目标为查找键 (search key)。所谓最能匹配的地址,
也就是说,一个能够匹配的主机地址要优于一个能够匹配的网络地址;而一个能够匹配的网
络地址要优于默认地址。
每条路由表项都有一个对应的网络掩码,尽管在主机路由中没有存储掩码,但它隐含了
一个全1比特的掩码。我们对查找键和路由表项的掩码进行逻辑与运算,如果得到的值与该路
由表项的目的地址相同,则称该路由表项是匹配的。对于某个给定的查找键,可能会从路由
表中找到多条这样的匹配路由,所以在单个表同时包含网络路由和主机路由的情况下,我们

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 451
必须有效地组织该表,使得总能先找到那个更能匹配的路由。
让我们来讨论图 1 8 - 3给出的例子。图中给出了两个查找键,分别是 1 2 7 . 0 . 0 . 1和1 2 7 . 0 . 0 . 2。
为了更容易地说明逻辑与运算,图中同时给出了它们的十六进制值。图中给出的两个路由表
项分别是主机路由 1 2 7 . 0 . 0 . 1 (它隐含了一个全 1的掩码0 x f f f f f f f f)和网络路由1 2 7 . 0 . 0 . 0 (它的
掩码是0xff000000)。

查找键=127.0.0.1 查找键=127.0.0.2

主机路由 网络路由 主机路由 网络路由

1 查找键 7f000001 7f000001 7f000002 7f000002


2 路由表键 7f000001 7f000001 7f000001 7f000000
3 路由表掩码 ffffffff ff000000 ffffffff ff000000
4 1和3的逻辑与 7f000001 7f000000 7f000002 7f000000
2和4相等? 相等 相等 不等 相等

图18-3 分别以127.0.0.1和127.0.0.2为查找键的路由表查找示例

其中两个路由表项都能够匹配查找键 1 2 7 . 0 . 0 . 1,这时路由表的结构必须确保能够先找到
更能匹配该查找键的表项 (127.0.0.1)。
图18-4给出了对应于图 18-2的Net/3路由表的内部表示。执行带- A标志的n e t s t a t命令可
以导出路由表的树型结构,图 18-4就是根据导出的结果而建立的。

图18-4 对应于图 18-2的Net/3路由表

标有“ e n d”的两个阴影框是该树结构中带有特殊标志的叶结点,该标志代表树的端点。
左边的那个拥有一个全 0键,而右边的拥有一个全 1键。左边的两个标有“ e n d”和“ d e f a u l t”
的框垒在一起,这两个框有特殊的意义,它们与重复键有关,具体内容可参考 18.9节。
方角框被称为内部结点 (internal node)或简称为结点 ( n o d e ),圆角框被称为叶子。每一个
Linux公社 www.linuxidc.com
bbs.theithome.com
452 计计TCP/IP详解 卷 2:实现

内部结点对应于测试查找键的一个比特位,其左右各有一个分枝。每一个叶子对应于一个主
机地址或者对应于一个网络地址。如果在叶子下面有一个十六进制数,那么这个叶子就对应
于一个网络地址,该十六进制数就是叶子的网络掩码。如果在叶子下面没有十六进制的掩码,
那么这个叶子就是一个主机地址,其隐含的掩码是 0xffffffff。
有一些内部结点也含有网络掩码,在后面的学习中,我们将会了解这些掩码在回溯过程
中是如何使用的。图中的每一个结点还包含了一个指向其父结点的指针 (没有在图中表示出来 ),
它能使树结构的回溯、删除及非递归操作更加方便。
比特比较是运用在插口地址结构上的,因此,在图 1 8 - 4中给出的比特位置是从插口地址
结构中的起始位置开始算的。图 18-5给出了sockaddr_in结构中的比特位置。
比特:0 32 63
长度 族
端口号 IP地址 (全0)
(16) (2)
1字节 1 2 4 8

图18-5 Internet插口地址结构的比特位置

I P地址的最高位比特是比特 3 2,最低位是比特 6 3。此外还列出了长度是 1 6,地址族为


2(AF_INET),这两个数值在我们所列举的例子中将会遇到。
为了解释这些例子,还需要给出树中不同 IP地址的比特表示形式。它们都被列在图 18-6中,
该图还给出了下面例子中要用到的一些其他 I P地址的比特表示形式。该图采用了加粗的字体
来表示图18-4中分支点所对应的比特位置。
现在我们举一些特定的例子来说明路由表的查找过程是如何完成的。
1. 与主机地址匹配的例子
假定主机地址 1 2 7 . 0 . 0 . 1是查找键—待查找的目的地址。比特 3 2为0,因此,沿树顶点向
左分支继续查找,到下一个结点。比特 3 3为1,因此,从该结点右分支继续查找,到下一个结
点。比特 6 3为1,因此,从右分支继续查找,到下一个结点。而下一个结点是个叶子,此时查
找键( 1 2 7 . 0 . 0 . 1 )与叶子中的地址 ( 1 2 7 . 0 . 0 . 1 )相比较。它们完全匹配,这样查找函数就可以返回
该路由表项。
32 bit IP地址 点分割表示
比特
位置

图18-6 图18-2和图18-4中IP地址的比特表示形式

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 453
2. 与主机地址匹配的例子
再假定查找键是地址 1 4 0 . 2 5 2 . 1 3 . 3 5。比特3 2为1,因此,沿树顶点向右分支继续查找。比
特 3 3 为 0 ,比特 3 6 为 1 ,比特 5 7为 0 ,比特 6 2 为1 ,比特 6 3 为 1 ,因此,查找在底部标有
140.252.13.35的叶子处终止。查找键与路由表键完全匹配。
3. 与网络地址匹配的例子
假定查找键是 1 2 7 . 0 . 0 . 2。比特 3 2为0,比特 3 3为1,比特 6 3为0 ,因此,查找在标有
1 2 7 . 0 . 0 . 0的叶子处终止。查找键和路由表键并没有完全匹配,因此,需要进一步看它是不是
一个能够匹配的网络地址。对查找键和网络掩码 (0 x f f 0 0 0 0 0 0)进行逻辑与运算,得到的结
果与该路由表键相同,即认为该路由表项能够匹配。
4. 与默认地址匹配的例子
假定查找键是 1 0 . 1 . 2 . 3。比特 3 2为0,比特3 3为0,因此,查找在标有“ e n d”和“d e f a u l t”
并带有重复键的叶子处终止。在这两个叶子中重复的路由表键是 0 . 0 . 0 . 0。查找键与路由表键
值没有完全匹配,因此,需要进一步看它是不是一个能够匹配的网络地址。这种匹配运算要
对每个含网络掩码的重复键都试一遍。第一个键 (标有e n d )没有网络掩码,可以跳过不查。第
二个键 (默认表项 )有一个 0 x 0 0 0 0 0 0 0 0的掩码。查找键和这个掩码进行逻辑与运算,所得结
果和路由表键(0)相等,即认为该路由表项能够匹配。这样默认路由就被用做匹配路由。
5. 带回溯过程的与网络地址匹配的例子
假定查找键是 1 2 7 . 0 . 0 . 3。比特 3 2为0,比特 3 3 为1,比特 6 3为1 ,因此,查找在标有
1 2 7 . 0 . 0 . 1的叶子处终止。查找键和路由表键没有完全匹配。由于这个叶子没有网络掩码,无
法进行网络掩码匹配的尝试。此时就要进行回溯。
回溯算法在树中向上移动,每次移动一层。如果遇到的内部结点含有掩码,则对查找关
键字和该掩码进行逻辑与运算,得到一个键值,然后以这个键值作为新的查找键,在含该掩
码的内部结点为开始的子树中进行另一次查找,看是否能找到匹配的结点。如果找不到,则
回溯过程继续沿树上移,直到到达树的顶点。
在这个例子中,查找上移一层到达比特 6 3对应的结点,该结点含有一个掩码。于是对查
找键和掩码 (0 x f f 0 0 0 0 0 0)进行逻辑与运算,得到一个新的查找键,其值为 127.0.0.0。然后从
该结点开始查找 1 2 7 . 0 . 0 . 0。比特6 3为0,因此,沿左分支到达标有 1 2 7 . 0 . 0 . 0的叶子上。用新的
查找键与路由表键相比较,它们是相等的,因此认为这个叶子是匹配的。
6. 多层回溯的例子
假定查找键是 11 2 . 0 . 0 . 1 。比特 3 2 为0,比特 3 3为 1,比特 6 3为1,因此,查找在标有
1 2 7 . 0 . 0 . 1的叶子处终止。该键与查找键不相等,并且路由表项中没有网络掩码,因此需要进
行回溯。
查找过程向上移动一层,到达比特 6 3对应的结点,该结点含有一个掩码。对查找关键字
和该掩码 (0 x f f 0 0 0 0 0 0)进行逻辑与运算,然后再从这个结点开始进一步查找。在新的查找
键中比特 6 3为0,因此,沿左分支到达标有 1 2 7 . 0 . 0 . 0的叶子。比较之后发现逻辑与运算得到的
查找键(112.0.0.0)和路由表键并不相等。
因此继续向上回溯一层,从比特 63对应的结点上移到比特 33对应的结点。但这个结点没有
掩码,再继续向上回溯。到达的下一层是树的顶点 ( 比特 3 2 ),它有一个掩码。对查找键
(112.0.0.1)和该掩码(0x00000000)进行逻辑与运算后,从该点开始一个新的查找。在新的查找

Linux公社 www.linuxidc.com
bbs.theithome.com
454 计计TCP/IP详解 卷 2:实现

键中,比特32为0,比特33也为0,因此,查找在标有“end”和“default”的叶子处结束。通过
与重复键列表中的每一项进行比较,发现默认键与新的查找键相匹配,因此采用默认路由。
从这个例子中可以知道,如果在路由表中存在默认路由,那么当回溯最终到达树的顶点
时,它的掩码为全 0比特,这使得查找向树中最左边叶子的方向进行搜索,最终与默认路由相
匹配。
7. 带回溯和克隆过程、并与主机地址相匹配的例子
假定查找键是 2 2 4 . 0 . 0 . 5。比特 3 2为1,比特 3 3 1,比特 3 5为0,比特 6 3为1,因此,查找在
标有2 2 4 . 0 . 0 . 1的叶子处结束。路由表的键值和查找关键字并不相等,并且该路由表项不包含
网络掩码,因此要进行回溯。
回溯向上移动一层,到达比特 6 3对应的结点。这个结点含有掩码 0 x f f 0 0 0 0 0 0,因此,
对查找键和该掩码进行逻辑与运算,产生一个新的查找键,即 2 2 4 . 0 . 0 . 0。再从这个结点开始
一次新的查找。在新的查找键中比特 6 3为0,于是沿左分支到达标有 2 2 4 . 0 . 0 . 0的叶子。这个路
由表键和逻辑与运算得到的查找键相匹配,因此这个路由表项是匹配的。
该路由上设置了“克隆”标志 (见图1 8 - 2 ),因此,以 2 2 4 . 0 . 0 . 5为地址创建一个新的叶子。
新的路由表项是:
Destination Gateway Flags Refs Use Interface
224.0.0.5 link#1 UHL 0 0 le0

图1 8 - 7从比特 3 5对应的结点开始,给出了图 1 8 - 4中
路由表树右边部分的新的排列。注意,无论何时向树中
添加新的叶子,都需要两个结点:一个作为叶子,另一
个作为测试某一位比特的内部结点。
新创建的表项就被返回给查找 224.0.0.5的调用者。
8. 大图
图1 8 - 8是一张比较大的图,它描述了所有涉及到的
数据结构。该图的底部来自于图 3-32。
现在我们将解释图中的几个要点,在后面,本章还
将给出详细的阐述。
• r t _ t a b l e s是指向 r a d i x _ n o d e _ h e a d结构的
指针数组。每一个地址族都有一个数组单元与之 图18-7 插入224.0.0.5路由表项后
对应。r t _ t a b l e s [ A F _ I N E T ]指向I n t e r n e t路由 图18-6的改动

表树的顶点。
• r a d i x _ n o d e _ h e a d结构包含三个 r a d i x _ n o d e结构。这三个结构是在初始化路由树
时创建的,中间的是树的顶点。它对应于图 1 8 - 4中最上端标有“ bit 32 ”的结点框。三
个r a d i x _ n o d e结构中的第一个是图 1 8 - 4中最左边的叶子 (与默认路由共享的重复 ),第
三个结构是最右边的叶子。在一个空的路由表中,就只包含这三个 r a d i x _ n o d e结构,
我们将会看到rn_inithead函数是如何构建它们的。
• 全局变量 m a s k _ r n h e a d也指向一个 r a d i x _ n o d e _ h e a d结构。它是包含了所有掩码的
一棵独立树的首部结构。观察图 1 8 - 4中给出的八个掩码可知,有一个掩码重复了四次,
有两个掩码重复了一次。通过把掩码放在一棵单独的树中,可以做到对每一个掩码只需
要维护它的一个备份即可。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 455
• 路由表树是用 r t e n t r y 结构创建的,在图 1 8 - 8中,有两个 r t e n t r y 结构。每一个
r t e n t r y结构包含两个 r a d i x _ n o d e结构,因为每次向树中插入一个新的路由时,都
需要两个结点:一个是内部结点,对应于某一位测试比特;另一个是叶子,对应于一个
主机路由或一个网络路由。在每一个 r t e n t r y结构中,给出了内部结点对应的要测试
的那位比特以及叶子中所包含的地址。

图18-8 路由表中涉及的数据结构

r t e n t r y结构中的其余部分是该路由的一些其他重要信息。虽然我们只给出了该结构中
的一个指向 ifnet结构的指针,但在这个结构中还包含了指向 ifaddr结构的指针、该路由的

Linux公社 www.linuxidc.com
bbs.theithome.com
456 计计TCP/IP详解 卷 2:实现

标志、指向另一个 rtentry结构的指针(如果该路由是一个非直接路由 )和该路由的度量等等。


• 存在于每一个 UDP和TCP插口(图22-1)中的协议控制块 PCB(见第22章)中包含了一个指向
r t e n t r y结构的r o u t e结构。每次发送一个 IP数据报时, UDP和TCP输出函数都传递一
个指向 P C B中r o u t e结构的指针,作为调用 i p _ o u t p u t的第三个参数。使用相同路由
的PCB都指向相同的路由表项。

18.3 选路插口

在4.3BSD Reno的路由表做了变动后,路由子系统和进程间的交互过程也要做出变动,这
就引出了选路插口 (routing socket)的概念。在 4.3BSD Reno之前,是由进程 (如r o u t e命令)通
过发出定长的ioctl来修改路由表的。 4.3BSD Reno采用新的PF_ROUTE域把它改变成一种更
为通用的消息传递模式。进程在 P F _ R O U T E域中创建一个原始插口 (raw socket),就能够向内
核发送选路消息,以及从内核接收选路消息 (如重定向或来自于内核的其他的异步通知 )。
图1 8 - 9给出了 1 2种不同类型的选路消息。消息类型位于 r t _ m s g h d r结构(图1 9 - 1 6 )中的
rtm_type字段。进程只能发送其中的5种消息(写入到选路插口中),但可以接收全部的12种消息。
我们将在第 19章给出这些选路消息的详细讨论。

rtm_type 发往内核? 从内核发出? 描 述 结构类型

RTM_ADD • • 添加路由 rt-msghdr


RTM_CHANGE • • 改变网关、度量或标志 rt-msghdr
RTM_DELADDR • 从接口中删除地址 ifa-msghdr
RTM_DELETE • • 删除路由 rt-msghdr
RTM_GET • • 报告度量及其他路由信息 rt-msghdr
RTM_IFINFO • 接口打开或关闭等 rt-msghdr
RTM_LOCK • • 锁定指明的度量 rt-msghdr
RTM_LOSING • 内核怀疑某路由无效 rt-msghdr
RTM_MISS • 查找这个地址失败 rt-msghdr
RTM_NEWADDR • 接口中添加了地址 ifa-msghdr
RTM_REDIRECT • 内核得知要使用不同的路由 rt-msghdr
RTM_RESOLVE • 请求将目的地址解析成链路层地址 rt-msghdr

图18-9 通过选路插口交换的消息类型

18.4 代码介绍

路由选择中使用的各种结构和函数是通过五个 C文件和三个头文件来定义的。图 1 8 - 1 0列
出了这些文件。
通常,前缀 r n _表示r a d i x结点函数,这些函数可以对 P a t r i c i a树进行查找和操作,前
缀raw_表示路由控制块函数, rout_、rt_和rt这三个前缀表示常用的选路函数。
尽管有的文件和函数以 r a w为前缀,但在所有的选路章节中我们仍使用术语选路
控制块(routing control block)而不是原始控制块。这是为了防止与第 32章中讨论的原
始I P控制块及其函数相混淆。虽然原始控制块及相关函数不仅仅用于 N e t / 3中的选路
插口(使用这些结构和函数的原始 O S I协议之一 ),但是本书中我们只用做 P F _ R O U T E
域中的选路插口。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 457
文 件 描 述

net/radix.h radix结点定义
net/raw_cb.h 选路控制块定义
net/route.h 选路结构
net/radix.c radix结点(Patricia树)函数
net/raw_cb.c 选路控制块函数
net/raw_usrreq.c 选路控制块函数
net/route.c 选路函数
net/rtsock.c 选路插口函数

图18-10 本章中讨论的文件

和 程序

插口接收缓存 各种系统调用
系统调用 系统初始化

接口状态改变 接口启动和断
开时由各 i o c t l
调用,以添加
或删除路由
给定 T C P连接 ICMP改变路由 由T C P / I P协议
上相继重传中 调用以查找到
的第四个 目的地的路由

图18-11 各选路函数之间的关系
Linux公社 www.linuxidc.com
bbs.theithome.com
458 计计TCP/IP详解 卷 2:实现

图1 8 - 11给出了一些基本的选路函数,并表示了它们之间的相互关系。其中带阴影的椭圆
是在本章和下面两章中要涉及的内容。在图中,我们还给出了每种类型的选路消息 (共12种)的
产生之处。
r t a l l o c 函数是由 I n t e r n e t 协议调用的,用于查找到达指定目的地的路由。在
i p _ r t a d d r 、i p _ f o r w a r d 、i p _ o u t p u t 和i p _ s e t m o p t i o n s 函数中都已出现过
rtalloc,在后面介绍的 in_pcbconnect和tcp_mss函数中也将会遇到它。
图18-11还给出了在选路域中创建插口的五个典型程序:
• arp处理ARP高速缓存,该ARP高速缓存被存储在 Net/3的IP路由表中 (见第21章);
• g a t e d和r o u t e d是选路守护进程,它们与其他路由器进行通信。当选路环境发生变化
时(指路由器及链路断开或连通 ),对内核的路由表进行操作;
• route通常是由启动脚本或系统管理员执行的一个程序,用于添加或删除路由;
• rwhod在启动时会调用一个选路 sysctl来测定连接的接口。
当然,任何进程 (具有超级用户的权限 )都能打开一个选路插口向选路子系统发送或从中接
收消息;在图18-11中,我们只给出了一些常用的系统程序。

18.4.1 全局变量

图18-12列出了在三个有关路由选择的章节中介绍的全局变量。

变 量 数据类型 描 述

rt_tables struct radix_node_head *[] 路由表表头指针的数组


mask_rnhead struct radix_node_head * 指向掩码表表头的指针
rn_mkfreelist struct radix_mask * 可用r a d i x _ m a s k结构的链表表头
max_keylen int 以字节为单位的路由表键值的最大长度
rn_zeros char * 长为m a x _ k e y l e n、值为全零比特的数组
rn_ones char * 长为m a x _ k e y l e n、值为全1比特的数组
maskedKey char * 长为m a x _ k e y l e n、掩码过的查找键数组
rtstat struct rtstat 路由选择统计 (图18-13)
rttrash int 未释放的非表中路由的数目
rawcb struct rawcb 选路控制块双向链表表头
raw_recvspace u_long 选路插口接收缓冲区的默认大小, 8192字节
raw_sendspace u_long 选路插口发送缓冲区的默认大小, 8192字节
route_cb struct route_cb 选路插口监听器的数目,每个协议的数目及总的数目
route_dst struct sockaddr 保存选路消息中目的地址的临时变量
route_src struct sockaddr 保存选路消息中源地址的临时变量
route_proto struct sockproto 保存选路消息中协议的临时变量

图18-12 在三个有关选路的章节中介绍的全局变量

18.4.2 统计量

图18-13列举了一些路由选择统计量,它们是在全局结构 rtstat中维护的。
在代码的处理中,我们可以发现计数器是怎样增加的。这些计数器在 SNMP中并未使用。
图1 8 - 1 4 给出了 n e t s t a -
t r s命令输出的一些统计数据的样例,该命令用于显示
rtstat结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 459
rtstat成员 描 述 在SNMP中的使用

rts_badredirect 无效重定向调用的数目
rts_dynamic 由重定向创建的路由数目
rts_newgateway 由重定向修改的路由数目
rts_unreach 查找失败的次数
rts_wildcard 由通配符匹配的查找次数 (从未使用)

图18-13 在rtstat 结构中维护的路由选择统计数据

n e t s t a t - r s的输出 r t s t a t 成员

1029 bad routing redirects rts_badredirect


0 dynamically created routes rts_dynamic
0 n e w g a t e w a y s d u e t o r e d i r e c t sr t s _ n e w g a t e w a y
0 destinations found unreachablr ets_unreach
0 uses of a wildcard route rts-wildcard

图18-14 路由选择统计数据样例

18.4.3 SNMP变量

图18-15给出了名为 ipRouteTable的IP路由表以及相应的内核变量。

IP路由表,index = <ipRouteDest>

SNMP变量 变 量 描 述

ipRouteDest rt_key IP目的地址。值为 0 . 0 . 0 . 0时,代表默认路由


ipRouteIfIndex r t _ i f p,i f _ i n d e x 接口号: ifIndex
ipRouteMetric1 -1 基本的路由度量。其含义取决于选路协议的值 (ipRoute-
Proto)。值为-1,表示没有使用
ipRouteMetric2 -1 可选的路由度量
ipRouteMetric3 -1 可选的路由度量
ipRouteMetric4 -1 可选的路由度量
ipRouteNextHop rt_gateway 下一跳路由器的 IP地址
ipRouteType (见正文) 路由类型: 1=其他,2=无效路由, 3=直接的,4=间接

ipRouteProto (见正文) 路由协议:1=其他,4=ICMP重定向,8=RIP, 13=OSPF,
14= BGP 等
ipRouteAge (未实现) 从路由最后一次被修改或被确定为正确时起的秒数
ipRouteMask rt_mask 在和i p R o u t e D e s t比较前,与目的主机 IP地址进行逻
辑与运算的掩码
ipRouteMetric5 -1 可选的路由度量
ipRouteInfo NULL 本选路协议特定的 MIB定义的引用

图18-15 IP路由表: ipRouteTable

如果在r t _ f l a g s中将标志 R T F _ G A T E W A Y置位,则该路由就是远程的, i p R o u t e T y p e


等于4;否则该路由就是直达的, i p R o u t e T y p e值为3。对于i p R o u t e P r o t o,如果将标志
RTF_DYNAMIC或RTF_MODIFIED置位,则该路由就是由ICMP来创建或修改的,值为 4,否则
为其他情况,其值为1。最后,如果rt_mask指针为空,则返回的掩码就是全 1(即主机路由)。

Linux公社 www.linuxidc.com
bbs.theithome.com
460 计计TCP/IP详解 卷 2:实现

18.5 Radix结点数据结构

在图18-8中可以发现每一个路由表的表头都是一个radix_node_head结构,而选路树中所
有的结点(包括内部结点和叶子)都是radix_node结构。radix_node_head结构如图18-16所示。

图18-16 radix_node_head 结构:每棵选路树的顶点

92 rnh_treetop指向路由树顶端的radix_node结构。可以看到 r a d i x _ n o d e _ h e 结
ad
构的最后一项分配了三个 radix_node结构,其中中间的那个被初始化成树的顶点 (图18-8)。
93-94 rnh_addrsize和rnh_pktsize目前未被使用。

r n h _ a d d r s i z e是为了能够方便地将路由表代码导入到系统中去,因为系统的
插口地址结构中没有标识其长度的字节。 r n h _ p k t s i z e是为了能够利用 r a d i x结点
机制直接检查分组头结构中的地址,而无需把该地址拷贝到某个插口地址结构中去。

95-110 从rnh_addaddr到rnh_walktree是七个函数指针,它们所指向的函数将被调用
以完成对树的操作。如图 1 8 - 1 7所示,r n _ i n i t h e a d仅初始化了其中的四个指针,剩下的三
个指针在Net/3中未被使用。 成 员 被r n _ i n i t h e a d初始化为
111-112 图1 8 - 1 8给出了组成树中结点的 rnh_addaddr rn_addroute
radix_node结构。在图18-8中,我们可以发 rnh_addpkt NULL
现,在radix_node_head中分配了三个这样 rnh_deladdr rn_delete
rnh_delpkt NULL
的r a d i x _ n o d e结构,而在每一个 r t e n t r y
rnh_matchaddr rn_match
结构中分配了两个radix_node结构。 rnh_matchpkt NULL
41-45 前五个成员是内部结点和叶子都有 rnh_walktree rn_walktree

的成员。后面是一个 u n i o n:如果结点是叶 图18-17 在radix_node_head 结构中的


子,那么它定义了三个成员;如果是内部结 七个函数指针

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计461
点,那么它定义了另外不同的三个成员。由于在 N e t / 3代码中经常使用 u n i o n中的这些成员,
因此,用一组#define语句定义它们的简写形式。
41-42 rn_mklist是该结点掩码链表的表头。我们将在 18.9节中描述该字段。 rn_p指向该
结点的父结点。
43 如果rn_b值大于或者等于零,那么该结点为内部结点;否则就是叶子。对于内部结点来
说, r n _ b就是要测试的比特位置:例如,在图 1 8 - 4中,树的顶结点的 r n _ b值为3 2。对于叶
子来说, rn_b是负的,它的值等于- 1减去网络掩码索引 (index of the network mask)。该索引
是指掩码中出现的第一个零的比特位置。图 18-19给出了图18-4中掩码的索引。

图18-18 radix_node 结构:路由树的结点

32 bit IP掩码(比特32-36) 索引

图18-19 掩码索引的例子

我们可以发现,其中的全 0掩码的索引是特殊处理的:它的索引是 0,而不是32。


44 内部结点的 r n _ b m a s k是个单字节的掩码,用于检测相应的比特位是 0还是1。在叶子中
它的值为0。很快我们将会看到如何运用成员 rn_bmask和成员rn_off。
45 图18-20给出了成员 rn_flags的三个值。

Linux公社 www.linuxidc.com
bbs.theithome.com
462 计计TCP/IP详解 卷 2:实现

常 量 描 述

RNF_ACTIVE 该结点是活的 (alive)(针对r t f r e e)


RNF_NORMAL 该叶子含有正常路由 (目前未被使用 )
RNF_ROOT 该叶子是树的根叶子

图18-20 rn_flags 的值

R N F _ R O O T标志只有在 r a d i x _ n o d e _ h e a d结构中的三个 radix结点(树的顶结点、左端结点和


右端结点)中才能设置。这三个结点不能从路由树中删除。
48-49 对于叶子而言, r n _ k e y指向插口地址结构, r n _ m a s k指向保存掩码的插口地址结
构。如果 r n _ m a s k为空,则其掩码为隐含的全 1值(即,该路由指向某个主机而不是某个网
络)。
图18-21例举了一个与图 18-4中的叶子140.252.13.32相对应的radix_node结构的例子。

指向比特63对应的radix_node{}

图18-21 与图18-4中的叶子 140.252.13.32相对应的radix_node 结构

该例子中还给出了图 18-22中描述的 radix_mask结构。我们把它的宽度略微缩小了一些,


以区分于 r a d i x _ n o d e结构;这两种结构在后面的很多图例中都会遇到。有关 r a d i x _ m a s k
结构的作用将在 18.9节中阐述。
值为-60的r n _ b相对应的索引为 59。r n _ k e y指向一个 s o c k a d d r _ i n结构,它的长度值
为1 6,地址族值为 2 (A F _ I N E T)。由r n _ m a s k和r m _ m a s k指向的掩码结构所含的长度值为 8,
地址族值为0(该族为AF_UNSPEC,但我们从未使用它 )。
50-51 当有多个叶子的键值相同时,使用 rn_dupedkey指针。具体内容将在18.9节中阐述。
52-58 有关rn_off的内容将在 18.8节中阐述。 rn_l和rn_r是该内部结点的左、右指针。
图18-22给出了radix_mask结构的定义。
76-83 该结构中包含了一个指向其掩码的指针: r m _ m a s k,实际上是一个保存掩码的插口
地址结构的指针。每一个 r a d i x _ n o d e结构对应一个 r a d i x _ m a s k结构的链表,这就允许每

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 463
个结点包含多个掩码:成员 r n _ m k l i s t 指向链表的第一个结点,然后每个结构的成员
r m _ m k l i s t指向链表的下一个结点。该结构的定义同时声名了全局变量 r n _ m k f r e e l i s t,
它是可用的 radix_mask结构链表的表头。

图18-22 radix_mask 结构

18.6 选路结构

访问内核路由信息的关键之处是:
1) rtalloc函数,用于查找通往目的地的路由;
2) route结构,它的值由 rtalloc函数填写;
3) route结构所指向的rtentry结构。
图1 8 - 8给出了U D P和T C P (参见第 2 2章)中使用的协议控制块 ( P C B ),其中包含一个 r o u t e
结构,见图 18-23。

图18-23 route 结构

r o _ d s t 被定义成一个一般的插口地址结构,但对于 I n t e r n e t协议而言,它就是一个
s o c k a d d r _ i n结构。注意,对这种结构类型的绝大多数引用都是一个指针,而 r o _ d s t是该
结构的一个实例而非指针。
这里,我们有必要回顾一下图 8-24。从该图可以得知,每次发送 IP数据报时,这些路由是
如何使用的。
• 如果调用者传递了一个 r o u t e结构的指针,那么就使用该结构。否则,就要用一个局部
r o u t e结构,其值设置为 0 (设置 r o _ r t为空指针 )。U D P 和T C P把指向它们的 P C B中
route结构的指针传递给 ip_output。
• 如果r o u t e结构指向一个r t e n t r y结构(r o _ r t指针为非空),同时所引用的接口仍然有
效;而且如果 r o u t e结构中的目的地址与 I P数据报中的目的地址相等,那么该路由就被
使用。否则,目的主机的 I P 地址将会设置在插口地址结构 s o _ d s t 中,并且调用
r t a l l o c来查找一条通向该目的主机的路由。在 TCP链接中,数据报的目的地址始终是
路由的目的地址,不会发生变化,但是 U D P应用可以通过 s e n d t o每次都把数据报发送
到不同的目的地。
• 如果r t a l l o c返回的r o _ r t是个空指针,则表明找不到路由,并且 i p _ o u t p u t返回一

Linux公社 www.linuxidc.com
bbs.theithome.com
464 计计TCP/IP详解 卷 2:实现

个差错。
• 如果在 r t e n t r y结构中设有R T F _ G A T E W A Y标志,那么该路由为非直接路由 (参见图1 8 -
2中的G标志)。接口输出函数的目的地址 (d s t)就变成网关的IP地址,即 r t _ g a t e w a y成
员,而不是 IP数据报的目的地地址。
图18-24给出了rtentry结构的定义。

图18-24 rtentry 结构

83-84 在该结构中包含有两个 r a d i x _ n o d e结构。正如我们在图 1 8 - 7的例子中所提到的,


每次向路由树添加一个新叶子的同时也要添加一个新的内部结点。 r t _ n o d e s[ 0 ]为叶子,
r t _ n o d e s[ 1 ]为内部结点。在图 1 8 - 2 4最后的两个 # d e f i n e语句提供了访问该叶结点的键和
掩码的简写形式。
86 图1 8 - 2 5 给出了储存在 r t _ f l a g s 中的各种常量以及在图 1 8 - 2 的“ F l a g s ”列中由
netstat输出的相应字符。

常 量 n e t s t a t标志 描 述

RTF_BLACKHOLE 无差错的丢弃分组 (环回驱动器 :图5-27)


RTF_CLONING C 使用中产生新的路由 (由ARP使用)
RTF_DONE d 内核的证实,表示消息处理完毕
RTF_DYNAMIC D (由重定向)动态创建
RTF_GATEWAY G 目的主机是一个网关 (非直接路由 )
RTF_HOST H 主机路由 (否则,为网络路由 )
RTF_LLINFO L 当r t _ l l i n f o指针无效时,由 ARP设置
RTF_MASK m 子网掩码存在 (未使用)
RTF_MODIFIED M (由重定向)动态修改
RTF_PROTO1 1 协议专用的路由标志
RTF_PROTO2 2 协议专用的路由标志 (ARP使用)
RTF_REJECT R 有差错的丢弃分组 (环回驱动器 :图5-27)
RTF_STATIC S 人工添加的路由 (r o u t e程序)
RTF_UP U 可用路由
RTF_XRESOLVE X 由外部守护进程解析名字 (用于X.25)

图18-25 rt_flags 的值

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 465
n e t s t a t不输出 R T F _ B L A C K H O L E标志。两个标志为小写字符的常量, R T F _ D O N E和
RTF_MASK,在路由消息中使用,但通常并不储存在路由表项中。
85 如果设置了R T F _ G A T E W A Y标志,那么 r t _ g a t e w a y所含的插口地址结构的指针就指向
网关的地址 (即网关的 I P地址)。同样, r t _ g w r o u t e就指向该网关的 r t e n t r y。后一个指针
在ether_output(图4-15)中用到。
87 rt_refcnt是一个计数器,保存正在使用该结构的引用数目。在 19.3节的最后部分将具
体描述该计数器。在图 18-2中,该计数器在“ Refs”列输出。
88 当分配该结构存储空间时, r t _ u s e被初始化为0。在图8 - 2 4中,可发现每次利用该路由
输出一份IP数据报时,其值会随之递增。该计数器的值在图 18-2的“Use”栏中输出。
89-90 rt_ifp和rt_ifa分别指接口结构和接口地址结构。在图 6-5中曾指出一个给定的接
口可以有多个地址,因此, rt_ifa是必需的。
92 rt_llinfo指针允许链路层协议在路由表项中储存该协议专用的结构指针。该指针通常
与RTF_LLINFO标志一起使用。图 21-1描述了ARP如何使用该指针。

图18-26 rt_metrics 结构

93 图18-26给出了rt_metrics结构,rtentry结构含有该结构。图 27-3显示了TCP使用了
该结构的六个成员。
54-65 rmx_locks是一个比特掩码,由它告诉内核后面的八个度量中的哪些禁止修改。该
比特掩码的值在图 20-13中给出。
A R P (参见第 2 1章)把r m x _ e x p i r e用作每一个 A R P路由项的定时器。与 r m x _ e x p i r e的
注释不同的是, rm_expire不是用作重定向的。
图1 8 - 2 8概括了我们上面所阐述的各种结构和这些结构之间的关系,以及所引用的各种插
口地址结构。图中给出的 r t e n t r y是图1 8 - 2中到1 2 8 . 3 2 . 3 3 . 5的路由。包含在 r t e n t r y中的另
一个 r a d i x _ n o d e 对应于图 1 8 - 4中位于该结点正上方的测试比特 3 6 的内部结点。第一个
i f a d d r所指的两个 s o c k a d d r _ d l结构如图 3 - 3 8所示。另外,从图 6 - 5中也可注意到 i f n e t
结构包含在 le_softc结构中,第二个 ifaddr结构包含在 in_ifaddr结构中。

18.7 初始化: route_init和rtable_i n i t函数

路由表的初始化过程并非是一目了然的,我们需要回顾一下第 7章中的d o m a i n结构。在描


述这些函数调用之前,图 18-27给出了各协议族中与 domain结构相关的字段。

Linux公社 www.linuxidc.com
bbs.theithome.com
466 计计TCP/IP详解 卷 2:实现

成 员 OSI值 Internet值 选路值 Unix值 XNS值 注 释

比特
字节

图18-27 domain 结构中与路由选择有关的成员

P F _ r o u t e 域是唯一具有初始化函数的域。同样,只有那些需要路由表的域才有
dom_rtattach函数,并且该函数总是rn_inithead。选路域和Unix域并不需要路由表。

都是

以太网地址

都是

三个都是

图18-28 选路结构小结

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 467
d o m _ r t o f f s e t成员是以比特为单位的选路过程中被检测的第一个比特的偏移量 (从域的
插口地址结构的起始处开始计算 )。d o m _ m a x r t k e y给出了该结构的字节长度。在本章的前一
部分的内容中,我们已经知道, s o c k a d d r _ i n 结构中的 I P 地址是从比特 3 2 开始的。
dom_maxrtkey成员是协议的插口地址结构的字节长度: sockaddr_in的字节长度为16。
图18-29列出了路由表初始化过程所包含的步骤。

初始化选路协议控制块的首部

分配并初始化

分配并初始化一个 结构

图18-29 初始化路由表时包含的步骤

在系统初始化时,内核的 m a i n函数将调用一次 d o m a i n i n i t函数。A D D D O M A I N宏用于

Linux公社 www.linuxidc.com
bbs.theithome.com
468 计计TCP/IP详解 卷 2:实现

创建一个 d o m a i n结构的链表,并调用每个域的 d o m _ i n i t函数(如果定义了该函数 )。正如图


18-27所示,route_init是唯一的一个dom_init函数,其代码如图 18-30所示。

图18-30 rout_init 函数

在图18-32中的函数rn_init只被调用一次。
在图 1 8 - 3 1 中的函数 r t a b l e _ i n i t 也只被调用一次。它接着调用所有域的 dom_
rtattach函数,这些函数为各自所属的域初始化一张路由表。

图18-31 rtable_init 函数:调用每一个域的 dom_rtattach 函数

从图 1 8 - 2 7中可知, r n _ i n i t h e a d 是唯一的一个 d o m _ r t a t t a c h 函数,关于 r n _


inithead函数将在下一节中介绍。

18.8 初始化: rn_init和rn_inithead函数

图1 8 - 3 2中的函数 r n _ i n i t只被r o u t e _ i n i t调用一次,用于初始化 r a d i x函数使用的一


些全局变量。
1. 确定max_keylen
750-761 检 查 所 有 domain结 构 , 并 将 全 局 变 量 max_keylen设 置 为 最 大 的
d o m _ m a x r t k e y值。在图 1 8 - 2 7中最大值是 3 2 (对应于 A F _ I S O),但是,在一个常用的不含
OSI和XNS协议的系统中, max_key为16,即sockaddr_in结构的大小。
2. 分配并初始化rn_zeros、rn_ones和maskedKey
762-769 先分配了一个大小为 m a x _ k e y l e n的三倍的缓存,并在全局变量 r n _ z e r o s中储
存该缓存的指针。 R _ M a l l o c是一个调用内核的 m a l l o c函数的宏,它指定了 M _ R T A B L E和
M _ D O N T W A I T的类型。我们还会遇到 B c m p、B c o p y、B z e r o和F r e e这些宏,它们对参数进
行适当分类,并调用名称相似的内核函数。
该缓存被分解成三个部分,每一部分被初始化成如图 18-33所示。
r n _ z e r o s是一个全0比特的数组,r n _ o n e s是一个全1比特的数组,m a s k e d K e y数组用
于存放被掩码过的查找键的临时副本。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 469

图18-32 rn_init 函数

个字节 个字节 个字节

图18-33 rn_zeros 、rn_ones 和maskedKey 数组

3. 初始化掩码树
770-772 调用 r n _ i n i t h e a d,初始化地址掩码路由树的首部;并使图 1 8 - 8中全局变量
mask_rnhead指向该radix_node_head结构。
从图1 8 - 2 7可知,对于所有需要路由表的协议, rn _ i n i t h e a d也是它们的 d o m _ a t t a c h
函 数。图 1 8 - 3 4 给 出 的 不 是 该 函 数 的 源 代 码 , 而 是 该 函 数为 I n t e r n e t 协议 创建的
radix_node_head结构。
这三个r a d i x _ n o d e结构组成了一棵树:中间的那个结构是树的顶点 (由r n h _ t r e e t o p
指向它 ),第一个结构是树的最左边的叶子,最后一个结构是树的最右边的叶子。这三个结点
的父指针(rn_p)都指向中间的那个结点。
r n h _ n o d e s[ 1 ] . r n _ b的值3 2是待测试的比特位置。它来自于 I n t e r n e t的d o m a i n结构中的
d o m _ r t o f f s e t成员(图1 8 - 2 7 )。它的字节偏移量及字节掩码被预先计算出来,这样就不需要
在处理过程中完成移位和掩码。其中,字节偏移量从插口地址结构起始处开始计算,它被存
放在 r a d i x _ n o d e结构的 r n _ o f f 成员中 ( 在这个例子中它的值为 4 ) ;字节掩码存放在
r n _ b m a s k成员中 (在这个例子中为 0 x 8 0 )。无论何时往树中添加 r a d i x _ n o d e结构,都要计

Linux公社 www.linuxidc.com
bbs.theithome.com
470 计计TCP/IP详解 卷 2:实现

算这些值,以便于在转发过程中加快比较的速度。其他的例子有:在图 1 8 - 4中检测比特 3 3的
两个结点的偏移量和字节掩码分别为 4和0x40;检测比特63的两个结点的偏移量和字节掩码分
别为7和0x01。
两个叶子中的rn_b成员的值- 33是由-1减去该叶子的索引而得到的。

最左边的叶子

内部结点
也是树的顶点

最右边的叶子

图18-34 rn_inithead 为Internet协议创建的 radix_node_head 结构

最左边结点的键是全 0(rn_zeros),最右边结点的键是全 1(rn_ones)。


这三个结点都设置了 R N F _ R O O T标志(我们省略了前缀 R N F _)。这说明它们都是构成树的
原始结点之一。它们也是唯一具有该标志的结点。
有一个细节我们没有提到,就是网络文件系统 ( N F S )也使用路由表函数。对于本
地主机的每一个装配点 (mount point)都分配一个 r a d i x _ n o d e _ h e a d结构,并且具
有一个指向这些结构的指针数组 (利用协议族检索 ),与r t _ t a b l e s数组相似。每次
输出装配点时,针对该装配点,把能装配该文件系统的主机的协议地址添加到适当
的树中去。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 471
18.9 重复键和掩码列表

在介绍查找路由表项的源代码之前,必须先理解 r a d i x _ n o d e结构中的两个字段:一个
是 r n _ d u p e d k e y ,它构成了附加的含重复键的 r a d i x _ n o d e 结构链表;另一个是
rn_mklist,它是含网络掩码的 radix_mask结构链表的开始。
先看一下图18-4中树的最左边标有“end”和“default”的两个框。这些就是重复键。最左
边设有RNF_ROOT标志的结点(在图18-34中的rnh_nodes[0])有一个为全0比特的键,但是它和
默认路由的键相同。如果创建一个 255.255.255.255的路由表项(但该地址是受限的广播地址,不
会在路由树中出现 ),则我们会在树的最右端结点 (该结点有一个值为全 1比特的键 )遇到同样的
问题。总的来说,如果每次都有不同的掩码,那么 Net/3中的radix结点函数就允许重复任何键。
图1 8 - 3 5给出了两个具有全 0比特重复键的结点。在这个图中,我们去掉了 r n _ f l a g s中
的RNF_前缀,并且省略了非空父指针、左指针和右指针,因为它们与要讨论的内容无关。

路由树的首部:图
18-4顶部比特32对应
的结点

比特33结点
的左指针

图18-35 值为全0的键的重复结点

Linux公社 www.linuxidc.com
bbs.theithome.com
472 计计TCP/IP详解 卷 2:实现

图中最上面的结点即为路由树的顶点—图1 8 - 4中顶部比特 3 2对应的结点。接下来的两


个结点是叶子 (它们的r n _ b为负值),其中第一个叶子的 r n _ d u p e d k e y成员指向第二个结点。
第一个叶子是图 18-34中的r n h _ n o d e s[0]结构,该结构是树的左边标有“ end”的结点—它
设有RNF_ROOT标志。它的键被 rn_inithead设为rn_zeros。
第二个叶子是默认路由的表项。它的 r n _ k e y指向值为 0 . 0 . 0 . 0的s o c k a d d r _ i n结构,并
具有一个全 0的掩码。由于掩码表中相同的掩码是共享的,因此,该叶子的 r n _ m a s k也指向
rn_zeros。
通常,键是不共享的,更不会与掩码共享。由于两个标有 “e n d”的结点的
r n _ k e y指针(具有R N F _ R O O T标志)是由r n _ i n i t h e a d(图1 8 - 3 4 )创建的,因此这两
个指针例外。左边标有 end的结点的键指向 rn_zeros,右边标有 “end”的结点的键
指向rn_ones。
最后一个是 r a d i x _ m a s k结构,树的顶结点和默认路由对应的叶子都指向这个结构。这
个列表是树的顶结点的掩码列表,在查找网络掩码时,回溯算法将使用它。 r a d i x _ m a s k结
构列表和内部结点一起确定了运用于从该结点开始的子树的掩码。在重复键的例子中,掩码
列表和叶子出现在一起,跟着的这个例子也是这样的。
现在我们给出一个特意添加到选路树中的重复键和所得到的掩码列表。在图 1 8 - 4中有一
个主机路由 1 2 7 . 0 . 0 . 1和一个网络路由 1 2 7 . 0 . 0 . 0。图中采用了 A类网络路由的默认掩码,即
0xff000000。如果我们把跟在 A类网络号之后的 24 bit分解成一个 16 bit子网号和一个8 bit主
机号,就可以为子网 127.0.0添加一个掩码为 0xffffff00的路由:
bsdi $ route add 127.0.0.0 -netmask 0xffffff00 140 252 13 33

虽然在这种情况下使用网络 1 2 7没什么实际意义,但是我们感兴趣的是所得到的路由表结
构。虽然重复键在 I n t e r n e t协议中并不常见 (除了前面例子中的默认路由之外 ),但是仍需要利
用重复键来为所有网络的 0号子网提供路由。
在网络号 1 2 7的这三个路由表项中存在一个隐含
的优先规则。如果查找键是 1 2 7 . 0 . 0 . 1,则它和这三个
路由表项都匹配,但是只选择主机路由,因为它是最
匹配的:其掩码 (0 x f f f f f f f f)含有最多的 1。如果
查找键是 1 2 7 . 0 . 0 . 2,它与两个网络路由匹配,但是掩
码 为 0 x f f f f f f 0 0 的子 网 0 的路 由比 掩码 为
0 x f f 0 0 0 0的路由更匹配。如果查找键为 1 2 7 . 0 . 2 . 3,
那么只与掩码为 0xff000000的路由表项匹配。
图1 8 - 3 6给出了添加路由之后得到的树结构,从
图18-4中对应比特33的内部结点处开始。由于这个重 图18-36 反映重复键 127.0.0.0的路由树
复键有两个叶子,我们用两个框来表示键值为 127.0.0.0的路由表项。
图18-37给出了所得到的 radix_nod和radix_mask结构。
首先看一下每一个 r a d i x _ n o d e的r a d i x _ m a s k结构的链表。最上端结点 (比特6 3 )的掩
码列表由 0 x f f f f f f 0 0及其后的 0 x f f 0 0 0 0 0 0组成。在列表中首先遇到的是更匹配的掩码,
这样它就能够更早地被测试到。第二个 r a d i x _ n o d e(r n _ b值为- 5 7的那个 )的掩码列表与第
一个相同。但是第三个 radix_node的掩码列表仅由值为 0xff000000的掩码构成。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 473

比特63对应的结点

图18-37 网络127.0.0.0的重复键的路由表结构举例

应注意的是,具有相同值的掩码之间可以共享,但是具有相同值的键之间不能共享。这
是因为掩码被保存在它们自己的路由树中,可以显式地被共享,而且值相同的掩码经常出现
(例如,每个 C类网络路由都有相同的掩码 0xffffff00),但是值相同的键却不常见。

18.10 rn_match函数

现在我们介绍 r n _ m a t c h函数,在 I n t e r n e t协议中,它被称为 r n h _ m a t c h a d d r函数。在

Linux公社 www.linuxidc.com
bbs.theithome.com
474 计计TCP/IP详解 卷 2:实现

后面学习中,我们可以看到它将被 r t a l l o c l函数调用 (而r t a l l o c 1函数将被 r t a l l o c函数


调用)。具体算法如下:
1) 从树的顶端开始搜索,直到到达与查找键的比特相应的叶子。检测该叶子,看能否得
到一个精确的匹配 (图18-38)。
2) 检测该叶结点,看是否能得到匹配的网络地址。
3) 回溯(图18-43)。
图18-38给出了rn_match的第一部分。

图18-38 rn_match 函数:沿着树向下搜索,查找严格匹配的主机地址

135-145 第一个参数 v_arg是一个插口地址结构的指针,第二个参数 head是该协议的指向


r a d i x _ n o d e _ h e a d结构的指针。所有协议都可调用这个函数 (图1 8 - 1 7 ),但调用时使用不同
的head参数。
在变量声明中,off是树的顶结点的 rn_off成员(对Internet地址,其值为4,见图18-34),

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 475
vlen是查找键插口地址结构中的长度字段 (对Internet地址,其值为16)。
1. 沿着树向下搜索到相应的叶子
146-155 这个循环从树的顶结点开始,然后沿树的左右分支搜索,直到遇到一个叶子为止
(r n _ b小于0)。每次测试相应比特时,都利用了事先计算好的 r n _ b m a s k中的字节掩码和事先
计算好的 rn_off中的偏移量。对于 Internet地址而言, rn_off为4、5、6或7。
2. 检测是否精确匹配
156-164 当遇到叶子时,首先检测能否精确匹配。比较插口地址结构中从协议族的 rn_off值
开始的所有字节。图 18-39给出了Internet插口地址结构的比较情况。

族 端口 IP地址 (全0)

这12个字节要进行比较

图18-39 比较sockaddr_in 结构时的各种变量

如果发现匹配不成功,就立刻跳到 on1。
通常, s o c k a d d r _ i n 的最后 8 个字节为 0 ,但是地址解析协议代理 (proxy
A R P ) ( 2 1 . 1 2节)会设置其中的一个为非零。这就允许一个给定的 I P地址有两个路由表
项:一个对应于正常 I P地址(最后8个字节为 0 ),另一个对应于相同 I P地址的地址解析
协议代理(最后8个字节中有一个为非零 )。
图18-39中的长度字节在函数的一开始时就赋值给了 v l e n,并且我们还会看到 r t a l l o c l
将利用family成员来选择路由表进行搜索。选路函数未使用 port成员。
3. 显示地检测默认地址
165-172 图1 8 - 3 5给出了存储在键为 0的重复叶子中的默认路由。第一个重复的叶子设有
R N F _ R O O T标志。因此,如果在匹配的结点中设有 R N F _ R O O T标志,并且该叶子含有重复键,
那么就返回指针 r n _ d u p e d k e y的值(即图1 8 - 3 5中含默认路由的结点的指针 )。如果路由树中
没有默认路由,则查找过程匹配左边标有“ end”的叶子 (键为全0比特);或者如果查找时遇到
右边标有“ e n d”的叶子 (键值为全 1比特),那么返回指针 t,它指向一个设有 R N F _ R O O T标志
的结点。我们将看到 r t a l l o c l会显式地检查匹配结点是否设有这个标志,并判断匹配是否
失败。
程序执行到此时, r n _ m a t c h函数已经到达了某个叶子上,但是它并不是查找键的精确
匹配。函数的下一部分将检测该叶子是否为匹配的网络地址,如图 18-40所示。
173-174 c p指向该查找键中那个不相等的字节。 m a t c h e d _ o f f被赋值为该字节在插口地
址结构中的位置偏移量。
175-183 do w h i l e循环反复与所有重复叶子中的每一个具有网络掩码的叶子进行比较。
下面我们通过一个例子来看这段代码。假定我们要在图 1 8 - 4 所示的路由表中查找 I P地址
1 4 0 . 2 5 2 . 1 3 . 6 0。查找会在标有 1 4 0 . 2 5 2 . 1 3 . 3 2 (比特6 2和6 3都为0 )的结点处终止,该结点包含一
个网络掩码。图 18-41给出了图 18-40中的for循环开始执行时的结构。
Linux公社 www.linuxidc.com
bbs.theithome.com
476 计计TCP/IP详解 卷 2:实现

图18-40 rn_match 函数:检测是否为匹配的网络地址

图18-41 比较网络掩码的例子

虽然查找键和路由表键都是 s o c k a d d r _ i n结构,但是掩码的长度并不相同。该掩码长度
是非零字节的最小数目。从该点之后直到 max_keylen之间的所有字节都为 0。
184-190 逐个字节地对查找关键字和路由表键进行异或运算,并将结果同网络掩码进行逻
辑与运算。如果所得到的字节出现非零值,就会由于不匹配而终止循环 (习题1 8 . 1 )。如果循环
正常终止,那么与网络掩码进行逻辑与运算后的查找键就和路由表项相匹配。程序将返回指
向该路由表项的指针。
查看I P地址的第四个字节,我们可以从图 1 8 - 4 2中看出本例子是如何匹配成功的,以及 I P
地址1 4 0 . 2 5 2 . 1 3 . 1 8 8是如何匹配失败的。采用这两个地址,是因为它们中的比特 5 7、6 2和6 3都
为0,查找都在图18-41给出的结点上终止。
第一个例子( 1 4 0 . 2 5 2 . 1 3 . 6 0 )匹配成功是因为逻辑与运算的结果为 0 (并且地址、键和掩码中
所有剩余的字节全都为 0)。另一个例子匹配不成功是因为逻辑与运算的结果为非零。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 477
查找键 = 140.252.13.60 查找键 = 140.252.13.188

查找键字节 ( * c p ) : 0011 1100 = 3c 1011 1100 = bc


路由表键字节 ( * c p 2 ) : 0010 0000 = 20 0010 0000 = 20
异或: 0001 1100 1001 1100
网络掩码字节 ( * c p 3 ) : 1110 0000 = e0 1110 0000 = e0
逻辑与: 0000 0000 1000 0000

图18-42 用网络掩码进行关键字匹配的例子

191 如果路由表项含有重复键,那么对每一个键都要执行一次该循环体。
r n _ m a t c h的最后一部分,如图 1 8 - 4 3所示,沿路由树向上回溯,以查找匹配的网络地址
或默认地址。

图18-43 rn_match 函数:沿树向上回溯

193-195 do while 循环沿着路由树一直向上,检测每一层的结点,直至检测到树的顶端为止。


196 指向父结点的指针的值被赋给了指针 t,即向上移动了一层。可见,在每一个结点中包
含一个父指针能够简化回溯操作。
197-210 对于回溯到的每一层,只要内部结点的掩码列表非空,就对该层进行检测。
r n _ m k l i s t是指向 r a d i x _ n o d e结构的链表的指针,链表中的每一个 r a d i x _ n o d e结构都
包含一个掩码,这些掩码将应用于从该结点开始的子树。程序中的内部 do w h i l e循环将遍
历每一个 radix_mask结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
478 计计TCP/IP详解 卷 2:实现

利用前面的例子, 1 4 0 . 2 5 2 . 1 3 . 1 8 8,图1 8 - 4 4给出了在最内层的 f o r循环开始时的各种数据


结构。这个循环对每个掩码中的字节和对应的查找键的字节进行逻辑与操作,并将结果保存
在全局变量 m a s k e d K e y 中。该掩码值为 0 x f f f f f f e 0 ,搜索会从图 1 8 - 4 中的叶结点
140.252.13.32处回溯两层,到达测试比特 62的结点。

查找键

图18-44 利用掩码过的查找键进行再次搜索的准备

f o r循环完成后,掩码过程也就完成了,再调用 r n _ s e a r c h(如图1 8 - 4 8所示),其调用参


数以 m a s k e d K e y为查找键,以指针 t为查找子树的顶点。图 1 8 - 4 5给出了我们所举例子中的
maskedKey的值。

图18-45 调用rn_search 时的maskedKey

字节0xa0是0xbc(188,查找键)和0xe0(掩码)逻辑与运算的结果。
211 r n _ s e a r c h从起点开始沿着树往下搜索,根据查找键来确定沿向左或向右的分支进行
搜索,直到到达某个叶子。在这个例子中,查找键有 9个字节,如图 18-45所示,所到达的是图
1 8 - 4中标有1 4 0 . 2 5 2 . 1 3 . 3 2的那个叶子,这是因为在字节 0 x a 0中比特6 2和6 3都为0。图1 8 - 4 6给
出了调用 Bcmp检验是否匹配时的数据结构。
由于这两个 9字节的字符串不相同,所以这次比较失败。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计479

图18-46 maskedKey 和新叶结点之间的比较

查找键:

图18-47 回溯到路由树的顶端和查找默认叶子的 rn_search

Linux公社 www.linuxidc.com
bbs.theithome.com
480 计计TCP/IP详解 卷 2:实现

212-221 该w h i l e循环处理各重复键,且处理每个重复键时的掩码不同。唯一被比较的重
复键是那个 rn_mask指针与m->rm_mask相等的键。下面以图 18-36和图18-37为例进行说明。
如果查找从比特 6 3的结点处开始,第一次内部 do w h i l e循环中, m指向r a d i x _ m a s k结构
0 x f f f f f f 0 0 。当 r n _ s e a r c h 返回指向第一个重复叶子 1 2 7 . 0 . 0 . 0的指针时,该叶子的
r m _ m a s k等于m - > r m _ m a s k,因此,就调用 B c m p。如果比较失败, m的值就被设置成指向列
表中的下一个 r a d i x _ m a s k结构(具有掩码 0 x f f 0 0 0 0 0 0)的指针,并且对新掩码再次执行 d o
w h i l e循环体。 r n _ s e a r c h再一次返回指向第一个重复叶子 1 2 7 . 0 . 0 . 0的指针,但是它的
r n _ m a s k并不等于 m - > r m _ m a s k。W h i l e继续进行到下一个重复叶子,它的 r n _ m a s k与m -
>rm_mask恰好相等。
现在回到查找键为 140.252.13.188的例子中,由于从检测比特 62的结点处开始的搜索失败,
因此,沿着树向上继续回溯,直到到达树的顶点,该顶点就是沿树向上的下一个 r n _ m k l i s t
为非空的结点。
图1 8 - 4 7给出了到达树的顶结点时的数据结构。此时,计算 m a s k e d K e y(为全 0 ),并且
r n _ s e a r c h从这个结点 (树的顶结点 )处开始,继续沿着树的左分支向下两层到达图 1 8 - 4中标
有“default”的叶子。
当r n _ s e a r c h返回时, x指向r n _ b值为-3 3的r a d i x _ n o d e,这是从树的顶端开始沿两
个左分支向下之后遇到的第一个叶子。但是 x - > r n _ m a s k(为空)与m - > r m _ m a s k不等,因此,
将x - > r n _ d u p e d k e y赋给x。用于测试的 w h i l e循环再次执行,但是,此时 x - r n _ m a s k等
于m - > r m _ m a s k,因此该 w h i l e循环终止。 B c m p对从m s t a r t开始的 1 2个值为 0的字节和从
x - > r n _ k e y加4开始的1 2个值为0的字节进行比较,结果相等,函数返回指针 x,该指针指向
默认路由的路由项。

18.11 rn_search函数

在前面一节中,我们已经知道 r n _ m a t c h调用了 r n _ s e a r c h来搜索路由表的子树。如图


18-48所示。

图18-48 rn_search 函数

这个循环和图 1 8 - 3 8中的相似。它在每一个结点上比较查找键中的一位比特,如果该比特

Linux公社 www.linuxidc.com
bbs.theithome.com
第 18章 Radix树路由表计计 481
为0,就通向左边的分支,如果该比特为 1,就通向右边的分支。在遇到一个叶子时终止搜索,
并返回指向该叶子的指针。

18.12 小结

每一个路由表项都由一个键来标识:在 IP协议中就是目的 IP地址,该IP地址可以是一个主


机地址或者是一个具有相应网络掩码的网络地址。一旦键的搜索确定了路由表项,在该表项
中的其他信息就会指定一个路由器的 I P地址,到目的地址的数据报就会发往该指定地址,还
会指明要用到的接口的指针、度量等等。
由I n t e r n e t协议维护的信息是 r o u t e结构,该 r o u t e结构只有两个成员构成:指向路由表
项的指针和目的地址。在 U D P、T C P和原始 I P使用的每个 I n t e r n e t协议控制块中,我们都会遇
到由Internet协议维护的 route结构。
P a t r i c i a树数据结构非常适合于路由表。由于路由表的查找要比添加或者删除路由频繁得
多,因此从性能的角度来看,在路由表中使用 P a t r i c i a树就更加有意义。 P a t r i c i a树虽然不利于
添加和删除这些附加工作,但是加快了查找的速度。 [Sklower 1991] 给出的 r a d i x树方法和
Net/1散列表的比较结果表明,用 radix树方法构造测试树用比 Net/1散列表法快一倍,搜索速度
快三倍。

习题

18.1 我们说过,在图 1 8 - 3中,查找键与路由表项匹配的一般条件是,它和路由表掩码的


逻辑与运算的结果等于路由表键。但是在图 1 8 - 4 0中采用了不同的测试方法。请建
立一个逻辑真值表以证明这两种方法等价。
18.2 假设某个 Net/3系统中的路由表需要 20 000个表项(IP地址)。在不考虑掩码的情况下,
请估算大约需要多大的存储器?
18.3 radix_node结构对路由表键的长度限制是多少?

Linux公社 www.linuxidc.com
bbs.theithome.com
第19章 选路请求和选路消息
19.1 引言

内核的各种协议并不直接使用前一章提供的函数来访问选路树,而是调用本章提供的几
个函数:rtalloc和rtalloc1是完成路由表查询的两个函数; rtrequest函数用于添加和
删除路由表项;另外大多数接口在接口连接或断开时都会调用函数 rtinit。
选路消息在两个方向上传递信息。进程(如route命令)或守护进程(routed或gated)把选路
消息写入选路插口,以使内核添加路由、删除路由或修改现有的路由。当有事件发生时,如接
口断开、收到重定向等,内核也会发送选路消息。进程通过选路插口来读取它们感兴趣的内容。
在本章中,我们将讨论这些选路消息的格式及其含义,关于选路插口的讨论将在下一章进行。
内核还提供了另一种访问路由表的接口,即系统的 s y s c t l调用,我们将在本章的结尾部
分阐述。该系统调用允许进程读取整个路由表或所有已配置的接口及接口地址。

19.2 rtalloc和rtalloc1函数

通常,路由表的查找是通过调用 r t a l l o c和r t a l l o c 1函数来实现的。图 1 9 - 1给出了


rtalloc。

图19-1 rtalloc 函数

58-65 参数r o是一个指针,它指向 TCP或UDP所使用的 Internet PCB(第22章)中的r o u t e结


构。如果 r o已经指向了某个 r t e n t r y结构(即r o _ r t非空),而该结构指向一个接口结构且路
由有效,则函数立即返回。否则, r t a l l o c1将被调用,调用的第二个参数为 1。我们很快会
看到该参数的用途。
如图1 9 - 2所示,r t a l l o c 1调用了r n h _ m a t c h a d d r函数,对于 I n t e r n e t地址来说,该函
数就是rn_match数(图18-17)。
66-76 第一个参数是一个指针,它指向一个含有待查找地址的插口地址结构。 sa_family
成员用于选择所查找的路由表。
1. 调用rn_match
77-78 如果符合下列三个条件,则查找成功。
1) 存在该协议族的路由表;
2) rn_match返回一个非空指针;并且

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 483
3) 匹配的radix_node结构没有设置RNF_ROOT标志。
注意,树中标有 end的两个叶子都设有 RNF_ROOT标志。

图19-2 rtalloc1 函数

2. 查找失败
94-101 在这三个条件中只要有一个条件没有得到满足,查找就会失败,并且统计值
r t s _ u n r e a c h也要递增。这时,如果调用 r t a l l c o1的第二个参数 (r e p o r t)为1,就会产生
一个选路消息。任何感兴趣的进程都可以通过选路插口读取该消息。选路消息的类型为
RTM_MISS,并且函数返回一个空指针。
79 如果三个条件都满足,则查找成功。指向匹配的 r a d i x _ n o d e结构的指针保存在 r t和
n e w r t中。注意,在r t e n t r y结构的定义中(图18-24),两个r a d i x _ n o d e结构在开头的位置
处,如图 1 8 - 8所示,其中第一个代表一个叶结点。因此, r n _ m a t c h返回的 r a d i x _ n o d e结
构的指针事实上是一个指向 rtentry结构的指针,该 rtentry结构是一个匹配的叶结点。

Linux公社 www.linuxidc.com
bbs.theithome.com
484 计计TCP/IP详解 卷 2:实现

3. 创建克隆表项
80-82 如果调用的第二个参数非零,而且匹配的路由表项设有 RTF_CLONING标志,则调用
r t r e q u e s t函数发送 R T M _ R E S O L V E命令来创建一个新的 r t e n t r y结构,该结构是查询结果
的克隆。 ARP将针对多播地址利用这一机制。
4. 克隆失败
83-87 如果r t r e q u e s t返回一个差错, n e w r t就被重新设置成 r n _ m a t c h所返回的表项,
并增加它的引用计数。然后程序跳转到 miss处,产生一条RTM_MISS消息。
5. 检查是否需要外部转换
88-91 如果r t r e q u e s t成功,并且新克隆的表项设有 R T F _ X R E S O L V E标志,则程序跳至
m i s s处,但这次产生的是 R T M _ R E S O L V E消息。该消息的目的是为了把路由创建的时间通知
给用户进程,在 IP地址到X.121地址的转换过程中会用到它。
6. 为正常的成功查找递增引用计数
92-93 当查找成功但没有设置 R T F _ C L O N I N G标志时,该语句将递增路由表项的引用计数。
这是本函数正常情况下的处理流程,之后程序返回一个非空的指针。
虽然是这样小的一段程序,但是在 r t a l l o c 1的处理过程中有很多选择。该函数有 7个不
同的流程,如图 19-3所示。

产生的
返回值
路由消息
参数 标志 返回 标志

查找失败

ptr
ptr
查找成功 OK ptr
OK ptr
差错 ptr

图19-3 rtalloc1 处理过程小结

需要解释的是,如果存在默认路由,前两行 (找不到路由表项的流程 )是不可能出现的。还


有,在第 5、第6两行中的 r t _ r e f c n t也做了递增,因为这两行在调用 r t r e q u e s t时使用了
RTM_RESOLVE参数,递增在rtrequest中完成。

19.3 宏RTFREE和rtfree函数

宏R T F R E E,如图19-4所示,仅在引用计数小于等于 1时才调用 r t f r e e函数;否则,它仅


完成引用计数的递减。
209-213 r t f r e e函数如图 1 9 - 5所示。当不存在对 r t e n t r y结构的引用时,函数就释放该
结构。例如,在图 2 2 - 7中,当释放一个协议控制块时,如果它指向一个路由表项,则需要调
用rtfree。
105-115 首先递减路由表项的引用计数,如果它小于等于 0并且该路由不可用,则该表项可
以被释放。如果该表项设有 R N F _ A C T I V E或R N F _ R O O T标志,那么这是一个内部差错。因为,
如果设有 R N F _ A C T I V E,那么该结构仍是路由表的一部分;如果设有 R N F _ R O O T,那么它是
一个由rn_inithead创建的标有 end的结构。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计485

图19-4 宏RTFREE

图19-5 rtfree 函数:释放一个 rtentry 结构

116 r t t r a s h是一个用于调试的计数器,记录那些不在选路树中但仍未释放的路由表项的
数目。当 r t r e q u e s t开始删除路由时,它被递增,然后在这儿递减。正常情况下,它的值应
该是0。
1. 释放接口引用
117-122 先查看引用计数。确认引用计数非负后, I F A F R E E将递减i f a d d r结构的引用计
数。当计数值递减为零时,调用 ifafree释放它。
2. 释放选路存储器
123-124 释放由路由表项关键字及其网关所占的存储器。我们会看到 r t _ s e t g a t e把它们
分配在存储器的同一个连着的块中。因此,只调用一个 F r e e就可以同时把它们释放。最后还
要释放rtentry结构。

路由表引用计数

路由表引用计数 (r t _ r e f c n t)的处理与其他许多引用计数的处理不同。我们看到,在图
1 8 - 2中,大多数路由的引用计数为 0,而这些没有引用的路由表项并没有被删除。原因就在
r t f r e e中:只有当 R T F _ U P标志被删除时,引用计数为 0的表项才会被删除。而仅当从选路
树中删除路由时,该标志才会被 rtrequest删除。

Linux公社 www.linuxidc.com
bbs.theithome.com
486 计计TCP/IP详解 卷 2:实现

大多数路由是按如下方式使用的。
•如果到某接口的路由是在配置该接口时自动创建的 (典型的,例如以太网接口的配置 ),
则r t i n i t用命令参数R T M _ A D D来调用r t q u e s t,以创建新的路由表项,并设置它的引
用计数为1。然后, rtinit在退出前把该引用计数递减成 0。
对于点到点接口的处理过程也是类似的,所以路由的引用计数也是从 0开始。
如果路由是由 r o u t e命令手工创建的,或者是由选路守护进程创建的,处理过程同样
是类似的。 r o u t e _ o u t p u t用命令参数 R T M _ A D D来调用 r t r e q u e s t,并设置新路由
的引用计数为1。在退出前,route_output把该引用计数递减到 0。
因此,所有新创建的路由都是从引用计数 0开始的。
• 当TCP或UDP在插口上发送 I P数据包时,i p _ o u t p u t调用r t a l l o c,r t a l l o c再调用
rtalloc1。如图19-3所示,如果找到了路由, rtalloc1就会递增其引用计数。
所查找到的路由称为被持路由 (held route),因为协议持有指向路由表项的指针,该指针
通常被包含在协议控制块中的 r o u t e结构里。一个被其他协议持有的 r t e n t r y结构是
不能被删除的。所以,在 rtfree中,当引用计数为 0时,rtentry结构才能被删除。
• 协议通过调用 R T F R E E或r t f r e e来释放被持路由。在图 8 - 2 4中,当i p _ o u t p u t检测到
目的地址改变时,我们已经使用了这种处理。在第 2 2章中,释放持有路由的协议控制块
时,我们还会遇到这种处理。
在后面的代码中可能会引起混淆的是, r t a l l o c1经常被调用以判断路由是否存在,而调
用者并非试图持有该路由。因为 r t a l l o c1递增了该路由的引用计数器,所以调用者就立即
递减该计数器。
考虑一个被 r t r e q u e s t删除的路由。它的 R T F _ U P标志被清除,并且,如果没有被持有
( 它的引用计数为 0 ) ,就要调用 r t f r e e 。但 r t f r e e 认为引用计数小于 0是错的,所以
r t r e q u e s t查看它的引用计数,如果小于等于 0,就递增该计数值并调用 r t f r e e。通常,这
将使引用计数变成 1,之后,rtfree把引用计数递减到 0,并删除该路由。

19.4 rtrequest函数

r t r e q u e s t函数是添加和删除路由表项的关键点。图 1 9 - 6给出了调用它的一些其他函
数。

图19-6 调用rtrequest 的函数

r t r e q u e s t是一个s w i t c h语句,每个 c a s e对应一个命令: R T M _ A D D、R T M _ D E L E T E


和RTM_RESOLVE。图19-7给出了该函数的开头一段以及 RTM_DELETE命令的处理。
290-307 第二个参数,dst,是一个插口地址结构,它指定在路由表中添加或删除的表项。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 487
表项中的 s a _ f a m i l y用于选择路由表。如果 f l a g s参数指出该路由是主机路由 (而不是到某
个网络的路由),则设置netmask指针为空,忽略调用者设置的任何值。

图19-7 rtrequest 函数:RTM_DELETE 命令

1. 从选路树中删除路由
309-315 r n h _ d e l a d d r函数(图1 8 - 1 7中的r n _ d e l e t e)从选路树中删除表项,返回相应
rtentry结构的指针,并清除 RTF_UP标志。
2. 删除对网关路由表项的引用
316-320 如果该表项是一个经过某网关的非直接路由,则 R T F R E E递减该网关路由表项的
引用计数。如它的引用计数被减为 0,则删除它。设置 r t _ g w r o u t e指针为空,并将 r t设置
成原来要删除的表项。

Linux公社 www.linuxidc.com
bbs.theithome.com
488 计计TCP/IP详解 卷 2:实现

3. 调用接口请求函数
321-322 如果该表项定义了 i f a _ r t r e q u e s t函数,就调用该函数。 A R P会使用该函数,
例如,在第 21章中用它来删除对应的 ARP表项。
4. 返回指针或删除引用
323-330 因为该表项在接着的代码里不一定被删除,所以递增全局变量 rttrash。如果调
用者需要选路树中被删除的 r t e n t r y结构的指针 (即如果r e t _ n r t非空),则返回该指针,但
此时不能释放该表项:调用者必须在使用完该表项后调用 r t f r e e来删除它。如果 r e t _ n r t
为空,则该表项被释放:如果它的引用计数小于等于 0,则递增该计数值,并调用 r t f r e e。
break语句将使函数退出。
图1 9 - 8给出了函数的下一部分,用于处理 R T M _ R E S O L V E命令。只有r t a l l o c1能够携带
此命令参数调用本函数。也只有在从一个设有 R T F _ C L O N I N G标志的表项中克隆一个新的表
项时,rtalloc1才这样用。

图19-8 rtrequest 函数:RTM_RESOLVE 命令

331-339 最后一个参数, r e t _ n r t ,在这个命令里的用途不同:它是一个设有


R T F _ C L O N I N G标志的路由表项的指针 (图1 9 - 2 )。新的表项具有相同的 r t _ i f a指针、相同的
rt_gateway和相同的标志(RTF_CLONING标志被清除 )。如果被克隆表项的 rt_genmask指
针为空,则新表项是一个主机路由,因此要设置它的 R T F _ H O S T标志;否则新表项为网络路
由,其网络掩码通过复制 r t _ g e n m a s k得到。在本节的结尾部分,我们给出了克隆带网络掩
码的路由的一个例子。这个 case将跳转至下个图中的 makeroute标记处继续进行。
图19-9给出了RTM_ADD命令的代码。
5. 定位相应的接口
340-342 函数i f a _ i f w i t h r o u t e为目的(d s t)查找适当的本地接口,并返回指向该接口
的ifaddr结构的指针。
6. 为路由表项分配存储器
343-348 分配了一个 r t e n t r y结构。在前一章中我们知道,该结构包含了两个选路树的
r a d i x _ n o d e结构及其他路由信息。该结构被清零,之后,其标志 r t _ f l a g s被设置成调用
本函数的 flags参数,同时再设置 RTF_UP标志。
7. 分配并复制网关地址
349-352 r t _ g a t e w a y函数(图19-11)为路由表(d s t)及其g a t e w a y分配了存储器,然后将
gateway复制到新分配的存储器中,并设置指针 rt_key、rt_gateway和rt_gwroute。
8. 复制目的地址

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 489

图19-9 rtrequest 函数:RTM_ADD 命令

353-357 把目的地址 (路由表表项 d s t)复制到r n _ k e y所指向的存储器中。如果提供了网络


掩码,则 r t _ m a s k e d c o p y对d s t和n e t m a s k进行逻辑与运算,得到新的表项。否则, d s t
就会被复制成新的表项。对 d s t和n e t m a s k进行逻辑与运算是为了确保表中的表项已经和它
的掩码进行了与运算。这样,查找表项与表中的表项进行比较时,只需要另外对查找表项和
掩码进行逻辑与运算就可以了。例如,下面的这个命令向以太网接口 l e0添加了另一个 I P地
址(一个别名),其子网为 12而不是13。
bsdi $ ifconfig le0 inet 140.252.12.63 netmask 0xffffffe0 alias

Linux公社 www.linuxidc.com
bbs.theithome.com
490 计计TCP/IP详解 卷 2:实现

该例子中存在的一个问题是,我们所指定的全 1的主机号是错误的。不过,该表项存入路
由表后,我们用 netstat验证可知该地址已经和掩码进行过逻辑与运算了。
Destination Gateway Flags Refs Use Interface
140.252.12.32 link#1 U C 0 0 le0

9. 往选路树中添加表项
358-366 r n h _ a d d a d d r函数(图1 8 - 1 7中的r n _ a d d r o u t e)向选路树中添加这个 r t e n t r y
结构,其中附带了它的目的地址和掩码。如果有差错产生,则释放该结构,并返回
EEXIST(即,该表项已经存在于路由表中了 )。
10. 保存接口指针
367-369 递增ifaddr结构的引用计数,并保存 ifaddr和ifnet结构的指针。
11. 为新克隆的路由复制度量
370-371 如果命令是 R T M _ R E S O L V E(不是R T M _ A D D),则把被克隆的表项中的整个度量结
构复制到新的表项里。如果命令是 RTM_ADD,则调用者可在函数返回后设置该度量值。
12. 调用接口请求函数
372-373 如果为该表项定义了 i f a _ r t r e q u e s t函数,则调用该函数。对于 R T M _ A D D和
RTM_RESOLVE命令,ARP都要用该函数来完成一些额外的处理。
13. 返回指针并递增引用计数
374-378 如果调用者需要该新结构的指针,则通过 ret_nrt返回该指针,并将引用计数值
从0递增到1。

例:克隆的带网络掩码的路由

仅当r t r e q u e s t的R T M _ R E S O L V E命令创建克隆路由时,才使用 r t _ g e n m a s k的值。如


果r t _ g e n m a s k指针非空,则它指向的插口地址结构就成了新创建路由的网络掩码。在我们
的路由表中,即图 1 8 - 2,克隆的路由是针对本地以太网和多播地址的。下面的例子引自
[Sklower 1991],它提供了克隆路由的不同用法。另外一个例子见习题 19.2。
考虑一个B类网络,如 128.1,它在点到点链路之外。子网掩码是 0x f f f f f f00,其中含8
比特的子网号和 8比特的主机号。我们要为所有可能的 2 5 4个子网提供路由表项,这些表项的
网关是与本机直接相连的路由器,该路由器知道如何到达与 128.1网络相连的链路。
假设该网关不是我们的默认路由器,则最简单的方法就是创建单个表项,该表项以
1 2 8.1.0.0为目的、以 0 x f f f f 0 0 0 0为掩码。可是,假设 1 2 8 . 1网络的拓扑使所有可能的 2 5 4
个子网中的每一个都有不同的运营特性: RT Ts、M T U s和时延等。那么如果每个子网都有单
独的路由表项,我们就能够看到,无论何时连接断开后, T C P都会刷新该路由表项的统计值,
如路由的 RT T、RT T变量等 (图2 7 - 3 )。尽管我们可以用 r o u t e命令手工地为 2 5 4个子网中的每
一个子网都添加路由表项,但更好的方法是采用克隆机制。
由系统管理员先创建一个以 1 2 8 . 1 . 0 . 0为目的地,以 0 x f f f f 0 0 0 0为网络掩码的表项。再
设置其 R T F _ C L O N I N G标志,并设置 g e n m a s k为0 x f f f f f f 0 0(与网络掩码不同 )。这时,如
果在路由表中查找 1 2 8 . 1 . 2 . 3 ,而路由表中没有子网 1 2 8 . 1 . 2 的表项,那么具有掩码
0 x f f f f 0 0 0 0的网络1 2 8 . 1的表项为最佳匹配。因为该表项设有 R T F _ C L O N I N G标志,所以要
创建一个新的表项,新表项以 128.1.2为目的地,以0xffffff00(genmask的值)为网络掩码。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 491
这样,下一次引用该子网内的主机时,如 128.1.2.88,最佳匹配就是新创建的表项。

19.5 rt_setgate函数

选路树中的每个叶子都有一个表项 ( r t _ k e y ,也就是在 r t e n t r y 结构开头的


r a d i x _ n o d e结构的r n _ k e y成员)和一个相关联的网关 (r t _ g a t e w a y)。在创建路由表项时,
它们都被指定为插口地址结构。 rt_setgate为这两个结构分配存储器,如图 19-10所示。

(叶子)

(结点)

(叶子)

(结点)

以太网地址

图19-10 路由表表项和相关网关示例

这个例子给出了图 18-2中的两个表项,它们的表项分别是 127.0.0.1和140.252.13.33。前一


个的网关成员指向一个 I n t e r n e t插口地址结构,后一个的网关成员指向一个含以太网地址的数
据链路插口地址。前一个是在系统初始化时,由 r o u t e系统将其添加到路由表中的;后一个
是由ARP创建的。
在图1 9 - 11中,我们有意识地把两个由 r t _ k e y指向的结构紧挨着画在一起,因为它们是
由rt_setgate一起分配的。

Linux公社 www.linuxidc.com
bbs.theithome.com
492 计计TCP/IP详解 卷 2:实现

图19-11 rt_setgate 函数

1. 依据插口地址结构设置长度
384-391 d l e n是目的插口地址结构的长度, g l e n是网关插口地址结构的长度。 R O U N D U P
宏把数值上舍入成 4的倍数个字节,但大多数插口地址结构的长度本身就是 4的倍数。
2. 分配存储器
392-401 如果还没有给该路由表表项和网关分配存储器,或 glen大于当前rt_gateway所
指向的结构的长度,则分配一片新的存储器,并使 rn_key指向新分配的存储器。
3. 使用分配给表项和网关的存储器
398-401 由于已经给表项和网关分配了一片足够大小的存储器,因此,直接将 n e w指向这
个已经存在的存储器。
4. 复制新网关
402 复制新的网关结构,并且设置 rt_gateway,使其指向插口地址结构。
5. 从原有的存储器中将表项复制到新存储器中
403-406 如果分配了新的存储器,则在网关字段被复制前,先复制路由表表项 d s t,并释
放原有的存储器片。
6. 释放网关路由指针

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 493
407-412 如果该路由表项含有非空的 r t _ g w r o u t e指针,则用 R T F R E E释放该指针所指向
的结构,并设置 rt_gwroute为空。
7. 查找并保存新的网关路由指针
413-415 如果路由表项是一个非直接路由,则 r t a l l o c1查找新网关的路由表项,并将它
保存在 r t _ g w r o u t e中。如果非直接路由指定的网关无效,则 r t _ s e t g a t e并不返回任何差
错,但rt_gwroute会是一个空指针。

19.6 rtinit函数

Internet协议添加或删除相关接口的路由时,对 rtinit的调用有四个。
• 在设置点到点接口的目的地址时, i n _ c o n t r o l调用r t i n i t两次。第一次调用指定
R T M _ D E L E T E命令,以删除所有现存的到该目的地址的路由 (图6 - 2 1 );第二次调用指定
RTM_ADD命令,以添加新路由。
• in_ifinit调用rtinit为广播网络添加一条网络路由或为点到点链路(图6-19)添加一条主
机路由。如果是给以太网接口添加的路由,则in_ifinit自动设置其RTF_CLONING标志。
• in_ifscrub调用rtinit,以删除一个接口现存的路由。
图19-12给出了rtinit函数的第一部分。 cmd参数只能是 RTM_ADD或RTM_DELETE。

图19-12 rt_init 函数:调用 rtrequest 处理命令

Linux公社 www.linuxidc.com
bbs.theithome.com
494 计计TCP/IP详解 卷 2:实现

1. 为路由获取目的地址
452 如果是一个到达某主机的路由,则目的地址是点到点链路的另一端。否则,我们处理的
就是一个网络路由,其目的地址是接口的单播地址 (经ifa_netmask掩码过的)。
2. 用网络掩码给网络地址掩码
453-459 如果要删除路由,则必须在路由表中查找该目的地址,并得到它的路由表项。如
果要删除的是一个网络路由且接口拥有相关联的网络掩码,则分配一个 m b u f ,用
r t _ m a s k e d c o p y对目的地址和调用参数中的掩码地址进行逻辑与运算,并将结果复制到
mbuf中。令dst指向mbuf中掩码过的复制值,它就是下一步要查找的目的地址。
3. 查找路由表项
460-469 r t a l l o c1在路由表中查找目的地址,如果能找到,则先递减该表项的引用计数
(因为r t a l l o c1递增了该引用计数 )。如果路由表中该接口的 i f a d d r指针不等于调用者的参
数,则返回一个差错。
4. 处理请求
470-473 r t _ r e q u e s t执行R T M _ A D D或R T M _ D E L E T E命令。当 r t _ r e q u e s t返回时,如
果之前分配了mbuf,则释放它。
图19-13给出了rtinit的后半部分。

图19-13 rtint 函数:后半部分

5. 删除成功时产生一个选路消息
474-480 如果删除了一个路由,并且 r t r e q u e s t返回0和被删除的 r t e n t r y结构的指针
(n r t中),就用 r t _ n e w a d d r m s g产生一个选路插口消息。如果引用计数小于等于 0,则递增
该引用计数,并用 rtfree释放该路由。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 495
6. 成功添加
481-482 如果添加了一个路由,并且 rtrequest返回了0和被添加的 rtentry结构的指针
(nrt),就递减其引用计数 (因为rtrequest递增了该引用计数 )。
7. 不正确的接口
483-494 如果新路由表项中接口的 i f a d d r指针不等于调用参数,则表明有差错产生。利
用r t r e q u e s t,通过调用 i f a _ i f w i t h r o u t e来测定新路由中的 i f a指针(r t r e q u e s t函数
如图 1 9 - 9所示 )。产生这个差错后,做如下步骤:向控制台输出一条出错消息;如果定义了
i f a _ r t r e q u e s t函数,就以 R T M _ D E L E T E为参数调用它;释放 i f a d d r结构;设置r t _ i f a
指针为调用者指定的值;递增接口的引用计数;如果定义了 i f a _ r t r e q u e s t函数,就以
RTM_ADD为参数调用它。
8. 产生选路消息
495 用rt_newaddrmsg为RTM_ADD命令产生一个选路插口消息。

19.7 rtredirect函数

当收到一个 ICMP重定向后, i c m p _ i n p u t调用r t r e d i r e c t及p f c t l i n p u t(图11-27)。


后一个函数又调用 u d p _ c t l i n p u t和t c p _ c t l i n p u t,这两个函数遍历所有的 UDP和TCP协
议控制块 ( P C B )。如果P C B连接到一个外部地址,而到该外部地址的方向已经被改变,并且该
PCB持有到那个外部地址的路由,则调用 r t f r e e释放该路由。下一次使用这些控制块发送到
该外部地址的 I P数据报时,就会调用 r t a l l o c,并在路由表中查找该目的地址,很可能会找
到一条新(改变过方向的)路由。
r t r e d i r e c t函数的作用是验证重定向中的信息,并立即更新路由表,产生选路插口消
息。图19-14给出了rtredirect函数的前半部分。
147-157 函数的参数包括: d s t,导致重定向的数据报的目的 I P地址(图8-1 8中的H D );
g a t e w a y,路由器的 I P地址,用作该目的的新网关字段 (图8-1 8中的R 2 );n e t m a s k,空指
针;f l a g s,设置了R T F _ G A T E W A Y标志和R T F _ H O S T标志;s r c,发送重定向的路由器的 IP
地址 (图8-1 8中的 R 1 );r t p,空指针。需要指出的是, i c m p _ i n p u t调用本函数时,参数
netmask和rtp是空指针,但是其他协议调用本函数时,这两个参数未必为空指针。
1. 新路由必须直接相连
158-162 新的网关必须是直接相连的,否则该重定向无效。
2. 查找目的地址的路由表项并验证重定向
163-177 调用r t a l l o c 1在路由表中查找到目的地址的路由。验证重定向时,下列条件必
须为真,否则该重定向无效,并且函数返回一个差错。要注意的一点是, i c m p _ i n p u t会忽
略从r t r e d i r e c t返回的任何差错。 I C M P也会忽略它,即不会为一个无效的重定向而产生一
个差错信息。
• 必须未设置 RTF_DONE标志;
• rtalloc必须已找到一个到 dst的路由表项;
• 发送重定向的路由器的地址 (src)必须等于当前为目的地址设置的 rt_gateway;
• 新网关的接口 (由i f a _ i f w i t h n e t返回的 i f a结构)必须等于当前为目的地址设置的接
口(rt_ifa),也就是说,新网关必须和当前网关在同一个网络上;并且

Linux公社 www.linuxidc.com
bbs.theithome.com
496 计计TCP/IP详解 卷 2:实现

• 新网关不能把到这个主机的路由改变为到它自己,也就是说,不能存在与 g a t e w a y相
等的带有单播地址或广播地址的连接着的接口。

图19-14 rtredirect 函数:验证收到的重定向

3. 必须创建一个新路由
178-185 如果到达目的地址的路由没有找到,或找到的路由表项是默认路由,则为该目的
地址创建一个新的路由。如程序注释所述,对于可访问多个路由器的主机来说,当默认路由
器出错时,它可以利用这种机制来获悉正确的路由器。判断是否为默认路由的测试方法是查
看该路由表项是否具有相关的掩码以及掩码的长度字段是否小于 2,因为默认路由的掩码是
rn_zeros(图18-35)。
图19-15给出了rtredirect函数的后半部分。
4. 创建新的主机路由
186-195 如果到达目的地址的当前路由是一个网络路由,并且重定向是主机重定向而不是

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 497
网络重定向,那么就为该目的地址建立一个主机路由,而不必去管现存的网络路由。我们要
提示的是, f l a g s参数总是指明 R T F _ H O S T标志,因为 Net/3 ICMP把所有收到的重定向都看
成主机重定向。

图19-15 rtrequest 函数:后半部分

5. 创建路由
196-201 r t r e q u e s t创建一个新路由,并将标志 R T F _ G A T E W A Y和R T F _ D Y N A M I C置位。
参数n e t m a s k是一个空指针,这是因为新路由是一个主机路由,它的掩码是隐含的全 1比特。
Linux公社 www.linuxidc.com
bbs.theithome.com
498 计计TCP/IP详解 卷 2:实现

stat指向一个计数器,它在后面的程序里递增。
6. 改变现存的主机路由
202-211 当到达目的地址的当前路由已经是一个主机路由时,才执行这段代码。此时,不需
要创建新的表项,但需要修改现存的表项:设置 RTF_MODIFIED标志,并调用rt_setgate来
修改路由表项的 rt_gateway字段,使其指向新的网关地址。
7. 如果目的地址直接相连,则忽略
212-213 如果到达目的地址的当前路由是一个直接路由 (没有设置RTF_GATEWAY标志),那
么该重定向针对的是一个已直接连接的目的地址。此时,函数返回 EHOSTUNREACH。
8. 返回指针并递增统计值
214-225 如果找到了路由表项,那么该表项被返回 (如果 r t p 非空且没有出错 )或者用
rtfree释放它。相关的统计值被递增。
9. 产生选路消息
226-232 r t _ a d d r i n f o 结构清零,并由 r t _ m i s s m s g 产生一个选路插口消息。
raw_input把该消息发送到所有对重定向感兴趣的进程。

19.8 选路消息的结构

选路消息由一个定长的首部和至多 8个插口地址结构组成。该定长首部是下列三种结构中
的一个:
• rt_msghdr
• if_msghdr
• ifa_msghdr
图18-11给出了产生不同消息的函数的概观图,图 18-9给出了每种消息类型所使用的结构。
选路消息三种首部结构的前三个成员的数据类型及其含义是相同的,分别为:消息的长度、
版本和类型。这样,消息接受者就可以对消息进行解码了。而且,每种结构都各有一个成员
来编码首部之后 8个可能的插口地址结构: r t m _ a d d r s、i f m _ a d d r s和i f a m _ a d d r s成员,
它们都是一个比特掩码。
图1 9 - 1 6给出了最常用的结构,即 r t _ m s g h d r。R T M _ I F I N F O消息使用了图 1 9 - 1 7中的
if_msghdr结构。RTM_NEWADDR和RTM_DELADDR消息使用图19-18中的ifa_msghdr结构。

图19-16 rt_msghdr 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 499

图19-17 if_msghdr 结构

图19-18 ifa_msghdr 结构

注意,这三种不同结构的前三个成员具有相同的数据结构和含义。
三个变量 r t m _ a d d r s、i f m _ a d d r s和i f a m _ a d d r s都是比特掩码,它们定义了首部之
后的插口地址结构。图 19-19给出了比特掩码用到的一些常量。

比特掩码 数组索引 rtsock.c


描 述
常 量 值 常 量 值 中的名字

RTA_DST Ox01 RTAX_DST 0 dst 目的插口地址结构


RTA_GATEWAY Ox02 RTAX_GATEWAY 1 gate 网关插口地址结构
RTA_NETMASK Ox04 RTAX_NETMASK 2 netmask 网络掩码插口地址结构
RTA_GENMASK Ox08 RTAX_GENMASK 3 genmask 克隆掩码插口地址结构
RTA_IFP Ox10 RTAX_IFP 4 ifpaddr 接口名称插口地址结构
RTA_IFA Ox20 RTAX_IFA 5 ifaaddr 接口地址插口地址结构
RTA_AUTHOR Ox40 RTAX_AUTHOR 6 重定向产生者的插口地址结构
RTA_BRD Ox80 RTAX_BRD 7 brdaddr 广播或点到点的目的地址
RTAX_MAX 8 r t i - i n t o [ ]数组的元素个数

图19-19 用来引用 rti_info 数组成员的常量

比特掩码的值可以用常数 1左移数组下标指定的位数而得到。例如, 0 x 2 0(R T A _ I F A)是1


左移五位(RTAX_IFA)。我们会在代码中看到这个过程。
插口地址结构总是按照数组下标递增的次序,一个接一个地出现的。例如,如果掩码是
0x8 7,则第一个插口地址结构的内容为目的地址,接着是网关地址,网络掩码,最后是广播
地址。
内核用图 1 9 - 1 9中的数组下标来引用 r t _ a d d r i n f o结构,如图1 9 - 2 0所示。该结构具有与
我们所述相同的比特掩码,以表示哪些地址存在。它的另一个成员指向那些插口地址结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
500 计计TCP/IP详解 卷 2:实现

图19-20 rt_addrinfo 结构:表示哪些地址存在的掩码和指向这些地址的指针

例如,如果 r t i _ a d d r s 成员中设置了 R T A _ G A T E W A Y 比特,则 r t i _ i n f o


成员就是含网关地址的插口地址结构的指针。对于 I n t e r n e t协议,该插口
[RTA_GATEWAY]
地址结构就是含网关的 IP地址的sockaddr_in结构。
图1 9 - 1 9中的第五栏给出了文件 r t s o c k . c中r t i _ i n f o数组成员相应的名称。它们的定
义形式如下:
#define dst info.rti_info[RTAX_DST]

在本章的很多源文件中我们都将遇到这些名称。元素 R T A X _ A U T H O R没有命名,因为进程
不会向内核传递该元素。
我们已经有两次遇到过 r t _ a d d r i n f o结构:在函数 r t a l l o c1 (图1 9 - 2 )和r t r e d i r e c t
中(图1 9 - 1 4 )。图1 9 - 2 1给出了 r t a l l o c1在路由表查找失败后调用 r t _ m i s s m s g时创建的该结
构的格式。

没有找到IP地址

图19-21 rtalloc1 传递给rt_missmsg 的rt_addrinfo 结构

所有未使用的指针都是空指针,因为该结构在使用前被设置成 0 。还要指出的是,
r t i _ a d d r s成员没有被初始化成相应的比特掩码,因为在内核使用该结构时, r t i _ i n f o数
组中的指针为空就代表不存在该插口地址结构。仅在进程和内核之间传递的消息里该掩码才
是必不可少的。
图19-22给出了rtredirect调用rt_missmsg时创建的该结构的格式。

导致路由改变的目标IP地址

待使用的新网关的IP地址

发出路由改变的路由器的IP地址

图19-22 rtredirect 传递给rt_missmsg 的rt_addrinfo 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 501
下一节中将介绍该结构是如何被放置在发送到其他进程的消息里的。
图1 9 - 2 3给出了 r o u t e _ c b结构,我们会在下一节中遇到。该结构由四个计数器组成,前
三个分别针对 IP、XNS和OSI协议,最后一个为“任意”计数器。每个计数器分别记录相应的
域中当前存在的选路插口的数目。
203-208 内核跟踪选路插口监听器的数目。这样,当不存在等待消息的进程时,内核就可
以避免创建选路消息以及调用 raw_input发送该消息。

图19-23 route_cb 结构:选路插口监听器的计数器

19.9 rt_missmsg函数

如图1 9 - 2 4所示,r t _ m i s s m s g函数使用了图 1 9 - 2 1和图1 9 - 2 2中的结构,并调用 r t _ m s g1


在m b u f链中为进程创建了相应的变长消息,之后调用 r a w _ i n p u t将该m b u f链传递给所有相
关的选路插口。

图19-24 rt_missmsg 函数

516-525 如果没有任何选路插口监听器,则函数立即退出。
1. 在mbuf链中创建消息
526-528 r t _ m s g1(19.12节)在mbuf链中创建相应的消息,并返回该链的指针。利用图 19-
2 2中的r t _ a d d r i n f o结构,图 1 9 - 2 5给出了所得到的 m b u f链的一个例子。这些信息之所以要
放在一个 m b u f链中,是因为 r a w _ i n p u t要调用 s b a p p e n d a d d r把该m b u f链添加到插口接收

Linux公社 www.linuxidc.com
bbs.theithome.com
502 计计TCP/IP详解 卷 2:实现

缓存的尾部。

链中的下一个mbuf

(后一半,8字节)

(16字节)

(76字节)

(未使用)
(84字节)

(16字节)

(前一半,8字节)

图19-25 由rt_msg1 创建的对应于图 19-22的mbuf链

2. 完成消息的创建
529-532 成员rtm_flags和rtm_errno被设置成调用者传递的值。成员 rtm_addrs的值
是由r t i _ a d d r s复制而得到的。在图 1 9 - 2 1和图1 9 - 2 2中,我们给出的 r t i _ a d d r s值为0,因
此,rt_msg1依据rti_info数组中的指针是否为空,来计算并保存相应的比特掩码。
3. 设置消息的协议,调用 raw_input
533-534 raw_input的后三个参数指定了选路消息的协议、源和目的。这三个结构被初始
化成:
struct sockaddr route_dst = { 2, PF_ROUTE, };
struct sockaddr route_src = { 2, PF_ROUTE, };
struct sockproto route_proto = { PF_ROUTE };

内核不会修改前两个结构。第三个结构 s o c k p r o t o,我们第一次遇到。图 1 9 - 2 6给出了


它的定义。

图19-26 sockproto 结构

该结构的协议族成员一直保持了它的初始值 P F _ R O U T E,但其协议成员的值在每一次调

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 503
用r a w _ i n p u t时都要进行设置。当进程调用 s o c k e t创建选路插口时,第三个参数定义了该
进程所感兴趣的协议。 r a w _ i n p u t的调用者再把 r o u t e _ p r o t o结构的 s p _ p r o t o c o l成员
设置成选路消息的协议。在 r t _ m i s s m s g这种情况下,该成员被设置成目的插口地址结构的
sa_family(如果调用者指定了该值 ),在图19-21和图19-22中,其值为 AF_INET。

19.10 rt_ifmsg函数

在图4-30中我们可以看出, i f _ u p和i f _ d o w m都调用了图19-27中的r t _ i f m s g。在接口


连接或断开时,该函数被用来产生一个选路插口消息。

图19-27 rt_ifmsg 函数

547-548 如果没有选路插口监听器,函数立即退出。
1. 在mbuf链中创建消息
549-552 rt_addrinfo结构被设置成0。rt_msg1在一个mbuf链中创建相应的消息。需要
注意的是, r t _ a d d r i n f o结构中的所有指针都为空,选路消息仅由定长的 i f _ m s g h d r结构
组成,而不含任何地址。
2. 完成消息的创建
553-557 把接口的索引、标志和 i f _ d a t a结构复制到m b u f中的报文里。把 i f m _ a d d r s比
特掩码设置成0。
3. 设置消息的协议,调用 raw_input
558-559 因为该消息可应用于所有的协议,所以其协议被设置成 0。并且该消息是关于某接
口的,而不是针对特定的目的地。 raw_input把该消息传递给相应的监听器。

Linux公社 www.linuxidc.com
bbs.theithome.com
504 计计TCP/IP详解 卷 2:实现

19.11 rt_newaddrmsg函数

从图1 9 - 1 3中可以看到,在接口上添加或从中删除一个地址时, r t i n i t要以R T M _ A D D或


R T M _ D E L E T E为参数调用r t _ n e w a d d r m s g。图19-28给出了r t _ n e w a d d r m s g函数的前半部
分。

图19-28 rt_newaddrmsg 函数的前半部分:创建 ifa_msghdr

580-581 如果没有选路插口监听器,函数立即退出。
1. 产生两个选路消息
582 本函数要产生两个选路报文,一个用于提供有关接口的信息,另一个提供有关地址的信
息。因此, f o r循环执行两次,每次产生一个消息。如果命令是 R T M _ A D D,则第一个消息的
类型是 R T M _ N E W A D D R,第二个消息的类型是 R T M _ A D D;如果命令是 R T M _ D E L E T E,则第一
个消息的类型是 R T M _ D E L E T E,第二个消息的类型是 R T M _ D E L A D D R 。R T M _ N E W A D D R和
R T M _ D E L A D D R消息的首部为 i f a _ m s g h d r结构,而 R T M _ A D D和R T M _ D E L E T E消息的首部为
rt_msghdr结构。
583 rt_addrinfo结构被设置为0。
2. 产生至多含四个地址的消息

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 505
588-591 这四个插口地址结构包含的是有关被添加或删除的接口地址的信息,它们的指针
存储于 r t i _ i n f o数组中。其中 i f a a d d r、i f p a d d r、n e t m a s k和b r d a d d r引用的是名为
i n f o的r t i _ i n f o数组中的成员,如图 19-19所示。r t _ m s g1在mbuf链中创建了相应的消息。
注意, s a也设置成指向 i f a _ a d d r结构的指针,我们将在函数的尾部看到选路消息的协议被
设置成该插口地址结构的族。
把 i f a _ m s g h d r 结构的其他成员设置成接口的索引、度量和标志。其比特掩码由
rt_msg1设置。
图1 9 - 2 9给出了 r t _ n e w a d d r m s g的后半部分。该部分用于创建 r t _ m s g h d r消息,该消
息中包含了有关被添加或删除的路由表项的信息。
3. 创建消息
600-609 r t _ m a s k、r t _ k e y和r t _ g a t e w a y这三个地址结构的指针存放在 r t i _ i n f o数
组中。 s a被设置成目的地址的指针,它的族将成为选路消息的协议。 r t _ m s g1在mbuf链中创
建相应的消息。
设置其余的 rt_msghdr结构成员。其中,比特掩码由 rt_msg1设置。
4. 设置消息的协议,调用 raw_input
616-619 设置消息的协议,并由 raw_input把消息发送给相应的监听器。函数在完成了两
次循环后返回。

图19-29 rt_newaddrmsg 函数的后半部分:创建 rt_msghdr 消息

19.12 rt_msg1函数

前三节的函数都调用 r t _ m s g 1函数来创建一个相应的选路消息。图 1 9 - 2 5还给出了一个


m b u f链,该链是由 r t _ m s g1按照图1 9 - 2 2中的r t _ m s g h d r和r t _ a d d r i n f o结构创建的。图
19-30给出了本函数的代码。

Linux公社 www.linuxidc.com
bbs.theithome.com
506 计计TCP/IP详解 卷 2:实现

图19-30 rt_msg1 函数:获取并初始化 mbuf

1. 得到mbuf并确定消息首部的长度
399-422 获得一个含分组首部的 mbuf,并将分组消息定长部分的长度存入 l e n中。图18-9
中各种类型的消息里,有两个使用 i f a _ m s g h d r结构,有一个使用 i f _ m s g h d r结构,其余的
九个使用rt_msghdr结构。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 507
2. 验证结构是否适合 mbuf
423-424 定长结构的大小必须完全适合分组首部 m b u f的数据部分,因为该 m b u f指针将被
m t o d转换成一个结构指针,之后通过指针来引用该结构。三个结构中最大的为 i f _ m s g h d r,
其长度为84,小于MHLEN(100)。
3. 初始化mbuf分组首部并使结构清零
425-428 初始化分组首部的两个字段,并将 mbuf中的结构设置成 0。
4. 将插口地址结构复制到 mbuf链中
429-436 调用者传递了一个 r t _ a d d r i n f o结构的指针。与 r t i _ i n f o中所有非空指针相
对应的插口地址结构都被 m _ c o p y b a c k复制到 m b u f里。将数值 1左移下标 R T X _x x x对应的位
数就可以得到相应的 R T A _ x x x 比特掩码 ( 图 1 9 - 1 9 ) 。将每个比特掩码用逻辑或添加到
r t i _ a d d r s成员中去,调用者在函数返回时可将它保存为相应的报文结构成员。 R O U N D U P
宏将每个插口地址结构的大小上舍入成下一个 4的倍数个字节。
437-440 在循环结束时,如果 mbuf分组首部的长度不等于 len,则表明函数m_copyback
没能获得所需的 mbuf。
5. 保存长度、版本和类型
441-445 把长度、版本和报文类型保存到报文结构的前三个成员中。再次说明一下,因为
所有的三种 x x x_ m s g h d r 结构都以相同的成员开始,所以尽管代码中的指针 r t m 是一个
rt_msghdr结构的指针,但它可以处理所有这三种情况。

19.13 rt_msg2函数

r t _ m s g 1在m b u f链中创建一个选路消息,调用它的三个函数接着又调用 r a w _ i n p u t,
从而把 m b u f结构附加到一个或多个插口的接收缓存中去。与 r t _ m s g 1不同, r t _ m s g 2在存
储器缓存中创建选路消息,而不是在 m b u f链中创建。并且 r t _ m s g 2有一个 w a l k a r g结构的
参数,在选路域中处理 s y s c t l系统调用时,有两个函数使用该参数调用了 r t _ m s g 2。以下
是两种调用 rt_msg2的情况:
1) route_output调用它处理 RTM_GET命令
2) sysctl_dumpentry和sysctl_iflist调用它处理 sysctl系统调用。
在给出rt_msg2的代码之前,图19-31给出了在第 2种情况下使用的 walkarg结构的定义。
我们在遇到它的各成员时再一一介绍。

图19-31 walkarg 结构:选路域内 sysctl 系统调用中使用

图19-32给出了rt_msg2函数的前半部分,它与 rt_msg1的前半部分类似。

Linux公社 www.linuxidc.com
bbs.theithome.com
508 计计TCP/IP详解 卷 2:实现

图19-32 rt_msg2 函数:复制插口地址结构

446-455 本函数将选路消息保存在一个存储器缓存中,调用者用 c p参数指定该缓存的起始


位置。调用者必须保证缓存足够长,以容纳所产生的选路消息。当 c p参数为空时, r t _ m s g2
不保存任何结果而是处理输入参数,并返回保存结果所需要的字节总数。这样可以帮助调用
者确定缓存的大小。我们可以看到 r o u t e _ o u t p u t就利用了这一机制,它调用本函数两次:
第一次确定缓存的大小;在获得了大小无误的缓存后,再次调用本函数以保存结果。
r o u t e _ o u t p u t调用本函数时,最后一个参数为空,但如果是作为 s y s c t l系统调用处理的
一部分被调用时,该参数就不是空指针了。
1. 确定结构的大小
458-470 定长消息结构的大小是根据消息类型来确定的。如果 c p指针非空,则把大小等于
定长消息结构长度的偏移量添加到 cp指针上去。
2. 复制插口地址结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 509
471-482 for循环查看rti_info数组的每个元素。遇到非空指针时,设置 rti_addrs比
特掩码中的相应比特,并将该插口地址结构复制到 cp中(如果cp指针非空),并修改长度变量。
图19-33给出了rt_msg2函数的后半部分。其代码用于处理可选参数 walkarg结构。

图19-33 rt_msg2 函数:处理可选参数 walkarg

483-484 当调用者传递了一个非空的 walkarg结构指针且函数第一次执行到这里时,该 if


语句的判断条件才为真。变量 s e c o n d _ t i m e被初始化成 0,它将在本 i f语句中被设置成 1,
然后程序往回跳转到图 1 9 - 3 2中的标号 a g a i n处。c p为空指针的测试是不必要的,因为当 w指
针非空时, cp指针一定是空,反之亦然。
3. 检查是否要保存数据
485-486 w _ n e e d e d将增大,其增量为报文长度的值。该变量的初始值为 0减去s y s c t l函
数中用户缓存的长度。例如,如果该缓存为 500比特,则w _ n e e d e d的初始值为 -5 0 0。只要该
变量为负值,则表明缓存内还有剩余空间。在调用进程中, w _ w h e r e是指向该缓存的指针。
w _ w h e r e为空表明调用进程不想要函数的处理结果,而仅想获得 s y s t c l处理结果的大小。
因此,当 w _ w h e r e为空时, r t _ m s g 2没有必要将数据复制给进程,也就是返回给调用者。同
样,r t _ m s g 2也没有必要为保存结构而申请缓存,也不需要返回到标号 a g a i n处再次执行,
因为再次执行是为了把结果填入到缓存里。本函数的处理实际上只有五种情况,如图 1 9 - 3 4所
示。
4. 第一次调用时或消息长度增加时分配缓存
487-493 w_tmemsize是w_tmem所指向的缓存的长度。它被sysctl_rtable初始化成0。

Linux公社 www.linuxidc.com
bbs.theithome.com
510 计计TCP/IP详解 卷 2:实现

因此,对于给定的 s y s c t l请求,在第一次调用 r t _ m s g 2时,必须为它分配一个缓存。同样,


当产生的结果的长度增加时,必须释放原有的缓存,并重新分配一个更大的缓存。

调 用 者 cp w w.w_where second_time 描 述

route_output 空 空 希望返回长度
非空 空 希望返回结果
空 非空 空 0 进程希望返回长度
sysctl_rtable 空 非空 非空 0 第一次执行,计算长度
非空 非空 非空 1 第二次执行,保存结果

图19-34 rt_msg2 函数的五种执行情况

5. 返回再执行一次并保存结果
494-499 如果w_tmemsize非空,则表明该缓存已经存在或刚被分配。设置 cp指向该缓存,
把s e c o n d _ t i m e设置成1,跳转至标号 a g a i n处。因为s e c o n d _ t i m e的值为1,所以第二次
执行到本图的第一个语句时, i f 语句的判断不再为真。如果 w _ t m e m 为空,则表明调用
malloc失败,因此,把进程中的缓存指针设置成空指针以阻止返回任何结果。
6. 保存长度、版本和类型
502-509 如果cp非空,则保存消息首部的前三个成员。函数返回报文的长度。

19.14 sysctl_rtable函数

本函数处理选路插口的 s y s c t l系统调用。如图 1 8 - 11所示, n e t _ s y s c t l函数调用了该


函数。
在解释其源代码之前,图 1 9 - 3 5给出了该系统调用关于路由表的一种典型的用法。这个例
子来自于 arp程序。

图 19-35
Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 511
mib数组的前三个元素引导内核调用 sysctl_rtable,以处理其余的元素。
mib[4]用于指定操作的类型,共支持 3种操作类型。
1) N E T _ R T _ D U M P:返回 m i b [3]指定的地址族所对应的路由表。如果地址族为 0,则返
回所有的路由表。
针对每一个路由表项,程序都将返回一个 R T M _ G E T选路消息。每个消息可能包含两个、
三个或四个插口地址结构。这些地址结构由指针 r t _ k e y、r t _ g a t e w a y、r t _ n e t m a s k和
rt_genmask所指向。其中最后两个指针可能为空。
2) N E T _ R T _ F L A G S:与前一个命令相同,但 m i b [5]指定了一个 R T F _xxx标志(图18-25),
程序仅返回那些设置了该标志的表项。
3) NET_RT_IFLIST:返回所有已配置接口的信息。如果 mib[5]的值不是零,则程序仅
返回if_index为相应值的接口。否则,返回所有 ifnet链表上的接口。
针对每个接口,将返回一个 R T M _ I F I N F O消息,该消息传递了有关接口本身的一些信息。
之后用一个 R T M _ N E W A D D R消息传递接口的 i f _ a d d r l i s t上的每个 i f a d d r 结构。如果
mib[3]的值为非0,则RTM_NEWADDR消息仅返回那些地址族与 mib[3]相匹配的地址。否则,
mib[3]为0,将返回所有地址的信息。
这个操作是为了替代 S I O C G I F C O N F i o c t(图4
l -26)
与该系统调用有关的一个问题是,该系统返回信息的数量是可变的,这种变化取决于路
由表表项的数目和接口的数目。因此,第一次调用 s y s c t l所指定的第三个参数通常是个空指
针,也就是说,不需要返回任何选路信息,只要把该信息所占的比特数目返回即可。从图 1 9 -
3 5中我们可以看出,进程第一次调用 s y s c t l之后调用了 m a l l o c,接着再调用 s y s c t l来获
取信息。第二次调用时,通过第四个参数, s y s c t l又返回了该比特数目 (该数目与上次相比
可能会有变化 )。通过该数目我们可以得到指针 l i m,它指向的位置位于返回的最后一个字节
之后。进程接着就遍历缓存中的每个消息,利用 rtm_msglen可找到下一个消息。
图19-36给出了不同的Net/3程序访问路由表和接口列表时指定的这六个 mib变量的值。

图19-36 调用sysctl 获取路由表和接口列表的程序举例

前三个程序从路由表中提取路由表项,后三个从接口列表中提取数据。 r o u t e程序仅支
持I n t e r n e t选路协议,所以它指定 m i b [3]的值为 A F _ I N E T,而g a t e d还支持其他协议,
所以它对应的mib[3]的值为0。
图19-37画出了三个 sysctl_xxx函数的结构,在后面的几节中会逐个予以阐述。
图19-38给出了sysctl_rtable函数。
1. 验证参数
705-719 当进程调用 sysctl来设置一个路由表中不支持的变量时,使用 new参数。因此该
参数必须是一个空指针。

Linux公社 www.linuxidc.com
bbs.theithome.com
512 计计TCP/IP详解 卷 2:实现

720-721 n a m e l e n必须是 3,因为系统调用处理到这儿时, n a m e 数组中有三个元素:


n a m e [0],地址族(进程中它被指定为 m i b [3]);n a m e [1],操作(m i b [4]);以及n a m e [2],
标志(mib[5])。

系统调用

针对每一个被
选中的叶子

在缓存中创建路由报
文并将之复制给进程

图19-37 支持针对选路插口的 sysctl 系统调用的函数

图19-38 sysctl_rtable 函数:处理 sysctl 系统调用请求

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 513

图19-38 (续)

2. 初始化walkarg结构
723-728 把w a l k a r g结构(图19-31)设置成0,并初始化下列成员:把 w _ w h e r e设置成调用
进程中为结果准备的缓存地址; w _ g i v e n是该缓存的比特数目 (当w _ w h e r e为空指针时,作
为输入参数,该成员没有实际含义,但在输出时它必须被设置成将要返回的结果的长度 );
w _ n e e d e d被设置成缓存的大小的负数; w _ o p指明操作类型 (值为N E T _ R T _x x x);w _ a r g被
设置成标志值。
3. 导出路由表
731-738 N E T _ R T _ D U M P和N E T _ R T _ F L A G S操作的处理是相同的:利用一个循环语句遍历
所有的路由表 (r t _ t a b l e数组),如果系统使用了该路由表,并且地址族调用参数为 0或地址
族调用参数与该路由表的族相匹配,则调用 r n h _ w a l k t r e e函数来处理整个路由表。图 1 8 -
1 7所给出的 r n h _ w a l k t r e e函数是通常使用的 r n _ w a l k t r e e函数。该函数的第二个参数的
值是另一个函数的地址,这个函数 (s y s c t l _ d u m p e n t r y)将被调用以处理选路树的每一个叶
子。r n _ w a l k t r e e 的第三个参数是个任意类型的指针,该指针将传递给
s y s c t l _ d u m p e n t r y 函数。在这里,它指向一个 w a l k a r g 结构,该结构包含了有关
sysctl调用的所有信息。

Linux公社 www.linuxidc.com
bbs.theithome.com
514 计计TCP/IP详解 卷 2:实现

4. 返回接口列表
739-740 NET_RT_IFLIST操作调用sysctl_iflist函数来处理所有的 ifnet结构。
5. 释放缓存
743-744 如果由rt_msg2分配的缓存里含有选路消息,则释放该缓存。
6. 更新w_needed
745 r t _ m s g 2函数把每个消息的长度都加入到 w _ n e e d e d中。而该变量被我们初始化成
w_given的负数,所以它的值可以表示成:
w_needed = 0 - w_given + totalbytes

式中, t o t a l b y t e s表示由r t _ m s g 2函数添加的所有消息的长度总和。通过往 w _ n e e d e d中


加入w_given的值,我们就能得到所有消息的字节总数:
w_needed = 0 - w_given + totalbytes + w_given
= totalbytes

因为等式中两个 w _ g i v e n的值最终相互抵消,所以当进程所指定的 w _ w h e r e是个空指针


时,就没有必要初始化 w_given的值。事实上,图 19-35中的变量needed就没有被初始化。
7. 返回报文的实际长度
746-749 如果w h e r e指针非空,则通过 g i v e n指针返回保存在缓存中的字节数。如果返回
的数值小于进程指定的缓存的大小,则返回一个差错,因为返回信息被截短了。
8. 返回报文长度的估算值
750-752 当w h e r e 指针为空时,进程只想获得要返回的字节总数。为了防止在两次
s y s c t l调用之间相应的表被增大,我们将该字节总数扩大了 1 0 %。1 0 %这个增量的确定没有
特定的理由。

19.15 sysctl_dumpentry函数

在前一节中我们阐述了被 s y s c t l _ r t a b l e调用的r n _ w a l k t r e e是如何调用本函数的。


图19-39给出了本函数的代码。
623-630 每次调用本函数时,第一个参数指向一个 r a d i x _ n o d e结构,同时它也是一个
rtentry结构的指针,第二个参数指向一个由 sysctl_rtable初始化了的 walkarg结构。
1. 检测路由表项的标志
631-632 如果进程指定了标志值(mib[5]),则忽略那些rt_flags成员中没有设置该标志的
表项。在图19-36中,我们可以看到arp程序使用这种方法来选择那些设有 RTF_LLINFO标志的
表项,因为 ARP仅对这些表项感兴趣。
2. 构造选路消息
633-638 r t i _ i n f o数组中的下列四个指针是从路由表项中复制而得的: d s t、g a t e、
n e t m a s k和g e n m a s k。前两个总是非空的,但另外两个可以是空指针。调用 r t _ m s g 2是为
了构造一个 RTM_GET消息。
3. 复制消息回传给进程
639-651 如果进程需要返回一个报文,并且 rt_msg2分配了一个缓存,则将选路报文中的
其余部分填写到 w _ t m e m所指向的缓存中去,并调用 c o p y o u t复制消息回传给进程。如果复
制成功,就增大 w_where,增加的数目等于所复制的字节的数目。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 515

图19-39 sysctl_dumpentry 函数:处理一个路由表项

19.16 sysctl_iflist函数

图1 9 - 4 0给出了本函数的代码。本函数由 s y s c t l _ r t a b l e直接调用,用来把接口列表返
回给进程。
本函数由一个 f o r循环组成,该循环从指针 i f n e t开始,针对每个接口重复执行。接着
用 w h i l e 循环处理每个接口的 i f a d d r 结构链表。函数将针对每个接口产生一个
RTM_IFINFO选路消息,并且针对每个地址产生一个 RTM_NEWADDR消息。
1. 检测接口索引
654-666 进程可以指定一个非零的标志参数 (图1 9 - 3 6 中的 m i b [ 5] )。函数用接口的
if_index值与之比较,只有匹配时,才进行处理。
2. 创建选路消息
667-670 在R T M _ I F I N F O消息中只返回了唯一的一个插口地址结构,即 i f p a d d r。该
R T M _ I F I N F O消息是由 r t _ m s g 2创建的。 i n f o结构中的 i f p a d d r指针被设置成 0,因为该
info结构还要用来产生后面的 RTM_NEWADDR消息。
3. 复制消息回传给进程
671-681 如果进程需要返回消息,则填入 i f _ m s g h d r结构的其余部分,用 c o p y o u t给进

Linux公社 www.linuxidc.com
bbs.theithome.com
516 计计TCP/IP详解 卷 2:实现

程复制该缓存,并增大 w_where。

图19-40 sysctl_iflist 函数:返回接口列表及其地址

4. 循环处理每一个地址结构,检测其地址族
682-684 处理接口的每一个 i f a d d r结构。进程也可以指定一个非零地址族 (图1 9 - 3 6中的
Linux公社 www.linuxidc.com
bbs.theithome.com
第 19 章 选路请求和选路消息计计 517
mib[3])来选择仅处理那些指定族的接口地址。
5. 创建选路消息
685-688 r t _ m s g 2创建的每个 R T M _ N E W A D D R消息中最多可以返回三个插口地址结构:
ifaddr、netmask和brdaddr。
6. 复制消息回传给进程
689-699 如果进程需要返回消息,则填入 ifa_msghdr结构的其余部分,用 copyout给进
程复制缓存,并增大 w_where。
701 因为info数组还要在下一个接口消息中使用,所以程序将其中的这三个指针设置成 0。

19.17 小结

所有选路消息的格式都是相同的 — 一个定长的结构,后面跟着若干个插口地址结构。
共有三种不同类型的消息,各自具有相应的定长结构,每种定长结构的前三个元素都分别标
识消息的长度、版本和类型。每种结构中的比特掩码指定哪些插口地址结构跟在定长结构之
后。
这些消息以两种方式在内核与进程之间传递。消息可以在任意一个方向上传递,并且都
是通过选路插口每次读或写一个消息。这就使得一个超级用户进程对内核路由表的访问具有
完全的读写能力。选路守护进程 (如routed和gated)就是这样实现其期望的选路策略的。
另外,任何一个进程都可以用 s y s c t l系统调用来读取内核路由表的内容。这种方法不需
要涉及选路插口,也不需要特别的权限。最终的结果通常包含许多选路消息,该结果作为系
统调用的一部分被返回。由于进程不知道结果的大小,因此,为系统调用提供了一种方法来
返回结果的大小而不返回结果本身。

习题

19.1 R T F _ D Y N A M I C和R T F _ M O D I F I E D标志之间有什么区别?对于一个给定的路由表


项,它们可以同时设置吗?
19.2 当用下列命令添加默认路由时会有什么情况发生?
bsdi $ route add default -cloning -genmask 255.255.255.255 sun

19.3 某路由表包含了 1 5个A R P表项和2 0个路由,试估算用 s y s c t l导出该路由表时需要


多少空间。

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口
20.1 引言

一个进程使用选路域 (routing domain)中的一个插口来发送和接收前一章所描述的选路报


文。socket系统调用需要指定一个 PF_ROUTE的族类型和一个 SOCK_RAW的插口类型。
接着,该进程可以向内核发送以下五种选路报文:
1) RTM_ADD:增加一条新路由。
2) RTM_DELETE:删除一条已经存在的路由。
3) RTM_GET:取得有关一条路由的所有信息。
4) RTM_CHANGE:改变一条已经存在路由的网关、接口或者度量。
5) RTM_LOCK:说明内核不应该修改哪个度量。
除此之外,该进程可以接收其他七个选路报文,这些报文是在发生某些事件时,如接口
下线和收到重定向报文等等,由内核生成的。
本章简介选路域、为每个选路插口创建的选路控制块、处理进程产生的报文的函数
(r o u t e _ o u t p u t)、发送选路报文给一个或多个进程的函数 (r a w _ i n p u t)、以及不同的支持
一个选路插口上所有插口操作的函数。

20.2 routedomain和protosw结构

在描述选路插口函数之前,我们需要讨论有关选路域的其他一些细节;在选路域中支持
的SOCK_RAW协议;以及每个选路插口所附带的选路控制块。
图20-1列出了称为 routedomain的PF_ROUTE域的domain结构。

成 员 值 描 述

dom_family PF_ROUTE 域的协议族


dom_name route 名字
dom_init route_init 域的初始化,图18-30
dom_externalize 0 在选路域中不使用
dom_dispose 0 在选路域中不使用
dom_protosw routesw 协议交换结构,图 20-2
dom_protoswNPROTOSW 指向协议交换结构之后的指针
dom_next 由d o m a i n i n i t填入,图7-15
dom_rtattch 0 在选路域中不使用
dom_rtoffset 0 在选路域中不使用
dom_maxrtkey 0 在选路域中不使用

图20-1 routedomain 结构

与支持多个协议 ( T C P 、 U D P 和 I C M P 等 )的 I n t e r n e t 域不一样,在选路域中只支持
SOCK_RAW类型的一种协议。图 20-2列出了PF_ROUTE域的协议转换项。

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 519
成 员 routesw[0] 描 述

pr_type SOCK_RAW 原始插口


pr_domain &routedomain 选路域部分
pr_protocol 0
pr_flags PR_ATOMI|PR_ADDR 插口层标志,协议处理时不使用
pr_input raw_input 不使用这项, r a w _ i n p u t直接调用
pr_output route_output P R U _ S E N D请求所调用
pr_ctlinput raw_ctlinput 控制输入函数
pr_ctloutput 0 不使用
pr_usrreq route_usrreq 对一个进程通信请求的响应
pr_init raw_init 初始化
pr_fasttimo 0 不使用
pr_slowtimo 0 不使用
pr_drain 0 不使用
pr_sysctl sysctl_rtable 用于s y s c t l(8)系统调用

图20-2 选路协议 protosw 的结构

20.3 选路控制块

每当采用如下形式的调用创建一个选路插口时,
socket(PF_ROUTE, SOCK_RAW, protocol);

对协议的用户请求函数 (r o u t e _ u s r r e q)的一个对应的 P R U _ A T T A C H请求分配一个选路


控制块,并且将它链接到插口结构上。 p ro t o c o l参数可以将发送给这个插口上的进程的报文类
型限制为一个特定族。例如,如果将 protocol参数说明为 A F _ I N E T,只有包含了I n t e r n e t地
址的选路报文将被发送给这个进程。 p ro t o c o l参数为 0将使得来自内核的所有选路报文都发送
给这个插口。
记住我们把这些结构称为选路控制块,而不是原始控制块 (raw control block) ,
是为了避免与第 32章中的原始 IP控制块相混淆。
图20-3显示了rawcb结构的定义。

图20-3 rawcb 结构

另外,分配了一个相同名字的全局结构, r a w c b,作为这个双向链表的头。图 2 0 - 4显示


了这种情况。
39-47 我们在图 1 9 - 2 6中显示了 s o c k p r o t o的结构。它的 s p _ f a m i l y成员变量被设置为
P F _ R O U T E , s p _ p r o t o c o l 成员变量被设置为 s o c k e t 系 统 调 用 的 第 三 个 参 数 。
Linux公社 www.linuxidc.com
bbs.theithome.com
520 计计TCP/IP详解 卷 2:实现

r c b _ f a d d r成员变量被永久性地设置为指向 r o u t e _ s r c的指针,我们在图 1 9 - 2 6中描述了


route_src。rcb_laddr总是一个空指针。
描述符 描述符

插口层
协议层

所有选路控制块的
双向链接循环列表

图20-4 原始协议控制块与其他数据结构的关系

20.4 raw_init函数

在图2 0 - 5中显示的 r a w _ i n i t函数是图 2 0 - 2中的p r o t o s w结构的协议初始化函数。我们


在图18-29中描述了选路域的完整初始化过程。
38-42 这个函数将头结构的下一个和前一个指针设置为指向自身来对这个双向链表进行初
始化。

图20-5 raw_init 函数:初始化选路控制块的双向链表

20.5 route_output函数

如同我们在图 1 8 - 11所显示的,当给协议的用户请求函数发送 P R U _ S E N D请求时,就会调

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 521
用r o u t e _ o u t p u t,这是一个进程向一个选路插口进行写操作所引起的。在图 1 8 - 9中,我们
给出了内核接受的、由进程发出的五种不同类型的选路报文。
因为这个函数是由一个进程的写操作激活的,来自于该进程的数据 (发送给进程的选路报
文)被放在一个由 s o s e n d开始的m b u f链中。图 2 0 - 6显示了大概的处理步骤,假定进程发送了
一个R T M _ A D D命令,说明三个地址:目的地址、它的网关和一个网络掩码 (因此,这是一个网
络路由,而不是一个主机路由 )。
进程 进程 进程

raw_input
选择的进程
mbuf链 mbuf链

进程写
的数据
目的IP地址

网关IP地址

网络掩码

从rtm_addrs位掩码构
造的rt_addrs指针数组

图20-6 一个进程发出的RTM_ADD 命令的处理过程示例

在这个图中有几点需要引起注意,我们在介绍 r o u t e _ o u t p u t的源代码时讨论了这里需
要注意的大多数情况。另外,为了节省空间,我们省略了 r t _ a d d r i n f o结构中每个数组下
标的RTAX_前缀。
• 进程通过设置比特掩码 r t m _ a d d r s来说明在定长的 r t _ m s g h d r结构之后有哪些插口地

Linux公社 www.linuxidc.com
bbs.theithome.com
522 计计TCP/IP详解 卷 2:实现

址结构。我们显示了一个值为 0 x 0 7的比特掩码,表示有一个目的地址、一个网关地址
和一个网络掩码 (图1 9 - 1 9 )。R T M _ A D D命令需要前两个地址;第三个地址是可选的。另
一个可选的地址, genmask说明了用来产生克隆路由的掩码。
• write系统调用(sosend函数)将来自进程的一个缓存数据复制到内核的一个 mbuf链中。
• m _ c o p y d a t a将m b u f链中数据复制到 r o u t e _ o u t p u t使用m a l l o c获得的一个缓存中。
访问存储在单个连续缓存中的结构以及结构后面的若干插口地址结构,比访问一个 mbuf
链更容易。
• r o u t e _ o u t p u t 调用 r t _ x a d d r s函数取得比特掩码,并且构造一个指向缓存的
r t _ a d d r i n f o结构。 r o u t e _ o u t p u t中的代码使用图 1 9 - 1 9中第五栏显示的名字来引
用这些结构。比特掩码也要复制到 rti_addrs成员中。
• r o u t e _ o u t p u t一般要修改r t _ m s g h d r结构。如果发生了一个错误,相应的 e r r n o值
被返回到 r t m _ e r r n o 中 ( 例如,如果路由已经存在,则返回 E E X I S T ) ;否则,
RTF_DONE标志与进程提供的 rtm_flags执行逻辑或操作。
• r t _ m s g h d r结构以及接着的地址成为 0个或多个正在读选路插口的进程的输入。首先使
用m _ c o p y b a c k将缓存转换为一个 m b u f链。r a w _ i n p u t经过所有的选路 P C B,并且传
递一个复制给对应的进程。我们还显示了一个带有选路插口的进程,如果该进程没有禁
止SO_USELOOPBACK插口选项,就会收到它写给那个插口的每个报文的一个复制。
为了避免收到它们自己的选路报文的一个复制,有些程序,如 r o u t e,将第二
个参数置为 0来调用shutdown,以防止从选路插口上收到任何数据。
我们分成七个部分分析 route_output的源代码。图20-7显示了这个函数的大概流程。

图20-7 route_output 处理步骤小结

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计523

图20-7 (续)

route_output的第一部分显示在图 20-8中。

图20-8 route_output 函数:初始化处理,从 mbuf链中复制报文

Linux公社 www.linuxidc.com
bbs.theithome.com
524 计计TCP/IP详解 卷 2:实现

图20-8 (续)

1. 检查mbuf的合法性
113-136 检查mbuf的合法性:它的长度必须至少是一个 rt_msghdr结构的大小。从 mbuf
的数据部分取出第一个长字,里面包含了 rtm_msglen的值。
2. 分配缓存
137-142 分配一个缓存来存放整个报文, m_copydata将报文从mbuf链复制到缓存。
3. 检查版本号
143-146 检查报文的版本号。如果将来引入了新版本的选路报文,这个成员变量可以用来
对早期版本提供支持。
147-149 进程的ID被复制到rtm_pid,进程提供的比特掩码被复制到该函数的一个内部结
构i n f o.r t i _ a d d r s。函数r t _ x a d d r s(在下一节显示 )填入i n f o结构的第 8个插口地址指针
来指示当前包含报文的缓存。
4. 需要的目的地址
150-151 所有的命令都需要一个目的地址。如果 i n f o.r t i _ i n f o [ R T A X _ D S T ]项是一个
空指针,就需要一个 EINVAL。记住dst引用了这个数组成员 (图19-19)。
5. 处理可选的 genmask
152-159 genmask是可选的,它是在设置了 RTF_CLONING标志后(图19-8),用作所创建路
由的网络掩码。 r n _ a d d m a s k将这个掩码加入到掩码树中,并首先在掩码树中查找是否存在
与这个掩码相同的条目,如果找到,就引用那个条目。如果在掩码树中找到或者将这个掩码
加入到掩码树中,还要再检查一下掩码树中的那个条目是否真等于 genmask的值,如果等于,
则genmask指针就被替代为掩码树中那个掩码的指针。
图20-9显示了route_output函数处理RTM_ADD和RTM_DELETE的下一部分。
162-163 RTM_ADD命令要求进程说明一个网关。
164-165 r t r e q u e s t处理该请求。如果输入的路由是一个主机路由,则 n e t m a s k指针可
以为空。如果一切 OK,则saved_nrt返回新的路由表项的指针。
166-172 将r t _ m e t r i c s结构从调用者缓存中复制到路由表项中。引用计数减 1,并且保
存genmask指针(可能是一个空指针 )。
173-176 处理R T M _ D E L E T E命令非常简单,因为所有的工作都由 r t r e q u e s t来完成。既
然最后一个参数是一个空指针,如果引用计数为 0,r t r e q u e s t就调用r t f r e e从路由表中删
除指定的项 (图19-7)。
下一步的处理过程显示在图 20-10中,它显示了R T M _ G E T、R T M _ C H A N G E和R T M _ L O C K命

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 525
令的公共代码。

图20-9 route_output 函数:进程 RTM_ADD 和RTM_DELETE 命令

图20-10 route_output 函数:RTM_GET 、RTM_CHANGE 和RTM_LOCK 的公共处理部分

6. 查找已经存在的项
177-182 因为三个命令都引用了一个已经存在的项,所以用 rtalloc1函数来查找这个项。
如果没有找到,则返回一个 ESRCH。
7. 不允许网络匹配
183-187 对于R T M _ C H A N G E和R T M _ L O C K命令,一个网络匹配是不合适的:需要一个路由
表关键字的精确匹配。因此,如果 d s t参数不等于路由表关键字,这个匹配就是一个网络匹
配,返回一个ESRCH。
Linux公社 www.linuxidc.com
bbs.theithome.com
526 计计TCP/IP详解 卷 2:实现

8. 使用网络掩码来查找正确的项
188-193 即使是一个精确的匹配,如果存在网络掩码不同的重复表项,仍然必须查找正确
的项。如果提供了一个 n e t m a s k参数,就要在掩码表中查找它 (m a s k _ r n h e a d)。如果找到
了, n e t m a s k指针就被替代为掩码树中相应掩码的指针。检查重复表项列表的每个叶结点,
查找一个 r n _ m a s k指针等于 n e t m a s k的项。这个测试只是比较指针,而不是指针所指向的结
构。这是因为所有的掩码都出现在掩码树中,并且每个不同的掩码只有一个副本出现在这个
掩码树中。大多数情况下,表项不会重复,因此 f o r循环只执行一次。如果一个主机路由项
被修改了,不应该提供一个网络掩码,因此, n e t m a s k和r n _ m a s k都是空指针 (两者是相等
的)。但是,如果有一个附带掩码的项被修改了,那个掩码必须作为 netmask参数提供。
194-195 如果for循环终止时没有找到一个匹配的网络掩码,就返回 ETOOMANYREFS。
注释XXX表示这个函数必须做所有的工作来找到需要的项。所有这些细节在其他一些
类似rtalloc1的函数中都应该被隐藏,rtalloc1检测网络匹配,并且处理掩码参数。
这个函数的下一部分继续处理 R T M _ G E T 命令,显示在图 2 0 - 1 1 中。这个命令与

图20-11 route_output 函数:RTM_GET 处理过程

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 527
r o u t e _ o u t p u t支持的其他命令的区别在于它能够返回比传递给它的更多的数据。例如,只
需要输入一个插口地址结构,即目的地址,但至少返回两个插口地址结构,即目的地址和它
的网关。参看图 20-6,这就意味着为 m_copydata复制数据所分配的缓存可能需要扩充大小。
9. 返回目的地址、网关和掩码
198-203 r t i _ i n f o数组中存储了四个指针: d s t、g a t e、n e t m a s k和g e n m a s k。后两
个可能是空指针。 info结构中的这些指针指向进程将要返回的各个插口地址结构。
10. 返回接口信息
204-213 进程可以在 r t m _ f l a g s比特掩码中设置 R T A _ I F P和R T A _ I F A掩码。如果设置了
一项或两项,就表示进程想要接收这个路由表项所指示的 i f a d d r结构:接口的链路层地址
(由r t _ i f p-> a d d r l i s t指向)以及这个路由项的协议地址 (由r t _ i f a-> i f a _ a d d r指向)的内
容。接口索引也会被返回。
11. 构造应答报文
214-224 将第三个指针置为空,调用 r t _ m s g 2来计算相应于 R T M _ G E T的选路报文和 i n f o
结构所指向的地址的长度。如果结果报文的长度超过了输入报文的长度,就会分配一个新的
缓存,输入报文被复制到新的缓存中,老的缓存被释放, rtm被重新设置为指向新缓存。
225-230 再次调用rt_msg2,这次调用时第三个指针非空,因为在缓存中已经构造了一个
结果报文。这次调用填入 rt_msghdr结构的最后三个成员项。
图20-12显示了RTM_CHANGE和RTM_LOCK命令的处理过程。
12. 改变网关
231-233 如果进程传递了一个gate地址,rt_setgate就被调用来改变这个路由表项的网关。
13. 查找新的接口
234-244 新的网关 (如果被改变)可能也需要 r t _ i f p和r t _ i f a指针。进程可以通过传递一
个i f p a d d r插口地址结构或者一个 i f a a d d r插口地址结构来说明这些新的值。先看第一个,
然后再看第二个。如果进程两个结构都没传递, rt_ifp和rt_ifa指针就被忽略。
14. 检验接口是否改变
245-256 如果找到了一个接口 (i f a非空),则该路由的现有 r t _ i f a指针要和新的值进行比
较。如果数值已经改变了,则两个针对 r t _ i f p和r t _ i f a的新值需要存储到路由表的表项中
去。在这样做之前,先要用 R T M _ D E L E T E命令调用该接口的请求函数 (如果定义了该函数的
话)。这个删除动作是必须的,因为从一种类型的网络到另一种类型的网络,它们的链路层信
息可能会有很大的差别 (比如说从一个X.25网络改变成以太网的路由 ),同时我们还必须通知输
出例程。
15. 更新度量
257-258 rt_setmetrics修改路由表项的度量。
16. 调用接口请求函数
259-260 如果定义了一个接口请求函数,它就会和 RTM_ADD命令一起被调用。
17. 保存克隆生成的掩码
261-262 如果进程指定了 g e n m a s k 参数,就将在图 2 0 - 8 中获得的掩码的指针保存在
rt_genmask中。
18. 修改加锁度量的比特掩码

Linux公社 www.linuxidc.com
bbs.theithome.com
528 计计TCP/IP详解 卷 2:实现

266-270 R T M _ L O C K命令修改保存在 r t _ r m x.r m x _ l o c k s中的比特掩码。图 2 0 - 1 3显示了


这个比特掩码中不同比特的值,每个度量一个值。

图20-12 route_output 函数:RTM_CHANGE 和RTM_LOCK 处理过程

路由表项中 r t _ m e t r i c s结构的 r m x _ l o c k s成员是告诉内核哪些度量不要管的比特掩


码。即, r m x _ l o c k s指定的那些度量内核不能修改。内核惟一能使用这些度量的地方是和
T C P一起,如图 2 7 - 3所示。 r m x _ p k s e n t度量不能被初始化或加锁,但是内核也从来没有引
用或修改过这个成员。
进程发出的报文中的 r t m _ i n i t s 值是一个比特掩码,指出哪些度量刚刚被

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 529
常 量 值 描 述

RTV_MTU 0x01 初始化或者锁住 rmx_mtu


RTV_HOPCOUNT 0x02 初始化或者锁住 rmx_hopcount
RTV_EXPIRE 0x04 初始化或者锁住 rmx_expire
RTV_RPIPE 0x08 初始化或者锁住 rmx_recvpipe
RTV_SPIPE 0x10 初始化或者锁住 rmx_sendpipe
RTV_SSTHRESH 0x20 初始化或者锁住 rmx_ssthresh
RTV_RTT 0x40 初始化或者锁住 rmx_rtt
RTV_RTTVAR 0x80 初始化或者锁住 rmx_rttvar

图20-13 对度量初始化或加锁的常量
r t _ s e t m e t r i c s初始化过。报文中的 r t m _ r m x.r m x _ l o c k s值是一个指出哪些度量现在应
该加锁的比特掩码。 r t _ r m x.r m x _ l o c k s的值是一个指出路由表中哪些度量当前被加锁的比
特掩码。首先,任何将要初始化的比特 ( r t m _ i n i t s ) 都要解锁。任何既被初始化
(rtm_inits)又被加锁(rtm_rmx.rmx_locks)的比特都必须加锁。
273-275 这个d e f a u l t是用于图 2 0 - 9开始的s w i t c h语句,用来处理进程发出的报文中除
了所支持的五个命令以外的其他选路命令。
route_output的最后一部分显示在图 20-14中,用来发送应答给 raw_input。

图20-14 route_output 函数:将结果传递给 raw_input

Linux公社 www.linuxidc.com
bbs.theithome.com
530 计计TCP/IP详解 卷 2:实现

图20-14 (续)

19. 返回错误或 OK
276-282 flush是该函数开头定义的 senderr宏所跳转的标号。如果发生了一个错误,错
误就在rtm_errno成员中返回;否则,就设置 RTF_DONE标志。
20. 释放拥有的路由
283-284 如果拥有一条路由,就要被释放。如果找到,在图 20-10的开始位置对rtalloc1
的调用拥有这条路由。
21. 没有进程接收报文
285-296 SO_USELOOPBACK插口选项的默认值为真,表示发送进程将会收到它发送给选路
插口的每个选路报文的一个复制 (如果发送者不接收一个复制的报文,它就不能收到 R T M _ G E T
返回的任何信息 )。如果没有设置这个选项,并且选路插口的总数小于或等于 1,就没有其他
进程接收报文,并且发送者不想要一个复制报文。缓存和 mbuf链都会被释放,该函数返回。
22. 没有环回复制报文的其他监听者
297-299 至少有一个其他的监听者而不是发送进程不想要一个复制报文。指针 r p,默认是
空,被设置成指向发送者的选路控制块,它也用来作为发送者不想要复制报文的一个标志。
23. 将缓存转换成mbuf链
300-303 缓存被转换成一个 mbuf链(图20-6),然后释放缓存。
24. 避免环回复制
304-305 如果设置了rp,则某个其他的进程可能想要报文,但是发送者不想要一个复制。发
送者的选路控制块的sp_family成员被临时设置为0,但是报文的sp_family(route_proto
结构,显示在图 19-26中)有一个PF_ROUTE的族。这个技巧防止raw_input将结果的一个复制
传递给发送进程,因为raw_input不会将一个复制传递给sp_family为0的任何插口。
25. 设置选路报文的地址族
306-308 如果dst是一个非空的指针,则那个插口地址结构的地址族成为选路报文的协议。
对于Internet协议,这个值将是PF_INET。通过raw_input,一个复制被传递给合适的监听者。
309-313 如果调用进程的sp_family成员被临时设置为0,它就被复位成正常值,PF_ROUTE。

20.6 rt_xaddrs函数

在将来自进程的选路报文从 m b u f 链复制到一个缓存以及将来自进程的比特掩码
(r t m _ a d d r s)复制到r t _ a d d r i n f o结构的r t i _ i n f o成员之后,只从 r o u t e _ o u t p u t中调
用一次 r t _ x a d d r s 函数 (图2 0 - 8 ) 。r t _ x a d d r s的目的是获取这个比特掩码,并且设置
rti_info数组的指针,使之指向缓存中相应的地址。图 20-15显示了这个函数。
330-340 指针数组被设置成0,因此,所有在比特掩码中不出现的地址结构的指针都将是空。
341-347 测试比特掩码中 8个(R T M _ M A X)可能比特的每一个 (如果设置 ),将相应于插口地址
结构的一个指针存到 r t i _ i n f o数组中。 A D V A N C E宏以插口地址结构的 s a _ l e n字段为参数,

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 531
上舍入为4个字节的倍数,相应地增加指针 cp。

图20-15 rt_xaddrs 函数:将指针填入 rti_info 数组

20.7 rt_setmetrics函数

这个函数在 r o u t e _ o u t p u t中调用了两次:增加一条新路由时和改变一条已经存在的路
由时。来自进程的选路报文的 r t m _ i n i t s成员说明了进程想要初始化 r t m _ r m x数组中的哪
些度量。比特掩码中的比特的值显示在图 20-13中。
请注意, r t m _ a d d r s和r t m _ i n i t s都是来自进程的报文中的比特掩码,前者说明了接
下来的插口地址结构,而后者说明哪些度量将被初始化。为了节省空间,在 r t m _ a d d r s中没
有设置比特的插口地址结构也不会出现在选路报文中。但是整个 r t _ m e t r i c s总是以定长的
rt_msghdr结构的形式出现—在rtm_inits中没有设置比特的数组成员将被忽略。
图20-16显示了rt_setmetrics函数。

图20-16 rt_setmetrics 函数:设置 rt_metrics 结构中的成员

Linux公社 www.linuxidc.com
bbs.theithome.com
532 计计TCP/IP详解 卷 2:实现

314-318 w h i c h参数总是进程的选路报文的 r t m _ i n i t s 成员。 i n 指向进程的 r t _


metrics结构,而out指向将要创建或修改的路由表项的 rt_metrics结构。
319-329 测试比特掩码中8比特的每一比特,如果该比特被设置,就复制相应的度量。请注意
当使用RTM_ADD创建一个新的路由表项时, route_output调用了rtrequest,后者将整个
路由表项设置为0(图19-9)。因此,在选路报文中,进程没有说明的任何度量,其默认值都是0。

20.8 raw_input函数

向一个进程发送的所有选路报文—包括由内核产生的和由进程产生的—都被传递给
raw_input,后者选择接收这个报文的进程。图 18-11总结了调用 raw_input的四个函数。
当创建一个选路插口时,族总是 P F _ R O U T E;而协议,s o c k e t的第三个参数,可能为 0,
表示进程想要接收所有的选路报文;或者是一个如同 A F _ I N E T的值,限制插口只接收包含指
定协议族地址的报文。为每个选路插口创建一个选路控制块 ( 2 0 . 3节),这两个值分别存储在
rcb_proto结构的sp_family和sp_protocol成员中。
图20-17显示了raw_input函数。

图20-17 raw_input 函数:将选路报文传递给 0个或多个进程

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 533

图20-17 (续)

51-61 在我们所看到的四个对 r a w _ i n p u t的调用中, p r o t o、s r c和d s t参数指向三个全


局变量 r o u t e _ p r o t o、r o u t e _ s r c和r o u t e _ d s t,这些变量都如同图 1 9 - 2 6所示的那样被
声明和初始化。
1. 比较地址族和协议
62-67 f o r循环遍历每个选路控制块来查找一个匹配。控制块里的族 (一般是P F _ R O U T E)必
须与s o c k p r o t o结构的族相匹配,否则这个控制块就被略过。接下来,如果控制块里的协议
(s o c k e t的第三个参数 )非空,它必须匹配 s o c k p r o t o结构的族;否则,这个报文被略去。
因此,以0协议创建了一个选路插口的进程将收到所有的选路报文。
2. 比较本地的和外部的地址
68-81 如果指定了的话,这两个测试比较了控制块里的本地地址和外部地址。目前,进程
不能设置控制块的 r c b _ l a d d r或者r c b _ f a d d r成员。一般来说,进程使用 b i n d设置前者,
用c o n n e c t设置后者,但对于 N e t / 3中的选路插口这是不可能的。作为替代,我们将看到
r o u t e _ u s r r e q将插口固定地连接到 r o u t e _ s r c插口地址结构,这是可行的,因为它总是
这个函数的 src参数。
3. 将报文添加到插口的接收缓存中
82-107 如果last非空,它指向最近看到的应该接收这个报文的 socket结构。如果这个变
量非空,就使用 m _ c o p y和s b a p p e n d a d d r将这个报文的一个复制添加到那个插口的接收缓
存中,并且在这个接收缓存等待的任何进程都会被唤醒。然后, l a s t被设置成指向在以前的
测试中刚刚匹配的插口。使用 l a s t 是为了在只有一个进程接收报文的情况下避免调用
m_copy(一个代价昂贵的操作 )。
如果有N个进程接收报文,那么前 N-1个接收一个复制报文,最后一个进程收到的是这个
报文本身。
在这个函数里递增的 s o c k e t变量并没有被用到。因为只有当报文被传递给一个进程后它
才会被递增,所以,如果在函数的结尾这个变量的值是 0,就表示没有进程接收该报文 (但是
Linux公社 www.linuxidc.com
bbs.theithome.com
534 计计TCP/IP详解 卷 2:实现

变量值没有在任何地方保存 )。

20.9 route_usrreq函数

r o u t e _ u s r r e q是选路协议的用户请求函数。它被不同的操作调用。图 20-18显示了这个
函数。

图20-18 route_usrreq 函数:处理 PRU_ xxx请求

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 535
1. PRU_ATTACH:分配控制块
64-77 当进程调用s o c k e t时,就会发出 P R U _ A T T A C H请求。为一个选路控制块分配内存。
M A L L O C返回的指针保存在 s o c k e t结构的s o _ p c b成员中。如果分配了内存, r a w c b结构被
设置成0。
2. PRU_DETACH:计数器递减
78-87 c l o s e系统调用发出 P R U _ D E T A C H请求。如果 s o c k e t结构指向一个协议控制块,
r o u t e _ c b结构的计数器中有两个被减 1:一个是 a n y _ c o u n t;另一个是基于该协议的计数
器。
3. 处理请求
88-90 函数raw_usrreq被调用来进一步处理 PRU_xxx请求。
4. 计数器递增
91-104 如果 请求是 P R U _ A T T A C H , 并 且 插 口 指 向 一 个 选 路 控 制 块 , 就要检查
r a w _ u s r r e q是否返回一个错误。然后, r o u t e _ c b结构的计数器中的两个被递增:一个是
any_count,另一个是基于该协议的计数器。
5. 连接插口
105-106 选路控制块里的外部地址被设置成 route_src。这将永久地连接到新的插口来接
收PF_ROUTE族的选路报文。
6. 默认情况下使能 SO_USELOOPBACK
107-111 使能SO_USELOOPBACK插口选项。这是一个默认使能的插口选项—其他所有的
选项默认都被禁止。

20.10 raw_usrreq函数

r a w _ u s r r e q 完成在选路域中用户请求处理的大部分工作。在上一节中它被
r o u t e _ u s r r e q函数所调用。用户请求的处理被划分成这两个函数,是因为其他的一些协议
(例如OSI CLNP)调用r a w _ u s r r e q而不是 r o u t e _ u s r r e q。r a w _ u s r r e q并不是想要成为
一个协议的 p r _ u s r r e q函数,相反,它是一个被不同的 p r _ u s r r e q函数调用的公共的子例
程。
图2 0 - 1 9显示了r a w _ u s r r e q函数的开始和结尾。其中的 s w i t c h语句体在该图后面的图
中单独讨论。
1. PRU_CONTROL请求是不合法的
119-129 PRU_CONTROL请求来自于 ioctl系统调用,在路由选择域中不被支持。
2. 控制信息不合法
130-133 如果进程传递控制信息 (使用s e n d m s g系统调用 ),就会返回一个错误,因为路由
选择域中不使用这个可选的信息。
3. 插口必须有一个控制块
134-137 如果s o c k e t结构没有指向一个选路控制块,就返回一个错误。如果创建了一个
新的插口,调用者 (即r o u t e _ u s r r e q)有责任在调用这个函数之前分配这个控制块,并且将
指针保存在 so_pcb成员中。
262-269 这个switch语句的default子句处理case子句没有处理的两个请求:PRU_BIND

Linux公社 www.linuxidc.com
bbs.theithome.com
536 计计TCP/IP详解 卷 2:实现

和P R U _ C O N N E C T。这两个请求的代码是提供的,但在 N e t /3中被注释掉了。因此,如果在一
个选路插口上发出 b i n d或c o n n e c t系统调用,就会引起一个内核的告警 (panic)。这是一个程
序错误(bug)。幸运的是创建这种类型的插口需要有超级用户的权限。

图20-19 raw_usrreg 函数体

我们现在讨论单个的 c a s e语句。图 2 0 - 2 0显示了对 P R U _ A T T A C H和P R U _ D E T A C H请求的


处理。
139-148 P R U _ A T T A C H请求是s o c k e t系统调用的一个结果。一个选路插口只能由一个超
级用户的进程创建。
149-150 函数 r a w _ a t t a c h(图2 0 - 2 4 )将控制块链接到双向链接列表中。 n a m 参数是
socket的第三个参数,被存储在控制块中。
151-159 PRU_DETACH是由close系统调用发出的请求。对一个空的 rp指针的测试是多余
的,因为在 switch语句之前已经进行过这个测试了。
160-161 raw_detach(图20-25)从双向链接表中删除这个控制块。
图20-21显示了PRU_CONNECT2、PRU_DISCONNECT和PRU_SHUTDOWN请求的处理。
186-188 PRU_CONNECT2请求来自于 socketpair系统调用,在路由选择域中不被支持。
189-196 因为一个选路插口总是连接的(图20-18),所以PRU_DISCONNECT请求在PRU_DETACH请

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 537

图20-20 raw_usrreq 函数:PRU_ATTACH 和PRU_DETACH 请求

求之前由 c l o s e发出。插口必须已经和一个外部地址相连接,这对于一个选路插口来说总是
成立的。raw_disconnect和soisdisconnected完成这个处理。
197-202 当参数指定在这个插口上没有更多的写操作时, s h u t d o w n 系统调用发出
PRU_SHUTDOWN请求。socantsendmore禁止以后的写操作。

图20-21 raw_usrreq 函数:PRU_CONNECT2 、PRU_DISCONNECT 和PRU_SHUTDOWN 请求

对一个选路插口最常见的请求:PRU_SEND、PRU_ABORT和PRU_SENSE显示在图20-22中。
203-217 当进程向插口写时, sosend发出了PRU_SEND请求。如果指定了一个 nam参数,

Linux公社 www.linuxidc.com
bbs.theithome.com
538 计计TCP/IP详解 卷 2:实现

图20-22 raw_usrreq 函数:PRU_SEND 、PRU_ABORT 和PRU_SENSE 请求


即进程使用 s e n d t o 或者 s e n d m s g 指定了一个目的地址,就会返回一个错误,因为
route_usrreq总是为一个选路插口设置 rcb_faddr。
218-222 m 指 向 的 m b u f链 中 的 信 息 被 传 递 给 协 议 的 p r _ o u t p u t 函 数 , 也 就 是
route_output。
223-227 如果发出了一个 PRU_ABORT请求,则该控制块被断开连接,插口被释放,然后被
断开连接。
228-232 fstat系统调用发出PRU_SENSE请求。函数返回 OK。
图20-23显示了剩下的PRU_xxx请求。

图20-23 raw_usrreq 函数:最后部分

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 539

图20-23 (续)

233-243 这五个请求不被支持。
244-261 PRU_SOCKADDR和PRU_PEERADDR请求分别来自于getsockname和getpeername系统调
用。前者总是返回一个错误,因为设置本地地址的bind系统调用在路由选择域中不被支持。后者
总是返回插口地址结构route_src的内容,这个内容是由route_usrreq作为外部地址设置的。

20.11 raw_attach、raw_detach和ra w _ d i s c o n n e c t函数

r a w _ a t t a c h函数,显示在图 2 0 - 2 4中,被 r a w _ i n p u t调用来完成 P R U _ A T T A C H请求的


处理。

图20-24 raw_attach 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
540 计计TCP/IP详解 卷 2:实现

49-64 调用者必须已经分配了原始的协议控制块。 soreserve将发送和接收缓存的高水位


标记设置为 8192。这对于选路报文应该是绰绰有余了。
65-67 s o c k e t结构的一个指针和 d o m _ f a m i l y(即图20-1中用于选路域的 P F _ R O U T E)以及
proto参数(socket调用的第三个参数 )一起被存储到协议控制块中。
68-70 insque将这个控制块加入到由全局变量 rawcb作为头指针的双向链接表的前面。
r a w _ d e t a c h函数,显示在图 2 0 - 2 5中,被 r a w _ i n p u t调用来完成 P R U _ D E T A C H请求的
处理。

图20-25 raw_detach 函数

75-84 s o c k e t结构中的 s o _ p c b指针被设置成空,然后释放这个插口。使用 r e m q u e从双


向链接表中删除该控制块,使用 free来释放被控制块占用的内存。
r a w _ d i s c o n n e c t 函数,显示在图 2 0 - 2 6 中,被 r a w _ i n p u t 调用来完成 P R U _
DISCONNECT和PRU_ABORT请求的处理。
88-94 如果该插口没有引用一个描述符, raw_detach释放该插口和控制块。

图20-26 raw_disconnect 函数

20.12 小结

一个选路插口是 P F _ R O U T E域中的一个原始插口。选路插口只能被一个超级用户进程创
建。如果一个没有权限的进程想要读内核包含的选路信息,可以使用选路域所支持的 s y s c t l
系统调用(我们在前一章中描述过 )。
在本章中,我们第一次碰到了与插口相联系的协议控制块 ( P C B )。在选路域中,一个专门
的r a w c b包含了有关选路插口的信息:本地和外部的地址、地址族和协议。我们将在第 2 2章
中看到用于 U D P、T C P和原始 I P插口的更大的 I n t e r n e t协议控制块 (i n p c b)。然而概念是相同
的: s o c k e t 结构被插口层使用,而 P C B ,一个 r a w c b 或一个 i n p c b ,被协议层使用。
socket结构指向该 PCB,后者也指向前者。

Linux公社 www.linuxidc.com
bbs.theithome.com
第20章 选 路 插 口计计 541
route_output函数处理进程可以发出的五个请求。依赖于协议和地址族,raw_input将
一个选路报文发送给一个或多个选路插口。对一个选路插口的不同的 P R U _ x x x请求由
r a w _ u s r r e q和r o u t e _ u s r r e q处理。在后面的章节中,我们将碰到另外的 x x x_ u s r r e q函
数,每个协议 ( U D P、T C P和原始I P )对应一个,每个函数都由一个 s w i t c h语句组成用来处理
每一个请求。

习题

20.1 当进程向一个选路插口写一个报文时,列出两种进程可以从 r o u t e _ o u t p u t收到


返回值的方法。哪种方法更可靠?
20.2 因为 r o u t e s w结构的 p r _ p r o t o c o l成员为 0,所以当进程对 s o c k e t系统调用指
定了一个非 0的protocol参数时,会发生什么情况?
20.3 路由表中的路由 (和ARP项不同)永远不会超时。试在路由上实现一个超时机制。

Linux公社 www.linuxidc.com
bbs.theithome.com
第21章 ARP:地址解析协议
21.1 介绍

地址解析协议 ( A R P )用于实现 I P地址到网络接口硬件地址的映射。常见的以太网网络接口


硬件地址长度为 48 bit。A R P同时也可以工作在其他类型的数据链路下,但在本章中,我们只
考虑将IP地址映射到 48 bit的以太网地址。 ARP在RFC 826[Plummer 1982]中定义。
当某主机要向以太网中另一台主机发送 IP数据时,它首先根据目的主机的 IP地址在ARP高
速缓存中查询相应的以太网地址, A R P高速缓存是主机维护的一个 I P地址到相应以太网地址
的映射表。如果查到匹配的结点,则相应的以太网地址被写入以太网帧首部,数据报被加入
到输出队列等候发送。如果查询失败, A R P会先保留待发送的 I P数据报,然后广播一个询问
目的主机硬件地址的 ARP报文,等收到回答后再将 IP数据报发送出去。
以上只是简要描述了 A R P协议的基本工作过程,下面我们将结合 N e t / 3中的A R P实现来详
细描述其具体细节。卷 1的第4章包含了ARP的例子。

21.2 ARP和路由表

N e t / 3中A R P的实现是和路由表紧密关联的,这也是为什么我们要在描述路由表结构之后
再来讲解 A R P的原因。图 2 1 - 1显示了本章中我们描述 A R P要用到的一个例子。整个图是与本
书中用到的网络实例相对应的,它显示了 bsdi主机上当前 ARP缓存的相关结构。其中 Ifnet、
i f a d d r和i n _ i f a d d r结构是由图3-32和图6-5简化而来的,所以在这里忽略了在第 3章和第6
章中描述过的这三个结构中的某些细节。例如,图中没有画出在两个 i f a d d r 结构之后的
s o c k a d d r _ d l结构— 而仅仅是概述了这两个结构中的相应信息。同样,我们也仅仅是概
述了三个in_ifaddr结构中的信息。
下面,我们简要概述图中的有关要点。细节部分将随着本章的进行而详细展开。
1) l l i n f o _ a r p结构的双向链表包含了每一个 A R P已知的硬件地址的少量信息。同名全
局变量llinfo_arp是该链表的头结点,图中没有画出第一位的 la_prev指针指向最后一项,
最后一项的 la_next指针指向第一项。该链表由 ARP时钟函数每隔5分钟处理一次。
2) 每一个已知硬件地址的 I P地址都对应一个路由表结点 (r t e n t r y结构)。l l i n f o _ a r p
结构的 l a _ r t 指针成员用来指向相应的 r t e n t r y 结构,同样地, r t e n t r y 结构的
r t _ l l i n f o 指针成员指向 l l i n f o _ a r p 结构。图中对应主机 s u n ( 1 4 0 . 2 5 2 . 1 3 . 3 3 ) 、
s v r 4( 1 4 0 . 2 5 2 . 1 3 . 3 4 )和b s d i( 1 4 0 . 2 5 2 . 1 3 . 3 5 )的三个路由表结点各自具有相应的 l l i n f o _ a r p
结构。如图 18-2所示。
3) 而在图的最左边第四个路由表结点则没有对应的 l l i n f o _ a r p结构,该结点对应于本
地以太网 ( 1 4 0 . 2 5 2 . 1 3 . 3 2 )的路由项。该结点的 r t _ f l a g s中设置了 C比特,表明该结点是被用
来复制形成其他结点的。设置接口 IP 地址功能的 i n _ i f i n i t函数(图6 - 1 9 )通过调用 r t i n i t
函数来创建该结点。其他三个结点是主机路由结点 ( H标志),并由 b s d i向其他机器发送数据

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 543
时通过ARP间接调用路由相关函数产生的 (L标志)。
4) rtentry结构中的rt_gateway指针成员指向一个sockaddr_dl结构变量。如果保存
物理地址长度的结构sdl_alen成员为6,那么sockaddr_dl结构就包含相应的硬件地址信息。
5) 路由结点变量的 r t _ i f p成员的相应指针成员指向对应网络设备接口的 i f n e t结构。
中间的两个路由结点对应的是以太网上的其他主机,这两个结点都指向 l e _ s o f t c[ 0 ]。而右
边的路由结点对应的是 bsdi,指向环回结构loif。因为rt_ifp.if_output指向输出函数,
所以目的为本机的数据报被路由至环回接口。

图21-1 ARP与路由表和接口结构的关系

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
544 计计TCP/IP详解 卷 2:实现

6) 每一个路由结点还有指向相应的 i n _ i f a d d r 结构的指针变量 (图 6 - 8 中指出了


i n _ i f a d d r结构内的第一个成员是一个 i f a d d r结构,因此, r t _ i f a同样是指向了 i f a d d r
结构变量 )。在本图中,我们只显示一个路由结点的相应指向,其余的路由结点具有同样的性
质。而一个接口如 l e 0,可以同时设置多个 IP地址,每个 IP地址都有对应的 i n _ i f a d d r结构,
这就是为什么除了 rt_ifp之外还需要 rt_ifa的原因。
7) l a _ h o l d成员是指向mbuf链表的指针。当要向某个 IP传送数据报时,就需要广播一个
A R P请求。当内核等待 A R P回答时,存放该待发数据报的 m b u f链的头结点的地址信息就存放
在la_hold里。当收到 ARP回答后,la_hold指向的mbuf链表中的IP数据被发送出去。
8) 路由表结点中rt_metric结构的变量 rmx_expire存放的是与对应的 ARP结点相关的
定时信息,用来实现删除超时 (通常20分钟)的ARP结点。
在4.3BSD Reno中,路由表结构定义有了很大的变化,但 4.3BSD Reno和N e t / 2 .
4 . 4 B S D中依然定义有 A R P缓存,只是去除了作为单独结构的 A R P 缓存链表,而把
ARP信息放在了路由表结点里。
在N e t / 2中, A R P表是一个结构数组,其中每个元素包含有以下成员: I P地址、
以太网地址、定时器、标志和一个指向 m b u f的指针 (类似于图 2 1 - 1中的l a _ h o l d成
员)。在Net/3中,我们可以看到,这些信息被分散到多个相互链接的结构里。

21.3 代码介绍

如图21-2所示,共有包含 9个ARP函数的一个 C文件和两个头文件。

文 件 描 述

net/if arp.h a r p h d r结构的定义


n e t i n e t / i f e t h e r . h 多个结构和常量的定义
netinet/if_ether.d ARP函数

图21-2 本章中讨论的文件

图2 1 - 3显示了 A R P函数与其他内核函数的关系。该图中还说明了 A R P函数与第 1 9章中某


些子函数的关系,下面将逐步解释这些关系。

21.3.1 全局变量

本章中将介绍10个全局变量,如图 21-4所示。

21.3.2 统计量

保存A R P的统计量有两个全局变量: a r p _ i n u s e和a r p _ a l l o c a t e d,如图 2 1 - 4所示。


前者用来记录当前正在使用的 A R P结点数,后者用来记录在系统初始化时分配的 A R P结点数。
两个统计数都不能由 netstat程序输出,但可以通过调试器来查看。
可以使用命令 arp -a来显示当前ARP缓存的信息,该命令使用 s y s c t l系统调用,参数
如图19-36所示。图21-5显示该命令的一个输出结果。
由于图1 8 - 2中对应多播组 2 2 4 . 0 . 0 . 1的相应路由表项设置了 L标志,而同时由于 a r p程序查
询带有 R T F _ L L I N F O标志位的 A R P结点,所以该程序也输出多播地址。后面我们将解释为什
么该表项标识为“ incomplete”,而在它上面的表项是“ permanent”。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 545
arp程序
程序 选路插口
进程
内核

接收到ARP请求/回答 针对每个ARP结点
时产生软件中断

以太网设备驱动器 每5分钟

超时

接口输出函数

赋IP地址时增加路由结点:使用RTM_ADD
命令带RTF_UP和RTF_CLONING标志

针对所有以太网设备的
ifa_rtrequest函数

图21-3 ARP函数和内核中其他函数的关系

变 量 数据类型 描 述

llinfo_arp struct l l i n f o _ a r p l l i n f o _ a r双
p 向链接表的表头
arpintrq struct ifqueue 来自以太网设备驱动程序的 ARP输入队列
arpt_prune Int 检查ARP链表的时间间隔的分钟数 (5)
arpt_keep Int ARP结点的有效时间的分钟数 (20)
arpt_down Int ARP洪泛算法的时间间隔的秒数 (20)
arp_inuse Int 正在使用的 ARP结点数
arp_allocated Int 已经分配的 ARP结点数
arp_maxtries Int 对一个IP地址发送ARP请求的重试次数 (5)
arpinit_done Int 初始化标志
uselookback int 对本机使用环回(默认)

图21-4 本章介绍的全局变量

Linux公社 www.linuxidc.com
bbs.theithome.com
546 计计TCP/IP详解 卷 2:实现

图21-5 与图18-2相应的arp -a命令的输出

21.3.3 SNMP变量

在卷1的2 5 . 8节中我们讲过,最初的 SNMP MIB定义了一个地址映射组,该组对应的是系


统的当前 ARP缓存信息。在 MIB-II中不再使用该组,而用各个网络协议组 (如IP组)分别包含地
址映射表来替代。注意,从 N e t / 2到N e t / 3,将单独结构的 A R P缓存演化为在路由表中集成的
ARP信息是与SNMP的变化并行的。

IP地址映射表, index = <ipNetToMediaIfIndex>.<ipNetToMediaNetAddress>

名 称 成 员 描 述

ipNetToMediaIfIndex if_index 相应接口: i f I n d e x


ipNetToMediaPhysAddress rt_gateway 硬件地址
ipNetToMediaNetAddress rt_key IP地址
ipNetToMediaType rt_flags 映射类型: 1=其他,2=失效,3=动态,4=静态(见正文)

图21-6 IP地址映射表: ipNetToMediaTable

图2 1 - 6所示的是 M I B - I I中的一个 I P地址映射表, i p N e t T o M e d i a T a b l e,该表保存的值


来自于路由表结点和相应的 ifnet结构。
如果路由表结点的生存期为 0,则被认为是永久的,也即静态的。否则就是动态的。

21.4 ARP 结构

在以太网中传送的 ARP分组的格式图21-7所示。

硬件类型,
协议类型,
硬件地址长度,
协议地址长度,

以太网目的 以太网源 帧 发送者硬件 发送者IP 目标硬件 目标IP


op
地址 地址 类型 地址 地址 地址 地址
6 bytes
以太网首部 ARP首部
以太网ARP字段

图21-7 在以太网上使用时 ARP请求或回答的格式

结构e t h e r _ h e a d e r定义了以太网帧首部;结构 a r p h d r定义了其后的 5个字段,其信息


用于在任何类型的介质上传送 A R P请求和回答; e t h e r _ a r p结构除了包含 a r p h d r结构外,
还包含源主机和目的主机的地址。
结构arphdr的定义如图 21-8所示。图21-7显示了该结构中的前 4个字段。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 547

图21-8 arphdr 结构:通用的 ARP请求/回答数据首部

图2 1 - 9显示了 e t h e r _ a r p结构的定义,其中包含了 a r p h d r结构、源主机和目的主机的


IP地址和硬件地址。注意, ARP用硬件地址来表示 48 bit以太网地址,用协议地址来表示 32 bit
IP地址。

图21-9 ether_arp 结构

每个A R P结点中,都有一个 l l i n f o _ a r p结构,如图2 1 - 1 0所示。所有这些结构组成的链


接表的头结点是作为全局变量分配的。我们经常把该链接表称为 A R P高速缓存,因为在图 2 1 -
1中,只有该数据结构是与 ARP结点一一对应的。

图21-10 llinfo_arp结构

在N e t / 2及以前的系统中,很容易识别作为 A R P高速缓存的数据结构,因为每一
个ARP结点的信息都存放在单一的结构中。而 Net/3则把ARP信息存放在多个结构中,
没有哪个数据结构被称为 A R P高速缓存。但是为了讨论方便,我们依然用 A R P高速
缓存的概念来表示一个 ARP结点的信息。
104-106 该双向链接表的前两项由 insque和remque两个函数更新。 la_rt指向相关的路

Linux公社 www.linuxidc.com
bbs.theithome.com
548 计计TCP/IP详解 卷 2:实现

由表结点,该路由表结点的 rt_llinfo成员指向la_rt。
107 当ARP接收到一个要发往其他主机的 IP数据报,且不知道相应硬件地址时,必须发送一
个A R P请求,并等待回答。在等待 A R P回答时,指向待发数据报的指针存放在 l a _ h o l d中。
收到回答后,la_hold所指的数据报被发送出去。
108-109 l a _ a s k e d记录了连续为某个 IP地址发送请求而没有收到回答的次数。在图 21-24
中,我们可以看到,当这个数值达到某个限定值时,我们就认为该主机是关闭的,并在其后
一段时间内不再发送该主机的 ARP请求。
110 这个定义使用路由结点中 r t _ m e t r i c s结构的r m x _ e x p i r e成员作为 A R P定时器。当
值为0时,ARP项被认为是永久的;当为非零时,值为当结点到期时算起的秒数。

21.5 arpwhohas函数

a r p w h o h a s函数通常由 a r p r e s o l v e调用,用于广播一个 A R P请求。如图 2 1 - 11所示。


它还可由每个以太网设备驱动程序调用,在将 I P地址赋予该设备接口时主动发送一个地址联
编信息(图6 - 2 8中的S I O C S I F A D D Ri o c t l)。主动发送地址联编信息不但可以检测在以太网
中是否存在 I P地址冲突,并且可以使其他机器更新其相应信息。 a r p w h o h a s只是简单调用下
一部分将要介绍的 arprequest函数。

图21-11 arpwhohas 函数:广播一个 ARP请求

196-202 a r p c o m结构(图3 - 2 6 )对所有以太网设备是通用的,是 l e _ s o f t c结构(图3 - 2 0 )的


一部分。 a c _ i p a d d r成员是接口的 I P地址的复制,当 S I O C S I F A D D Ri o c t l执行时由驱动
程序填写(图6-28)。ac_enaddr是该设备的以太网地址。
该函数的第二个参数 a d d r,是 A R P请求的目的 I P 地址。在主动发送动态联编信息时,
a d d r等于a c _ i p a d d r,所以a r p r e q u e s t的第二和第三个参数是一样的,即发送 I P地址和目
的IP地址在主动发送动态联编信息时是一样的。

21.6 arprequest函数

a r p r e q u e s t函数由 a r p w h o h a s函数调用,用于广播一个 A R P请求。该函数建立一个


ARP请求分组,并将它传送到接口的输出函数。
在分析代码之前,我们先来看一下该函数建立的数据结构。传送 A R P请求需要调用以太
网设备的接口输出函数 e t h e r _ o u t p u t。e t h e r _ o u t p u t的一个参数是 m b u f,它包含待发
送数据,即图 2 1 - 7中以太网类型字段后的所有内容。另外一个参数包含目的地址的端口地址
结构。通常情况下,该目的地址是 I P地址 ( 例如,在图 2 1 - 3 中, i p _ o u t p u t 调用
e t h e r _ o u t p u t )。特殊情况下,端口地址的 s a _ f a m i l y被设为 A F _ U N S P E C,即告知
e t h e r _ o u t p u t它所带的是一个已填充的以太网帧首部,包含了目的主机的硬件地址,这就

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 549
防止了 e t h e r _ o u t p u t去调用a r p r e s l o v e而导致死循环。图 2 1 - 3中没有显示这种循环,在
a r p r e q u e s t 下面的接口输出函数是 e t h e r _ o u t p u t。如果 e t h e r _ o u t p u t再去调用
arpresolve,将导致死循环。
图2 1 - 1 2显示了该函数建立的两个数据结构 m b u f和s o c k a d d r。另外还有两个函数中用到
的指针eh和ea。

以太网首部

1字节 14字节

未使用
(72字节)

(28字节)

图21-12 arprequest 建立的sockaddr 和mbuf

图21-13给出了arprequest函数的源代码。

图21-13 arprequest 函数:创建一个 ARP请求并发送

Linux公社 www.linuxidc.com
bbs.theithome.com
550 计计TCP/IP详解 卷 2:实现

图21-13 (续)

1. 分配和初始化mbuf
209-223 分配一个分组数据首部的 mbuf,并对两个长度字段赋值。 M H _ A L I G N将28字节的
e t h e r _ a r p结构置于 mbuf的尾部,并相应地设置 m _ d a t a指针的值。将该数据结构置于 mbuf
尾部,是为了允许 ether_output预先考虑将 14字节的以太网帧首部置于同一 mbuf中。
2. 初始化指针
224-226 给ea和eh两个指针赋值,并将 ether_arp结构的值赋为 0。bzero的惟一目的是
将目的硬件地址置 0,该结构中其余 8个字段已被设成相应的值。
3. 填充以太网帧首部
227-229 目的以太网地址设为以太网广播地址,并将以太网帧类型设为 ETHERTYPE_ARP。
注意代码中的注释,接口输出函数将该字段从主机字节序转化为网络字节序,该函数还将填
充本机的以太网地址。图 21-14显示了不同以太网帧类型字段的常量值。

常 量 值 描 述

ETHERTYPE_IP 0x0800 IP帧


ETHERTYPE_ARP 0x0806 ARP帧
ETHERTYPE_REVARP 0x8035 逆ARP帧
ETHERTYPE_IPTRAILERS 0x1000 尾部封装 (已废弃)

图21-14 以太网帧类型字段

RARP 将硬件地址映射成 I P地址,通常在无盘工作站系统引导时使用。一般来说, R A R P


部分不属于内核 TCP/IP实现,所以本书将不作描述,卷 1的第5章讲述了RARP的概念。
4. 填充ARP字段
2 3 0 - 2 3 7 填充了 e t h e r _ a r p的所有字段,除了 A R P请求所要询问的目的硬件地址。常量
A R P H R D _ E T H E R的值为1时,表示硬件地址的格式是 6字节的以太网地址。为了表示协议地址
是4字节的IP地址,a r p _ p r o的值设为图 21-14中所指的IP协议地址类型(0x800)。图21-15显示
了不同的ARP操作码。本章中,我们将看到前两种。后两种在 RARP中使用。
5. 填充sockaddr,并调用接口输出函数
238-241 接口地址结构的 sa_family成员的值设为AP_UNSPEC,sa_member成员的值设
为16。调用接口输出函数 ether_output。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 551
常 量 值 描 述

ARPOP_REQUEST 1 解析协议地址的 ARP请求


ARPOP_REPLY 2 回答ARP请求
ARPOP_REVREQUEST 3 解析硬件地址的 RARP请求
ARPOP_REVREPLY 4 回答RARP请求

图21-15 ARP 操作码

21.7 arpintr函数

在图4-13中,当ether_input函数接收到帧类型字段为 ETHERTYPE_ARP的以太网帧时,
产生优先级为 N E T I S R _ A R P的软件中断,并将该帧挂在 A R P输入队列 a r p i n t r q的后面。当
内核处理该软件中断时,调用 arpintr函数,如图 21-16所示。

图21-16 arpintr 函数:处理包含 ARP请求/回答的以太网帧

319-343 w h i l e循环一次处理一个以太网帧,直到处理完队列中的所有帧为止。只有当帧
的硬件类型指明为以太网地址,并且帧的长度大于或等于 a r p h d r结构的长度加上两个硬件地
址和两个协议地址的长度时,该帧才能被处理。如果协议地址的类型是 E T H E R T Y P E _ I P或
ETHERTYPE_IPTRAILERS时,调用in_arpinput函数,否则该帧将被丢弃。
注意 i f 语句中对条件的检测顺序。共两次检查帧的长度。首先,当帧长大于或等于
a r p h d r结构的长度时,才去检查帧结构中的其他字段;然后,利用 a r p h d r中的两个长度字
段再次检查帧长。
Linux公社 www.linuxidc.com
bbs.theithome.com
552 计计TCP/IP详解 卷 2:实现

21.8 in_arpinput函数

该函数由 a r p i n t r调用,用于处理接收到的 A R P请求/回答。 A R P本身的概念比较简单,


但是加上许多规则后,实现就比较复杂,下面先来看一下两种典型情况:
1) 如果收到一个针对本机 I P地址的请求,则发送一个回答。这是一种普通情况。很显然,
我们将继续从那个主机收到数据报,随后也会向它回送报文。所以,如果我们还没有
对应它的 A R P结点,就应该添加一个 A R P结点,因为这时我们已经知道了对方的 I P地
址和硬件地址。这会优化其后与该主机的通信。
2) 如果收到一个 A R P回答,那么此时 A R P结点是完整的,因此就知道了对方的硬件地址。
该地址存放在sockaddr_dl结构中,所有发往该地址的数据报将被发送。
A R P请求是被广播发送的,所以以太网上的所有主机都将看到该请求,当然包括那些
非目的主机。回想一下 a r p r e q u e s t函数,在发送 A R P请求时,帧中包含着请求方的
IP地址和硬件地址,这就产生了下面的情况:
3) 如果其他主机发送了一个 A R P请求或回答,其中发送方的 I P地址与本机相同,那么肯
定有一个主机配置有误。 Net/3将检测到该差错,并向管理员登记一个报文 (这里我们不
分请求或回答,因为 i n _ a r p i n o u t不检查操作类型,但是 A R P回答将被单播,只有
目的主机才能收到信息 )。
4) 如果主机收到来自其他主机的请求或回答,对应的 ARP结点早已存在,但硬件地址发生
了变化,那么 ARP结点将被更新。这种情况是这样发生的:其他主机以不同的硬件地址
重新启动,而本机的对应 ARP结点还未失效。这样,根据机器重启动时主动发送动态联
编信息,可以使主机不至于因其他主机重启动后导致的 ARP结点失效而不能通信。
5).主机可以被配置为代理 A R P服务器。这种情况下,主机可以代其他主机响应 A R P请求,
在回答中提供其他主机的硬件地址。代理 A R P回答中对应目的硬件地址的主机必须能
够把IP数据报转发至ARP请求中指定的目的主机。卷 1的4.6节讨论了代理ARP。
一个N e t / 3系统可以配置成代理 A R P服务器。这些 A R P结点可以通过 a r p命令添加,该命令
中指定 I P地址、硬件地址并使用关键词 p u b。我们将在图 2 1 - 2 0中看到该实现,并在 21-12 节
讨论其实现细节。
将in_arpinput的分析分为四部分,图 21-17显示了第一部分。
358-375 ether_arp结构的长度由调用者 (arp_intr函数)验证,所以 ea指针指向接收到
的分组。 A R P操作码 (请求或回答 )被拷贝至 o p字段,但具体值要到后面来验证。发送方和目
的方的IP地址拷贝到 isaddr和itaddr。
1. 查找匹配的接口和 IP地址
376-382 搜索本机的 Internet地址链表(in_ifaddr结构的链表,图 6-5)。要记住一个接
口可以有多个 I P地址。收到的数据报中有指向接收接口 i f n e t结构的指针(在m b u f数据报的首
部),f o r循环只考虑与接收接口相关的 I P地址。如果查询到有 I P地址等于目的方 I P地址或发
送方IP地址,则退出循环。
383-384 如果循环退出时,变量 maybe_ia的值为0,说明已经搜索了配置的 IP地址整个链
表而没有找到相关项。函数跳至 o u t(图2 1 - 1 9 ),丢弃 m b u f,并返回。这种情况只发生在收到
ARP请求的接口虽然已初始化但还没有分配 IP地址时。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 553
385 如果退出循环时, m a y b e _ i a值不为0,即找到了一个接收端接口,但没有一个 I P地址
与目的方 I P地址或发送方 I P地址匹配,则 m y a d d r的值设为该接口的最后一个 I P地址;否则
(正常情况),myaddr包含与目的方或发送方的 IP地址匹配的本地 IP地址。

图21-17 in_arpinput 函数:查找匹配接口

图21-18显示了in_arpinput函数的第二部分,执行分组的验证。

图21-18 in_arpinput 函数:验证接收到的分组

Linux公社 www.linuxidc.com
bbs.theithome.com
554 计计TCP/IP详解 卷 2:实现

2. 验证发送方的硬件地址
386-388 如果发送方的硬件地址等于本机接口的硬件地址,那是因为收到了本机发出的请
求,忽略该分组。
389-395 如果发送方的硬件地址等于以太网的广播地址,说明出了差错。记录该差错,并
丢弃该分组。
3. 检查发送方 IP地址
396-402 如果发送方的IP地址等于myaddr,说明发送方和本机正在使用同一个 IP地址。这
也是一个差错—要么是发送方,要么是本机系统配置出了差错。记录该差错,在将目的 I P
地址设为 m y a d d r后,程序转至 r e p l y(图2 1 - 1 9 )。注意该 A R P分组本来要送往以太网中其他
主机的 — 该分组本来不是要送给本机的。但是,如果这种形式的 I P地址欺骗被检测到,应
记录差错,并产生回答。
图21-19显示了in_arpinput函数的第三部分。

图21-19 in_arpinput 函数:创建新的 ARP结点或更新已有的 ARP结点

4. 在路由表中搜索与发送方 IP地址匹配的结点
403 arplookup在ARP高速缓存中查找符合发送方的 IP地址(isaddr)。当ARP分组中的目
的I P地址等于本机 I P时,如果要创建新的 A R P结点,那么第二个参数是 1,如果不需要创建新
的A R P结点,那么第二个参数是 0。如果本机就是目的主机,总是要创建 A R P结点的,除非一
个查找其他主机的广播分组,这种情况下只是在已有的 A R P结点中查询。正如前面提到的,
如果主机收到一个对应它自己的 A R P请求,则说明以太网中有其他主机将要与它通信,所以
应该创建一个对应该主机的 ARP结点。
第三个参数是 0,意味着不去查找代理 A R P结点 (后面要证明 )。返回值是指向 l l i n f o _

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 555
arp结构的指针;如果查不到或没有创建,返回值就是空。
5. 更新已有结点或填充新的结点
404 只有当以下三个条件为真时 if语句才执行:
1) 找到一个已有的 ARP结点或成功创建一个新的 ARP结点(即la非空);
2) ARP结点指向一个路由表结点 (rt);
3) 路由表结点的re_gateway字段指向一个sockaddr_dl结构。
对于每一个目的并非本机的广播 A R P请求,如果发送方的 I P地址不在路由表,则第一个
条件为假。
6. 检查发送方的硬件地址是否已改变
405-408 如果链路层地址长度 (s d l _ a l e n)非0,说明引用的路由表结点是现存的而非新创
建的,则比较链路层地址和发送方的硬件地址。如果不同,则说明发送方的硬件地址已经改
变,这是因为发送方以不同的以太网地址重新启动了系统,而本机的 A R P结点还未超时。这
种情况虽然很少出现,但也必须考虑到。记录差错信息后,程序继续往下执行,更新 A R P结
点的硬件地址。
在这个记录报文中,发送方的 IP地址必须转换为主机字节序,这是一个错误。
7. 记录发送方硬件地址
409-410 将发送方的硬件地址写入路由表结点中 r t _ g a t e w a y成员指向的 s o c k a d d r _ d l
结构。 s o c k a d d r _ d l结构的链路层地址长度 (s d l _ a l e n)也被设为 6。该赋值对于最近创建
的ARP结点是需要的(习题21-3)。
8. 更新最近解析的 ARP结点
411-412 在解析了发送方的硬件地址后,执行以下步骤。如果时限是非零的,则将被复位
成2 0分钟(a r p t _ k e e p)。a r p命令可以创建永久的 A R P结点,即该结点永远不会超时。这些
A R P结点的时限值置为 0。在图 2 1 - 2 4中我们将看到,在发送 A R P请求(非永久性 A R P结点)时,
时限被设为本地时间,它是非 0的。
413-414 清 除 R T F _ R E J E C T 标志 , l a _ a s k e d 计数 器 设为 0。 我们 将看 到 ,在
arpresolve中使用最后两个步骤是为了防止 ARP洪泛。
415-420 如果ARP中保持有正在等待 ARP解析该目的方硬件地址的 mbuf,那么将mbuf 送至
接口输出函数 (如图2 1 - 1所示)。由于该 m b u f是由A R P保持的,即目的地址肯定是在以太网上,
所以接口输出函数应该是 e t h e r _ o u t o u t。该函数也调用 a r p r e s o l v e,但这时硬件地址已
被填充,所以允许 mbuf加入实际的设备输出队列。
9. 如果是ARP回答分组,则返回
421-426 如果该ARP操作不是请求,那么丢弃接收到的分组,并返回。
i n _ a r p i n p u t的剩下部分如图 2 1 - 2 0所示,产生一个对应于 A R P请求的回答。只有当以
下两种情况时才会产生 ARP回答:
1) 本机就是该请求所要查找的目的主机;
2) 本机是该请求所要查找的目的主机的 ARP代理服务器。
函数执行到这个时刻,已经接收了 A R P请求,但 A R P请求是广播发送的,所以目的主机
可能是以太网上的任何主机。
10. 本机就是所要查找的目的主机

Linux公社 www.linuxidc.com
bbs.theithome.com
556 计计TCP/IP详解 卷 2:实现

427-432 如果目的IP地址等于myaddr,那么本机就是所要查找的目的主机。将发送方硬件
地址拷贝到目的硬件地址字段 (发送方现在变成了目的主机 ),a r p c o m结构中的接口以太网地
址拷贝到源硬件地址字段。 ARP回答中的其余部分在 else语句后处理。

图21-20 in_arpinput函数:形成 ARP回答,并发送出去

11. 检测本机是否目的主机的 ARP代理服务器


433-437 即使本机不是所要查找的目的主机,也可能被配置为目的主机的 ARP代理服务器。
再次调用 a r p l o o k u p函数,将第二个参数设为 0,第三个参数设为 S I N _ P R O X Y,这将在路由
表中查找 S I N _ P R O X Y标志为1的结点。如果查找不到 (这是通常情况,本机收到了以太网上其
他ARP请求的拷贝 ),out处的代码将丢弃 mbuf,并返回。
12. 产生代理回答
437-442 处理代理 A R P请求时,发送方的硬件地址变成目的硬件地址, A R P结点中的以太
网地址拷贝到发送方硬件地址。该 A R P结点中的硬件地址可以是以太网中任一台主机的硬件
地址,只要它可以向目的主机转发 I P数据报。通常,提供代理 A R P服务的主机会填入自己的
硬件地址,当然这不是要求的。代理ARP结点是由系统管理员用 arp命令带关键字pub创建的,
标明目的IP地址(这是路由表项的关键值 )和在ARP回答中返回的以太网地址。
13. 完成构造ARP回答分组
443-444 继续完成 A R P回答分组的构建。发送方和目标的硬件地址已经填充好了,现在交
换发送方和目标的 I P地址。目的I P地址在i t a d d r中,如果发现以太网中有其他主机使用同一

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 557
IP地址,则该值已经被填充了 (见图21-18)。
445-446 ARP操作码字段设为 A R P O P _ R E P L Y,协议地址类型设为 E T H E R T Y P E _ I P。旁边
加了注释“你需要确定”,是因为当协议地址类型为 E T H E R T Y P E _ I P T R A I L E R S时a r p i n t r
也会调用该函数,但现在跟踪封装 (trailer encapsulation)已不再使用了。
14. 用以太网帧首部填充 sockaddr
447-452 sockaddr结构用14字节的以太网帧首部填充,如图 21-12所示。目的硬件地址变
成了以太网目的地址。
453-455 将ARP回答传送至接口输出函数,并返回。

21.9 ARP定时器函数

A R P结点一般是动态的—需要时创建,超时时自动删除。也允许管理员创建永久性结
点,前面我们讨论的代理结点就是永久性的。回忆一下图 21-1和图21-10中最后的 # d e f i n e语
句,路由度量结构中的 rmx_expire成员就是用作ARP定时器的。

21.9.1 arptimer函数

如图21-21所示,该函数每 5分钟被调用一次。它查看所有 ARP结点是否超时。

图21-21 arptimer 函数:每5分钟查看所有 ARP定时器

1. 设置下一个时限
80 a r p _ r t r e q u e s t函数使 a r p t i m e r函数第一次被调用,随后 a r p t i m e r每隔 5分钟
(arpt_prune)使自己被调用一次。
2. 查看所有ARP结点
81-86 查看A R P结点链表中的每一个结点。如果定时器值是非零的 (不是一个永久结点 ),
而且时间已经超时,那么 a r p t f r e e就删除该结点。如果 r t _ e x p i r e是非零的,它的值是从
结点超时起到现在的秒数。

21.9.2 arptfree函数

如图21-22所示,a r p t f r e e函数由a r p t i m e r函数调用,用于从链接 l l i n f o _ d l表项的

Linux公社 www.linuxidc.com
bbs.theithome.com
558 计计TCP/IP详解 卷 2:实现

列表中删除一个超时的 ARP结点。
1. 使正在使用的结点无效 (不删除)
467-473 如 果 路 由 表 引 用计 数 器 值 大 于 0, 而 且 r t _ g a t e w a r y 成 员 指 向 一个
sockaddr_dl结构,则arptfree执行以下步骤:
1) 将链路层地址长度设为 0;
2) 将la_asked计数器值设为0;
3) 清除RTF_REJECT标志。
随后函数返回。因为路由表引用计数器值非零,所以该路由结点不能删除。但是将
sdl_alen值设为0,该结点也就无效了。下次要使用该结点时,还将产生一个 ARP请求。

图21-22 arptfree 函数:删除或使一个 ARP结点无效

2. 删除没有被引用的结点
474-475 r t r e q u e s t 删除路由结点,在 2 1 . 1 3节中,我们将看到它调用了 arp_
r t r e q u e s t。a r p _ r t r e q u e s t函数释放所有该 A R P结点保持的 m b u f (由l a _ h o l d指针所指
向),并删除相应的 llinfo_arp结点。

21.10 arpresolve函数

在图4 - 1 6中,e t h e r _ o u t p u t函数调用 a r p r e s o l v e函数以获得对应某个 I P地址的以太


网地址。如果已知该以太网地址,则 a r p r e s l o v e返回值为 1,允许将待发 IP数据报挂在接口
输出队列上。如果不知道该以太网地址,则 a r p r e s o l v e返回值为 0,a r p s o l v e函数利用
llinfo_arp结构的la_hold成员指针“保持(held)”待发IP数据报,并发送一个ARP请求。
收到ARP回答后,再将保持的 IP数据报发送出去。
a r p r e s o l v e应避免 A R P洪泛,也就是说,它不应在尚未收到 A R P回答时高速重复发送
A R P请求。出现这种情况主要有两个原因,第一,有多个 I P数据报要发往同一个尚未解析硬
件地址的主机;第二,一个 I P数据报的每个分片都会作为独立分组调用 e t h e r _ o u t p u t。
11 . 9节讨论了一个由分片引起的 A R P洪泛的例子及相关的问题。图 2 1 - 2 3显示了 a r p r e s o l v e
的前半部分。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 559
252-261 d s t是一个指向 s o c k a d d r _ i n的指针,它包含目的 I P地址和对应的以太网地址
(一个6字节的数组 )。

图21-23 arpresolve 函数:查找所需的 ARP结点

1. 处理广播和多播地址
262-270 如果mbuf的M_BCAST标志置位,则用以太网广播地址填充目的硬件地址字段,函
数返回1。如果M _ M C A S T标志置位,则宏 E T H E R _ M A P _ I P _ M U L T I C A S T(图12-6)将D类地址映
射为相应的以太网地址。
2. 得到指向llinfo_arp结构的指针
271-276 目的地址是单播地址。如果调用者传输了一个指向路由表结点的指针,则将 l a设
置为相应的l l i n f o _ a r p结构。否则, a r p l o o k u p根据给定 IP的地址搜索路由表。第二个参
数是1,告诉a r p l o o k u p如果搜索不到相应的 A R P结点就创建一个新的;第三个参数是 0,即
意味着不去查找代理 ARP结点。
277-281 如果rt或la中有一个是空指针,说明刚才请求分配内存时失败,因为即使不存在
已有结点, a r p l o o k u p也已经创建了一个, r t和l a都不应是空值。记录一个差错报文,释
放分组,函数返回 0。
图2 1 - 2 4显示了a r p r e s o l v e的后半部分。它检查 A R P结点是否有效,如无效,则发送一
个ARP请求。

Linux公社 www.linuxidc.com
bbs.theithome.com
560 计计TCP/IP详解 卷 2:实现

图21-24 arpresolve 函数:检查 ARP结点是否有效,如无效,则发送一个 ARP请求

3. 检查ARP结点的有效性
282-291 即使找到了一个 A R P结点,还需检查其有效性。如以下条件成立,则 A R P结点是
有效的:
1) 结点是永久有效的 (时限值为0),或尚未超时;
2) 由rt_gateway指向的插口地址结构的 sdl_family字段为AF_LINK;
3) 链路层地址长度值 (sdl_alen)不等于0。
a r p t f r e e使一个仍被引用的 A R P结点失效的方法是将 s d l _ a l e n值置0。如果结点是有
效的,则将 sockaddr_dl中的以太网地址拷贝到 desten,函数返回 1。
4. 只保持最近的IP数据报
292-299 此时,已经有了 A R P结点,但它没有一个有效的以太网地址,因此,必须发送一
个ARP请求。将l a _ h o l d指针指向mbuf,同时也就释放了刚才 l a _ h o l d所指的内容。这意味
着,在发送 A R P请求到收到 A R P回答之间,如果有多个发往同一目的地的 I P数据报要发送,
只有最近的一个 I P数据报才被 l a _ h o l d保留,之前的全部丢弃。 N F S就是这样的一个例子,
如果N F S要传送一个 8 5 0 0字节的I P数据报,需要将其分割成 6个分片。如果每个分片都在发送
ARP请求到收到 ARP回答之间由 ip_output送往ether_output,那么前5个分片将被丢弃,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 561
当收到ARP回答时,只有最后一个分片被保留了下来。这会使 NFS超时,并重发这 6个分片。
5. 发送ARP请求,但避免ARP洪泛
300-314 RFC 1122要求ARP避免在收到 ARP回答之前以过高的速度对一个以太网地址重发
ARP请求。Net/3采用以下方法来避免 ARP洪泛:
• Net/3不在同一秒钟内发送多个对应同一目的地的 ARP请求;
• 如果在连续 5个A R P请求(也就是5秒钟)后还没有收到回答,路由结点的 R T F _ R E J E C T标
志置1,时限设为往后的 2 0秒。这会使 e t h e r _ o u t p u t在2 0秒内拒绝发往该目的地址的
IP数据报,并返回 EHOSTDOWN或EHOSTUNREACH(如图4-15所示)。
• 20 秒钟后, arpresolve会继续发送该目的主机的 ARP请求。
如果时限值不等于 0 (非永久性结点 ),则清除 R T F _ R E J E C T标志,该标志是在早些时候为
避免A R P洪泛而设置的。计数器 l a _ a s k e d记录的是连续发往该目的地址的 A R P请求数。如
果计数器值为 0或时限值不等于当前时钟 (只需看一下当前时钟的秒钟部分 ),那么需要再发送
一个A R P请求。这就避免了在同一秒钟内发送多个 A R P请求。然后将时限值设为当前时钟的
秒钟部分(也就是微秒部分, time_tv_usec被忽略)。
将l a _ a s k e d所含计数器值与限定值 5 (a r p _ m a x t r i e s)比较,然后加 1。如果小于5,则
arpwhohas发送ARP请求;如果等于 5,则ARP已经达到了限定值:将RTF_REJECT标志置1,
时限值置为往后的 20秒钟,la_asked计数器值复位为 0。
图2 1 - 2 5显示了一个例子,进一步解释了 a r p r e s o l v e和e t h e r _ o u t p u t为了避免 A R P
洪泛所采用的算法。

数据报数

时间

ARP ARP ARP ARP ARP ARP


返回
请求 请求 请求 请求 请求 请求
RTF_REJECT EHOSTDOWN
打开

图21-25 避免ARP洪泛所采用的算法

图中总共显示了 2 6秒的时间,从 1 0到3 6。我们假定有一个进程每隔 0 . 5秒发送一个 I P数据


报,也就是说,一秒钟内有两个数据报等待发送。数据报依次被标号为 1 ~ 5 2。我们还假定目
的主机已经关闭,所以收不到 ARP回答。ARP将采取以下行动:
• 假定当进程写数据报 1时la_asked的值为0。la_hold设为指向数据报 1,rt_expire
值设为当前时钟 (10),la_asked值变为1,发送ARP请求。函数返回 0。
• 进程写数据报 2时,丢弃数据报 1,l a _ h o l d指向数据报 2。由于 r t _ e x p i r e值等于当
前时钟(10),所以不发送ARP请求,函数返回,返回值为 0。
• 进程写数据报 3时,丢弃数据报 2,l a _ h o l d 指向数据报 3。由于当前时钟 ( 11 )不等于
r t _ e x p i r e( 1 0 ),所以将 r t _ e x p i r e设为11。l a _ a s k e d值为1,小于 5,所以发送
ARP请求,并将 la_asked值置为2。
• 进程写数据报 4时,丢弃数据报 3,l a _ h o l d指向数据报 4。由于 r t _ e x p i r e值等于当

Linux公社 www.linuxidc.com
bbs.theithome.com
562 计计TCP/IP详解 卷 2:实现

前时钟(11),所以无须其他动作,函数返回 0。
• 对于数据报 5 ~ 1 0,情况都是一样的。在数据报 9到达后,发送 A R P请求后, l a _ a s k e d
值被设为5;
• 进程写数据报 11时,丢弃数据报 1 0,l a _ h o l d指向数据报 11 。当前时钟 ( 1 5 )不等于
r t _ e x p i r e( 1 4 ),所以将 r t _ e x p i r e的值设为 1 5。此时 l a _ a s k e d的值不再小于 5,
A R P避免洪泛的算法开始作用: R T F _ R E J E C T标志位置 1,r t _ e x p i r e的值被设为
35(即往后20秒),la_asked的值设为0,函数返回 0。
• 进程写数据报 12时,e t h e r _ o u t p u t注意到R T F _ R E J E C T标志位为 1,而且当前时钟小
于rt_expire(35),因此,返回EHOSTDOWN给发送者(通常是ip_output)。
• 从数据报13到50,都返回 EHOSTDOWN给发送者。
• 当进程写数据报 5 1时,尽管此时的 R T F _ R E J E C T标志位仍然为 1,但当前时钟的值 ( 3 5 )
不再小于 r t _ e x p i r e( 3 5 ),因此不会返回出错信息。调用 a r p r e s o l v e,整个过程重
新开始, 5秒钟内发送5个ARP请求,然后是 20秒钟的等待,直到发送者放弃或目的主机
响应ARP请求。

21.11 arplookup函数

a r p l o o k u p函数调用选路函数 r t a l l o c l在Internet路由表中查找ARP结点。我们已经看
到过3次调用arplookup的情况:
1) 在in_arpinput中,在接收到ARP分组后,对应源 IP地址查找或创建一个 ARP结点。
2) 在i n _ a r p i n p u t中,接收到 A R P请求后,查看是否存在目的硬件地址的代理 A R P结
点。
3) 在arpresolve中,查找或创建一个对应待发送数据报 IP地址的ARP结点。
如果a r p l o o k u p执行成功,则返回一个指向相应 l l i n f o _ a r p结构的指针,否则返回一
个空指针。
a r p l o o k u p带有三个参数,第一个参数是目的 I P地址;第二个参数是个标志,为真时表
示若找不到相应结点就创建一个新的结点;第三个参数也是一个标志,为真时表示查找或创
建代理ARP结点。
代理 A R P 结点通过定义一个不同形式的 I n t e r n e t插口地址结构来处理,即 s o c k a d d r _
inarp结构,如图 21-26所示。该结构只在 ARP中使用。

图21-26 sockaddr_inarp 结构

111-119 前面8个字节与s o c k a d d r _ i n结构相同, s i n _ f a m i l y被设为A F _ I N E T。最后8

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 563
个字节有所不同: s i n _ s r c a d d r、s i n _ t o s和s i n _ o t h e r成员。当结点作为代理结点时,
只用到sin_other成员,并将其设为 SIN_PROXY(1)。
图21-27显示了arplookup函数。

图21-27 arplookup 函数:在路由表中查找 ARP结点

1. 初始化sockaddr_inarp结构,准备查找
480-489 s i n _ a d d r 成员设为将要查找的 I P 地址。如果 p r o x y 参数值不为 0,则
sin_other成员设为SIN_PROXY;否则设为 0。
2. 路由表中查找结点
490-492 r t a l l o c l在I n t e r n e t路由表中查找 I P地址,如果 c r e a t e参数值不为 0,就创
建一个新的结点。如果找不到结点,则函数返回值为 0(空指针)。
3. 减少路由表结点的引用计数值
493 如果找到了结点,则减少路由表结点的引用计数。因为,此时ARP不再被认为像运输层一
样“持有”路由表结点,因此,路由表查找时对rt_refcnt计数的递增,应在这里由ARP取消。
494-499 如果将标志 R T F _ G A T E W A Y置位,或者标志 R T F _ L L I N F O没有置位,或者由
r t _ g a t e w a y指向的插口地址结构的地址族字段值不是 A F _ L I N K,说明出了某些差错,返回
一个空指针。如果结点是这样创建的,应创建一个记录报文。
记录报文中对arptnew的注释是针对老版本 Net/2中创建ARP结点的。
如果r t a l l o c l由于匹配结点的 R T F _ C L O N I N G标志置位而创建一个新的结点,那么函数
arp_rtrequest (21.13节)也要被rtrequest调用。

21.12 代理ARP

N e t / 3支持代理 A R P,有两种不同类型的代理 A R P结点,可以通过 a r p命令及 p u b选项将

Linux公社 www.linuxidc.com
bbs.theithome.com
564 计计TCP/IP详解 卷 2:实现

它们加入到路由表中。添加代理 A R P选项会使 a r p _ r t r e q u e s t主动发送动态联编信息 (如图


21-28所示),因为在创建结点时 RTF_ANNOUNCE标志位被置 1。
代理A R P结点的第一种类型:它允许将网络内的某一主机的 I P地址填入到 A R P高速缓存
内。硬件地址可以设为任意值。这种结点加入到路由表中时使用了直接的掩码 0 x f f f f f f f f。
加掩码的目的是即使插口地址的 S I N _ P R O X Y标志位为 1,在调用图2 1 - 2 7中的r t a l l o c l时能
与该结点匹配。于是在调用图 2 1 - 2 0中的 a r p l o o k u p 时也能与该结点匹配,目的地址的
SIN_PROXY置位。
如果本网中的主机 H 1不能实现 A R P,那么可以使用这种类型的代理 A R P结点。作为代理
的主机代替H 1回答所有的A R P请求,同时提供创建代理 A R P结点时设定的硬件地址 (比如可以
是H1的以太网地址)。这种类型的结点可以通过 arp -a命令查看,它带有“ published”符号。
第二种类型的代理 A R P结点用于已经存有路由表结点的主机。内核为该目的地址创建另
外一个路由表结点,在这个新的结点中含有链路层的信息 ( 如以太网地址 )。该新结点中
结构(图2 1 - 2 6 )的s i n _ o t h e r成员的S I N _ P R O X Y标志置位。回想一下,
sockaddr_inarp
搜索路由表时是比较 1 2字节的I n t e r n e t插口地址 (图1 8 - 3 9 )。只有当该结构的最后 8字节非零时,
才会用到 S I X _ P R O X Y标志位。当 a r p l o o k u p指定送往 r t a l l o c l的结构中的s i n _ o t h e r成
员中SIN_PROXY的值时,只有路由表中那些匹配的结点的 SIN_PROXY标志置位。
这种类型的代理 A R P结点通常指明了作为代理 A R P服务器的以太网地址。如果某代理
ARP结点是为主机HD创建的,一般有以下步骤:
1) 代理服务器收到来自主机 HS的查找HD硬件地址的广播 ARP请求,主机 HS认为HD在本
地网上;
2) 代理服务器回答请求,并提供本机的以太网地址;
3) HS 将发往HD的数据报发送给代理服务器;
4) 收到发往HD的数据报后,代理服务器利用路由表中关于 HD的信息将数据报转发给HD。
路由器netb使用这种类型的代理 ARP结点,见卷 1 4.6节中的例子。可以通过命令 a r p -
a来查看这些带有“ published (proxy only)”的结点。

21.13 arp_rtrequest函数

图2 1 - 3简要显示了 A R P函数和选路函数之间的关系。在 A R P中,我们将调用两个路由表


函数:
1) a r p l o o k u p调用r t a l l o c l查找A R P结点,如果找不到匹配结点,则创建一个新的
ARP结点。
如果在路由表中找到了匹配结点,且该结点的 R T F _ C L O N I N G标志位没有置位
(即该结点就是目的主机的结点 ),则返回该结点。如果 RTF_CLONING标志位被置位,
r t a l l o c l 以 R T M _ R E S O L V E 命令为参数调用 r t r e q u e s t 。图 1 8 - 2 中的
140.252.13.33和140.252.13.34结点就是这么创建的,它们是从 140.252.13.32的结点复
制而来的。
2) arptfree以RTM_DELETE命令为参数调用 rtrequest,删除对应 ARP结点的路由表
结点。
此外,a r p命令通过发送和接收路由插口上的路由报文来操纵 ARP高速缓存。a r p以命令
Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 565
R T M _ R E S O L V E、R T M _ D E L E T E和R T _ G E T 为参数发布路由信息。前两个参数用于调用
rtrequest,第三个参数用于调用 rtallocl。
最后,当以太网设备驱动程序获得了赋予该接口的 IP地址后,rtinit增加一个网络路由。
于是r t r e q u e s t函数被调用,参数是 R T M _ A D D,标志位是 R T F _ U P和R T F _ C L O N I N G。图18-
2中140 252 13 32结点就是这么创建的。
在第1 9章中我们讲过,每一个 i f a d d r结构都有一个指向函数 (i f a _ r t r e q u e s t成员)的
指针,该函数在创建或删除一个路由表结点时被自动调用。在图 6-17中,对于所有以太网设备,
i n _ i f i n i t将该指针指向 a r p _ r t r e q u e s t函数。因此,当调用路由函数为 A R P创建或删除
路由表结点时,总会调用 arp_rtrequest。当任意路由表函数被调用时, a r p _ r t r e q u e s t
函数的作用是做各种初始化或退出处理所需的工作。例如:当创建新的 A R P 结点时,
a r p _ r t r e q u e s t 内要为 l l i n f o _ a r p结构分配内存。同样,当路由函数处理完一个
RTM_DELETE命令后,arp_rtrequest的工作是删除llinfo_arp结构。
图21-28显示了arp_rtrequest函数的第一部分。

图21-28 arp_rtrequest 函数:RTM_ADD 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
566 计计TCP/IP详解 卷 2:实现

图21-28 (续)

1. 初始化A R P t i m e o u函数
t
92-105 第一次调用 arp_rtrequest函数时(系统初始化阶段,在对第一个以太网接口赋 IP
地址时 ),t i m e o u t函数在一个时钟滴答内调用 a r p t i m e r函数。此后, A R P定时器代码每 5
分钟运行一次,因为 arptimer总是要调用 timeout的。
2. 忽略间接路由
106-107 如果将标志 RTF_GATEWAY置位,则函数返回。 RTF_GATEWAY标志表明该路由表
结点是间接的,而所有 ARP结点都是直接的。
108 一个带有三种可能的 switch语句:RTM_ADD、RTM_RESOLVE和R T M _ D E L E T E (后两
种在后面的图中显示 )。
3. RTM_ADD命令
109 RTM_ADD命令出现在以下两种情况中:执行 arp命令手工创建ARP结点或者 rtinit函
数对以太网接口赋 IP地址(图21-3)。
4. 向后兼容
110-117 若标志RTF _ H O S T没有置位,说明该路由表结点与一个掩码相关 (也就是说是网络
路由,而非主机路由 )。如果掩码不是全 1,那么该结点确实是某一接口的路由,因此,将标
志R T F _ C L O N I N G置位。如注释中所述,这是为了与某些旧版本的路由守护程序兼容。此外,
/etc/netstart中的命令:
route add -net 224.0.0.0 -interface bsdi

为图18-2所示网络创建带有 RTF_CLONING标志的路由表结点。
5. 初始化到接口的网络路由结点
118-126 若标志R T F _ C L O N I N G(i n _ i f i n i t为所有以太网接口设置该标志 )置位,那么该
路由表结点是由 r t i n i t添加的。 r t _ s e t g a t e为s o c k a d d r _ d l结构分配空间,该结构由
r t _ g a t e w a y指针所指。与图 2 1 - 1中1 4 0 . 2 5 2 . 1 3 . 3 2的路由表结点相关的就是该数据链路插口
地址结构。 s d l _ f a m i l y 和s d l _ l e n成员的值是根据静态定义的 n u l l _ s d 而初始化的,
s d l _ t y p e(可能是 I F T _ E T H ER )和s d l _ i n d e x成员的值来自接口的 i f n e t结构。该结构不
包含以太网地址, sdl_alen成员的值为 0。
127-128 最后将时限值设为当前时间,也就是结点的创建时间,执行 b r e a k后返回。对于
在系统初始化时创建的结点,它们的 r m x _ e x p i r e值为系统启动的时间。注意,图 2 1 - 1中该
路由表结点没有相应的 l l i n f o _ a r p结构,所以它不会被 a r p t i m e r处理。但是要用它的
s o c k a d d r _ d l结构,对于以太网中特定主机的路由结点来说,要复制的是 r t _ g a t e w a y结
构,用 R T M _ R E S O L V E命令参数创建路由表结点时, r t r e q u e s t 复制该结构。此外,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 567
netstat程序将sdl_index的值输出为 link#n,见图18-2。
6. 发送免费ARP请求
130-135 若将标志 R T F _ A N N O U N C E置位,则该结点是由 a r p命令带p u b选项创建的。该选
项有两个分支: (1) s o c k a d d r _ i n a r p结构中s i n _ o t h e r成员的S I N _ P R O X Y标志被置位;
( 2 )标志R T F _ A N N O U N C E被置位。因为标志 R T F _ A N N O U N C E被置位,所以 a r p r e q u e s t广播
免费A R P请求。注意,第二个和第三个参数是相同的,即该 A R P请求中,发送方 I P地址和目
的方IP地址是一样的。
136 继续执行针对RTM_RESOLVE命令的case语句。
图2 1 - 2 9 显示了 a r p _ r t r e q u e s t函数的第二部分,处理 R T M _ R E S O L V E 命令。当
r t a l l o c l找到一个 R T F _ C L O N I N G标志位置位的路由表结点且 r t a l l o c l的第二个参数值
(a r p l o o k u p的c r e a t e参数)不为0时,调用该命令。需要分配一个新的 l l i n f o _ a r p结构,
并将其初始化。

图21-29 arp_rtrequest 函数:RTM_RESOLVE 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
568 计计TCP/IP详解 卷 2:实现

图21-29 (续)

7. 验证sockaddr_dl结构
137-144 验证r t _ g a t e w a y指针所指的 s o c k a d d r _ d l结构的s a _ f a m i l y和s a _ l e n成员
的值。接口类型 (可能是IFT_ETHER)和索引值填入新的 sockaddr_dl结构。
8. 处理路由变化
145-146 正常情况下,该路由表结点是新创建的,并没有指向一个 l l i n f o _ a r p结构。如
果l a指针非空,则在路由已发生了变化时调用 a r p _ r t r e q u e s t。此时l l i n f o _ a r p已经分
配,执行break,函数返回。
9. 初始化llinfo_arp结构
147-158 分配一个 l l i n f o _ a r p结构, r t _ l l i n f o中存有指向该结构的指针。统计值变
量a r p _ i n u s e和a r p _ a l l o c a t e d各加1,l l i n f o _ a r p结构置 0。将l a _ h o l d指针置空,
la_asked值置0。
159-161 将r t指针存储于 l l i n f o _ a r p结构中,置R T F _ L L I N F O标志位。如图 1 8 - 2所示,
A R P创建的三个结点 1 4 0 . 2 5 2 . 1 3 . 3 3、1 4 0 . 2 5 2 . 1 3 . 3 4和1 4 0 . 2 5 2 . 1 3 . 3 5都有L标志,和 2 4 0 . 0 . 0 . 1一
样。arp程序只检查该标志 (图19-36)。最后insque将llinfo_arp加入到链接表的首部。
就这样创建了一个 A R P结点: r t r e q u e s t创建路由表结点 (经常为以太网克隆一个特定
网络的结点 ),a r p _ r t r e q u e s t分配和初始化 l l i n f o _ a r p结构。剩下只需广播一个 A R P请
求,在收到回答后填充主机的以太网地址。事件发生的一般次序是: a r p r e s o l v e 调用
a r p l o o k u p,于是 a r p _ r t r e q u e s t被调用(中间可能跟有函数调用,见图 2 1 - 3 )。当控制返
回到arpresolve时,发送ARP广播请求。
10. 处理发给本机的特例情况
162-173 这是4 . 4 B S D新增的测试特例部分 (注释是老版本留下的 )。它创建了图 2 1 - 1中最右
边的路由表结点,该结点包含了本机的 IP地址(140.252.13.35)。i f语句检测它是否等于本机 IP
地址,如等于,那么这个刚创建的结点代表的是本机。
11. 将结点置为永久性,并设置以太网地址
174-176 时限值设为0,意味着该结点是永久有效的—永远不会超时。从接口的 a r p c o m
结构中将硬件地址拷贝至 rt_gateway所指的sockaddr_dl结构中。
12. 将接口指针指向环回接口
177-178 若全局变量 usrloopback值不为0(默认为1),则将路由表结点内的接口指针指向
环回接口。这意味着,如果有数据报发给自己,就送往环回接口。在 4 . 4 B S D以前的版本中,
可以通过/etc/netstart文件中的命令:
route add 140.252.13.35 127.0.0.1

来建立从本机 I P地址到环回接口的路由。 4 . 4 B S D仍然支持这种方式,但已不是必需的了。当

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 569
第一次有数据报发给本机 I P地址时,我们刚才看到的代码会自动创建一个这样的路由。此外,
这些代码对于一个接口只会执行一次。一旦路由表结点和永久性 A R P结点创建好后,它们就
不会超时,所以不会再次出现对本机 IP地址的RTM_RESOLLVE命令。
arp_rtrequest函数的最后部分如图 21-30所示,处理RTM_DELETE请求。从图21-3中,
我们可以看到,该命令是由 a r p命令产生的,用于手工删除一个结点;或者在一个 A R P结点
超时时由arptfree产生。

图21-30 arp_rtrequest函数:RTM_DELETE 命令

13. 验证la指针
182-183 la指针应该是非空的,也就是说路由表结点必须指向一个 llinfo_arp结构;否
则,执行break,函数返回。
14. 删除llinfo_arp结构
184-190 统计值变量 a r p _ i n u s e 减1,r e m q u e 从链表中删除 l l i n f o _ a r p 结构。
r t _ l l i n f o指针置0,清除R T F _ L L I N F O标志。如果该ARP结点保持有mbuf(即该ARP请求未
收到回答),则将mbuf释放。最后释放 llinfo_arp结构。
注意, s w i t c h语句中没有包含 d e f a u l t情况,也没有考虑 R T M _ G E T命令。这是因为
a r p程序产生的 R T M _ G E T命令全部由 r o u t e _ o u t p u t函数处理,并不调用 r t r e q u e s t。此
外,见图 2 1 - 3,在 R T M _ G E T 命令产生的对 r t a l l o c l调用中,指定第二个参数是 0,所以
rtallocl并不调用rtrequest。

21.14 ARP和多播

如果一个 I P数据报要采用多播方式发送, i p _ o u t p u t检测进程是否已将某个特定的接口


赋予插口 (见图12-40)。如果已经赋值,则将数据报发往该接口,否则, i p _ o u t p u t利用路由
表选择输出接口 (见图8 - 2 4 )。因此,对于具有多个多播发送接口的系统来说, I P路由表应指定
每个多播组的默认接口。
在图18-2中我们看到,路由表中有一个结点是为网络 224.0.0.0创建的,该结点具有“ flag”
标志。所有以 2 2 4开头的多播组都以该结点指定的接口 ( l e 0 )为默认接口。对于其他的多播组
(以2 2 5 ~ 2 3 9开头),可以分别创建新的路由表结点,也可以对某个指定多播组创建一个路由表
结点。例如,可以为 2 2 4 . 0 . 11 (网络定时协议 )创建一个与 2 2 4 . 0 . 0 . 0不同的路由表结点。如果路
由表中没有对应某个多播组的结点,同时进程没有用 I P _ M U L T I C A S T _ I F插口选项指明接口,
那么该组的默认接口成为路由表中默认路由的接口。其实图 1 8 - 2中对应 2 2 4 . 0 . 0 . 0的路由表结

Linux公社 www.linuxidc.com
bbs.theithome.com
570 计计TCP/IP详解 卷 2:实现

点并不是必要的,因为默认接口就是 le0。
如果选定的接口是以太网接口,则调用 a r p r e s o l v e将多播组地址映射为相应的以太网
地址。在图 2 1 - 2 3中,映射通过调用宏 E T H E R _ M A P _ I P _ M U L T I C A S T来完成。该宏所做的就
是将该多播组地址的低 23位与一个常量逻辑或 (图12-6),映射不需要 ARP请求和回答,也不需
要进入ARP高速缓存。每次需要映射时,调用该宏。
如果多播组是从另外一个结点复制得来的,那么多播组地址会出现在 A R P缓存里,如图
2 1 - 5所示。因为这些结点将 R T F _ L L I N F O标志置位。它们不会有 A R P请求和回答,所以说不
是真正的 A R P结点。它们也没有相应的链路层地址,宏 E T H E R _ M A P _ I P _ M U L T I C A S T就可以
完成映射。
这些多播组的 A R P结点的时效与正常的 A R P结点不同。在为某个多播组创建一个路由表
结点时,如图 1 8 - 2中的2 2 4 . 0 . 0 . 1,r t r e q u e s t从被克隆的结点中复制 r t _ m e t r i c s结构(图
1 9 - 9 )。图2 1 - 2 8中,网络路由结点的 r m x _ e x p i r e值被设为 R T M _ A D D命令执行的时间,也即
系统初始化的时间。为 224.0.0.1设置的结点也设置为同样的时间。
这就意味着在下次 arptimer执行时,对应多播组224.0.0.1的ARP结点总是超时的。所以,
当下一次在路由表中查找时就需重新创建该结点。

21.15 小结

ARP提供了IP地址到硬件地址的映射,本章讲述了如何实现这种映射。
Net/3实现与以往的 BSD 版本有很大不同。 ARP信息被存放在多个结构里面:路由表、数
据链路插口地址结构和 llinfo_arp结构。图21-1显示了这些结构之间的关系。
发送一个 A R P请求是很简单的:正确填充相关字段后,将请求广播发送出去就行了。处
理请求就要复杂一些,因为每个主机都收到了广播的 A R P 请求。除了响应请求外,
i n _ a r p i n p u t还要检测是否有其他主机正与它使用同一个 IP地址。因为每一个 ARP请求中包
含发送方的 IP和硬件地址,所以网络上的所有主机都可以通过它来更新自己的 ARP结点。
在局域网中, A R P洪泛将是一个问题, N e t / 3是第一个考虑这种问题的 B S D版本。对于同
一个目的地,一秒钟内只可发送一个 A R P请求,如果连续 5个请求都没有收到回答,必须暂停
20秒钟才可再发送去往该目的地的 ARP请求。

习题

21.1 图21-17中给局部变量ac赋值时,做过什么假设?
21.2 如果我们先 p i n g本地以太网的广播地址,之后执行 a r p -a,就可以发现几乎所有
本地以太网上的其他主机的表项都填入到了 ARP高速缓存中。这是为什么?
21.3 查看代码并解释为什么图 21-19中需要把sdl_alen的值赋为6。
21.4 在N e t / 2中有一个独立于路由表而存在的 A R P表,每次调用 a r p r e s o l v e时,都要
在该ARP表中查找。试与 Net/3的方法比较,哪个更有效?
21.5 N e t / 2中的A R P代码显式地设置 A R P高速缓存中非完整表项的超时为 3分钟,非完整
表项是指正在等待 A R P回答的表项。但我们从没有提过 N e t / 3如何处理该超时,那
么Net/3何时才认为非完整表项超时?
21.6 当N e t / 3系统作为一个路由器并且导致洪泛的分组来自其他主机时,为避免 A R P洪

Linux公社 www.linuxidc.com
bbs.theithome.com
第 21章 ARP:地址解析协议计计 571
泛要做哪些变动?
21.7 图21-1中给出的四个rmx_expire变量的值是什么?代码在何处设置该值?
21.8 对广播 A R P请求的每个主机,本章中引起要创建一个 A R P结点的代码需要做哪些变
动?
21.9 为了验证图 2 1 - 2 5中的例子,作者运行了卷 1附录C的s o c k程序,每隔 500 ms向本地
以太网上一个不存在的主机发送一个 U D P数据报 (程序的 - p选项改为等待的毫秒
数)。但是在返回第一个 EHOSTDOWN差错之前,仅无差错地发送了 10个UDP数据报,
而不是图21-25所示的11个,这是为什么?
21.10 修改A R P,使得它在等待 A R P回答时持有到目的主机的所有分组,而不是持有最
近的一个。如何实现这种改变?是否像每个接口的输出队列一样,需要一个限
制?是否需要改变数据结构?

Linux公社 www.linuxidc.com
bbs.theithome.com
第22章 协议控制块
22.1 引言

协议层使用协议控制块 (PCB)存放各UDP和TCP插口所要求的多个信息片。 Internet协议维


护Internet 协议控制块 (Internet protocol control block)和TCP控制块(TCP control block)。因为
UDP是无连接的,所以一个端结点需要的所有信息都可以在 Internet PCB中找到;不存在 UDP
控制块。
Internet PCB含有所有 U D P和T C P端结点共有的信息:外部和本地 I P地址、外部和本地端
号、I P首部原型、该端结点使用的 I P选项以及一个指向该端结点目的地址选路表入口的指针。
TCP控制块包含了TCP为各连接维护的所有结点信息:两个方向的序号、窗口大小、重传次数
等等。
本章我们描述 N e t / 3所用的Internet PCB,在详细讨论 T C P时再探讨 T C P控制块。我们将研
究几个操作 Internet PCB的函数,会在描述 U D P和T C P时遇到它们。大多数的函数以 i n _ p c b
开头。
图2 2 - 1总结了协议控制块以及它们与 f i l e和s o c k e t结构之间的关系。该图中有几点要
考虑:
• 当s o c k e t或a c c e p t创建一个插口后,插口层生成一个 f i l e结构和一个s o c k e t结构。
文件类型是 D T Y P E _ S O C K E T,U D P端结点的插口类型是 S O C K _ D G R A M,T C P端结点的
插口类型是 SOCK_STREAM。
• 然后调用协议层。 UDP创建一个 Internet PCB(一个i n p c b结构),并把它链接到 s o c k e t
结构上:so_pcb成员指向inpcb结构,inp_socket成员指向socket结构。
• T C P做同样的工作,也创建它自己的控制块 (一个t c p c b结构),并用指针 i n p _ p p c b和
t_inpcb把它链接到 inpcb上。在两个 UDP inpcb中,inp_ppcb成员是一个空指针,
因为UDP不负责维护它自己的控制块。
• 我们显示的其他四个 i n p c b结构的成员,从 i n p _ f a d d r到i n p _ l p o r t,形成了该端结
点的插口对:外部 IP地址和端口号,以及本地 IP地址和端口号。
• UDP和TCP用指针inp_next和inp_prev维护一个所有Internet PCB的双向链表。它们
在表头分配一个全局 i n p c b结构(命名为 u d b和t c b),在该结构中只使用三个成员:下
一个和前一个指针,以及本地端口号。后一个成员中包含了该协议使用的下一个临时端
口号。
Internet PCB是一个传输层数据结构。 T C P、U D P和原始 I P使用它,但 I P、I C M P或I C M P
不用它。
我们还没有讲过原始 I P,但它也用 Internet PCB。与T C P和U D P不同,原始 I P在P C B中不
用端口号成员,原始 I P 只用本章中提到的两个函数: i n _ p c b a l l o c 分配 P C B ,
in_pcbdetach释放PCB。第32章将讨论原始IP。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 573
描述符 描述符 描述符 描述符

插口层
协议层

所有UDP的双向循环链表
Internet协议控制块

所有TCP的双向循环链表
Internet协议控制块和相关的TCP控制块

图22-1 Internet协议控制块以及与其他结构之间的关系

22.2 代码介绍

所有PCB函数都在一个C文件和一个包含定义的头文件中,如图 22-2所示。

文 件 描 述

netinet/in_pcb.h i n _ p c b结构定义
netinet/in_pcb.c PCB函数

图22-2 本章中讨论的文件

Linux公社 www.linuxidc.com
bbs.theithome.com
574 计计TCP/IP详解 卷 2:实现

22.2.1 全局变量

本章只引入一个全局变量,如图 22-3所示。

变 量 数据类型 描 述

zeroin_addr s t r u c t i n _ a d d r 32 bit 全零IP地址

图22-3 本章中引入的全局变量

22.2.2 统计量

Internet PCB和TCP PCB都是内核的m a l l o c函数分配的 M _ P C B类型。这只是内核分配的


大约 6 0种不同类型内存的一种。例如, m b u f的类型是 M _ B U F,s o c k e t结构分配的类型是
M_SOCKET。
因为内核保持所分配的不同类型内存缓存的计数器,所以维护着几个 P C B数量的统计量。
v m s t a t -m命令显示内核的内存分配统计信息, n e t s t a t -m命令显示的是m b u f分配统计
信息。

22.3 inpcb的结构

图22-4是inpcb结构的定义。这不是一个大结构,只占 84个字节。

图22-4 inpcb 结构

43-45 i n p _ n e x t和i n p _ p r e v为U D P和T C P的所有 P C B形成一个双向链表。另外,每个


P C B都有一个指向协议链表表头的指针 (i n p _ h e a d)。对U D P表上的P C B,i n p _ h e a d总是指
向udb(图22-1);对TCP表上的PCB,这个指针总是指向 tcb。
46-49 下面四个成员: i n p _ f a d d r、i n p _ f p o r t、i n p _ l a d d r和i n p _ l p o r t,包含了
这个IP端结点的插口对:外部 IP地址和端口号,以及本地 IP地址和端口号。 PCB中以网络字节
序而不是以主机字节序维护这四个值。
运输层的 T C P和U D P都使用 Internet PCB。尽管在这个结构里保存本地和外部 I P
地址很有意义,但端口号并不属于这里。端口号及其大小的定义是由各运输层协议
Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 575
指定的,不同的运输层可以指定不同的值。 [Partridge 1987] 提出了这个问题,其中
版本1的R D P采用8 bit 的端口号,需要用 8 bit 的端口号重新实现几个标准内核程序。
版本2的RDP [Partridge和Hinden 1990] 采用16 bit端口号。实际上,端口号属于运输
层专用控制块,例如 T C P的t c p c b。可能会要求采用一种新的 U D P专用的 P C B。尽管
这个方案可行,但却可能使我们马上要讨论的几个程序复杂化。
50-51 inp_socket是一个指向该PCB的socket结构的指针,inp_ppcb是一个指针,它
指向这个 PCB的可选运输层专用控制块。我们在图 22-1中看到, i n p _ p p c b和TCP一起指向对
应的 t c p c b,但U D P不用它。 s o c k e t和i n p c b之间的链接是双向的,因为有时内核从插口
层开始,需要对应的 Internet PCB( 如用户输出 ),而有时内核从 P C B开始,需要找到对应的
socket结构(如处理收到的IP数据报)。
52 如果IP有一个到外部地址的路由,则它被保存在 ipp_route入口处。我们将看到,当收
到一个ICMP重定向报文时,将扫描所有 Internet PCB,找到那些外部 IP地址与重定向IP地址匹
配的P C B,将其i n p _ r o u t e入口标记成无效。当再次将该 P C B用于输出时,迫使 I P重新找一
条到该外部地址的新路由。
53 inp_flags成员中存放了几个标志。图 22-5显示了各标志。

inp_flags 描 述

INP_HDRINCL 进程提供整个 IP首部(只有原始插口 )


INP_RECVOPTS 把到达IP选项作为控制信息接收 (只有UDP,还没有实现 )
INP_RECVRETOPTS 把回答的IP选项作为控制信息接收 (只有UDP,还没有实现 )
INP_RECVDSTADDR 把IP目的地址作为控制信息接收 (只有UDP)
INP_CONTROLOPTS INP_RECVOPTS|INP-RECVRETOPTS|INP_RECVDSTADDR

图22-5 inp_flags 值

54 PCB中维护一个 IP首部的备份,但它只使用其中的两个成员, TOS和TTL。TOS被初始化


为0 (普通业务 ),T T L被运输层初始化。我们将看到, T C P和U D P都把T T L的默认值设为 6 4。
进程可以用 IP_TOS或IP_TTL插口选项改变这些默认值,新的值记录在 inpcb-inp_ip结构
中。以后, TCP和UDP在发送IP数据报时,却把该结构用作原型 IP首部。
55-56 进程也可以用 I P _ O P T I O N S插口选项设置外出数据报的 I P选项。函数i p _ p c b o p t s
把调用方选项的备份存放在一个 m b u f中,i n p _ o p t i o n s成员是一个指向该 m b u f的指针。每
次T C P和U D P调用i p _ o u t p u t函数时,就把一个指向这些 I P首部的指针传给 I P,I P将其插到
出去的IP数据报中。类似地, inp_moptions成员是一个指向用户 IP多播选项备份的指针。

22.4 in_pcballoc和in_pcbdetach函数

在创建插口时, T C P、U D P和原始 I P会分配一个 Internet PCB 。系统调用 s o c k e t发布


PRU_ATTACH请求。在UDP情况下,我们将在图 23-33中看到,产生的调用是
struct socket *so;
int error;

error = in_pcballoc(so, &udb);

图22-6是in_pcballoc函数。
1. 分配PCB,初始化为零

Linux公社 www.linuxidc.com
bbs.theithome.com
576 计计TCP/IP详解 卷 2:实现

图22-6 in_pcballoc 函数:分配一个 Internet PCB

36-45 i n _ p c b a l l o c使用宏M A L L O C调用内核的内存分配器。因为这些 PCB总是作为系统


调用的结果分配的,所以总能等到一个。
Net/2和早期的伯克利版本把 Internet PCB和TCP PCB都保存在mbuf中。它们的大
小分别是 8 0和1 0 8字节。 N e t / 3版本中的大小变成了 8 4和1 4 0字节,所以 T C P控制块不
再适合存放在mbuf中。Net/3使用内核的内存分配器而不是 mbuf分配两种控制块。
细心的读者会注意到图 2 - 6的例子中,为 P C B分配了 1 7个m b u f,而我们刚刚讲到
Net/3不再用mbuf存放Internet PCB和TCP PCB。但是, Net/3的确用mbuf存放Unix域
的P C B,这就是计数器所指的。 n e t s t a t输出的 m b u f统计信息是针对内核为所有协
议族分配的 mbuf,而不仅仅是Internet协议族。
bzero把PCB设成0。这非常重要,因为 PCB 中的IP地址和端口号必须被初始化成 0。

图22-7 in_pcbdetach 函数:释放一个 Internet PCB

2. 把结构链接起来
46-49 i n _ h e a d成员指向协议的 PCB表头(u d b或t c p),i n p _ s o c k e t成员指向s o c k e t结

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 577
构,新的 PCB结构被加到协议的双向链表上 (i n s q u e),s o c k e t结构指向该 PCB。i n s q u e函
数把新的PCB放到协议表的表头里。
在发布P R U _ D E T A C H请求后,释放一个 Internet PCB,这是在关闭插口时发生的。图 2 2 - 7
显示了in_pcbdetach函数,最后将调用它。
252-263 s o c k e t结构中的 P C B指针被设成 0,s o f r e e释放该结构。如果给这个 P C B分配
的是一个有 IP选项的mbuf,则由m_free将其释放。如果该 PCB 中有一个路由,则由 rtfree
将其释放。所有多播选项都由 ip_freemoptions释放。
264-265 remque把该PCB从协议的双向链表中移走,该 PCB使用的内存被返回给内核。

22.5 绑定、连接和分用

在研究绑定插口、连接插口和分用进入的数据报的内核函数之前,我们先来看一下内核
对这些动作施加的限制规则。
1. 绑定本地IP地址和端口号
图22-8是进程在调用bind时可以指定的本地 IP地址和本地端口号的六种组合。
前三行通常是服务器的—它们绑定某个特定端口,称为服务器的知名端口 ( w e l l - k n o w n
p o r t ),客户都知道这些端口的值。后三行通常是客户的—它们不考虑本地的端口,称为临
时端口(ephemeral port),只要它在客户主机上是唯一的。
大多数服务器和客户在调用 b i n d时,都指定通配 I P地址。如图 2 2 - 8所示,在第 3行和第6
行中,用*表示。

本地IP地址 本地端口 描 述

单播或广播 非零 一个本地接口,特定端口
多播 非零 一个本地多播组,特定端口
* 非零 任何本地接口或多播组,特定端口
单播或广播 0 一个本地接口,内核选择端口
多播 0 一个多播组,内核选择端口
* 0 任何本地接口,内核选择端口

图22-8 bind 的本地IP地址和本地端口号的组合

如果服务器把某个特定 I P地址绑定到某个插口上 (也就是说,不是通配地址 ),那么进入的


IP数据报中,只有那些以该特定 IP地址作为目的IP地址的IP数据报—不管是单播、广播或多
播—都被交付给该进程。自然地,当进程把某个特定单播或广播 I P地址绑定到某个插口上
时,内核验证该 IP地址与一个本地接口对应。
尽管可能,但很少出现客户程序绑定某个特定 I P地址的情况 (图2 2 - 8中的第 4行和第 5行)。
通常客户绑定通配 I P地址(图2 2 - 8中的最后一行 ),让内核根据自己选择的到服务器的路由来选
择外出的接口。
图2 2 - 8没有显示如果客户程序试图绑定一个已经被其他插口使用的本地端口时会发生什
么情况。默认情况下,如果一个端口已经被使用,进程是不能绑定它的。如果发生这种情况,
则返回 E A D D R I N U S E差错(地址正在被使用 )。正在被使用 (in use)的定义很简单,就是只要存
在一个 P C B,就把该端口作为它的本地端口。“正在被使用”的概念是相对于绑定协议的:
TCP或UDP,因为TCP端口号与UDP端口号无关。

Linux公社 www.linuxidc.com
bbs.theithome.com
578 计计TCP/IP详解 卷 2:实现

Net/3允许进程指定以下两个插口选项来改变这个默认行为:
SO_REUSEADDR 允许进程绑定一个正在被使用的端口号,但被绑定的 I P地址(包括通配
地址)必须没有被绑定到同一个端口。
例如,如果连到的接口的 I P地址是1 4 0 . 2 5 2 . 1 . 2 9,则一个插口可以被绑
定到1 4 0 . 2 5 2 . 1 . 2 9,端口 5 5 5 5;另一个插口可绑定到 1 2 7 . 0 . 0 . 1,端口
5 5 5 5;还有一个插口可以绑定到通配 I P地址,端口 5 5 5 5。在第二种和
第三种情况下调用 b i n d 之前,必须先调用 s e t s o c k o p t ,设置
SO_REUSEADDR选项。
SO_REUSEPORT 允许进程重用 I P地址和端口号,但是包括第一个在内的各个 I P地址和
端口号,必须指定这个插口选项。和 S O _ R E U S E A D D R一样,第一次绑
定端口号时要指定插口选项。
例如,如果连到的接口具有 1 4 0 . 2 5 2 . 1 . 2 9的I P地址,并且某个插口绑定
到140.252.1.29,端口6666,并指定SO_REUSEPORT插口选项,则另一
个插口也可以指定同一个插口选项,并绑定140.252.1.29,端口6666。
本节的后面将讨论在后一个例子中,当到达一个目的地址是 1 4 0 . 2 5 2 . 1 . 2 9,目的端口是
6666的IP数据报时,会发生什么情况。因为这两个插口都被绑定到该端结点上。
S O _ R E U S E P O R T是Net/3新加上的,在4.4BSD中是为支持多播而引入的。在这个
版本之前,两个插口是不可能绑定到同一个 IP地址和同一个端口号的。
不幸的是, S O _ R E U S E P O R T不是原来的标准多播源程序的内容,所以对它的支
持并不广泛。其他支持多播的系统,如 Solaris 2.x,让进程指定SO_REUSEADDR来表
明允许把多个端口绑定到同一 IP地址和相同的端口号。
2. 连接一个UDP插口
我们通常把 c o n n e c t系统调用和 T C P客户联系起来,但是 U D P客户或 U D P服务器也可能
调用c o n n e c t,为插口指定外部 I P地址和外部端口号。这就限制插口必须只与某个特定对方
交换UDP数据报。
当连接U D P插口时,会有一个副作用:本地 I P地址,如果在调用 b i n d时没有指定,会自
动被connect设置。它被设成由 IP选路指定对方所选择的本地接口地址。
图22-9显示了UDP插口的三种不同的状态,以及函数为终止各状态调用的伪代码。

本地插口 外部插口 描 述

localIP.lport foreignIP.fport 限制到一个对方:


s o c k e t ( ), b i n d ( * .lport), c o n n e c t (foreignIP, fport)
s o c k e t ( ), b i n d (localIP, lport), c o n n e c t (foreignIP, fport)
localIP.lport *.* 限制在本地接口上到达的数据报: localIP
s o c k e t ( ) , b i n(dlocalIP, lport)
*.lport *.* 接收所有发到 lport的数据报:
s o c k e t ( ) , b i n d (*, lport)

图22-9 UDP插口的本地和外部 IP地址和端口号规范

前三个状态叫做已连接的 U D P插口(connected UDP socket),后两个叫做未连接的 U D P插


口(unconnected UDP socket)。两个没有连接上的 U D P插口的区别在于,第一个具有一个完全

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 579
指定的本地地址,而第二个具有一个通配本地 IP地址。
3. 分用TCP接收的IP数据报
图2 2 - 1 0显示了主机 s u n上的三个 Te l n e t服务器的状态。前两个插口处于 L I S T E N状态,等
待进入的连接请求,加三个连接到 I P地址是140 252 111的主机上的端口 1 5 0 0。第一个监听插
口处理在接口 1 4 0 . 2 5 2 . 1 2 9上到达的连接请求,第二个监听插口将处理所有其他接口 (因为它的
本地IP地址是通配地址 )。

本地地址 本地端口 外部地址 外部端口 TCP状态

140.252.1.29 23 * * LISTEN
* 23 * * LISTEN
140.252.1.29 23 140.252.1.11 1500 ESTABLISHED

图22-10 本地端口是 23的三个TCP插口

两个具有未指定的外部 I P地址和端口号的监听插口都显示了出来,因为插口 A P I不允许


T C P服务器限制任何一个值。 T C P服务器必须 a c c e p t客户的连接,并在连接建立完成之后
(也就是说,当 T C P的三次握手结束之后 )被告知客户的 I P地址和端口号。只有到这个时候,如
果服务器不喜欢客户的 I P地址和端口号,才能关闭连接。这并不是对 T C P要求的特性,这只
是插口API通常的工作方式。
当T C P收到一个目的端口是 2 3的报文段时,它调用 i n _ p c b l o o k u p,搜索它的整个
Internet PCB表,找到一个匹配。马上我们会研究这个函数,将看到它有优先权,因为它的通
配匹配 (wildcard match)数最少。为了确定通配匹配数,我们只考虑本地和外部的 I P地址,不
考虑外部端口号。本地端口号必须匹配,否则我们甚至不考虑 P C B。通配匹配数可以是 0、
1(本地IP地址或外部 IP地址)或2(本地和外部 IP地址)。
例如,假定到达报文段来自 1 4 0 . 2 5 2 . 1 . 11,端口 1 5 0 0,目的地是 1 4 0 . 2 5 2 . 1 . 2 9,端口 2 3。
图22-11是图22-10中三个插口的通配匹配数。

本地地址 本地端口 外部地址 外部端口 TCP状态 通配匹配数

140.252.1.29 23 * * LISTEN 1
* 23 * * LISTEN 2
140.252.1.29 23 140.252.1.11 1500 ESTABLISHED 0

图22-11 从{140.252.1.11, 1500}到{140.252.1.29, 23}的到达报文段

第一个插口匹配这四个值,但有一个通配匹配 (外部I P地址)。第二个插口也和到达报文段


匹配,但有两个通配匹配 (本地和外部 I P地址)。第三个插口是一个没有通配匹配的完全匹配。
Net/3使用第三个插口,它具有最小通配匹配数。
继续这个例子,假定到达报文段来自 1 4 0 . 2 5 2 . 1 . 11,端口 1 5 0 1,目的地是 1 4 0 . 2 5 2 . 1 . 2 9,
端口23。图22-12显示了通配匹配数。

本地地址 本地端口 外部地址 外部端口 TCP状态 通配匹配数

140.252.1.29 23 * * LISTEN 1
* 23 * * LISTEN 2
140.252.1.29 23 140.252.1.11 1500 ESTABLISHED

图22-12 从{140.252.1.11, 1501}到{140.252.1.29, 23}的到达报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
580 计计TCP/IP详解 卷 2:实现

第一个插口匹配有一个通配匹配;第二个插口匹配有两个通配匹配;第三个插口根本不
匹配,因为外部端口号不相等 (只有当P C B中的外部 I P地址不是通配地址时,才比较外部端口
号)。所以选择第一个插口。
在这两个例子中,我们没有提到到达 T C P报文段的类型:假定图 2 2 - 11中的报文段包含数
据或对一个已经建立的连接的确认,因为它是发送到一个已经建立的插口上的。我们还假定,
图2 2 - 1 2中的报文段是一个到达的连接请求 (一个S Y N ),因为它是发送给一个正在监听的插口
的。但是 i n _ p c b l o o k u p的分用代码并不关心这些。如果 T C P报文段对交付的插口来说是错
误的类型,我们将在后面看到, T C P会处理这种情况。现在,重要的是,分用代码只把 I P数
据报中的源和目的插口对的值与 PCB中的值进行比较。
4. 分用UDP接收的IP数据报
U D P数据报的交付比我们刚才研究的 T C P的例子要复杂得多,因为可以把 U D P数据报发
送到一个广播或多播地址。因为 N e t / 3 (以及大多数支持多播的系统 )允许多个插口有相同的本
地IP地址和端口,所以如何处理多个接收方的情况呢? Net/3的规则是:
1) 把目的地是广播 I P地址或多播 I P地址的到达 U D P数据报交付给所有匹配的插口。这里
没有“最好的”匹配的概念 (也就是具有最小通配匹配数的匹配 )。
2) 把目的地是单播 I P地址的到达 U D P数据报只交付给一个匹配的插口,就是具有最小通
配匹配数的插口。如果有多个插口具有相同的“最小”通配匹配数,那么具体由哪个插口来
接收到达数据报依赖于不同的实现。
图2 2 - 1 3显示了四个我们将在后面例子中使用的 U D P插口。要使四个 U D P插口具有相同的
本地端口号需要使用 S O _R E U S E A D D R或SO_REUSEPORT。前两个插口已经被连接到一个外部
IP地址和端口号,后面两个没有任何连接。

本地地址 本地端口 外部地址 外部端口 说 明

140.252.1.29 577 140.252.1.11 1500 已连接,本地 IP = 单播


140.252.13.63 577 140.252.13.35 1500 已连接,本地 IP = 广播
140.252.13.63 577 * * 未连接,本地 IP = 广播
* 577 * * 未连接,本地 IP = 通配地址

图22-13 四个本地端口为 577的UDP插口

考虑目的地是 1 4 0 . 2 5 2 . 1 3 . 6 3 ( 位于子网 1 4 0 . 2 5 2 . 1 3 上的广播地址 ) ,端口 5 7 7 ,来自


140.252.13.34,端口1500。图22-14显示它被交付给第三和第四个插口。

本地地址 本地端口 外部地址 外部端口 交 付?

140.252.1.29 577 140.252.1.11 1500 不,本地和外部 IP不匹配


140.252.13.63 577 140.252.13.35 1500 不,外部 IP不匹配
140.252.13.63 577 * * 交付
* 577 * * 交付

图22-14 接收从{140.252.13.34, 1500}到{140.252.13.63, 577}的数据报

广播数据报不交付给第一个插口,因为本地 IP地址和目的 IP地址不匹配,外部 IP地址和源


IP地址也不匹配。也不把它交付给第二个插口,因为外部 IP地址和源 IP地址不匹配。
对于下一个例子,考虑目的地是140.252.129(一个单播地址),端口577,来自140 252 1.111,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 581
端口1500。图22-15显示了把该数据报交付给哪个端口。

本地地址 本地端口 外部地址 外部端口 交 付?

140.252.1.29 577 140.252.1.11 1500 交付,0个通配匹配


140.252.13.63 577 140.252.13.35 1500 不,本地和外部 IP不匹配
140.252.13.63 577 * * 不,本地 IP不匹配
* 577 * * 不,2个通配匹配

图22-15 接收从{140.252.1.11, 1500}到{140.252.1.29, 577}的数据报

该数据报和第一个插口匹配,且没有通配匹配;也和第四个插口匹配,但有两个通配匹
配。所以,它被交付给第一个插口,最好的匹配。

22.6 in_pcblookup函数

in_pcblookup函数有几个作用:
1) 当TCP或UDP收到一个IP数据报时, in_pcblookup扫描协议的 Internet PCB表,寻找
一个匹配的 PCB,来接收该数据报。这是运输层对收到数据报的分用。
2) 当进程执行 b i n d系统调用,为某个插口分配一个本地 IP地址和本地端口号时,协议调
用in_pcbbind,验证请求的本地地址对没有被使用。
3) 当进程执行b i n d系统调用,请求给它的插口分配一个临时端口时,内核选了一个临时
端口,并调用 i n _ p c b b i n d检查该端口是否正在被使用。如果正在被使用,就试下一个端口
号,以此类推,直到找到一个没有被使用的端口号。
4) 当进程显式或隐式地执行 c o n n e c t系统调用时,i n _ p c b b i n d验证请求的插口对是唯
一的(当在一个没有连接上的插口上发送一个 UDP数据报时,会隐式地调用 c o n n e c t,我们将
在第23章看到这种情况 )。
在第2种、第 3种和第 4种情况下, i n _ p c b b i n d调用i n _ p c b l o o k u p。两个选项使该函
数的逻辑显得有些混乱。首先,进程可以指定 S O _ R E U S E A D D R或S O _ R E U S E P O R T插口选项,
表明允许复制本地地址。
其次,有时通配匹配也是允许的 (例如,一个到达 U D P数据报可以和一个自己的本地 I P地
址有通配符的 P C B匹配,意味着该插口将接收在任何本地接口上到达的 U D P数据报),而其他
情况下,一个通配匹配是禁止的 (例如,当连接到一个外部 IP地址和端口号时 )。
在原始的标准 I P多播代码中,出现了这样的注释“ i n _ p c b l o o k u p的逻辑比较
模糊,也没有一点说明……”。形容词模糊比较保守。
公开的I P多播码是 B S D/3 8 6的,是由 Craig Leres从4 . 4 B S D派生而来的。他修改
了该函数过载的语义,只对上面的第 1种情况使用 i n _ p c b l o o k u p。第2种和第 4种
情况由一个新函数 i n _ p c b c o n f l i c t处理。情况 3由新函数 i n _ u n i q u e p o r t处理。
把原来的功能分成几个独立的函数就显得更清楚了,但在我们描述的 N e t / 3版本中,
整个逻辑还是结合在一个函数 in_pcblookup中。
图22-16显示了in_pcblookuup函数。
该函数从协议的 P C B表的表头开始,并可能会遍历表中的每个 P C B。变量 m a t c h记录了
到目前为止最佳匹配的指针入口, m a t c h w i l d记录在该匹配中的通配匹配数。后者被初始化

Linux公社 www.linuxidc.com
bbs.theithome.com
582 计计TCP/IP详解 卷 2:实现

成3,比可能遇到的最大通配匹配数还大 (任何大于2的值都可以)。每次循环时, w i l d c a r d从
0开始,计数每个 PCB的通配匹配数。
1. 比较本地端口号
第一个比较的是本地端口号。如果 PCB 的本地端口和lport参数不匹配,则忽略该 PCB。

图22-16 in_pcblookuup 函数:搜索所有 PCB寻找匹配

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 583
2. 比较本地地址
419-4 27 in_pcbl o o k u p比较PCB内的本地地址和 l a d d r参数。如果有一个是通配地址,
另一个不是,则 w i l d c a r d计数器加 1。如果都不是通配地址,则它们必须一样;否则忽略这
个PCB 。如果都是通配地址,则什么也不改变:它们不可比,也不增加 w i l d c a r d计数器。
图22-17对四种不同的情况做了小结。

PCB本地IP laddr参数 描 述

不是* * wildcard++
不是* 不是* 比较IP地址,如果不相等,则略过 PCB
* * 不能比较
* 不是* wildcard++

图22-17 in_pcblookup 做的四种IP地址比较

3. 比较外部地址和外部端口号
428-437 这几行完成与我们刚才讲的同样的检查,但是用外部地址而不是本地地址。而且,
如果两个外部地址都不是通配地址,则不仅两个 I P地址必须相等,而且两个外部端口也必须
相等。图22-18对外部IP地址的比较作了总结。

PCB外部IP faddr参数 描 述

不是* * wildcard++
不是* 不是* 比较IP地址和端口,如果不相等,则略过 PCB
* * 不能比较
* 不是* wildcard++

图22-18 in_pcblookup 做的四种外部 IP地址比较

可以对图 2 2 - 1 8中的第二行进行另外的外部端口号比较,因为一个 P C B不可能具有非通配


外部地址,且外部端口号为 0。这个限制是由 c o n n e c t加上的,我们马上就会看到,该函数
要求一个非通配外部 I P地址和一个非零外部端口。但是,也可能,并且通常都是具有一个通
配本地地址和一个非零本地端口。我们在图 22-10和图22-13看到过这种情况。
4. 检查是否允许通配匹配
4 3 8-4 3 9 参数flags可以被设成INPLOOKUP_WILDCARD,意味着允许匹配中包含通配匹
配。如果在匹配中有通配匹配 (w i l d c a r d非零),并且调用方没有指定这个标志位,则忽略这
个PCB。当TCP和UDP调用这个函数分用一个到达数据报时,总是把 INPLOOKUP_WILDCARD
置位,因为允许通配匹配 (记住我们用图 2 2 - 1 0和图2 2 - 1 3所作的例子)。但是,当这个函数作为
connect系统调用的一部分而调用时,为了验证一个插口对没有被使用,把 flags参数设成0。
5. 记录最佳匹配,如果找到确切匹配,则返回
440-447 这些语句记录到目前为止找到的最佳匹配。重复一下,最佳匹配是具有最小通配
匹配数的匹配。如果一个匹配有一个或两个通配匹配,则记录该匹配,循环继续。但是,如
果找到一个确切的匹配 (wildcard是0),则循环终止,返回一个指向该确切匹配 PCB的指针。
例子—分用收到的 TCP报文段
图2 2 - 1 9取自我们在图 2 2 - 11中的 T C P 的例子。假定 i n _ p c b l o o k u p 正在分用一个从
140.252.1.11即端口1500到140.252.1.29即端口23的数据报。还假定PCB的顺序是图中行的顺序。

Linux公社 www.linuxidc.com
bbs.theithome.com
584 计计TCP/IP详解 卷 2:实现

laddr是目的IP地址,lport是目的TCP端口,faddr是源IP地址,fport是源TCP端口。

PCB值
wildcard
本地地址 本地端口 外部地址 外部端口

140.252.1.29 23 * * 1
* 23 * * 2
140.252.1.29 23 140.252.1.11 1500 0

图22-19 laddr =140.252.1.29,lport= 23,faddr = 140.252.1.11,fport= 1500

当把第一行和到达报文段比较时, w i l d c a r d 是 1 ( 外部 I P 地址 ) , f l a g s 被设成
I N P L O O K U P _ W I L D C A R D,所以把 m a t c h设成指向该 P C B,m a t c h w i l d设为1。因为还没有
找到确切的匹配,所以循环继续。下一次循环中, w i l d c a r d是2 (本地和外部 I P地址),因为
比 m a t c h w i l d 大,所以不记录该入口,循环继续。再次循环时, w i l d c a r d 是 0,比
m a t c h w i l d( 1 )小,所以把这个入口记录在 m a t c h中。因为已经找到了一个确切的地址,所
以终止循环,把指向该 PCB的指针返回给调用方。
如果T C P和U D P只用 i n _ p c b l o o k u p来分用到达数据报,就可以对它进行简化。首先,
没有必要检查 f a d d r或l a d d r是否是通配地址,因为它们是收到数据报的源和目的 I P地址。
参数flags以及与相应的检测也可以不要,因为允许通配匹配。
这一节讨论了 i n _ p c b l o o k u p 函数的机制。我们在讨论 in_pcbbind和
in_pcbconnect如何调用这个函数后,将继续回来讨论它的意义。

22.7 in_pcbbind函数

下一个函数 i n _ p c b b i n d,把一个本地地址和端口号绑定到一个插口上。从五个函数中
调用它:
1) bind为某个TCP插口调用(通常绑定到服务器的一个知名端口上 );
2) b i n d为某个 U D P插口调用 (绑定到服务器的一个知名端口上,或者绑定到客户插口的
一个临时端口上 );
3) c o n n e c t为某个TCP插口调用,如果该插口还没有绑定到一个非零端口上 (对TCP客户
来说,这是一种典型情况 );
4) l i s t e n为某个TCP 插口调用,如果该插口还没有绑定到一个非零端口 (这很少见,因
为是TCP 服务器调用 listen,TCP服务器通常绑定到一个知名端口上,而不是临时端口 );
5) 从i n _ p c b c o n n e c t( 2 2 . 8节)调用,如果本地 I P地址和本地端口号被置位 (当为一个
UDP插口调用 connect,或为一个未连接 UDP插口调用sendto时,这种情况比较典型 )。
在第3种、第4种和第 5种情形下,把一个临时端口号绑定到该插口上,不改变本地 I P地址
(在它已经被置位的情况下 )。
称情形1和情形2为显式绑定 (explicit bind),情形3、4和5为隐式绑定 (implicit bind)。我们
也注意到,尽管在情形 2时,服务器绑定到一个知名端口是很正常的,但那些用远程过程调用
(RPC)启动的服务器也常常绑定到临时端口上,然后用其他程序注册它们的临时端口,该程序
维护在该服务器的 R P C程序号与其临时端口之间的映射 (例如,卷 1的2 9 . 4节描述的 S u n端口映
射器)。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 585
我们分三部分显示 in_pcbbind函数。图22-20是第一部分。

图22-20 in_pcbbind 函数:绑定本地地址和端口号

64-67 前两个测试验证至少有一个接口已经被分配了一个 I P地址,且该插口还没有绑定。


不能两次绑定一个插口。
68-71 这个if语句有点令人疑问。总的结果是如果 SO_REUSEADDR和SO_REUSEPORT都没
有置位,就把wild设置成INPLOOKUP_WILDCARD。
对U D P来说,第二个测试为真,因为 P R _ C O N N R E Q U I R E D对无连接插口为假,对面向连
接的插口为真。
第三个测试就是疑问所在 [ Torek 1992] 。插口标志 S O _ A C C E P T C O N N 只被系统调用
l i s t e n置位( 1 5 . 9节),该值只对面向连接的服务器有效。在正常情况下,一个 T C P服务器调
用s o c k e t、b i n d,然后调用 l i s t e n。因而,当 i n _ p c b b i n d被b i n d调用时,就清除了这
个插口标志位。即使进程调用完 s o c k e t 后就调用 l i s t e n ,而不调用 b i n d ,T C P的
P R U _ L I S T E N请求还是调用 i n _ p c b b i n d,在插口层设置 S O _ A C C E P T C O N N标志位之前,给
插口分配一个临时端口。这意味着 i f语句中的第三个测试,测试 S O _ A C C E P T C O N N是否没有
置位,总是为真。因此 if语句等价于
if ((so->so_options & (SO_REUSEADDR|SO_REUSEPORT)) == 0 &&
((so->so_proto->pr_flags & PR_CONNREQUIRED)==0||1)
wild = INPLOOKUP_WILDCARD;

因为任何与 1作逻辑或运算的结果都为真,所以这等价于
if ((so->so_options & (SO_REUSEADDR|SO_REUSEPORT)) == 0 )
wild = INPLOOKUP_WILDCARD;

这样简单且容易理解:如果任何一个 R E U S E插口选项被置位, w i l d就是 0。如果没有


R E U S E选项被置位,则把 w i l d设成I N P L O O K U P _ W I L D C A R D。换言之,当函数在后面调用
in_pcblookup时,只有在没有 REUSE选项处于开状态时,才允许通配匹配。
Linux公社 www.linuxidc.com
bbs.theithome.com
586 计计TCP/IP详解 卷 2:实现

in_pcbbind的下一部分,显示在图 22-22中,函数处理可选 nam参数。


72-75 只有当进程显式调用 b i n d时, n a m参数才是一个非零指针。对一个隐式的绑定
( c o n n e c t、l i s t e n或i n _ p c b c o n n e c t的副作用,本节开始的情形 3、4和5 ),n a m是一个
空指针。当指定了该参数时,它是一个含有 s o c k a d d r _ i n结构的m b u f。图2 2 - 2 1显示了非空
参数nam的四种情形。

nam参数 PCB成员被设成:

localIP lport inp_laddr inp_iport 说 明

不是* 0 localIP 临时端口 localIP必须是本地接口


不是* 非零 localIP lport 交付给i n _ p c b l o o k u p
* 0 * 临时端口
* 非零 * lport 交付给 i n _ p c b l o o k u p

图22-21 in_pcbbind 的nam 参数的四种情形

76-83 对正确的地址族的测试被注释掉了,但在函数 i n _ p c b c o n n e c t(图2 2 - 2 5 )中执行了


等价的测试。我们希望两者或者都有或者都没有。
85-94 Net/3测试被绑定的IP地址是否是一个多播组。如果是,则 SO_REUSEADDR选项被认
为与SO_REUSEPORT等价。
95-99 否则,如果调用方绑定的本地地址不是通配地址,则 ifa_ifwithaddr验证该地址
与一个本地接口对应。
注释“ y e c h ”可能是因为插口地址结构中的端口号必须是 0 ,因为 i f a _
ifwithaddr对整个结构作二进制比较,而不仅仅比较 IP地址。
这是进程在调用系统调用之前必须把插口地址结构全部置零的几种情况之一。
如果调用 b i n d ,并且插口地址结构 ( s i n _ z e r o [ 8 ] ) 的最后 8 个字节非零,则
ifa_ifwithaddr将找不到请求的接口, in_pcbbind会返回一个错误。
100-105 当调用方绑定了一个非零端口时,也就是说,进程要绑定一个特殊端口号 (图2 2 -
2 1 中 的 第 2 种 和 第 4 种 情 形 ) , 就 执 行 下 一 个 i f语 句 。 如 果 请 求 的 端 口 小 于
1 0 2 4 (I P P O R T _ R E S E R V E D),则进程必须具有超级用户的优先权限。这不是 I n t e r n e t协议的一
部分,而是伯克利的习惯。使用小于 1 0 2 4的端口号,我们称之为保留端口 (reserved port),例
如,r c m d函数 [Stevens 1990]使用的端口,r l o g i n和r s h客户程序又调用该函数,作为服务
器对它们身份认证的一部分。
106-109 然后调用函数in_pcblookup(图22-16),检测是否已经存在一个具有相同本地 IP
地址和本地端口号的 P C B。第二个参数是通配 I P地址(外部I P地址),第三个参数是一个为 0的
端口号 (外部端口号 )。第二个参数的通配值导致 i n _ p c b l o o k u p忽略该 P C B的外部 I P地址和
外部端口 —只把本地 IP 地址和本地端口号分别和 s i n - > s i n _ a d d r及l p o r t进行比较。我
们前面提到,只有当所有REUSE插口选项都没有被设置时,才把wild设成
INPLOOKUP_WILDCARD。
111 调用方的本地IP地址值存放在PCB中。如果调用方指定,它可以是通配地址。在这种情
况下,由内核选择本地 I P地址,但要等到晚些时候插口连接上时。这就是为什么说本地 I P地
址是根据外部IP地址,由IP路由选择决定。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 587

图22-22 in_pcbbind 函数:处理可选的 nam 参数

当调用方显式绑定端口 0,或n a m参数是一个空指针 (隐式绑定 )时,i n _ p c b b i n d的最后


一部分处理分配一个临时端口。
113-122 这个协议(TCP或UDP)使用的下一个临时端口号被维护在该协议的 PCB表的head:
t c b或u d b。除了协议的 h e a d P C B中的i n p _ n e x t和i n p _ b a c k指针外, i n p c b结构另一个
唯一被使用的元素是本地端口号。令人迷惑的是,这个本地端口在 head PCB中是主机字节序,
而在表中其他PCB上,却是网络字节序!使用从1024开始的临时端口号
(I P P O R T _ R E S E R V E D),每次加 1,直到 5 0 0 0 (I P P O R T _ U S E R R E S E R V E D),然后又从 1 0 2 4重
新开始循环。该循环一直执行到 in_pcbbind找不到匹配为止。
1. SO_REUSEADDR举例
让我们通过一些普通的例子,来了解一下 i n _ p c b b i n d与i n _ p c b l o o k u p及两个R E U S E

Linux公社 www.linuxidc.com
bbs.theithome.com
588 计计TCP/IP详解 卷 2:实现

插口选项之间的交互。

图22-23 in_pcbbind 函数:选择一个临时端口

1) T C P或U D P通常以调用 s o c k e t和b i n d开始。假定一个调用 b i n d的T C P服务器,指定


了通配IP 地址和它的非零知名端口 23(Telnet服务器)。还假定该服务器还没有运行,进
程没有设置 SO_REUSEADDR插口选项。
i n _ p c b b i n d把I N P L O O K U P _ W I L D C A R D作为最后一个参数,调用 i n _ p c b l o o k u p。
i n _ p c b l o o k u p中的循环没有找到匹配的 P C B,就假定没有其他进程使用服务器的知
名TCP端口,返回一个空指针。一切正常, in_pcbbind,返回0。
2) 假定和上面相同的情况,但当再次试图启动服务器时,该服务器已经开始运行。
当调用i n _ p c b l o o k u p时,它发现了本地插口为 {*, 23}的PCB。因为w i l d c a r d计数
器是0,所以 i n _ p c b l o o k u p返回指向这个入口的指针。因为 r e u s e p o r t是0,所以
in_pcbbind返回EADDRINUSE。
3) 假定与上面相同的情况,但当第二次试图启动服务器时,指定了 S O _ R E U S E A D D R插口
选项。
因为指定了这个插口选项,所以 i n _ p c b b i n d在调用i n _ p c b l o o k u p时,最后一个参
数为0。但本地插口为 {*, 23}的P C B仍然匹配,因为 i n _ p c b l o o k u p无法比较两个通
配地址 (图2 2 - 1 7 ),所以 w i l d c a r d为0。i n _ p c b b i n d又返回 E A D D R I N U S E,避免启
动两个具有相同本地插口的服务器例程,不管是否指定了 SO_REUSEADDR。
4) 假定有一个 Te l n e t服务器已经以本地插口 {*, 23} 开始运行,而我们试图以另一个本地
插口{140.252.13.35,23}启动另一个服务器。
假定没有指定 S O _ R E U S E A D D R ,调用 i n _ p c b l o o k u p 时,最后一个参数为
INPLOOKUP_WILDCARD。当它与含有*.23的PCB比较时,wildcard计数器被设为1。
因为允许通配匹配,所以在扫描完所有 TCP PCB 后,就把这个匹配作为最佳匹配。
in_pcbbind返回EADDRINUSE。
5) 这个例子与上一个相同,但为第二个试图绑定本地插口 {140.252.13.35, 23}的服务器指
定了SO_REUSEADDR插口选项。
现在, i n _ p c b l o o k u p的最后一个参数是 0,因为指定了插口选项。当与本地插口为
{*, 23}的P C B比较时, w i l d c a r d计数器为 1,但因为最后的 f l a g s参数是0,所以跳
过这个入口,不把它记作匹配。在比较完所有 TCP PCB 后,函数返回一个空指针,
in_pcbbind返回0。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 589
6) 假定当我们试图以本地插口{*, 23} 启动第二个服务器时,第一个Telnet服务器以本地插口
{140.252.13.35, 23}启动。与前面的例子一样,但这一次我们以相反的顺序启动服务器。
第一个服务器的启动没有问题,假定没有其他插口绑定到端口 2 3。当我们启动第二个
服务器时,i n _ p c b l o o k u p的最后一个参数是 I N P L O O K U P _ W I L D C A R D,假定没有指
定S O _ R E U S E A D D R插口选项。当和具有本地插口 {140.252.13.35, 23} 的P C B比较时,
w i l d c a r d被设成1,记录这个入口。在比较完所有 TCP PCB后,返回指向这个入口的
指针。导致 in_pcbbind返回EADDRINUSE。
7) 如果我们启动同一个服务器的两个例程,并且都是非通配本地 I P地址,会发生什么情
况?假定我们以本地插口 {140.252.13.35, 23}启动第一个 Te l n e t服务器,然后试图用本
地插口{127.0.0.1, 23}启动第二个服务器,且不指定 SO_REUSEADDR。
当第二个服务器调用 i n _ p c b b i n d时,它调用 i n _ p c b l o o k u p ,最后一个参数是
INPLOOKUP_WILDCARD。当比较具有本地插口{140.252.13.35, 23}的PCB时,因为本地
IP地址不相等,所以跳过它。in_pcblookup返回一个空指针,in_pcbbind返回0。
从这个例子中我们看到, SO_REUSEADDR插口选项对非通配 IP地址没有影响。事实上,
只有当wildcard大于0 时,也就是说,当PCB入口具有一个通配IP地址,或者绑定的IP
地址是一个通配地址时,才检查in_pcblookup中的INPLOOKUP_WILDCARD标志位。
8) 作为最后一个例子,假定我们试图启动同一服务器的两个例程,具有相同的非通配本
地IP 地址127.0.0.1。
启动第二个服务器时,in_pcblookup总是返回具有相同本地插口的匹配PCB。不管是否
指定SO_REUSEADDR插口选项,都发生这种情况,因为对这种比较,wildcard计数器总
是0。因为in_pcblookup返回一个非空指针,所以in_pcbbind返回EADDRINUSE。
从这些例子中,我们可以指出本地 I P地址和 S O _ R E U S E A D D R插口选项的绑定规则。这些
规则如图 2 2 - 2 4所示。假定 l o c a l I P1和l o c a l P2是在本地主机上有效的两个不同的单播或广播 I P
地址,l o c a l m c a s t I P是一个多播组。我们还假定进程要绑定到一个已经绑定到某个已存在 P C B
的非零端口号。
我们需要区分单播或多播地址和一个多播地址,因为我们看到, i n _ p c b b i n d认为对多
播地址, SO_REUSEADDR与SO_REUSEPORT是一样。
SO_REUSEADDR
存在PCB 试图绑定 描 述
关 开
LocalIP1 localIP1 错误 错误 每个IP地址和端口一个服务器
localIP1 localIP2 正确 正确 每个本地接口一个服务器
localIP1 * 错误 正确 一个接口一个服务器,其他接口一个服务器
* localIP1 错误 正确 一个接口一个服务器,其他接口一个服务器
* * 错误 错误 不能复制本地插口 (和第一个例子一样 )
localIP1 localIP1 错误 正确 多个多播接收方

图22-24 SO_REUSEADDR 插口选项对绑定本地 IP地址的影响

2. SO_REUSEPORT插口选项
Net/3中对SO_REUSEPORT的处理改变了in_pcbbind的逻辑,只要指定了SO_REUSEPORT,
就允许复制本地插口。换言之,所有服务器都必须同意共享同一本地端口。

22.8 in_pcbconnect函数

函数in_pcbconnect为插口指定 IP地址和外部端口号。有四个函数调用它:

Linux公社 www.linuxidc.com
bbs.theithome.com
590 计计TCP/IP详解 卷 2:实现

1) connect为某个TCP插口(某个TCP客户的请求 )调用;
2) connect为某个UDP插口(对UDP客户是可选的, UDP服务器很少见)调用;
3) 当在一个没有连接上的 UDP插口(普通)上输出数据报时从 sendto调用;
4) 当一个连接请求 (一个S Y N报文段 )到达一个处于 L I S T E N状态(对T C P服务器是标准的 )
的TCP插口时, tcp_input调用。
在以上四种情况下,当调用 i n _ p c b c o n n e c t时,通常,但不要求,不指定本地 IP 地址
和本地端口。因此,在没有指定的情形下,由 i n _ p c b c o n n e c t的一个函数给它们赋一个本
地的值。
我们将分四个部分讨论 in_pcbconnect函数。图22-25显示了第一部分。

图22-25 in_pcbconnect 函数:验证参数,检查外部 IP地址

1. 确认参数
130-143 n a m参数指向一个包含 s o c k a d d r _ i n结构以及外部 I P地址和端口号的 m b u f。这
些行确认参数并验证调用方不打算连接到端口号为 0的端口上。
2. 特别处理到 0.0.0.0和255.255.255.255的连接
134-160 对全局变量 i n _ i f a d d r 的检查证实已配置了一个 I P接口。如果外部地址是
0.0.0.0(INADDR_ANY),则用最初的IP接口的IP地址代替0.0.0.0。这就是说,调用进程是连接到
这个主机上的一个对等实体的。如果外部 I P地址是255.255.255.255 (I N A D D R _ B R O A D C A S T),

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计591
而且原来的接口支持广播,则用原来接口的广播地址代替 2 5 5 . 2 5 5 . 2 5 5 . 2 5 5。这样, U D P应用
程序无需计算它的 IP 地址,就可以在原来的接口上广播 — 它可以简单地把数据报发送给
255.255.255.255,由内核把这个地址转换成该接口合适的 IP地址。
下一部分代码,如图 2 2 - 2 6所示,处理没有指定本地地址的情况。对 T C P和U D P客户程序
来说,本节开始的表中的情形 1、2和3是非常普遍的。

图22-26 in_pcbconnect 函数:没有指定本地 IP地址

3. 如果路由不再有效,则释放该路由

Linux公社 www.linuxidc.com
bbs.theithome.com
592 计计TCP/IP详解 卷 2:实现

164-175 如果P C B中含有一条路由,但该路由的目的地址和已经连接上的外部地址不同,


或者SO_DONTROUTE插口选项被置位,则放弃该路由。
为了理解为什么一个 P C B会含有一条相关路由,考虑本节开始的表中的情形 3:每次在一
个未连接上的插口上发送 UDP数据报时,就调用in_pcbconnect。每次进程调用sendto时,
U D P输出函数调用 i n _ p c b c o n n e c t、i p _ o u t p u t和i n _ p c b d i s c o n n e c t。如果在该插口
上发送的所有数据报都具有相同的目的 I P地址,则第一次通过 i n _ p c b c o n n e c t时,就分配
了一条路由,从此时开始可以使用该路由。但是,因为 UDP应用程序可能在每次调用 s e n d t o
时,都向不同的 I P地址发送数据报,所以必须比较目的地址和保存的路由。当目的地址改变
时,就放弃该路由。 ip_output也作同样的检查,这看起来似乎是多余的。
S O _ D O N T R O U T E插口选项告诉内核旁路掉正常的选路决策,把该 I P数据报发到本地连接
的接口,该接口的 IP网络地址和目的地址的网络部分匹配。
4. 获取路由
176-185 如果没有置位SO_DONTROUTE插口选项,则PCB中没有到目的地的路由,就要调
用rtalloc获取一条路由。
5. 确定外出的接口
186-205 这一节代码的意图是让 i a指向一个接口地址结构 (i n _ i f a d d r,6.5节),该结构
中包含了该接口的 I P地址。如果 P C B中的路由仍然有效,或者如果 r t a l l o c找到一条路由,
并且该路由不是到回环接口的,则使用相应的接口。否则,调用 i f a _ w i t h d s t a d d r和
i f a _ w i t h n e t检查该外部 IP地址是否在一个点到点链路的另一端,或者位于一个连到的网络
上。两个函数都要求插口地址结构中的端口号为 0,以便在调用期间保存在 f p o r t中。如果失
败,就用原来的 IP 地址(in_ifaddr),如果没有配置接口 (in_ifaddr为0),则返回错误。
图22-27显示了in_pcbconnect的下一部分,处理目的地址是多播地址的情况。
206-223 如果目的地址是一个多播地址,且进程指定了多播分组的外出接口 (用
I P _ M U L T I C A S T _ I F插口选项 ),则该接口的 I P地址被用作本地地址。搜索所有 I P接口,找到
与插口选项所指定接口的匹配。如果该接口不存在,则返回错误。
224-225 图 2 2 - 2 6的开头是处理通配本地地址情形的完整代码。指向本地接口 ia的
sockaddr_in结构的指针保存在 ifaddr中。
in_pcblookup的最后一部分显示在图 22-28中。
6. 验证插口对是唯一的
227-233 i n _ p c b l o o k u p验证插口对是唯一的。外部地址和外部端口号是指定给
i n _ p c b c o n n e c t的参数的值。本地地址是已经绑定到该插口的值,或者是 i f a d d r中我们刚
刚介绍的代码计算出来的值。本地端口可以是 0,对T C P客户程序来说这是典型的。我们将在
这部分代码的后面看到,为本地端口选择了一个临时端口。
这个测试避免从相同的本地地址和本地端口上建立两个到同一外部地址和外部端口的
TCP连接。例如,如果我们与主机 s u n上的回显服务器建立了一个 TCP连接,然后试图从同一
本地端口 ( 8 8 8 8,用- b选项指定 )建立另一条到同一服务器的连接,调用 i n _ p c b l o o k u p后返
回一个匹配,导致 connect返回差错EADDRINUSE(我们用卷1附录C的sock程序)。
bsdi S sock -b 8888 sun echo & 启动后台的第一个
bsdi S sock -A -b 8888 sun echo 然后再试一次
connect() error: Address already in use

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计593

图22-27 in_pcbconnect 函数:目的地址是一个多播地址

图22-28 in_pcbconnect 函数:验证插口对是唯一的

我们指定- A选项,设置SO_REUSEADDR插口选项,使 bind成功,但是connect不成功。


这是一个人为的例子,因为我们显式地把两个插口都绑定到同一本地端口上 (8888)。在正常情
形下,主机 b s d i上的两个不同客户程序连接到 s u n的回显服务器上,当第二个客户程序调用
图22-28中的in_pcblookup函数时,本地端口将是 0。
这个测试也避免了两个 U D P插口从相同的本地端口上连接到同一个外部地址。但这个测
试不能避免两个 U D P插口从同一个本地端口上交替地向同一个外部地址发送数据报,只要它
们都不调用 c o n n e c t。因为 U D P插口在 s e n d t o系统调用的过程中,只是临时连接到一个对
等实体上。
7. 隐式绑定和分配临时端口

Linux公社 www.linuxidc.com
bbs.theithome.com
594 计计TCP/IP详解 卷 2:实现

234-238 如果插口的本地地址仍然是通配匹配的,则把它设置成 i f a d d r中保存的值。这


是一个隐式绑定: 2 2 . 7节开始时讲的情形 3、4和5。首先,检查本地端口是否已经被绑定,如
果没有, i n _ p c b b i n d 就把该插口绑定到一个临时端口。调用 i n _ p c b b i n d 和给
inp_laddr赋值的顺序很重要,因为如果本地地址不是通配地址,则 in_pcbbind会失败。
8. 把外部地址和外部端口存放在 PCB中
239-240 这个函数的最后一步设置 P C B的外部I P地址和外部端口号成员。如果这个函数成
功返回,我们就能保证 PCB中的插口对 —本地的和外部的—都有了特定的值。

IP源地址与外出接口地址

在IP数据报的源地址和用来发送该数据报接口的 IP地址之间有些微妙的差别。
T C P和U D P把P C B成员 i n p _ l a d d r用作该 I P数据报的源地址。它可由进程设成任何被
bind配置的接口的IP 地址(在in_pcbbind中调用ifa_ifwithaddr验证应用程序想要的本
地地址)。只有当本地地址是一个通配地址时, i n _ p c b c o n n e c t才给它赋值。而当这种情况
发生时,本地地址是根据外出接口分配的 (因为目的地址已知 )。
但是,外出接口也是根据目的 I P地址,由 i p _ o u t p u t确定的。在多接口主机上,当进程
显式绑定一个不同于外出接口的本地地址时,源地址有可能是一个本地接口的 I P地址,且该
接口不是外出的接口。这种情况是允许的,因为 Net/3选择了弱端系统模式 (8.4节)。

22.9 in_pcbdisconnect函数

i p _ p c b d i s c o n n e c t把U D P插口断连。把外部 I P地址设成全 0 (I N A D D R _ A N Y),外部端


口号设成0,就把外部相关内容删除了。
这是在已经在一个未连接上的 U D P插口上发送了一个数据报后,在一个连接上的 U D P插
口上调用 c o n n e c t 时做的。在第一种情况下,调用 s e n d t o 的次序是: U D P 调用
i n _ p c b c o n n e c t 把插口临时连接到目的地, u d p _ o u t p u t 发送数据报,然后
in_pcbdisconnect删除临时连接。
当关闭插口时,不调用 i n _ p c b d i s c o n n e c t,因为 i n _ p c b d e t a c h处理释放 P C B。只
有当一个不同的地址或端口号要求重用该 PCB时,才断连。
图22-29显示了in_pcbdisconnect函数。

图22-29 in_pcbdisconnect 函数:与外部地址和端口号断连

如果该 P C B不再有文件表引用 (S S _ N O F D R E F置位),则i n _ p c b d e t a c h(图2 2 - 7 )释放该


PCB。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 595
22.10 in_setsockaddr和in_setpee r a d d r函数

g e t s o c k n a m e系统调用返回插口的本地协议地址 (例如, I n t e r n e t插口的 IP 地址和端口


号 ), g e t p e e r n a m e 系统调用返回外部协议地址。两个系统调用终止时,都发布一个
P R U _ S O C K A D D R 或 P R U _ P E E R A D D R 请求。然后协议调用 i n _ s e t s o c k a d d r 或
in_setpeeraddr。图22-30显示了以上的第一种情况。

图22-30 in_setsockaddr 函数:返回本地地址和端口号

参数n a m是一个指针,该指针指向一个用来存放结果的 m b u f:一个s o c k a d d r _ i n结构,


系统调用复制给进程的备份。该代码填写插口地址结构的内容,并把 IP 地址和端口号从
Internet PCB拷贝到sin_addr和sin_port成员中。
图2 2 - 3 1显示了 i n _ s e t p e e r a d d r函数。它基本上等同于图 2 2 - 3 0中的代码,但从 P C B中
拷贝了外部 IP地址和端口号。

图22-31 in_setpeeraddr 函数:返回外部地址和端口号

22.11 in_pcbnotify、in_rtchange和i n _ l o s i n g函数

当收到一个 I C M P差错时,调用 i n _ p c b n o t i f y函数,把差错通知给合适的进程。通过


对所有的 P C B搜索一个协议 ( T C P或U D P ),并把本地和外部 I P地址及端口号与 I C M P差错返回
的值进行比较,找到“合适的进程”。例如,当因为一些路由器丢掉了某个 T C P报文段而收到
Linux公社 www.linuxidc.com
bbs.theithome.com
596 计计TCP/IP详解 卷 2:实现

ICMP源抑制差错时, TCP必须找到产生该差错的连接的 PCB,放慢在该连接上的传输速度。


在显示该函数之前,我们必须回顾一下它是怎样被调用的。图 2 2 - 3 2总结了处理 I C M P差
错时调用的函数。两个有阴影的椭圆是本节描述的函数。

或 和
协议的控制 pfctlinput: 所有
输入函数 协议的控制输入函数

其他ICMP
(见正文)
报文

(软件中断)

图22-32 ICMP差错处理总结

当收到一个 I C M P报文时,调用 i c m p _ i n p u t。I C M P的五种报文按差错来划分 (图11 - 1和


图11-2):
• 目的主机不可达;
• 参数问题;
• 重定向;
• 源抑制;
• 超时。
重定向的处理不同于其他四个差错。所有其他的 ICMP报文(查询)的处理见第 11章。
每个协议都定义了它的控制输入函数,即 p r o t o s w结构( 7 . 4节)中的p r _ c t l i n p u t入口。
对T C P和U D P,它们分别称为 tc p _ c t l i n p u t和u d p _ c t l i n p u t,我们将在后面几章给出它
们的代码。因为收到的 I C M P差错中包含了引起差错的数据报的 I P首部,所以引起该差错的协
议( T C P或U D P )是已知的。这五个 I C M P差错中的四个将引起对协议的控制输入函数的调用。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 597
重定向的处理不同:调用函数 p f c t l i n p u t,它继续调用协议族 ( I n t e r n e t )中所有协议的控制
输入函数。 TCP和UDP是Internet协议族中仅有的两个具有控制输入函数的协议。
重定向的处理是特殊的,因为它们不仅影响产生重定向的数据报,还将影响所有到该目
的地的IP数据报。另一方面,其他四个差错只需由产生差错的协议进行处理。

图22-33 in_spcbnotify 函数:把差错通知传给进程

Linux公社 www.linuxidc.com
bbs.theithome.com
598 计计TCP/IP详解 卷 2:实现

有关图2 2 - 3 2我们要做的最后一点说明是, T C P在处理源抑制差错时,与其他差错的处理


不同,而重定向由 i n _ p c b n o t i f y 特别处理:不管引起差错的是什么协议,都调用
in_rtchange函数。
图2 2 - 3 3显示了 i n _ p c b n o t i f y函数。当 T C P调用它时,第一个参数是 t c b的地址,最后
一个参数是函数 t c p _ n o t i f y 的地址。对 U D P来说,这两个参数分别是 u d b的地址和函数
udp_notify的地址。
1. 验证参数
306-324 验证cmd参数和目的地址族。检测外部地址,保证它不是 0.0.0.0。
2. 特殊处理重定向
325-338 如果差错是重定向,则对它的处理是特殊的 (差错PRC_HOSTDEAD是一种旧的差错,
由IMP产生。目前的系统再也看不到这种差错了—它是一个历史产物 )。外部端口、本地端口
和本地地址都被设成全 0,这样后面的 f o r循环就不会比较它们了。对于重定向,我们需要该
循环只根据外部 IP 地址选出接收通知的PCB,因为主机是在这个IP地址上接收到重定向的。而
且,为重定向调用的函数是 in_rtchange(图22-34),而不是调用方指定的notify参数。
339 全局数组inetctlerrmap把协议无关差错码 (图11-19中的PRC_xxx值)映射到它对应的
Unix的errno值(图11-1的最后一栏 )。
3. 为所选的PCB调用通知函数
341-353 这个循环选择要通知的 P C B。可以通知多个 P C B—该循环在找到匹配后仍然继
续。第一个 i f语句结合了五个检测,如果这五个中有任一个为真,则跳过该 P C B:( 1 )如果外
部地址不相等; ( 2 )如果该 P C B没有对应的 s o c k e t结构;( 3 )如果本地端口不相等; ( 4 )如果本
地地址不相等;或 (5)如果外部端口不相等。外部地址必须匹配,但只有当对应的参数非零时,
才比较其他三个外部和本地参数。当找到一个匹配时,调用 notify函数。

22.11.1 in_rtchange函数

我们看到,当 ICMP差错是一个重定向时, i n _ p c b n o t i f y调用i n _ r t c h a n g e函数。对


所 有 外 部地址与已重定向的 I P 地址匹配的 P C B, 都调用该函数。图 2 2 - 3 4 显示了
in_rtchange函数。

图22-34 in_rtchange 函数:使路由无效

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 599
如果该PCB中有路由,则rtfree释放该路由,且该 PCB 成员被标记为空。此时,我们不
用重定向返回的路由来更新路由。当这个 P C B被再次使用时, i p _ o u t p u t会根据内核的选路
表重新分配新的路由,而该选路表是在调用 pfctlinput之前,由重定向报文更新的。

22.11.2 重定向和原始插口

让我们来研究一下重定向、原始插口和缓存在 P C B中的路由之间的交互。如果我们运行
P i n g程序,该程序使用一个原始插口,收到来自被 p i n g的I P地址发来的 I C M P重定向差错。
Ping程序继续使用原来的路由,而不是已重定向的路由。我们可以从以下过程来看。
我们从位于 1 402 521 网络上的 g e m i n i主机 p i n g位于14 025 213 网络上的 s v r 4主机。
gemini的默认路由是gateway,但分组应该被发送到路由器netb。图22-35显示了这个安排。
客户
重新选路到140.252.1.183

ICMP回显请求

以太网140.252.1

目的地

以太网140.252.13

图22-35 ICMP重定向举例

我们希望gateway在收到第一个ICMP回显请求时,发一个重定向。

选项- s使每隔一秒发送一次 I C M P回显请求,选项 - v打印每个收到的 I C M P报文(不仅仅是


ICMP回显回答)。
每个I C M P回显请求引出一个重定向,但 p i n g使用的原始插口从来不通知重定向改变它正
在使用的路由。第一次计算出来并被保存在 P C B 中的路由,使 I P 数据报被发送到路由器
g a t e w a y{ 1 4 0 . 2 5 2 . 1 . 4 },应该更新它,使数据报能被发送到路由器 n e t b{ 1 4 0 . 2 5 2 . 1 . 1 8 3 }上。
我们看到, gemini上的内核接收ICMP重定向,但它们被略过了。
如果我们终止ping程序,并重新运行它,我们就再也看不到重定向了:
gemini $ ping -sv svr4

Linux公社 www.linuxidc.com
bbs.theithome.com
600 计计TCP/IP详解 卷 2:实现

PING 140.252.13.34: 56 data bytes


64 bytes from svr4 (140.252.13.34): icmp_seq=0,time=388. ms
64 bytes from svr4 (140.252.13.34): icmp_seq=1. time=363. ms

这个不正常的原因是原始 I P插口代码 (第3 2章)没有控制输入函数。只有 T C P和U D P有控制


输入函数。当收到重定向时, ICMP更新内核的选路表,调用 p f c t l i n p u t(图22-32)。但是因
为原始IP协议没有控制输入函数,所以不释放与Ping的原始插口相关的 PCB中高速缓存的路由。
但是,当我们第二次运行 p i n g程序时,根据内核更新后的选路表分配路由,所以我们看不到
重定向了。

22.11.3 ICMP差错和UDP插口

插口A P I令人迷惑的一部分是,不把在 U D P插口上收到的 I C M P差错传给应用程序,除非


该应用程序在该插口上发布 c o n n e c t,限制该插口的外部 IP 地址和端口号。现在我们来看一
下 in_pcbnotify是如何实施这一限制的。
考虑某个 I C M P 插口不可达,这大概是 U D P 插口上最普通的一种 I C M P 差错了。
i n _ p c b n o t i f y的d s t参数内的外部 IP 地址和外部端口号是引起 I C M P差错的 I P地址和端口
号。但是,如果该进程已经在该插口上发布 c o n n e c t 命令,则 P C B 的i n p _ f a d d r 和
i n p _ f p o r t成员都是 0,避免 i n _ p c b n o t i f y在该插口上调用 n o t i f y函数。图 2 2 - 3 3中的
for循环将跳过每个 UDP PCB。
产生这个限制的原因有两个。首先,如果正在发送的进程有一个未连接上的 U D P插口,
则该插口对中唯一的非零元素是本地端口 (假定该进程不调用 b i n d)。这是i n _ p c b n o t i f y在
分用进入的 I C M P差错,并把它传给正确进程时,唯一可用的值。尽管很少发生,但也可能有
多个进程都绑定到相同的本地端口上,所以具体由哪个进程接收 I C M P差错就不明确了。还有
一种可能就是,发送引起 I C M P差错数据报的进程已经终止了,而另一个进程又开始运行并使
用同一本地端口。这也不太可能,因为临时端口是从 1 0 2 4到5 0 0 0按顺序分配的,只有循环一
遍以后才可能重用同一端口号 (图22-23)。
这个限制的第二个原因是,内核给进程的差错通知—一个e r r n o值—是不够的。考
虑某个进程连续三次在一个未连接上的 UDP插口上调用 s e n d t o函数,向三个不同的目的地发
送一个U D P数据报,然后用 r e c v f r o m等待回答。如果其中一个数据报生成一个 I C M P端口不
可达差错,且内核将向该进程发布的 r e c v f r o m返回对应的差错 (E C O N N R E F U S E D),那么,
errno值并没有告诉进程是哪个数据报产生了该差错。内核具有ICMP差错所要求的所有信息,
但是插口API并不提供手段把这些信息返回给该进程。
因此,如果进程想要得到在某个 U D P插口上的这些 I C M P差错通知,在设计时就必须决定
插口只能连接到一个对等实体上。如果在该连接上的插口返回 E C O N N R E F U S E D差错,毫无疑
问就是该对等实体产生的差错。
还有一种远程可能性,会把 I C M P差错交付给错误的进程。假设某个进程发送了一个 U D P
数据报,引起一个 I C M P差错,但它在收到该差错之前终止了。另一个进程在收到该差错之前
开始运行,并且绑定到同一个本地端口,连接到相同的外部地址和外部端口上,导致这个新
进程接收到前面的 I C M P差错。由于U D P缺少内存,所以无法避免这种情况的发生。我们将看
到TCP用它的TIME_WAIT状态处理这个问题。
在我们前面的例子中,应用程序绕开这个限制的一个办法是使用三个连接上的 U D P插口,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 601
而不是一个未连接上的插口,并在其中任意一个有收到的数据报或差错要读写时,调用
select函数来确定。
这里我们有一种情形是内核有足够的信息而 A P I (插口)的信息不足。大多数 U n i x
系统V及其他常见的 A P I ( T L I ),其逆为真: T L I函数t _ r c v u d e r r可以返回对等实体
的I P地址、端口号以及一个差错值。但大多数 T C P / I P的S V R 4流实现都不为 I C M P提
供手段,把差错传递给一个未连接上的 UDP端节点。
在理想情况下, i n _ p c b n o t i f y把ICMP差错交付给所有匹配的 UDP插口,即使
唯一的非通配匹配是本地端口。返回给进程的差错将包括产生差错的目的 I P地址和
目的UDP端口,允许进程确定该差错是否是它发送的数据报产生的。

22.11.4 in_losing函数

处理PCB的最后一个函数图 22-36的i n _ l o s i n g。当TCP的某个连接的重传定时器连续第


三次超时时,调用该函数。

图22-36 in_losing 函数:使高速缓存路由信息无效

1. 产生选路报文
361-374 如果P C B中有一个路由,则丢掉该路由。用要失效的高速缓存路由的有关信息填
充一个 r t _ a d d r i n f o结构。然后调用 r t _ m i s s m s g函数,从 R T M _ L O S I N G类型的选路插口
中生成一个报文,指明有关该路由的问题。
2. 删除或释放路由

Linux公社 www.linuxidc.com
bbs.theithome.com
602 计计TCP/IP详解 卷 2:实现

375-384 如果高速缓存路由是由一个重定向生成的 ( R T F _ D Y N A M I C 置位 ),则用请求


R T M _ D E L E T E调用r t r e q u e s t,删除该路由。否则释放高速缓存的路由,这样,当该插口上
有下一个输出时,为它重新分配一条到目的地的路由—希望是一条更好的路由。

22.12 实现求精

毫无疑问,这一章我们遇到的最耗时的算法是 i n _ p c b l o o k u p做的对PCB 的线性搜索。


22.6节一开始,我们就注意到有四种情况会调用这个函数。可以忽略对 b i n d和c o n n e c t的调
用,因为 T C P和U D P在分用每个收到的 I P数据报时,调用它们的次数比调用 i n _ p c b l o o k u p
的少得多。
后面几章我们将看到, T C P和U D P试图帮助这个线性搜索,它们都维护一个指向该协议
引用的最后一个 P C B的指针:一个单入口高速缓存。如果高速缓存的 P C B的本地地址、本地
端口、外部地址和外部端口与收到的数据报的值匹配,则协议根本就不调用 in_pcblookup。
如果协议数据适合分组列模型 [ J a i n和Routhier 1986] ,这个简单的高速缓存效果很好。但是,
如果数据不适合这个模型,例如,看起来象联机交易处理系统的数据入口,则单入口高速缓
存的效率很低[McKenney和Dove 1992]。
一个稍好一点的 P C B安排的建议是,当引用某个 P C B时,把它移到该 P C B表的最前面
( [ M c K e n n e y和Dove 1992] 把这个想法给了 Jon Crowcroft ;[ P a r t r i d g e和Pink 1993]把它给了
Gary Delp)。移动 P C B很容易,因为该表是一个双向链表,而且 i n _ p c b l o o k u p的第一个参
数是一个指向该表表头的指针。
[ M c K e n n e y和Dove 1992]把原始的 N e t / 1实现(没有高速缓存 ),一种提高的单入口发送-接
收高速缓存,“移到最前面”启发算法,以及他们自己的使用散列链的算法做了比较。他们指
出,在散列链上维护一个 P C B的线性表比其他算法的性能提高了一个数量级。散列链的唯一
耗费是需要内存存放散列链的链头,以及计算散列函数。他们也考虑把“移到最前面”启发
算法与他们的散列链算法结合,结论是只增加一些散列链,更为简单。
BSD线性搜索和散列表搜索的另一个比较是在 [Hutchinson和Peterson 1991]中。他们指出,
随着散列表中插口数量的增加,分用一个进入的 U D P数据报所需要的时间是常量,但线性搜
索所需要的时间随插口数量的增加而增加。

22.13 小结

每个Internet插口都有一个相关的 Internet PCB:TCP、UDP和原始IP。它包含了 Internet插


口的一般信息:本地和外部 I P地址,指向一个路由结构的指针等等。给定协议的所有 P C B都
放在该协议维护的一个双向链表上。
本章中,我们研究了多个操作 PCB的函数,对其中的三个作了详细的讨论:
1) TCP 和U D P调用i n _ p c b l o o k u p分用每个进入的数据报。它选择接收数据报的插口,
考虑通配匹配。
i n _ p c b b i n d也调用这个函数来验证本地地址和本地进程是唯一的; i n _ p c b c o n n e c t
调用这个函数验证本地地址、本地进程、外部地址和外部进程的组合是唯一的。
2) i n _ p c b b i n d显式或隐式地把一个本地地址和本地端口号绑定到一个插口。当进程调
用b i n d时,发生显式绑定;当一个 T C P客户程序调用 c o n n e c t而不调用 b i n d时,或当一个

Linux公社 www.linuxidc.com
bbs.theithome.com
第 22 章 协议控制块 计计 603
UDP进程调用sendto或connect而不调用bind时,发生隐式绑定。
3) i n _ p c b c o n n e c t设置外部地址和外部进程。如果进程还没有设置本地地址,计算一
条到外部地址的路由,结果的本地接口成为本地地址。如果进程还没有设置本地端口,
in_pcbbind为插口选择一个临时端口。
图2 2 - 3 7对多种T C P和U D P应用程序以及存放在 P C B中的本地地址,本地端口、外部地址
和外部端口的值做了总结。我们还没有讨论完图 2 2 - 3 7中T C P和U D P进程的所有动作,将在后
面的章节中继续讨论。

应用程序 本地地址: 本地端口: 外部地址: 外部端口:


inp_laddr inp_lport inp_faddr inp_fport

T C P 客 户 程 序 : in_pcbconnect in_pcbconnect foreignIP fport


c o n n e c t( f o re i g n I P, 调用 r t a l l o c 为 调用 i n _ p c b b i n d选
fport) f o re i g n I P分配路由。 择临时端口。
本地地址是本地接口
T C P客户程序: b i n d localIP lport foreignIP fport
(localIP, lport) connect
(foreignIP,fport)
T C P 客 户 程 序 : in_pcbconnect lport fport fport
bind(*, lport) connect 调用 r t a l l o c 为
(foreignIP, fport) foreignIP分配路由。
本地地址是本地接口
T C P 客户程序 : b i n d localIP i n _ p c b b i n d选择 foreignIP fport
(l o c a l I P, 0) c o n n e c t 临时端口
(foreignIP, fport)

TCP服务器程序: localIP lport I P首 部 内 的 源 T C P 首部内


b i n d( l o c a l I P, lport ) 地址 的源端口地址
l i s t e n()accept()
TCP服务器程序:bind I P首部里的目的地 lport f o re i g n I P。发 T C P首部内
(* , lport ) l i s t e n ( ) 址 送 完 数 据 报 后 , 的源端口地址
a c c e p t() 置位为0.0.0.0

U D P 客 户 程 序 : in_pcbconnect in_pcbconnect foreignIP f p o r t 。发送


s e n d t o(foreignIP, fport ) 调用 r t a l l o c 为 调用 i n _ p c b b i n d选 完数据报后,
f o reignIP 分配路由。 择临时端口。后面调 置位为0
本地地址是本地接口。 用s e n d t o时不改变
在发送完数据报之后,
置位为0.0.0.0
U D P 客 户 程 序 : in_pcbconnect in_pcbconnect foreignIP fport
c o n n e c t ( f o re i g n I P, 调用 r t a l l o c 为 调用 i n _ p c b b i n d选
fport) w rite() f o re i g n I P分配路由。 择临时端口。后面调
本地地址是本地接 用write时不改变
口。后面调用 w r i t e
时不改变

图22-37 in_pcbbind 和in_pcbconnect 的总结

Linux公社 www.linuxidc.com
bbs.theithome.com
604 计计TCP/IP详解 卷 2:实现

习题

22.1 在图 2 2 - 2 3中,当进程请求一个临时端口,而所有临时端口都被使用时,会发生什
么情况?
22.2 在图2 2 - 1 0中,我们显示了两个有正在监听插口的 Te l n e t服务器:一个具有特定本地
I P地址;另一个的本地 I P地址是通配地址。你的系统的 Te l n e t守护程序允许你指定
本地IP地址吗?如果允许,如何指定?
22.3 假定某个插口被绑定到本地插口 {140.252.1.29, 8888} ,且这是唯一使用本地插口
8 8 8 8的插口。 ( 1 ) 当有另一个插口绑定到 {140.252.1.29, 8888} 时,请执行
i n _ p c b b i n d的所有步骤,假定没有任何插口选项。 ( 2 )当有另一个插口绑定到通
配I P地址,端口 8 8 8 8时,执行 i n _ p c b b i n d的所有步骤,假定没有任何插口选项。
( 3 ) 当有另一个插口绑定到通配 I P 地址,端口 8 8 8 8 时,且设定了插口选项
SO_REUSEADDR,执行in_pcbbind的所有步骤。
22.4 UDP分配的第一个临时端口号是什么?
22.5 当进程调用 bind时,必须填充sockaddr_in结构中的哪一个元素?
22.6 如果进程要 b i n d一个本地广播地址时,会发生什么情况?如果进程要 b i n d受限广
播地址(255.255.255.255)时,会发生什么情况?

Linux公社 www.linuxidc.com
bbs.theithome.com
第23章 UDP:用户数据报协议
23.1 引言

用户数据报协议,即 U D P,是一个面向数据报的简单运输层协议:进程的每次输出操作
只产生一个 UDP数据报,从而发送一个 IP数据报。
进程通过创建一个 Internet域内的S O C K_D G R A M类型的插口,来访问 UDP。该类插口默认
地称为无连接的 ( u n c o n n e c t e d )。每次进程发送数据报时,必须指定目的 I P地址和端口号。每
次从插口上接收数据报时,进程可以从数据报中收到源 IP地址和端口号。
我们在22.5节中提到, UDP插口也可以被连接到一个特殊的 IP地址和端口号。这样,所有
写到该插口上的数据报都被发往该目的地,而且只有来自该 I P地址和端口号的数据报才被传
给该进程。
本章讨论UDP的实现。

23.2 代码介绍

9个UDP函数在一个 C文件中, 2个UDP定义的头文件,如图 23-1所示。


图2 3 - 2显示了6个主要的 U D P函数与其他内核函数之间的关系。带阴影的椭圆是本章我们
讨论的6个函数,另外还有其他 3个函数是这 6个函数经常调用的。

文 件 描 述

netinet/udp.h u d p h d r结构定义
netinet/udp var.h 其他UDP定义
netinet/udp_usrreq.c UDP函数

图23-1 本章中讨论的文件

系统调用 系统初始化 插口接收缓冲区 多个系统调用

软件中断
图23-2 UDP函数与内核其他函数之间的关系

Linux公社 www.linuxidc.com
bbs.theithome.com
606 计计TCP/IP详解 卷 2:实现

23.2.1 全局变量

本章引入的全局变量,如图 22-3所示。

变 量 数据类型 描 述

udb Struct inpcb UDP PCB 表的表头


udp_last_inpcb Struct inpcb * 指向最近收到的数据报的指针:“向后一个”高速缓存
udpcksum Int 用于计算和验证 UDP检验和的标志位
udp_in Struct s o c k a d d r _ i n 在输入时存放发送方的 IP地址
udpstat Struct udpstat UDP统计(图23-4)
udp_recvspace u_long 插口接收缓存的默认大小, 41 600 字节
udp_sendspace u_long 插口发送缓存的默认大小, 9 216 字节

图23-3 本章中引入的全局变量

23.2.2 统计量

全局结构 u d p s t a t维护多种 U D P统计量,如图 2 3 - 4所示。讨论代码的过程中,我们会看


到何时增加这些计数器的值。

udpstat成员 描 述 SNMP使用的

udps_badlen 收到所有数据长度大于分组的数据报个数 •
udps_badsum 收到有检验和错误的数据报个数 •
udps_fullsock 收到由于输入插口已满而没有提交的数据报个数
udps_hdrops 收到分组小于首部的数据报个数 •
udps_ipackets 所有收到的数据报个数 •
udps_noport 收到在目的端口没有进程的数据报个数 •
udps_noportbcast 收到在目的端口没有进程的广播/多播数据报个数 •
udps_opackets 全部输出数据报的个数 •
udps_pcbcachemiss 收到的丢失 pcb高速缓存的输入数据报个数

图23-4 在udpstat 结构中维持的 UDP统计

图23-5显示了执行 n e t s t a t -后
s 输出的统计信息。

输出 成员

(见正文)

图23-5 UDP统计样本

提交的U D P数据报的个数 (输出的倒数第二行 )是收到的数据报总数 (u d p s _ i p a c k e t s)减


去图23-5中它前面的 6个变量。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 607
23.2.3 SNMP变量

图2 3 - 6显示了 U D P组中的四个简单 S N M P变量,这四个变量在实现该变量的 u d p s t a t结


构中计数。
图23-7显示了UDP监听器表,称为udpTable。SNMP为这个表返回的值是取自 UDP PCB,
而不是udpstat结构。

SNMP变量 u d p s t a t成员 描 述

udpInDatagrams udps_ipackets 收到的所有提交给进程的数据报个数


udpInErrors udps_hdrops + 收到的由于某些原因不可提交的 U D P数据报个数,
udps_badsum+ 这些原因中不包括在目的端口没有应用程序的原因 (例
udps_badlen 如,UDP检验和差错 )
udpNoPorts udps_noport + 收到的所有目的端口没有应用进程的数据报
udps_noportbcast
udpOutDatagrams udps_opackets 发送的数据报的个数

图23-6 udp 组中的简单 SNMP变量

UDP监听器表,索引 =<udpLocalAddress>.<udpLocalPort>

SNMP变量 PCB变量 描 述

udpLocalAddress inp_laddr 这个监听器的本地 IP


udpLocalPort inp_lport 这个监听器的本地端口号

图23-7 UDP监听器表: udpTable

23.3 UDP的protosw结构

图23-8显示了UDP的协议交换入口

成 员 i n e t s w[1] 描 述

pr_type SOCK_DGRAM UDP提供数据报分组服务


pr_domain &inetdomain UDP是Internet域的一部分
pr_protocol IPPROTO_UDP (17) 出现在IP首部的i p _ p字段
pr_flags PR_ATOMIC|PR_ADDR 插口层标志,协议处理没有使用
pr_input Udp_input 从IP层接收报文
pr_output 0 UDP没有使用
pr_ctlinput udp_ctlinput ICMP差错的控制输入函数
pr_ctloutput ip_ctloutput 响应来自进程的管理请求
pr_usrreq udp_usrreq 响应来自进程的通信请求
pr_init udp_init 初始化UDP
pr_fasttimo 0 UDP没有使用
pr_slowtimo 0 UDP没有使用
pr_drain 0 UDP没有使用
pr_sysctl udp_sysctl 对sysctl (8)系统调用

图23-8 UDP的protosw 结构

本章我们描述五个以 u d p _开头的函数。另外我们还要介绍第 6个函数 u d p _ o u t p u t,它

Linux公社 www.linuxidc.com
bbs.theithome.com
608 计计TCP/IP详解 卷 2:实现

不在协议交换入口,但当输出一个 UDP数据报时, udp_usrreq会调用它。

23.4 UDP的首部

UDP首部定义成一个 udphdr结构。图23-9是C结构,图23-10是UDP首部的图。

图23-9 udphdr 结构

16位源端口号 16位目的端口号
8字节

16位UDP长度 16位UDP检验和

数据(如果有)

图23-10 UDP首部和可选数据

在源代码中,通常把 U D P 首部作为一个紧跟着 UDP 首部的 I P首部来引用。这就是


u d p _ i n p u t如何处理收到的 IP数据报,以及 u d p _ o u t p u t如何构造外出的 IP数据报。这种联
合的IP/UDP首部是一个 udpiphdr结构,如图 23-11所示。

图23-11 udpiphdr 结构:联合的 IP/UDP首部

20字节的IP首部定义成一个 ipovly结构,如图 23-12所示。


不幸的是,这个结构并不是一个真正的如图 8 - 8所示的 I P首部。大小相同 ( 2 0字节),但字

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 609
段不同。我们将在 23.6节讲UDP检验和的计算时回来讨论这个不同之处。

图23-12 ipovly 结构

23.5 udp_init函数

domaininit函数在系统初始化时调用 UDP的初始化函数(udp_init,图23-13)。
这个函数所做的唯一的工作是把头部 P C B (u d b)的向前和向后指针指向它自己。这是一个
双向链表。
u d b P C B 的其他部分都被初始化成 0,尽管在这个头部 P C B 中唯一使用的字段是
i n p _ l p o r t,它是要分配的下一个 U D P临时端口号。在解习题 2 2 . 4时,我们提到,因为这个
本地端口号被初始化成 0,所以第一个临时端口号将是 1024。

图23-13 udp_init 函数

23.6 udp_output函数

当应用程序调用以下五个写函数中的任意一个时,发生 UDP 输出。这五个函数是:send、


s e n d t o、s e n d m s g、w r i t e或w r i t e v。如果插口已连接上的,则可任意调用这五个函数,
尽管用s e n d t o或s e n d m s g不能指定目的地址。如果插口没有连接上,则只能调用 s e n d t o和
s e n d m s g,并且必须指定一个目的地址。图 23-14总结了这五个函数,它们在终止时,都调用
udp_output,该函数再调用 ip_output。
五个函数终止调用 s o s e n d,并把一个指向 m s g h d r结构的指针作为参数传给该函数。要
输出的数据被分装在一个 m b u f链上, s o s e n d把一个可选的目的地址和可选的控制信息放到
mbuf中,并发布 PRU_SEND请求。
UDP调用函数udp_output,该函数的第一部分如图 23-15所示。四个参数分别是: inp,
指向插口Internet PCB的指针; m,指向输出 mbuf链的指针; addr,一个可选指针,指向某个
m b u f,存放分装在一个 s o c k a d d r _ i n结构中的目的地址; c o n t r o l,一个可选指针,指向
一个mbuf,其中存放着sendmsg中的控制信息。
1. 丢掉可选控制信息
333-344 m_freem丢弃可选的控制信息,不产生差错。 UDP输出不使用任何控制信息。

Linux公社 www.linuxidc.com
bbs.theithome.com
610 计计TCP/IP详解 卷 2:实现

注释x x x是因为忽略控制信息且不产生错误。其他协议如路由选择域和 T C P,当


进程传递控制信息时,都会产生一个错误。

库函数

系统调用
通过

把数据、目的地址和控制
信息放到mbuf中
请求

放到接口的输出队列上

图23-14 五个写函数如何终止调用 udp_output

2. 临时连接一个未连接上的插口
345-359 如 果 调 用 方 为 U D P 数 据 报 指 定 了 目 的 地 址 (a d d r 非 空 ) , 则 插 口 是由
i n _ p c b c o n n e c t临时连接到该目的地址的,并在该函数的最后被断连。在连接之前,要作
一个检测,判断插口是否已经连接上。如果已连接上,则返回错误 E I S C O N N。这就是为什么
sendto在已连接上的插口上指定目的地址时,会返回错误。
在临时连接插口之前, s p l n e t停止I P的输入处理。这样做的原因是因为,临时连接将改
变插口 P C B中的外部地址、外部端口以及本地地址。如果在临时连接该 P C B的过程中处理某
个收到的 U D P数据报,可能把该数据报提交给错误的进程。把处理器设置成比 s p l n e t优先,
只能阻止软件中断引发执行 I P输入程序 (图1 - 1 2 ),它不能阻止接口层接收进入的分组,并把它
们放到IP的输入队列中。
[ P a r t r i d g e和Pink 1993] 注意到临时连接插口的这个操作开销很大,用去每个
UDP传送将近三分之一的时间。
在临时连接之前, P C B的本地地址被保存在 l a d d r中,因为如果它是通配地址,它将被

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 611
in_pcbconnect在调用in_pcbbind时改变。
如果进程调用了 c o n n e c t,则应用于目的地址的同一规则也将适用,因为两种情况都将
调用in_pcbconnect。

图23-15 udp_output 函数:临时连接一个未连接上的插口

Linux公社 www.linuxidc.com
bbs.theithome.com
612 计计TCP/IP详解 卷 2:实现

360-364 如果进程没有指定目的地址,并且插口没有连接上,则返回 ENOTCONN。


3. 在前面加上 IP/UDP首部
366-373 M _ P R E P E N D在数据的前面为 I P和U D P首部分配空间。图 1 - 8是一种情况,假定
m b u f链上的第一个 m b u f已经没有空间存放首部的 2 8个字节。习题 2 3 . 1详细给出了其他情况。
需要指定标志位 M _ D O N T W A I T ,因为如果插口是临时连接的,则 I P 处理被阻塞,所以
M_PREPEND也应被阻塞。
早期的伯克利版本不正确地指定了这里的 M_WAIT。

23.6.1 在前面加上 IP/UDP首部和mbuf簇

在M _ P R E P E N D宏和m b u f簇之间有一个微妙的交互。如果 s o s e n d把用户数据放到一个簇


中,则该簇的最前面的 5 6个字节 (m a x _ h d r,图7 - 1 7 )没有使用,这就为以太网、 I P和U D P首
部提供了空间。避免 M _ P R E P E N D仅仅为存放这些首部而另外再分配一个 m b u f。M _ P R E P E N D
调用M_LEADINGSPACE来计算在mbuf的前面有多大的空间可以使用:
#define M_LEADINGSPACE(m) \
((m)->m_flags & M_EXT ? /* (m)->m_data - (m)->m_ext_buf */ 0 : \
(m)->m_flags & M_PKTHDR ? (m)->m_data - (m)->m_pktdat : \
(m)->m_data - (m)->m_dat)

正确地计算出簇前面可用空间大小的代码被注释掉了,如果数据在簇内,该宏总是返回 0。
这意味着,当用户数据也在某个簇中时, M _ P R E P E N D总是为协议首部分配一个新的 mbuf,而
不再使用 sosend分配的用于存放首部的空间。
M _ L E A D I N G S P A C E中注释掉正确代码的原因是因为该簇可能被共享 ( 2 . 9节),而
且,如果它被共享,使用簇中数据报前面的空间可能会擦掉其他数据。
U D P数据不共享簇,因为 u d p _ o u t p u t不保存数据的备份。但是 T C P在它的发
送缓存内保存数据备份 (等待对该数据的确认 ),而且如果数据不在簇内,则说明它是
共享的。但 t c p _ o u t p u t不调用M _ L E A D I N G S P A C E,因为s o s e n d只为数据报协议
在簇前面留 5 6个字节,所以, t c p _ o u t p u t总是调用 M G E T H D R为协议首部分配一个
mbuf。

23.6.2 UDP检验和计算和伪首部

在讨论u d p _ o u t p u t的后一部分之前,我们描述一下 U D P如何填充 I P / U D P首部的某些字


段,如何计算 U D P检验和,以及如何传递 I P / U D P首部及数据给 I P输出的。这些工作很巧妙地
使用了ipovly结构。
图23-16显示了u d p _ o u t p u t在由m指向的mbuf链的第一个存储器上构造的 28字节IP/UDP
首部。没有阴影的字段是 u d p _ o u t p u t填充的,有阴影的字段是 i p _ o u t p u t填充的。这个图
显示了首部在线路上的格式。
U D P检验和的计算覆盖了三个区域: ( 1 )一个1 2字节的伪首部,其中包含 I P首部的字段;
(2)8 字节U D P首部,和 ( 3 ) U D P数据。图 2 3 - 1 7显示了用于检验和计算的 1 2字节伪首部,以及
UDP首部。用于计算检验和的 UDP首部等价于出现在线路上的 UDP首部(图23-16)。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 613
0 15 16 31
4位首部 8位服务类型
4位版本号 16位全长(字节数)
长度 (TOS)

3位
16位标识符 13位分片偏移
标志

8位生存时间
8位协议 16位首部检验和 20字节
(TTL)

32位源IP地址

32位目的IP地址

16位源端口号 16位目的端口号
8字节
16位UDP长度 16位UDP检验和

图23-16 IP/UDP首部:UDP填充没有阴影的字段, IP填充有阴影的字段

0 15 16 31

32位源IP地址

UDP
32位目的IP地址 伪首部

零 8位协议(17) 16位UDP长度

16位源端口号 16位目的端口号
UDP
首部
16位UDP长度 16位UDP检验和

图23-17 检验和计算所使用的伪首部和 UDP首部

在计算UDP检验和时使用以下三个事实: (1)在伪首部(图23-17)中的第三个 32 bit字看起来


与I P首部(图2 3 - 1 6 )中的第三个 32 bit字类似:两个 8 bit值和一个 16 bit值。( 2 )伪首部中三个 3 2
bit值的顺序是无关的。事实上,Internet检验和的计算不依赖于所使用的 16 bit值的顺序(8.7节)。
(3)在检验和计算中另外再加上一个全 0的32 bit字没有任何影响。
u d p _ o u t p u t利用这三个事实,填充 u d p i p h d r结构(图2 3 - 11 )的字段,如图 2 3 - 1 8所示。
该结构包含在由 m指向的mbuf链的第一个 mbuf中。
在2 0字节I P首部( 5个成员: u i _ x 1、u i _ p r、u i _ l e n、u i _ s r c和u i _ d s t)中的最后三
个32 bit字被用作检验和计算的伪首部。 I P首部的前两个 32 bit字(u i _ n e x t和u i _ p r e v)也用
在检验和计算中,但它们被初始化成 0,所以不影响最后的检验和。
图23-19总结了我们描述的操作:
1) 图23-19中最上面的图是伪首部的协议定义,与图 23-17对应。

Linux公社 www.linuxidc.com
bbs.theithome.com
614 计计TCP/IP详解 卷 2:实现

0 15 16 31

20字节
8位生存时间 8位协议 16位首部检验和

32位源IP地址

32位目的IP地址

16位源端口号 16位目的端口号
8字节

16位UDP长度 16位UDP检验和

图23-18 udp_output 填充的udpiphdr

伪首部 UDP首部

源IP地址 目的IP地址 UDP 目的 UDP UDP 检验和计算的


0 pr 源端口
长度 端口 长度 检验和 协议目的标
多个0不影响 相同的字段 相同的字段
检验和计算 不同的顺序 相同的顺序

0 0 0 pr UDP 源IP 目的IP


源端口
目的 UDP UDP 用于检验和
长度 地址 地址 端口 长度 检验和 计算的结构

v l T 分片 T IP 源IP 目的IP 目的 UDP UDP


e e O IP 标识 源端口
偏移 T 端口 长度 检验和 线路上的首部
pr
r n S 长度 L 检验和 地址 地址

IP首部 UDP首部

图23-19 填充IP/UDP首部和计算 UDP检验和的操作

2) 中间的图是源代码使用的 udpiphdr结构,与图 23-11对应(为图的可读性,省略了所有


成员的前缀u i _)。这是u d p _ o u t p u t在mbuf链上的第一个 mbuf中构造的结构,然后被用于计
算UDP检验和。
3) 下面的图是出现在线路上的 I P / U D P首部,与图 2 3 - 1 6对应。上面有箭头的 7个字段是
u d p _ o u t p u t在检验和计算之前填充的。上面有星号的 3个字段是 u d p _ o u t p u t在检验和计
算之后填充的。其他 6个有阴影的字段是 ip_output填充的。
图23-20是udp_output函数的后半部分。
1. 为检验和计算准备伪首部
374-387 把u d p i p h d r结构(图2 3 - 1 8 )的所有成员设置成它们相应的值。 P C B中的本地和外
部插口已经是网络字节序,但必须把 U D P的长度转换成网络字节序。 U D P的长度是数据报的
Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 615
字节数(l e n,可以是 0 )加上UDP 首部的大小 ( 8 )。UDP 长度字段在UDP 检验和计算中出现了
两次:ui_len和ui_ulen。有一个是冗余的。

图23-20 udp_output 函数:填充首部、计算检验和并传给 IP

2. 计算检验和
388-395 计算检验和时,首先把它设成 0,然后调用 in_cksum。如果UDP检验和是禁止的
(一个坏的想法—见卷1的11 . 3节),则检验和的结果是 0。如果计算的检验和是 0,则在首部
中保存 1 6个1,而不是 0 (在求补运算中,全 1和全0都是0 )。这样,接收方就可以区分没有检验
和的U D P分组(检验和字段为 0 )和有检验和的 U D P分组了,后者的检验和值为 0 ( 1 6位的检验和
是16个1)。
变量u d p c k s u m(图2 3 - 3 )通常默认值为 1,使能 U D P检验和。对内核的编译可对
4.2BSD兼容,把 udpcksum初始化为0。
3. 填充UDP长度、TTL和TOS
396-398 指针ui指向一个指向某个标准 IP首部的指针 (ip),UDP设置IP首部内的三个字段。
I P长度字段等于 U D P数据报中数据的个数加上 I P / U D P首部大小 2 8。注意, I P首部的这个字段

Linux公社 www.linuxidc.com
bbs.theithome.com
616 计计TCP/IP详解 卷 2:实现

以主机字节序保存,不象首部其他多字节字段,是以网络字节序保存的。 i p _ o u t p u t在发送
之前,把它转换成网络字节序。
把IP首部里的 TTL和TOS字段的值设成插口 PCB中的值。在创建插口时, UDP设置这些默
认值,进程可用 s e t s o c k o p t改变它们。因为这三个字段—I P长度、 T T L和TO S—不是
伪首部的内容, U D P 检验和计算时也没有用到它们,所以,在计算检验和之后,调用
ip_output之前,必须设置它们。
4. 发送数据报
400-402 ip_output发送数据报。第二个参数 inp_options,是进程可用setsockopt
设置的 I P选项。这些 I P选项是 i p _ o u t p u t放置到I P首部中的。第三个参数是一个指向高速缓
存在 P C B中的路由的指针,第四个参数是插口选项。传给 i p _ o u t p u t 的唯一插口选项是
S O _ D O N T R O U T E(旁路选路表 )和S O _ B R O A D C A S T(允许广播 )。最后一个参数是一个指向该插
口的多播选项的指针。
5. 断连临时连接的插口
403-407 如果插口是临时连接上的,则 i n _ p c b d i s c o n n e c t断连插口,本地 I P地址在
PCB中恢复,恢复中断级别到保存的值。

23.7 udp_input函数

进程调用五个写函数之一来驱动 UDP 输出。图23-14显示的函数都作为系统调用的组成部


分被直接调用。另一方面,当 I P在它的协议字段指定为 U D P的输入队列上收到一个 I P数据报
时,才发生 U D P 的输入。 I P 通过协议交换表 ( 图 8 - 1 5 ) 中的 p r _ i n p u t 函数调用函数
u d p _ i n p u t 。因为 I P 的输入是在软件中断级,所以 u d p _ i n p u t 也在这一级上执行。
u d p _ i n p u t的目标是把UDP数据报放置到合适的插口的缓存内,唤醒该插口上因输入阻塞的
所有进程。
我们对udp_input函数的讨论分三个部分:
1) UDP对收到的数据报完成一般性的确认;
2) 处理目的地是单播地址的 UDP数据报:找到合适的 PCB,把数据报放到插口的缓存内,
3) 处理目的地是广播或多播地址的 UDP数据报:必须把数据报提交给多个插口。
最后一步是新的,是为了在 Net/3中支持多播,但占用了大约三分之一的代码。

23.7.1 对收到的UDP数据报的一般确认

图23-21是UDP输入的第一部分。
55-65 u d p _ i n p u t的两个参数是: m,一个指向包含了该 I P数据报的 m b u f链的指针;
iphlen,IP首部的长度 (包括可能的 IP选项)。
1. 丢弃IP选项
67-76 如果有IP选项,则ip_stripoptions丢弃它们。正如注释中表明的, UDP 应该保
存IP选项的一个备份,使接收进程可以通过 I P _ R E C V O P T S插口选项访问到它们,但这个还没
有实现。
77-88 如果 m b u f链上的第一个 m b u f小于 2 8字节 ( I P 首部加上 U D P 首部的大小 ),则
m_pullup重新安排mbuf链,使至少有28个字节连续地存放在第一个 mbuf中。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 617

图23-21 udp_input 函数:对收到的 UDP数据报的一般确认

Linux公社 www.linuxidc.com
bbs.theithome.com
618 计计TCP/IP详解 卷 2:实现

图23-21 (续)

2. 验证UDP长度
333-344 与UDP 数据报相关的两个长度是: IP首部的长度字段 (ip_len)和UDP首部的长度
字段(u h _ u l e n)。前面讲到, i p i n t r在调用 u d p _ i n p u t之前,从 i p _ l e n中抽取出 I P首部
的长度(图10-11)。比较这两个长度,可能有三种可能性:
1) ip_len等于uh_ulen。这是通常情况。
2) i p _ l e n大于u h _ u l e n。I P首部太大,如图 2 3 - 2 2所示。代码相信两个长度中小的那个
( U D P首部长度 ),m _ a d j从数据报的最后移走多余的数据字节。 m _ a d j的第二个参数是负数,
在图 2 - 2 0中我们说,从 m b u f链的最后截断数据。在这种情况下, U D P的长度字段出现冲突。
如果是这样,假定发送方计算了 U D P的检验和,则不久检验和会检测到这个错误,接收方也
会验证检验和,从而丢弃该数据报。 I P长度字段必须正确,因为 I P根据接口上收到的数据量
验证它,而强制的 IP首部检验和覆盖了 IP首部的长度字段。
IP数据报

忽略的
IP首部 UDP首部 UDP数据
数据

UDP长度uh_ulen
IP长度ip_len加上IP首部长度

图23-22 UDP长度太小

3) ip_len小于uh_ulen。当UDP 首部的长度给定时, IP数据报比可能的小。图 23-23显


示了这种情况。这说明数据报有错误,必须丢弃,没有其他的选择:如果 U D P长度字段被破
坏,用UDP检验和是无法检测到的。需要用正确的 UDP长度来计算 UDP检验和。
IP数据报

IP首部 UDP首部 UDP数据 数据不可用

UDP长度uh_ulen
IP长度ip_len加上IP首部长度

图23-23 UDP长度太大

正如我们提到的, UDP长度是冗余的。在第 28章中我们将看到, TCP自己的首部


内没有长度字段—它用I P长度字段减去 I P和T C P首部的长度,以此确定数据报内数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 619
据的数量。为什么存在 U D P 长度字段呢?可能是为了加上少量的差错检测,因为
UDP检验和是可选的。
3. 保存IP首部的备份,验证 UDP检验和
102-106 udp_input在验证检验和之前保存 IP首部的备份,因为检验和计算会擦去原始 IP
首部的一些字段。
110 只有当的UDP检验和(udpcksum)是内核允许的,并且发送方也计算了 UDP检验和(收到
的检验和不为0)时,才验证检验和。
这个检测是不正确的。如果发送方计算了一个检验和,就应该验证它,不管外
出的检验和是否被计算。变量 u d p c k s u m应该只指定是否计算外出的检验和。不幸
的是,许多厂商都复制了这个检测,尽管厂商已经改变它们产品的内核,却默认地
允许UDP检验和。
1 1 1 - 1 2 0 在计算检验和之前, I P首部作为 i p o v l y结构(图2 3 - 1 8 )引用,所有字段的初始化
都是udp_output在计算UDP检验和(上一节)时初始化的。
此时,如果数据报的目的地是一个广播或多播 I P地址,将执行特别的代码。我们把这段
代码推迟到本节最后。

23.7.2 分用单播数据报

假定数据报的目的地是一个单播地址,图 23-24显示了执行的代码。

图23-24 udp_input 函数:分用单播数据报

Linux公社 www.linuxidc.com
bbs.theithome.com
620 计计TCP/IP详解 卷 2:实现

1. 检查“向后一个”高速缓存
206-209 UDP 维护一个指针,该指针指向最后在其上接收数据报的 Internet PCB ,
u d p _ l a s t _ i n p c b。在调用 i n _ p c b l o o k u p之前,可能必须搜索 U D P表上的 P C B,把最近
一次接收 P C B的外部和本地地址以及端口号和收到数据报的进行比较。这称为“向后一个”
高速缓存(one-behind cache)[Partridge和Pink 1993]。它是根据这样一个假设,即收到的数据报
极有可能要发往上一个数据报发往的同一端口 [Mogul 1991]。这个高速缓存技术是在 4 . 3 B S D
Tahoe版本中引入的。
210-213 高速缓存的 P C B和收到数据报之间的四个比较的次序是故意安排的。如果 P C B不
匹配,则应尽快结束比较。最大的可能性是目的端口号不相同——这就是为什么第一个检测
它。不匹配的可能性最小的是本地地址,尤其在只有一个接口的主机,所以它是最后一个检
测。
不幸的是,这种“向后一个”高速缓存技术代码,在实际中毫无用处 [ P a r t r i d g e和P i n k
1 9 9 3 ]。最普通的 U D P服务器类型只绑定它的知名端口,它的本地地址、外部地址和外部端口
都是通配地址。最普通的 UDP 客户程序类型并不连接它的 UDP 插口;它用s e n d t o指定每个
数据报的目的地址。因此,大多数时间 PCB内的i n p _ l a d d r、i n p _ f a d d r和i n p _ f p o r t都
是通配的。在高速缓存的比较中,收到数据报的这四个值永远都不是通配的,这意味着只有
当指定P C B的四个本地和外部值是非通配时,高速缓存入口与收到数据报的比较才可能相等。
这种情况只在连接上的 UDP插口上发生。
在系统 b s d i上,u d p p s _ p c b c a c h e m i s s计数器是 41 253,u d p s _ i p a c k e t s
计数器是42 485。小于3% 缓存命中率。
n e t s t a t-s 命令打印出 u d p s t a t结构 (图2 3 - 5 )的大多数字段。不幸的是,
N e t / 3版本,以及多数厂家的版本都不打印 u d p p s _ p c b c a c h e m i s s。如果你想看它
们的值,用调试器检查在运行的内核中的变量。
2. 搜索所有UDP的PCB
214-218 假定与高速缓存的比较失败,则 i n _ p c b l o o k u p寻找一个匹配。指定
I N P L O O K U P _ W I L D C A R D标志,允许通配匹配。如果找到一个匹配,则把指向该 P C B的指针
保存在udp_last_inpcb中,我们说它高速缓存了最后收到的 UDP数据报的PCB。
3. 生成ICMP端口不可达差错
220-230 如果没找到匹配的 P C B,UDP 通常产生一个 I C M P端口不可达差错。首先检测收
到的m b u f链的m _ f l a g s,看看该数据报是否是要发送到一个链路级广播或多播地址。有可能
会收到一个发送到链路级广播或多播地址的 I P数据报,具有单播地址,此时不应该产生 I C M P
端口不可达差错。如果成功产生 I C M P差错,则把I P首部恢复成收到它时的值 (s a v e _ i p),也
把IP长度设置成它原来的值。
链路级广播或多播地址的检测是冗余的。 i c m p _ e r r o r也做这个检测。这个冗
余检测的唯一好处是,在 u d p s _ n o p o r t b c a s t 计数器之外,还维护了
udps_noport计数器。
把i p h l e n 改回 i p _ l e n 是一个错误。 i c m p _ e r r o r也会做这项工作,使得
I C M P差错返回的 I P首部的I P长度字段是 2 0字节,这太大了。可以在 T r a c e r o u t e程

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 621
序(卷1的第8章)中加上几行新程序,在最终到达目的主机后,打印出 I C M P端口不可
达差错报文中的这个字段,可以测试系统是否有这个错误。
图23-25是处理单播数据报的代码,把数据报提交给与目的 PCB对应的插口。

图23-25 udp_input 函数:把单播数据报提交给插口

4. 返回源站IP地址和源站端口
231-236 收到的 I P数据报的源站 I P地址和源站端口被保存在全局 s o c k a d d r _ i n结构中的
Linux公社 www.linuxidc.com
bbs.theithome.com
622 计计TCP/IP详解 卷 2:实现

udp_in。在函数的后面,该结构作为参数传给了 sbappendaddr。
采用全局变量保存 I P地址和端口号不出现问题的原因是, u d p _ i n p u t是单线程的。当
i p i n t r调用它时,它在返回之前完整地处理了收到的数据报。而且, s b a p p e n d a d d r还把
该插口结构从全局变量复制一个 mbuf中。
5. IP_RECVDSTADDR插口选项
337-244 常数INP_CONTROLOPTS是三个插口选项的结合,进程可以设置这三个插口选项,
通过系统调用 r e c v m s g返回插口的控制信息 (图2 2 - 5 )。I P _ R E C V D S T A D D R把收到的 UDP 数
据报中的目的 I P地址作为控制信息返回。函数 u d p _ s a v e o p t分配一个 M T _ C O N T R O L类型的
mbuf,并把4字节的目的 IP地址存放在该缓存中。我们在 23.8节中介绍这个函数。
该插口选项与 4.3BSD Reno 一起出现,是为一般文件传输协议 T F T P的应用程序
设计的,它们不响应发给广播地址的客户程序请求。不幸的是,即使接收应用程序
使用这个选项,也很难确定目的 IP地址是否是一个广播地址 (习题23.6)。
当4.4BSD中加上了多播功能后,这个代码只对目的地是单播地址的数据报有效。
我们将在图 2 3 - 2 6看到,对发给多播地址的广播数据报还没有实现这个选项,根本无
法达到该选项的目的。
6. 未实现的插口选项
245-260 这段代码被注释掉了,因为它们不起作用。 I P _ R E C V O P T S插口选项的原意是把
收到数据报的 IP选项作为控制信息返回,而 I P _ R E C V R E T O P T S插口选项返回源路由信息。三
个 I P _ R E C V 插口选项对 m p 变量的操作构造了一个最多有三个 m b u f 的链表,该链表由
s b a p p e n d a d d r放置到插口的缓存。图 2 3 - 2 5显示的代码只把一个选项作为控制信息返回,
所以指向该 mbuf的m_next总是一个空指针。
7. 把数据加到插口的接收队列中
262-272 此时,已经准备好把收到的数据报 (m指向的mbuf链)以及一个表示发送方 IP地址和
端口的插口地址结构 (u d p _ i n)和一些可选的控制信息 (o p t s指向的mbuf,目的IP地址)放到插
口的接收队列中。这个工作由 s b a p p e n d a d d r完成。但在调用这个函数之前,要修正指针和
缓存链上的第一个 mbuf,忽略掉 UDP和IP首部。返回之前,调用 s o r w a k e u p唤醒插口接收队
列中的所有睡眠进程。
8. 返回差错
273-276 如果在 UDP 输入处理的过程中遇到错误, u d p _ i n p u t会跳转到 b a d标号语句,
释放所有包含该数据报以及控制信息 (如果有的话 )的mbuf链。

23.7.3 分用多播和广播数据报

现在返回到 u d p _ i n p u t处理发给广播或多播 I P地址数据报的这部分代码。如图 2 3 - 2 6所


示。
121-138 正如注释所表明的,这些数据报被提交给匹配的所有插口,而不仅仅是一个插口。
我们提到的 U D P接口不够指的是除非连接上插口,否则进程没有能力在 U D P插口上接收异步
差错(特别是ICMP端口不可达差错 )。我们22-11节讨论这个问题。
139-145 源站的 I P地址和端口号被保存在全局 s o c k a d d r _ i n结构的 u d p _ i n中,传给
sbappendaddr。更新mbuf链的长度和数据指针,忽略 UDP和IP首部。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 623

图23-26 udp_input 函数:分用广播或多播数据报

Linux公社 www.linuxidc.com
bbs.theithome.com
624 计计TCP/IP详解 卷 2:实现

图23-26 (续)

146-164 大的 f o r 循环扫描每个 UDP PCB ,寻找所有匹配 P C B 。这种分用不调用


in_pcblookup,因为它只返回一个 PCB,而广播或多播数据报可能需要提交给多个 PCB。
如果P C B的本地端口和收到数据报的本地端口不匹配,则忽略该入口。如果 P C B的本地
端口不是通配地址,则把它和目的 I P地址比较,如果不相等则跳过该入口。如果 P C B内的外
部地址不是通配地址,就把它和源站 I P地址比较,如果不匹配,则外部端口也必须和源站端
口匹配。最后一个检测假定,如果插口连接到某个外部 I P地址,则它也必须连接到一个外部
端口,反之亦然。这与 in_pcblookup函数的逻辑相同。
165-177 如果这不是第一个匹配 (last非空),则把该数据报放到上一个匹配的接收队列中。
因为当sbappendaddr完成后要释放mbuf链,所以m_copy先要做个备份。 sorwakeup唤醒
所有等待这个数据的进程, last保存指向匹配的 socket结构的指针。
使用变量 l a s t避免调用 m _ c o p y函数(因为要复制整个 m b u f链,所以耗费很大 ),除非有
多个接收方接收该数据报。在通常只有一个接收方的情况下, f o r循环必须把 l a s t设成指向
一个匹配 P C B,当循环终止时, s b a p p e n d a d d r把m b u f链放到插口的接收队列中—不做备
份。
1 7 8 - 1 88 如果匹配的插口没有设置 S O _ R E U S E P O R T或S O _ R E U S E A D D R插口选项,则没必
要再找其他匹配,终止该循环。在循环的外部,调用 s b a p p e n d a d d r把数据报放到这个插口
的接收队列中。
189-197 如果在循环的最后, l a s t为空,没找到匹配,则并不产生 I C M P差错,因为该数
据报是发给广播或多播 IP地址。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 625
198-204 最后的匹配入口 (可能是唯一的匹配入口 )把原来的数据报 (m)放到它的接收队列中。
在调用sorwakeup后,udp_input返回,因为完成了对广播或多播数据报的处理。
函数的其他部分 (图23-24)处理单播数据报。

23.7.4 连接上的UDP插口和多接口主机

在使用连接上的 U D P插口与多接口主机上的一个进程交换数据报时,有一个微妙的问题。
来自对等实体的数据报可能到达时具有不同的源站 IP地址,不能提交给连接上的插口。
考虑图23-27所示的例子。
PPP链路

服务器进程,
客户进程 未连接上的
连接到 UDP
140.252.1.29 以太网140.252.13

UDP请求,目的IP=140.252.1.29

UDP回答源IP=140.252.13.33

ICMP端口不可达

图23-27 连接上的 UDP插口向一个多接口主机发送数据报的例子

有三个步骤:
1) b s d i上的客户程序创建一个 UDP插口,并把它连接到 140.252.1.29,这是s u n上的PPP
接口,而不是以太网接口。客户程序在插口上把数据报发给服务器。
S u n上的服务器接收并收下该数据报,即使到达接口与目的 I P地址不同 (s u n是一个路由
器,所以不管它实现的是弱端系统模型或强端系统模型都没有关系 )。数据报被提交给在未连
接上的UDP插口上等待客户请求的服务器。
2) 服务器发一个回答,因为是在一个未连接上的 U D P插口上发送的,所以由内核根据外
出的接口(140.252.13.33)选择回答的目的 IP地址。请求的目的 IP地址不作为回答的源站地址。
b s d i收到回答时,因为 I P地址不匹配,所以不把它提交给客户程序的连接上的 U D P接
口。
3) 因为无法分用回答,所以 b s d i产生一个 I C M P端口不可达差错 (假定在 b s d i上没有其
他进程符合接收该数据报的条件 )。
这个例子的问题在于,服务器并不把请求中的目的 I P地址作为回答的源站 I P地址。如果
它这样做,就不存在这个问题了,但这个办法并不简单—见习题23.10。我们将在图28-16中
看到,如果一个 T C P服务器没有明确地把一个本地 I P地址绑定它的插口上,它就把来自客户
的目的IP地址用作来自它自己的源 IP地址。

23.8 udp_saveopt函数

如果进程指定了 I P _ R E C V D S T A D D R插口选项,则 u d p _ i n p u t调用u d p _ s a v e o p t,从


收到的数据报中接收目的 IP地址:

Linux公社 www.linuxidc.com
bbs.theithome.com
626 计计TCP/IP详解 卷 2:实现

*mp = udp_saveopt((caddr_t) & ip_dst, sizeof(struct in_addr),


IP_RECVDSTADDR);

图23-28显示了这个函数。

图23-28 udp_saveopt 函数:用控制信息创建 mbuf

20字节

16位控制信息
目的IP地址

图23-29 把收到的数据报的目的地址作为控制信息保存的 mbuf

276-286 参数包括p,一个指向存储在 mbuf中的信息的指针 (收到的数据报的目的 IP地址);


s i z e ,字节数大小 (在这个例子中是 4 ,I P 地址的大小 ) ;以及 t y p e ,控制信息的类型
(IP_RECVDSTADDR)。
290-299 分配一个mbuf,并且因为是在软件中断级执行代码,所以指定 M_DONTWAIT。指
针c p指向m b u f的数据部分,是一个指向 c m s g h d r结构(图1 6 - 1 4 )的指针。 b c o p y把I P首部中

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 627
的IP地址复制到 c m s g h d r结构的数据部分。然后设置紧跟在 c m s g h d r结构后面的 mbuf的长度
(在本例中设成16)。图23-29是mbuf的最后一个状态。
c m s g _ l e n字段包含了 c m s g h d r的长度(12)加上c m s g _ d a t a字段的长度(本例中是 4)。如
果应用程序调用 r e c v m s g接收控制信息,则它必须检查 c m s g h d r结构,确定 c m s g _ d a t a字
段的类型和长度。

23.9 udp_ctlinput函数

当i c m p _ i n p u t收到一个 I C M P差错(目的主机不可达、参数问题、重定向、源站抑制和
超时)时,调用相应协议的 pr_ctlinput函数:
if (ctlfunc = inetsw[ ip_protox[icp->icmp_ip.ip_p] ].pr_ctlinput)
(*ctlfunc)(code, (struct sockaddr *)&icmpsrc, &icp->icmp_ip);

对于UDP,调用图22-32显示的函数 udp_ctlinput。我们将在图23-30中给出这个函数。
314-322 参数包括cmd,图11-19的一个PRC_xxx常数;sa,一个指向sockaddr_in结构的
指针,该结构含有 ICM P报文的源站I P地址; i p,一个指向引起差错的 I P首部的指针。对于目
的站不可达、参数问题、源站抑制和超时差错, ip指向引起差错的IP首部。但当pfctlinput
为重定向(图22-32)调用udp_ctlinput时,sa指向一个 sockaddr_in结构,该结构中包含
要被重定向的目的地址, i p是一个空指针。最后一种情况没有信息丢失,因为我们在 2 2 . 11节
看到,重定向应用于所有连接到目的地址的 T C P和U D P插口。但对其他差错,如端口不可达,
需要非空的第三个参数,因为协议跟在IP首部后面的协议首部包含了不可达端口。
323-325 如果差错不是重定向,并且 P R C _x x x的值太大或全局数组 i n e t c t l e r r m a p中没
有差错码,则忽略该 ICMP差错。为理解这个检测,我们来看一下对收到的 ICMP所做的处理:
1) icmp_input把ICMP类型和码转换成一个 PRC_xxx差错码。
2) 把PRC_xxx差错码传给协议的控制输入函数。
3) Internet PCB协议( T C P和U D P )用i n e t c t l e r r m a p把P R C _x x x差错码映射到一个 U n i x
的errno值,这个值被返回给进程。

图23-30 udp_ctlinput 函数:处理收到的 ICMP差错

Linux公社 www.linuxidc.com
bbs.theithome.com
628 计计TCP/IP详解 卷 2:实现

图11-1和图11-2总结了ICMP报文的处理。
回到图 2 3 - 3 0 ,我们可以看到如何处理响应 U D P 数据报的 I C M P 源站抑制报文。
i c m p _ i n p u t把I C M P报文转换成差错 P R C _ Q U E N C H,并调用 u d p _ c t l i n p u t。但因为在图
11-2中,这个ICMP差错的errno行是空白,所以忽略该差错。
326-331 in_pcbnotify函数把该ICMP差错通知给恰当的PCB。如果udp_ctlinput的第
三个参数非空,则把引起差错数据报的源和目的UDP端口以及源IP地址,传给in_pcbnotify。

udp_notify函数

i n _ p c b n o t i f y函数的最后一个参数是一个指向函数的指针, i n _ p c b n o t i f y为每个准
备接收差错的PCB调用该函数。对 UDP,该函数是 udp_notify,如图23-31所示。
301-313 该函数的第二个参数 e r r n o保存在插口的 s o _ e r r o r变量中。通过设置这个插口
变量,使插口变成可读,并且如果进程调用 s e l e c t,插口也可写。然后唤醒插口上所有正在
等待接收或发送的进程接收该差错。

图23-31 udp_notify函数:通知进程一个异步差错

23.10 udp_usrreq函数

许多操作都要调用协议的用户请求函数。从图 2 3 - 1 4我们看到,在某个 U D P插口上调用五


个写函数中的任意一个,都以请求 PRU_SEND调用UDP 的用户请求函数结束。
图23-32显示了u d p _ u s r r e q的开始和结束。 s w i t c h单独在后面的图中给出。图 15-17显
示了该函数的参数。

图23-32 udp_usrreq 函数体

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 629

图23-32 (续)

417-428 PRU_CONTROL请求来自ioctl系统调用。函数 in_control完整地处理该请求。


429-432 在函数的开头定义 i n p时,把插口指针转换成 P C B指针。唯一允许 P C B指针为空
的时候是创建新插口时 (PRU_ATTACH)。
433-436 注释表明,只要在 UDP PCB表中增加或删除表项,代码必须由 splnet保护起来。
这是因为 u d p _ u s r r e q是作为系统调用的一部分来调用的,在它修改 P C B的双重链表时,不
能被UDP输入中断 (被IP输入作为软件中断调用 )。在修改 PCB的本地或外部地址或端口时,也
必须阻塞UDP输入,避免 in_pcblookup不正确地提交收到的 UDP数据报。
我们现在讨论每个case语句。图23-33语句中的PRU_ATTACH请求,来自socket系统调用。

图23-33 udp_usrreq 函数:PRU_ATTACH 和PRU_DETACH 请求

Linux公社 www.linuxidc.com
bbs.theithome.com
630 计计TCP/IP详解 卷 2:实现

438-447 如果插口结构已经指向一个 PCB,则返回 EINVAL。in_pcballoc分配一个新的


PCB,把它加到 UDP PCB表的前面,把插口结构和 PCB链接到一起。
448-450 s o r e s e r v e 为插口的发送和接收缓存保留缓存空间。如图 1 6 - 7 所示,
s o r e s e r v e只是实施系统的限制,并没有真正分配缓存空间。发送和接收缓存的默认大小分
别是9 2 1 6字节(u d p _ s e n d s p a c e)和41 600字节(u d p _ r e c v s p a c e)。前者允许最大 9 2 0 0字节
的数据报 (在NFS分组中,有 8 KB的数据),加上16字节目的地址的 s o c k a d d r _ i n结构。后者
允许插口上一次最多有 40个1024字节的数据报排队。进程可调用 setsockopt改变这些值。
451-452 进程通过s e t s o c k o p t函数可以改变 PCB中原型IP首部的两个字段: TTL和TOS。
T T L默认值是 6 4 (i p _ d e f t t l),TO S的默认值是 0 (普通服务 ),因为 i n _ p c b a l l o c把P C B初
始化为0。
453-455 close系统调用发布PRU_DETACH请求,调用图23-34所示的udp_detach函数。
本节后面的 PRU_ABORT请求也调用这个函数。

图23-34 udp_detach 函数:删除一个 UDP PCB

如果最后收到的 P C B指针(“向后一个”缓存 )指向一个已分离的 P C B,则把缓存的指针设


成指向UDP 表的表头(udb)。函数in_pcbdetach从UDP 表中移走PCB,并释放该 PCB。
回到u d p _ u s r r e q,P R U _ B I N D请求是系统调用 b i n d的结果,而P R U _ L I S T E N请求是系
统调用listen的结果。如图23-35所示。
456-460 in_pcbbind完成所有PRU_BIND请求的工作。
461-463 对无连接协议来说,PRU_LISTEN请求是无效的—只有面向连接的协议才使用它。

图23-35 udp_usrreq 函数:PRU_BIND 和PRU_LISTEN 请求

前面提到,一个 U D P应用程序,客户或服务器 (通常是客户 ),可以调用 c o n n e c t。它修


改插口发送或接收的外部 I P地址和端口号。图 2 3 - 6显示了 P R U _ C O N N E C T、P R U _ C O N N E C T 2
和PRU_ACCEPT请求。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 631
464-474 如果插口已经连接上,则返回 EISCONN。在这个时候,不应该连接上插口,因为
在一个已经连接上的 U D P插口上调用 c o n n e c t ,会在生成 P R U _ C O N N E C T请求之前生成
PRU_DISCONNECT请求。否则,由in_pcbconnect完成所有工作。如果没有遇到任何错误,
soisconnected就把该插口结构标记成已经连接上的。
475-477 socketpair系统调用发布PRU_CONNECT2请求,只适用于 Unix域的协议。
478-480 PRU_ACCEPT请求来自系统调用 accept,只适用于面向连接的协议。

图23-36 udp_usrreq 函数:PRU _CONNECT 、PRU _CONNECT2 和PRU _ACCEPT 请求

对于UDP插口,有两种情况会产生 PRU_DISCONNECT请求:
1) 当关闭了一个连接上的 UDP插口时,在 PRU_DETACH之前调用PRU_DISCONNECT。
2) 当在一个已经连接上的 U D P插口上发布 c o n n e c t时,s o c o n n e c t在P R U _ C O N N E C T
请求之前发布PRU_DISCONNECT请求。
PRU_DISCONNECT请求如图23-37所示。

图23-37 udp_usrreq 函数:PRU_DISCONNECT 请求

如果插口没有连接上,则返回 E N O T C O N N。否则,i n _ p c b d i s c o n n e c t把外部IP地址设


成0.0.0.0,把外部地址设成 0。本地地址也被设成 0.0.0.0,因为c o n n e c t可能已经设置了这个
PCB变量。
调用s h u t d o w n说明进程数据发送结束,产生 P R U _ S H U T D O W N请求,尽管对 U D P插口来

Linux公社 www.linuxidc.com
bbs.theithome.com
632 计计TCP/IP详解 卷 2:实现

说,很少有进程发布这个系统调用。图 2 3 - 3 8显示了 P R U _ S H U T D O W N 、P R U _ S E N D 和
PRU_ABORT请求。
492-494 socantsendmore设置插口的标志,阻止其他更多输出。
495-496 图2 3 - 1 4显示了五个写函数如何调用 u d p _ s u r r e q,发布P R U _ S E N D请求。u d p _
output发送该数据报, udp_usrreq返回,避免执行release标号语句(图23-32),因为还不
能释放包含数据的mbuf链(m)。IP输出把这个mbuf链加到合适的接口输出队列中,当发送完数据
后,由设备驱动器释放mbuf链。

图23-38 udp_usrreq 函数体: PRU _SHUTDOWN 、PRU _SEND 和PRU _ABORT 请求

内核中 U D P输出的唯一缓冲是在接口的输出队列中。如果插口的发送缓存内有存放数据
报和目的地址的空间,则 s o s e n d调用u d p _ u s r r e q,该函数调用 u d p _ o u t p u t。图2 3 - 2 0显
示,u d p _ o u t p u t继续调用 i p _ o u t p u t,i p _ o u t p u t 为以太网调用 e t h e r _ o u t p u t,把
数据报放到接口的输出队列中 (如果有空间)。如果进程调用 s e n d t o的动作比接口快,就可以
发送该数据报, ether_output返回ENOBUFS,并被返回给进程。
497-500 在UDP插口上从不发布 PRU_ABORT请求。但如果发布,则断连插口,分离 PCB。
P R U _ S O C K A D D R 和 P R U _ P E E R A D D R 请求分别来自系统调用 g e t s o c k n a m e 和
getpeername。这两个请求和 PRU_SENSE请求一起,如图 23-39所示。

图23-39 udp_usrreq 函数体: PRU _SOCKADDR 、PRU _PEERADDR 和PRU _SENSE 请求

501-506 函数i n _ s e t s o c k a d d r和i n _ s e t p e e r a d d r从PCB中取得信息,并把结果保存


在addr参数中。
507-511 系统调用 f s t a t产生P R U _ S E N S E请求。该函数返回 O K,但并不返回其他信息。
我们将在后面看到, TCP把发送缓存的大小作为 stat结构的st_blksize元素返回。
图23-40显示了其他 7个PRU_xxx请求,UDP插口不支持。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 633
对最后两个请求的处理略微有些不同,因为 PRU_RCVD不把指向mbuf的指针(m是一个非空
指针)作为参数传递,而PRU_RCVOOB则传递指向协议mbuf的指针来填充。两种情况下,立即返
回错误,不终止switch语句的执行,释放mbuf链。调用方用PRU_RCVOOB释放它分配的mbuf。

图23-40 udp_usrreq 函数体:不支持的 7个请求

23.11 udp_sysctl函数

U D P 的s y s c t l 函数只支持一个选项, U D P 检验和标志位。系统管理员可以禁止用
s y s c t l( 8 )程序使能或禁止 U D P检验和。图 2 3 - 4 1 显示了 u d p _ s y s c t l函数。该函数调用
sysctl_int取得或设置整数 udpcksum的值。

图23-41 udp_sysctl 函数

23.12 实现求精

23.12.1 UDP PCB高速缓存

在22.12节中,我们讲到 PCB搜索的一般性质,以及代码是如何线性搜索协议的 PCB表的。


现在我们把它和图 23-24中UDP使用的“向后一个”高速缓存结合起来。
“向后一个”高速缓存的问题发生在当高速缓存的 PCB中有通配值时 (本地地址,外部地址

Linux公社 www.linuxidc.com
bbs.theithome.com
634 计计TCP/IP详解 卷 2:实现

或外部端口):高速缓存的值永远不和收到的数据报匹配。 [Partridge和Pink 1993] 测试的一个


解决办法是,修改高速缓存,不比较通配值。也就是说,不再把 P C B中的外部地址和数据报
的源地址进行比较,而是只有当 PCB中的外部地址不是通配地址时,才比较这两个值。
这个办法有一个微妙的问题 [ P a r t r i d g e和Pink 1993]。假定有两个插口绑定到本地端口 5 5 5
上。其中一个有三个通配成份,而另一个已经连接到外部地址 1 2 8 . 1 . 2 . 3,外部端口 1 6 0 0。如
果我们高速缓存第一个 P C B,且有一个数据报来自 1 2 8 . 1 . 2 . 3,端口 1 6 0 0,则不能仅仅因为高
速缓存的值具有通配外部地址就不比较外部地址。这叫做高速缓存隐藏 (cache hiding)。在这
个例子中,高速缓存的 PCB隐藏了另一个更好匹配的 PCB。
为解决高速缓存隐藏,当在高速缓存加上或删除一个入口时,要做更多的工作。不能高
速缓存那些可能隐藏其他 P C B的P C B。但这很简单,因为普通情形是每个本地端口都有一个
插口。刚才我们给的例子中,两个插口都绑定到本地端口 5 5 5,尽管可能 (尤其在一个多接口
主机上),但很少见。
[ P a r t r i d g e和Pink 1993] 的另一个提高测试的也是记录最后发送的数据报的 P C B。这是
[Mogul 1991]提出的,指出在所有收到的数据报中,一半都是对最后发送的数据报的回答。在
这里高速缓存隐藏也是个问题,所以不高速缓存那些可能隐藏其他 PCB的PCB。
在通用系统上测试 [Partridge和Pink 1993] 的两种高速缓存结果是, 100 000个左右收到的
UDP数据报显示出 57%命中最近收到 PCB高速缓存, 30%命中最近发送 PCB高速缓存。相比于
没有高速缓存的版本, udp_input使用的CPU时间减少了一半还多。
这两种高速缓存还在某种程度上依赖于位置:刚刚到达的 U D P数据报极大可能来自与最
近收到或发送 U D P数据报相同的对等实体上。后者对发送一个数据报并等待回答的请求—应
答应用程序很典型。 [ M c K e n n e y和Dove 1992] 显示某些应用程序,如联机交易处理 (OLTP)系
统的数据入口,没有产生 [Partridge和Pink 1993] 观察到的很高的命中率。正如我们在 22.12节
中提到的,对于具有上千个 OLTP连接的系统来说,把 P C B放在哈希链上,相对于最近收到和
最近发送高速缓存而言,性能提高了一个数量级。

23.12.2 UDP检验和

提高实现性能的下一个领域是把进程和内核之间的数据复制与检验和计算结合起来。
Net/3中,
在输出操作中,每个数据都被处理两遍:一次是从进程复制到mbuf中(uiomove函数,被sosend
调用);另一次是计算UDP检验和(函数in_cksum被udp_output调用)。输入跟输出一样。
[ P a r t r i d g e和 Pink1993] 修改了图 2 3 - 1 4的 U D P 输出处理,调用一个 U D P 专有函数
u d p _ s o s e n d,而不是s o s e n d。这个新函数计算 UDP 首部和内嵌的伪首部的检验和 (不调用
通用的 i n _ c k s u m函数),然后用特殊函数 i n _ u i o m o v e把数据从进程复制到一个 m b u f链上
(不是通用函数 u i o m o v e),由这个新函数复制数据,更新检验和。采用这个技术,花在复制
数据和计算检验和的时间减少了 40%到45%。
在接收方情况就不同了。 UDP 计算U D P首部和伪首部的检验和,移走 U D P首部,把数据
报在合适的插口上排队。当应用程序读取数据报时, s o r e c e i v e 的一个特殊版本
(u d p _ s o r e c ei v e )在把数据复制到用户高速缓存的同时,计算检验和。但是,如果检验和不
正确,在整个数据报被复制到用户高速缓存之前,检测不到错误。对于普通的阻塞插口来说,
u d p _ s o r e c e i v e仅仅等待下一个数据报的到达。但是若插口是无阻塞的,且下一个数据报
还没有准备好传给进程,就必须返回差错 E W O U L D B L O C K。对于无阻塞读的 U D P插口来说,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 23章 UDP:用户数据报协议计计 635
这意味着插口接口的两个变化:
1) s e l e c t函数可以指示无阻塞 UDP 插口可读,但如果检验和失败,其中一个读函数依
然要返回错误EWOULDBLOCK。
2) 因为是在数据报被复制到用户高速缓存之后检测到检验和错误,所以即使读没有返回
数据,应用程序的高速缓存也被改变了。
即使是阻塞插口,如果有检验和错误的数据报包含了 1 0 0字节的数据,而下一个没有错误
的数据报包含 4 0字节的数据,则 r e c v f r o m的返回长度是 4 0,但跟在用户高速缓存后面的 6 0
字节没有改变。
[Partridge和Pink1993] 在六台不同计算机上,对单纯复制和有检验和的复制的计时作了比
较。结果显示,在许多体系结构的机器上,在复制操作中计算检验和无需额外时间。这种情
况是在内存访问速度和 CPU处理速度正确匹配的系统上的,目前许多 RISC处理器都符合条件。

23.13 小结
UDP 是一个无连接的简单协议,这是我们为什么在 T C P之前讨论它的原因。 U D P输出很
简单:I P和U D P首部放在用户数据的前面,尽可能填满首部,把结果传递给 i p _ o u t p u t。唯
一复杂的是 U D P检验和计算,包括只为计算 U D P检验和而加上一个伪首部。我们将在第 2 6章
遇到用于计算TCP检验和的伪首部。
当u d p _ i n p u t收到一个数据报时,它首先完成一个常规确认 (长度和检验和 );然后的处
理根据目的 I P地址是单播地址、广播或多播地址而不同。最多把单播数据报提交给一个进程,
但多播或广播数据报可能会被提交给多个进程。“向后一个”高速缓存适用于单播,其中维护
着一个指向在其上接收数据报的最近 Internet PCB的指针。但是,我们也看到,由于 UDP应用
程序普遍使用通配地址,所以这个高速缓存技术实际上毫无用处。
调用u d p _ c t l i n p u t函数处理收到的 I C M P报文, u d p _ u s r r e q函数处理来自插口层的
PRU_xxx请求。

习题
23.1 列出udp_output传给ip_output的mbuf链的五种类型(提示:看看 sosend)。
23.2 当进程为外出的数据报指定了 IP选项时,上一题会是什么答案?
23.3 UDP 客户需要调用bind吗?为什么?
23.4 如果插口没有连接上,并且图 23-15中调用M _ P R E P E N D失败,那么在u d p _ o u t p u t
里,处理器优先级会发生什么变化?
23.5 udp_output不检测目的端口 0。它可能发送一个具有目的端口 0的UDP数据报吗?
23.6 假定当把一个数据报发送到一个广播地址时, I P _ R E C V D S T A D D R插口选项有效,
你如何确定这个地址是否是一个广播地址?
23.7 谁释放udp_saveopt(图23-38)分配的mbuf?
23.8 进程如何断连连接上的 U D P插口?也就是说,进程调用 c o n n e c t并与对等实体交
换数据报,然后进程要断连插口。允许它调用 sendto,并向其他主机发送数据报。
23.9 我们在图22-25的讨论中,注意到一个用外部IP地址255.255.255.255调用connect的UDP
应用程序,在接口上发送时,是把该接口对应的广播地址作为目的IP地址。如果UDP应
用使用未连接的插口,用目的地址255.255.255.255调用sendto,会发生什么情况?

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议
24.1 引言

传输控制协议,即 T C P,是一种面向连接的传输协议,为两端的应用程序提供可靠的端
到端的数据流传输服务。它完全不同于无连接的、提供不可靠的数据报传输服务的 UDP协议。
我们在第 2 3章中详细讨论了 U D P的实现,有 9个函数、约 8 0 0行C代码。我们将要讨论的
TCP实现包括28个函数、约 4500行C代码,因此,我们将 TCP的实现分成 7章来讨论。
这几章中不包括对 T C P概念的介绍,假定读者已阅读过卷 1的第1 7章~第2 4章,熟悉 T C P
的操作。

24.2 代码介绍

T C P实现代码包括 7个头文件,其中定义了大量的 T C P结构和常量和 6个C文件,包含 T C P


函数的具体实现代码。文件如图 24-1所示。

文 件 描 述

netinet/tcp.h t c p h d r结构定义
netinet/tcp_debug.h t c p _ d e b u g结构定义
netinet/tcp_fsm.h TCP有限状态机定义
netinet/tcp_seg.h 实现TCP序号比较的宏定义
netinet/tcp_timer.h TCP定时器定义
netinet/tcp_var.h t c p c b (控制块)和t c p s t a t (统计)结构定义
netinet/tcpip.h T C P + I P首部定义

netinet/tcp_debug.c 支持S O _ D E B U G 协议端口号调试(第27.10节)


netinet/tcp_input.c t c p _ i n p u t及其辅助函数 (第28和第29章)
netinet/tcp_output.c t c p _ o u t p u t及其辅助函数 (第26章)
netinet/tcp_subr.c 各种TCP子函数(第27章)
netinet/tcp_timer.c TCP定时器处理 (第25章)
netinet/tcp_usrreq.c PRU_xxx请求处理(第30章)

图24-1 TCP各章中将讨论的文件

图24-2描述了各 TCP函数与其他内核函数之间的关系。带阴影的椭圆分别表示我们将要讨
论的9个主要的TCP函数,其中 8个出现在 protosw结构中(图24-8),第9个是tcp_output。

24.2.1 全局变量

图24-3列出了TCP函数中用到的全局变量。

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 637
插口接收缓冲区 多种系统调用
系统初始化

软件中断

每200ms 每500ms 内核mbuf耗尽

图24-2 TCP函数与其他内核函数间的关系

变 量 数据类型 描 述

tcb struct inpcb T C P I n t e r n e t P C表表头


B
tcp_last_inpcb struct inpcb * 指向最后收到报文段的 PCB的指针:“后面一个”高速缓存
tcpstat struct tcpstat TCP统计数据(图24-4)
tcp_outflags u_char 输出标志数组,索引为连接状态 (图24-16)
tcp_recvspace u_long 端口接收缓存大小默认值 (8192字节)
tcp_sendspace u_long 端口发送缓存大小默认值 (8192字节)
tcp_iss tcp_seq TCP发送初始序号 (ISS)
tcprexmtthresh int ACK重复次数的门限值 (3),触发快速重传
tcp_mssdflt int 默认MSS值(512字节)
tcp_rttdflt int 没有数据时 RTT的默认值(3秒)
tcp_do_rfrc1323 int 如果为真(默认值),请求窗口大小和时间戳选项
tcp_now u_long 用于RFC 1323时间戳实现的 500 ms计数器
tcp_keepidle int 保活:第一次探测前的空闲时间 (2小时)
tcp_keepintvl int 保活:无响应时两次探测的间隔时间 (75秒)
tcp_maxidle int 保活:探测之后、放弃之前的时间 (10分钟)

图24-3 后续章节中将介绍的全局变量

24.2.2 统计量

全局结构变量 t c p s t a t中保存了各种 T C P统计量,图 2 4 - 4描述了各统计量的具体含义。


在接下来的代码分析过程中,读者会了解到这些计数器数值的变化过程。

Linux公社 www.linuxidc.com
bbs.theithome.com
638 计计TCP/IP详解 卷 2:实现

tcpstat成员 描 述 SNMP使用

tcps_accepts 被动打开的连接数 •
tcps_closed 关闭的连接数 (包括意外丢失的连接 )
tcps_connattempt 试图建立连接的次数 (调用c o n n e c t ) •
tcps_conndrops 在连接建立阶段失败的连接次数 (SYN收到之前 ) •
tcps_connects 主动打开的连接次数 (调用c o n n e c t成功)
tcps_delack 延迟发送的 A C K数
tcps_drops 意外丢失的连接数 (收到SYN之后) •
tcps_keepdrops 在保活阶段丢失的连接数 (己建立或正等待 SYN)
tcps_keepprobe 保活探测指针发送次数
tcps_keeptimeo 保活定时器或连接建立定时器超时次数
tcps_pawsdrop 由于PSWS而丢失的报文段数
tcps_pcbcachemiss PCB高速缓存匹配失败次数
tcps_persisttimeo 持续定时器超时次数
tcps_predack 对ACK报文首部预测的正确次数
tcps_preddat 对数据报文首部预测的正确次数
tcps_rcvackbyte 由收到的 ACK报文确认的发送字节数
tcps_rcvackpack 收到的ACK报文数
tcps_rcvacktoomuch 收到的对未发送数据进行确认的 ACK报文数
tcps_rcvafterclose 连接关闭后收到的报文数
tcps_rcvbadoff 收到的首部长度无效的报文数 •
tcps_rcvbadsum 收到的检验和错误的报文数 •
tcps_rcvbyte 连续收到的字节数
tcps_rcvbyteafterwin 在滑动窗口己满时收到的字节数
tcps_rcvdupack 收到的重复 A C K报文的次数
tcps_rcvdupbyte 在完全重复报文中收到的字节数
tcps_rcvduppack 内容完全一致的报文数
tcps_rcvoobyte 收到失序的字节数
tcps_rcvoopack 收到失序的报文数
tcps_rcvpack 顺序接收的报文数
tcps_rcvpackafterwin 携带数据超出滑动窗口通告值的报文数
tcps_rcvpartdupbyte 部分内容重复的报文中的重复字节数
tcps_rcvpartduppack 部分数据重复的报文数
tcps_rcvshort 长度过短的报文数 •
tcps_rcvtotal 收到的报文总数 •
tcps_rcvwinprobe 收到的窗口探测报文数
tcps_rcvwinupd 收到的窗口更新报文数
tcps_rexmttimeo 重传超时次数
tcps_rttupdated RTT估算值更新次数
tcps_segstimed 可用于RTT测算的报文数
tcps_sndacks 发送的纯 ACK报文数(数据长度 =0)
tcps_sndbyte 发送的字节数
tcps_sndctrl 发送的控制 (SYN、FIN、RST)报文数(数据长度=0)
tcps_sndpack 发送的数据报文数 (数据长度>0)
tcps_sndprobe 发送的窗口探测次数 (等待定时器强行加入 1字节数据)
tcps_sndrexmitbyte 重传的数据字节数 •
tcps_sndrexmitpack 重传的报文数 •
tcps_sndtotal 发送的报文总数 •
tcps_sndurg 只携带URG标志的报文数 (数据长度=0)
tcps_sndwinup 只携带窗口更新信息的报文数 (数据长度=0)
tcps_timeoutdrop 由于重传超时而丢失的连接数

图24-4 tcpstat 结构变量中保存的 TCP统计量

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 639
在命令行输入netstat-s,系统将输出当前 TCP的统计值。图24-5的例子显示了主机连续
运行30天后,各统计计数器的值。由于某些统计量互相关联—一个保存数据分组数目,另一
个保存相应的字节数—图中做了一些简化。例如,表中第二行 tcps_snd(pack,byte)实际
表示了两个统计量, tcps_sndpack和tcps_sndbyte。
tcps_sndbyte值应为3 722 884 824字节,而不是-22 194 928字节,平均每个
数据分组有 450字节。类似的, tcps_rcvackbyte值应为3 738 811 552字节,而不
是-21 264 360字节(平均每个数据分组 565字节)。这些数据之所以被错误地显示,是
因为n e t s t a t程序中调用 p r i n t f语句时使用了 % d (符号整型 ),而非 %l u(无符号长
整型)。所有统计量均定义为无符号长整型,上面两个统计量的值己接近无符号 3 2位
长整型的上限(232-1=4 294 967 295)。

输出 成员

图24-5 TCP统计量样本

Linux公社 www.linuxidc.com
bbs.theithome.com
640 计计TCP/IP详解 卷 2:实现

24.2.3 SNMP变量

图 2 4 - 6列出了 SNMP TCP 组中定义的 1 4个 S N M P 简单变量,以及与它们相对应的


t c p s t a t结构中的统计量。前四项的常量值在 N e t / 3中定义,计数器 t c p C u r r E s t a b用于保
存TCP PCB表中Internet PCB的数目。
图24-7列出了tcpTable,即TCP监听表(listener table )。

SNMP变量 t c p s t a t成员或常量 描 述

tcpRtoAlgorithm 4 用于计算重传定时时限的算法:
1=其他;
2=RTO为固定值;
3=MIL-STD-1778附录B;
4=Van Jacobson的算法;
tcpRtoMin 1000 最小重传定时时限,以毫秒为单位
tcpRtoMax 64000 最大重传定时时限,以毫秒为单位
tcpMaxConn -1 可支持的最大 TCP连接数(-1表示动态设置 )
tcpActiveOpens tcps_connattempt 从CLOSED转换到SYN SENT的次数
tcpPassiveOpens tcps_accepts 从LISTEN转换到SYN RCVD的次数
tcpAttemptFails tcps_conndrops 从SYN_SENT或SYN_RCVD转换到CLOSED的
次数+从SYN_RCVD转换到LISTEN的次数
tcpEstabResets tcps_drops 从ESTABLISHED或CLOSE_WAIT转换到
CLOSED的次数
tcpCurrEstab (见正文) 当前位于 ESTABLISHED或CLOSE_WAIT状态
的连接数
tcpInSegs tcps_rcvtotal 收到的报文总数
tcpOutSegs tcps_sndtotal - 发送的报文总数,减去重传报文数
tcps_sndrexmitpack
tcpRetransSegs tcps_sndrexmitpack 重传的报文总数
tcpInErrs tcps_rcvbadsum + 收到的出错报文总数
tcps_rcvbadoff +
tcps_rcvshort
tcpOutRsts (未实现) RST标志置位的发送报文数

图24-6 tcp 组中的简单 SNMP变量

index = <tcpConnLocalAddress>.<tcpConnLocalPort>.<tcpConnRemAddress>.<tcpConnRemPort>

SNMP变量 PCB变量 描 述

tcpConnState t_state 连接状态: 1 = CLOSED, 2=LISTEN, 3 = SYN_SENT,


4 = SYN_RCVD, 5 = ESTABLISHED,6 = FIN_WAIT,
7 = FIN_WAIT_2, 8 = CLOSE_WAIT, 9 = LAST_ACK,
10 = CLOSING, 11 = TIME_WAIT,12 = 删除TCP控制块
tcpConnLocalAddress inp_laddr 本地IP地址
tcpConnLocalPort inp_lport 本地端口号
tcpConnRemAddress inp_faddr 远端IP地址
tcpConnRemPort inp_fport 远端端口号

图24-7 TCP监听表:tcpTable 中的变量

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 641
第一个 P C B变量(t _ s t a t e)来自T C P控制块 (图2 4 - 1 3 ),其他四个变量来自 Internet PCB
(图22-4)。

24.3 TCP 的protosw结构

图2 4 - 8列出了 TCP p r o t o s w结构的成员变量,它定义了 T C P协议与系统内其他协议间的


交互接口。

成员变量 inetsw[2] 描 述

pr_type SOCK_STREAM TCP提供字节流传输服务


pr_domain &inetdomain TCP属于Internet协议族
pr_ptotocol IPPROTO_TCP(6) 填充IP首部的i p _ p字段
pr_flags PR_CONNREQUIRED|PR_WANTRCVD 插口层标志,协议处理中忽略
pr_input tcp_input 从IP层接收消息
pr_output 0 TCP协议忽略该成员变量
pr_ctlinput tcp_ctlinput 处理ICMP错误的控制输入函数
pr_ctloutput tcp_ctloutput 在进程中响应管理请求
pr_usrreg tcp_usrreq 在进程中响应通信请求
pr_init tcp_init TCP初始化
pr_fasttimo tcp_fasttimo 快超时函数,每 200 ms 调用一次
pr_slowtimo tcp_slowtimo 慢超时函数,每 500 ms 调用一次
pr_drain tcp_drain 内核mbuf耗尽时调用
pr_sysctl 0 TCP协议忽略该成员变量

图24-8 TCP protosw 结构

24.4 TCP的首部

t c p h d r结构定义了 T C P首部。图 2 4 - 9给出了t c p h d r结构的定义,图 2 4 - 1 0描述了T C P首


部。

图24-9 tcphdr 结构

Linux公社 www.linuxidc.com
bbs.theithome.com
642 计计TCP/IP详解 卷 2:实现

16位源端口号 16位目的端口号

32位序号

20字节
32位确认序号

4位 保留
首部长度 (6位) 16位窗口大小

16位TCP检验和 16位紧急数据偏移量

选项(如果有)

数据(如果有)

图24-10 TCP首部及可选数据

大多数 R F C文档,相关书籍 (包括卷 1 )和接下来要讨论的 T C P实现代码,都把


t h _ u r p称为“紧急指针 ( u rgent pointer) ”。更准确的名称应该是“紧急数据偏移量
(urgent offset)”,因为这个字段给出的 16 bit无符号整数值,与 t h _ s e g序号字段相加
后,得到发送的紧急数据最后一个八位组的 32 bit序号(关于该序号应该是紧急数据最
后一个字节的序号,或者是紧急数据结束后的第一个字节的序号,一直存在着争议。
但就我们目前的讨论而言,这一点无关紧要 )。图2 4 - 1 3中,T C P代码把保存紧急数据
最后一个八位组的 32 bit 序号的 s n d _ u p正确地称为“紧急数据发送指针”。如果将
TCP首部的16 bit偏移量也称为“指针”,容易引起误解。在练习 26.6中,我们重申了
“紧急指针”和“紧急数据偏移量”间的区别。
T C P首部中4 bit的首部长度、接着的 6 bit的保留字段和 6 bit的码元标志,在 C结构中定义
为两个4 bit的比特字段,和紧跟的一个 8 bit字节。为了处理两个比特字段在 8 bit 字节中的存放
次序,C代码根据不同的主机字节存储顺序使用了 #ifdef语句。
还请注意, T C P中称4 bit 的t h _ o f f为“首部长度”,而C代码中称之为“数据偏移量”。
两种名称都正确,因为它表示 T C P首部的长度,包括可选项,以 32 bit为单位,也就是指向用
户数据第一个字节的偏移量。
th_flags成员变量包括6个码元标志比特,通过图 24-11中定义的名称读写。
N e t / 3中,T C P首部通常意味着“ I P首部 + TCP首部”。t c p _ i n p u t处理收到的 I P数据报
和t c p _ o u t p u t构造待发送的 I P数据报时都采用了这一思想。图 2 4 - 1 2中给出了 t c p i p h d r结
构的定义,形式化地描述了组合的 IP/TCP首部。
38-58 图23-19给出的ipovly结构定义了 20字节长度的 IP首部。通过前面章节的讨论可知,
尽管长度相同(20字节),但这个结构并不是一个真正的 IP首部。

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计643
th_flags 描 述

TH_ACK 确认序号(t h _ a c k)有效


TH_FIN 发送方字节流结束
TH_PUSH 接收方应该立即将数据提交给应用程序
TH_RST 连接复位
TH_SYN 序号同步(建立连接)
TH_URG 紧急数据偏移量 (t h _ u r p)有效

图24-11 th_flags 值

图24-12 tcpiphdr 结构定义:组合的 IP/TCP首部

24.5 TCP的控制块

在图2 2 - 1中我们看到,除了标准的 Internet PCB外,T C P还有自己专用的控制块, t c p c b


结构,而UDP则不需要专用控制块,它的全部控制信息都已包含在 Internet PCB中。
TCP控制块较大,需占用 140字节。从图 22-1中可看到,Internet PCB与TCP控制块彼此对
应,都带有指向对方的指针。图 24-13给出了TCP控制块的定义。

图24-13 tcpcb 结构:TCP控制块

Linux公社 www.linuxidc.com
bbs.theithome.com
644计计TCP/IP详解 卷 2:实现

图24-13 (续)

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 645
现在暂不讨论上述成员变量的具体含义,在后续代码中遇到时再详细分析。
图24-14列出了t_flags变量的可选值。

t_flags 描 述

TF_ACKNOW 立即发送ACK
TF_DELACK 延迟发送ACK
TF_NODELAY 立即发送用户数据,不等待形成最大报文段 (禁止Nagle算法)
TF_NOOPT 不使用TCP选项(永不填充TCP选项字段)
TF_SENTFIN FIN已发送
TF_RCVD_SCALE 对端在SYN报文中发送窗口变化选项时置位
TF_RCVD_TSTMP 对端在SYN报文中发送时间戳选项时置位
TF_REQ_SCALE 已经/将要在SYN报文中请求窗口变化选项
TF_REQ_TSTMP 已以/将要在SYN中请求时间戳选项

图24-14 t_flags 取值

24.6 TCP的状态变迁图

TCP协议根据连接上到达报文的不同类型,采取相应动作,协议规程可抽象为图 24-15所示
的有限状态变迁图。读者在本书的扉页前也可找到这张图,以便在阅读有关TCP的章节时参考。
图中的各种状态变迁组成了 T C P有限状态机。尽管 T C P协议允许从 L I S T E N状态直接变迁
到S Y N _ S E N T状态,但使用 SOCKET API编程时这种变迁不可实现 (调用l i s t e n后不可以调
用connect)。
TCP控制块的成员变量 t_state保存一个连接的当前状态,可选值如图 24-16所示。
图中还定义了 t c p _ o u t f l a g s数组,保存了处于对应连接状态时 t c p _ o u t p u t将使用的
输出标志。
图2 4 - 1 6还列出了与符号常量相对应的数值,因为在代码中将利用它们之间的数值关系。
例如,有下面两个宏定义:
#define TCPS_HAVERCVDSYN(s) ((s)>=TCPS_SYN_RECEIVED)
#define TCPS_HAVERCVDFIN(s) ((s)>=TCPS_TIME_WAIT)

类似地,连接未建立时,即 t _ s t a t e小于T C P S _ E S T A B L I S H E D时,t c p _ n o t i f y处理


ICMP差错的方式也不同。
T C P S _ H A V E R C V D S Y N的命名是正确的,但 T C P S _ H A V E R C V D F I N则可能引起误
解,因为在 CLOSE_WAIT、CLOSING和LAST_ACK状态也会收到FIN。我们将在第
29章中遇到该宏。

半关闭

当进程调用 s h u t d o w n且第二个参数设为 1时,称为“半关闭”。T C P发送F I N,但允许进


程在同一端口上继续接收数据 (卷1的18.5节中举例介绍了 TCP的半关闭)。
例如,尽管图 2 4 - 1 5中只在 E S TA B L I S H E D状态标注了“数据传输”,但如果进程执行了
“半关闭”,则连接变迁到FIN_WAIT_1状态和其后的FIN_WAIT_2状态,在这两个特定状态中,
进程仍然可以接收数据。

Linux公社 www.linuxidc.com
bbs.theithome.com
646 计计TCP/IP详解 卷 2:实现

起点

appl被动打开
send: <无>

超时

被动打开

关闭
或超时
同时打开 主动打开

关闭

数据传输状态

关闭

同时关闭

<无>

被动关闭

<无> <无>

超时
主动关闭

正常情况下,客户端的状态变迁
正常情况下,服务器端的状态变迁
appl: 应用程序执行操作引起的状态变迁
recv: 接收报文引起的状态变迁
send: 状态变迁中发送的报文

图24-15 TCP状态变迁图

24.7 TCP的序号

T C P连接上传输的每个数据字节,以及 S Y N、F I N等控制报文都被赋予一个 32 bit的序号。


T C P首部的序号字段 (图2 4 - 1 0 )填充了报文段第一个数据字节的 32 bit的序号,确认号字段填充

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 647
了发送方希望接收的下一序号,确认已正确接收了所有序号小于等于确认号减 1的数据字节。
换言之,确认号是 A C K发送方等待接收的下一序号。只有当报文首部的 A C K标志置位时,确
认序号才有效。读者将看到,除了在主动打开首次发送 S Y N时( S Y N _ S E N T状态,参见图 2 4 -
16中的tcp_outflags[2])或在某些RST报文段中, ACK标志总是被置位的。

t_state 值 描 述 tcp_outflags[]

TCPS_CLOSED 0 关闭 TH_RST | TH_ACK


TCPS_LISTEN 1 监听连接请求 (被动打开) 0
TCPS_SYN_SENT 2 已发送SYN(主动打开) TH_SYN
TCPS_SYN_RECEIVED 3 已发送并接收 SYN;等待ACK TH_SYN | TH_ACK
TCPS_ESTABLISHED 4 连接建立(数据传输) TH_ACK
TCPS_CLOSE_WAIT 5 已收到FIN,等待应用程序关闭 TH_ACK
TCPS_FIN_WAIT_1 6 已关闭,发送 FIN;等待ACK和FIN TH_FIN | TH_ACK
TCPS_CLOSING 7 同时关闭;等待 ACK TH_FIN | TH_ACK
TCPS_LAST_ACK 8 收到的FIN已关闭;等待 ACK TH_FIN | TH_ACK
TCPS_FIN_WAIT_2 9 已关闭,等待 FIN TH_ACK
TCPS_TIME_WAIT 10 主动关闭后 2MSL等待状态 TH_ACK

图24-16 t_state 取值

由于TCP连接是全双工的,每一端都必须为两个方向上的数据流维护序号。 TCP控制块中
(图24-13)有13个序号:8个用于数据发送 (发送序号空间),5个用于数据接收 (接收序号空间)。
图2 4 - 1 7给出了发送序号空间中 4个变量间的关系: s n d _ w n d、s n d _ u n a、s n d _ n x t和
snd_max。这个例子列出了数据流的第 1~第11字节。
提供的窗口
(由接收方通告)

可用窗口

无法发送直
发送尚未确认 到窗口移动
发送并已确认 能够发送

最早的未确认 下一个发送序号
过的序号

最大发送序号

图24-17 发送序号空间举例

一个有效的 ACK序号必须满足:
snd_una < 确认序号 <= snd_max

图2 4 - 1 7 的例子中,一个有效 A C K 的确认号必须是 5、6或7。如果确认号小于或等于


snd_una,则是一个重复的 ACK。它确认了已确认过的八位组,否则 s n d _ u n a不会递增超过
那些序号。

Linux公社 www.linuxidc.com
bbs.theithome.com
648 计计TCP/IP详解 卷 2:实现

tcp_output中有多处用到下面的测试,如果正发送的是重传数据,则表达式为真:
snd_nxt < snd_max

图2 4 - 1 8给出了图 2 4 - 1 7中连接的另一端:接收序号空间,图中假定还未收到序号为 4、5、


6的报文,标出了三个变量 rcv_nxt、rcv_wnd和rcv_adv。

接收窗口
(向发送方通告)

TCP已确认的序号
不允许接收的序号

下一个接收序号 通告序号最大值加1

图24-18 接收序号空间举例

如果接收报文段中携带的数据落在接收窗口内,则该报文段是一个有效报文段。换言之,
下面两个不等式中至少要有一个为真。
rcv_nxt <= 报文段起始序号 < rcv_nxt + rcv_wnd
rcv_nxt <=报文段终止序号 < rcv_nxt + rcv_wnd

报文段起始序号就是 T C P首部的序号字段, t i _ s e q。终止序号是序号字段加上 T C P数据


长度后减1。
例如,图24-19中的TCP报文段,携带了图 24-17中发送的三个字节,序号分别是 4、5和6。

63字节IP数据报

IP首部 IP选项 TCP首部 TCP选项

20字节 8 20 12 1 1 1

图24-19 TCP报文段在 IP数据报中传输

假定IP数据报中有8字节的IP任选项和12字节的TCP任选项。图12-20列出了各有关变量的
取值。

变 量 值 描 述

ip_hl 7 IP首部+IP任选项长度,以 32 bit 为单位(=28字节)


ip_len 63 IP数据报长度,以字节为单位 (20+8 +20+12+3)
ti_off 8 TCP首部+TCP任选项长度,以 32 bit为单位(=32字节)
ti_seq 4 用户数据第一个字节的序号
ti_len 3 TCP数据的字节数: i p _ l e n - ( i p _ h l×4)-(t i _ o f f×4)
6 用户数据最后一个字节的序号: t i _ s e q + t i _ l e n -1

图24-20 图24-19中各变量的取值

t i _ l e n并非T C P首部的字段,而是在对接收到的首部计算检验和及完成验证之后,根据
图2 4 - 2 0中的算式得到的结果,存储到外加的 I P结构中(图2 4 - 1 2 )。图中最后一个值并不存储到
变量中,而是在需要时直接从其他值中通过计算得到。

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 649
1. 序号取模运算
T C P必须处理的一个问题是序号来自有限的 3 2位取值空间: 0~4 294 967 295。如果某个
TCP连接传输的数据量超过 232字节,序号从4 294 967 295 回绕到0,将出现重复序号。
即使传输数据量小于 2 32字节,仍可能遇到同样的问题,因为连接的初始序号并不一定从 0
开始。各数据流方向上的初始序号可以是 0~4 294 967 295之间的任何值。这个问题使序号复
杂化。例如,序号 1可能大于序号4 294 967 295。
在tcp.h中,TCP序号定义为 u n s i g n e d l o n:
g
typedef u_long tcp_seq;

图24-21定义了4个用于序号比较的宏。

图24-21 TCP序号比较宏

2. 举例—序号比较
下面这个例子说明了 TCP序号的操作方式。假定序号只有 3 bit,0~7。图24-22列出了全部
8个序号和相应的二进制补码 (为求二进制补码,将二进制码中的所有 0变为 1,所有 1变为 0,
最后再加1)。给出补码形式,是因为 a-b =a+(b的补码)。

二进制码 二进制补码

图24-22 3 bit序号举例

表中最后三栏分别是 0-x、1-x和2-x。在这三栏中,如果定义计算结果是带符号整数 (注
意图 2 4 - 2 1 中的四个宏,计算结果全部强制转换为 i n t ) ,那么最高位为 1 表示值小于 0
(S E Q _ L T宏),最高位为 0且值不为0表示大于0 ( S E Q _ G T宏)。最后三栏中以横线分隔开四个负
值和四个非负值。
请注意图 2 4 - 2 2中的第四栏 (标注“0-x”),可看出 0小于1、2、3和4 (最高位比特为 1 ),而
0大于5、6和7(最高位比特为0且结果非0)。图24-23显示了这种关系。

0大于这些序号 0小于这些序号

图24-23 3 bit的TCP序号的比较

Linux公社 www.linuxidc.com
bbs.theithome.com
650 计计TCP/IP详解 卷 2:实现

图24-22中的第五栏 (1-x)也存在类似的关系,如图 24-24所示。

1大于这些序号 1小于这些序号

图24-24 3 bit的TCP序号的比较

图24-25是上面两图的另一种表示形式,使用圆环强调了序号的回绕现象。

小于1
小于0 大于0
大于1

图24-25 图24-23和图24-24的另一种表示形式

就T C P而言,通过序号比较来确定给定序号是新序号或重传序号。例如,在图 2 4 - 2 4的例
子中,如果 T C P正等待的序号为 1,但到达序号为 6,通过前面介绍的计算可知 6小于1,从而
判定这是重传的数据,可予以丢弃。但如果到达序号为 5,因为5大于1,TCP判定这是新数据,
予以保存,并继续等待序号为 2、3和4的八位组(假定序号为 5的数据字节落在接收窗口内 )。
图24-26扩展了图24-25中左边的圆环,用 TCP 32 bit的序号替代了3 bit 的序号。

小于0 大于0

图24-26 与序号0比较:采用 32 bit序号

图24-26右边的圆环强调了 32 bit序号空间的一半有 231个可用数字。

24.8 tcp_init函数

系统初始化时, domaininit函数调用TCP的初始化函数: tcp_init (图24-27)。


1. 设定初始发送序号
初始发送序号 (ISS),tcp_iss,被初始化为1。请注意,代码注释指出,这是错误的。后
面讨论TCP的“平静时间(quite time)”时,将简单介绍这一选择的原因。请读者自行与图 7-23
中IP标识符的初始化做比较,后者使用了当天的时钟。

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 651

图24-27 tcp_init 函数

2. TCP Internet PCB链表初始化


PCB首部(tcb)的previous指针和next指针都指向自己,这是一个空的双向链表。 tcb PCB
的其余成员均初始化为 0 (所有未明确初始化的全局变量均设为 0 )。事实上,除链表外,在该
P C B首部中只用了一个字段 i n p _ l p o r t:下一个分配的 T C P临时端口号。 T C P使用的第一个
临时端口号应为 1024,练习22.4的解答中给出了原因。
3. 计算最大协议首部长度
到目前为止,讨论过的协议首部的长度最大不超过 40字节,m a x _ p r o t o h d r设为40(组合
的I P / T C P首部长度,不带任何可选项 )。图7 - 1 7定义了该变量。如果 m a x _ l i n k h d r (通常为
1 6 )加4 0后大于放入单个 m b u f中带首部的数据报的数据长度 ( 1 0 0字节,图 2 - 7中的M H L E N),内
核将告警。

MSL和平静时间的概念

TCP协议要求如果主机崩溃,且没能保存打开 TCP连接上最后使用的序号,则重启后在一
个M S L ( 2分钟,平静时间 )内,不能发送任何 T C P报文段。目前,基本没有 T C P实现能够在系
统崩溃或操作员关机时保存这些信息。
M S L是最大报文段生存时间 (maximum segment lifetime),指任何报文段被丢弃前在网络
中能够存在的最大时间。不同的实现可选择不同的 M S L 。连接主动关闭后,将在
CLOSE_WAIT状态等待2个MSL时间(图24-15)。
RFC 793(Postel 1981c)建议M S L设定为 2分钟,但 N e t / 3实现中 M S L设为 3 0秒(图
25-3中定义的常量TCPTV_MSL)。
如果报文段在网络中出现延迟,协议会出现问题 (RFC 793 称之为漫游重复 ( w a n d e r i n g
d u p l i c a t e )。假定N e t / 3系统启动时 t c p _ i s s置为1 (图2 4 - 2 7 ),经过一段时间,在序号刚刚回绕
时系统崩溃。后面 25.5节中将介绍,tcp_iss每秒增加128 000,即重启后需经过 9.3小时序号
才会回绕。此外,每发送一个 c o n n e c t,t c p _ i s s将增加64 000,因此序号回绕时间必然早
于9.3小时。下面的例子说明了老的报文段怎样被错误地发送到现在的连接上。
1) 一个客户和服务器建立了一个连接。客户的端口号是 1024,发送了一个序号为 2的报文
段。该报文段在传送途中陷入路径循环,未能到达服务器。这个报文段成为“漫游重复”报
文段。

Linux公社 www.linuxidc.com
bbs.theithome.com
652 计计TCP/IP详解 卷 2:实现

2) 客户重发该报文段,序号依旧为 2。重发报文段到达服务器。
3) 客户关闭连接。
4) 客户主机崩溃。
5) 客户主机在崩溃后 40秒重启,TCP初始化tcp_iss为1。
6) 同一客户和同一服务器之间立即建立了一条新的连接,使用了同样的端口号:客户端
口号为1024,服务器方依然是其预知的端口号。客户发送的 SYN中初始序号置为 1。这条新的
使用同样端口对的连接称为原有连接的化身 (incarnation)。
7) 步聚1中的漫游重复报文段最终到达服务器,并被认为是新建连接中的合法报文段,尽
管它实际上属于原有连接。
图24-28列出了上述步骤发生的时间顺序。
步骤 客户发送序号为2的数据报文(成为漫游报文)
客户重传序号为2的数据报文,到达服务器
客户关闭了与服务器的连接
客户端主机崩溃

客户主机重启,ISS设定为1
客户服务器建立原有连接的化身
步骤1的漫游重复报文到达服务器

时间
图24-28 示例:旧报文段到达原有连接的化身

即使系统重启后, T C P通过当前时钟计算 I S S,问题同样存在。无论原有连接的 I S S设为


多少,由于序号会回绕,完全有可能重启后新建连接的 I S S接近等于重启前原有连接最后使用
的序号。
除了保存重启前所有已建连接的序号,解决这个问题的唯一方法就是重启后 T C P在M S L
内保持平静(不发送任何报文段 )。尽管问题有可能出现,但绝大多数 TCP中并未实现相应的解
决方法,因为多数主机仅重启时间就要长于 MSL。

24.9 小结

本章概要介绍了接下来的 6章中将要讨论的 T C P源代码。 T C P为每条连接建立自己的控制


块,保存该连接的所有变量和状态信息。
定义了TCP的状态变迁图, TCP在哪些条件下从一个状态变迁到另一个状态,每次变迁过
程中发送和接收了哪些报文段。状态变迁图还显示了连接建立和终止的过程。在后续 T C P讨
论中会经常引用该图。
TCP连接上传输的每个数据字节都有相应的序号, TCP在连接控制块中维护多个序号:有
些用于发送,有些用于接收 ( T C P工作于全双工方式 )。由于序号来自有限的 32 bit空间,会从

Linux公社 www.linuxidc.com
bbs.theithome.com
第24章 TCP:传输控制协议计计 653
最大值回绕到 0。本章解释了如何使用小于和大于测试来比较序号,在后续的 T C P代码中将不
断遇到序号的比较。
最后介绍了最简单的 T C P函数, t c p _ i n i t,完成对 Internet PCB 的T C P链表的初始化。
此外,还讨论了初始发送序号的选取问题。

习题

24.1 研究图24-5中的统计数据,计算每条连接上发送和接收的平均字节数。
24.2 在tcp_init中,内核告警是否合理?
24.3 执行netstat -a
,了解你的系统当前有多少个活跃的 TCP端点。

Linux公社 www.linuxidc.com
bbs.theithome.com
第25章 TCP的定时器
25.1 引言

从本章起,我们开始详细讨论 TCP的实现代码,首先熟悉一下在绝大多数 TCP函数里都会


遇到的各种定时器。
T C P为每条连接建立了七个定时器。按照它们在一条连接生存期内出现的次序,简要介
绍如下。
1 )“连接建立 (connection establishment)”定时器在发送 S Y N报文段建立一条新连接时启
动。如果没有在 75秒内收到响应,连接建立将中止。
2 )“重传( r e t r a n s m i s s i o n )”定时器在 T C P发送数据时设定。如果定时器已超时而对端的确
认还未到达, T C P将重传数据。重传定时器的值 (即T C P等待对端确认的时间 )是动态计算的,
取决于TCP为该连接测量的往返时间和该报文段已被重传的次数。
3 )“延迟ACK(delayed ACK)”定时器在 T C P收到必须被确认但无需马上发出确认的数据
时设定。 TCP等待200 ms后发送确认响应。如果,在这 200 ms内,有数据要在该连接上发送,
延迟的ACK响应就可随着数据一起发送回对端,称为捎带确认。
4)“持续 (persist )”定时器在连接对端通告接收窗口为 0,阻止TCP继续发送数据时设定。
由于连接对端发送的窗口通告不可靠 (只有数据才会被确认, ACK不会被确认 ),允许TCP继续
发送数据的后续窗口更新有可能丢失。因此,如果 T C P有数据要发送,但对端通告接收窗口
为0,则持续定时器启动,超时后向对端发送 1字节的数据,判定对端接收窗口是否已打开。
与重传定时器类似,持续定时器的值也是动态计算的,取决于连接的往返时间,在 5秒到6 0秒
之间取值。
5 )“保活 ( k e e p a l i v e )”定时器在应用进程选取了插口的 S O _ K E E P A L I V E选项时生效。如
果连接的连续空闲时间超过 2小时,保活定时器超时,向对端发送连接探测报文段,强迫对端
响应。如果收到了期待的响应, T C P可确定对端主机工作正常,在该连接再次空闲超过 2小时
之前,TCP不会再进行保活测试。如果收到的是其他响应, TCP可确定对端主机已重启。如果
连续若干次保活测试都未收到响应, T C P就假定对端主机已崩溃,尽管它无法区分是主机故
障(例如,系统崩溃而尚未重启 ),还是连接故障 (例如,中间的路由器发生故障或电话线断
了)。
6) FIN_WAIT_2定时器。当某个连接从FIN_WAIT_1状态变迁到FIN_WAIT_2状态(图24-15),
并且不能再接收任何新数据时 (意味着应用进程调用了 c l o s e,而非 s h u t d o w n,没有利用
T C P的半关闭功能 ),FIN_WAIT_2定时器启动,设为 1 0分钟。定时器超时后,重新设为 7 5秒,
第二次超时后连接被关闭。加入这个定时器的目的是为了避免如果对端一直不发送 FIN,某个
连接会永远滞留在 FIN_WAIT_2状态。
7) TIME_WAIT定时器,一般也称为 2MSL定时器。 2MSL指两倍的MSL,24.8节定义的最
大报文段生存时间。当连接转移到 T I M E _ WA I T状态,即连接主动关闭时,定时器启动。卷 1

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 655
的1 8 . 6节详细说明了需要 2 M S L等待状态的原因。连接进入 T I M E _ WA I T状态时,定时器设定
为1分钟( N e t / 3选用3 0秒的M S L ),超时后, T C P控制块和 Internet PCB被删除,端口号可重新
使用。
T C P包括两个定时器函数:一个函数每 200 ms调用一次 (快速定时器 );另一个函数每 5 0 0
m s调用一次 (慢速定时器)。延迟A C K定时器与其他 6个定时器有所不同:如果某个连接上设定
了延迟ACK定时器,那么下一次 200 ms定时器超时后,延迟的 ACK必须被发送 (ACK的延迟时
间必须在 0~200 ms之间)。其他的定时器每 500 ms递减一次,计数器减为 0时,就触发相应的
动作。

25.2 代码介绍

当某个连接的 T C P控制块中的 T F _ D E L A C K标志(图2 4 - 1 4 )置位时,允许该连接使用延迟


A C K定时器。 T C P控制块中的 t _ t i m e r数组包括 4个(T C P T _ N T I M E R S)计数器,用于实现其
他的6个定时器。图 2 5 - 1列出了数组的索引。下面简单地介绍这 6个计数器是如何实现除延迟
ACK定时器外的其余 6个定时器的。

常 量 值 描 述

TCPT_REXMT 0 重传定时器
TCPT_PERSIST 1 持续定时器
TCPT_KEEP 2 保活定时器或连接建立定时器
TCPT_2MSL 3 2MSL定时器或FIN_WAIT_2定时器

图25-1 t_timer 数组索引

t _ t i m e r中的每条记录,保存了定时器的剩余值,以 500 ms为计时单位。如果等于零,


则说明对应的定时器没有设定。由于每个定时器都是短整型,所以定时器的最大值只能设定
为16 383.5秒,约为4.5小时。

建连 重传 延迟ACK 持续 保活 FIN_
2MSL
定时器 定时器 定时器 定时器 定时器 WAIT_2

(2小时)
(75秒)
(10分钟)
(60秒)
(75秒)

图25-2 七个TCP定时器的实现

请注意,图 2 5 - 1中利用4个“定时计数器”实现了 6个T C P“定时器”,这是因为有些定时


器彼此间是互斥的。下面我们首先区分一下计数器与定时器。 T C P T _ K E E P计数器同时实现了
保活定时器和连接建立定时器,因为这两个定时器永远不会同时出现在同一条连接上。类似
地,2 M S L定时器和 F I N _ W A I T _ 2定时器都由 T C P T _ 2 M S L计数器实现,因为一条连接在同一

Linux公社 www.linuxidc.com
bbs.theithome.com
656 计计TCP/IP详解 卷 2:实现

时间内只可能处于其中的一种状态。图 2 5 - 2的第一行小结了 7个T C P定时器的实现方式,第二


行和第三行列出了其中 4个定时器初始化时用到的 3个全局变量(图24-3)和2个常量(图25-3)。注
意,有2个全局变量同时被多个定时器使用。前面已讨论过,延迟 A C K定时器直接受控于 T C P
的200 ms定时器,在本章后续部分将讨论其他 2个定时器的时间长度是如何设定的。
图25-3列出了Net/3实现中基本的定时器取值。

常 量 500ms的 秒 数 描 述
时钟滴答数

TCPTV_MSL 60 30 MSL,最大报文段生存时间
TCPTV_MIN 2 1 重传定时器最小值
TCPTV_REXMTMAX 128 64 重传定时器最大值
TCPTV_PERSMIN 10 5 持续定时器最小值
TCPTV_PERSMAX 120 60 持续定时器最大值
TCPTV_KEEP_INIT 150 75 连接建立定时器取值
TCPTV_KEEP_IDLE 14400 7200 第一次保活测试前连接的空闲时间 (2小时)
TCPTV_KEEPINTVL 150 75 对端无响应时保活测试间的间隔时间
TCPTV_SRTTBASE 0 特殊取值,意味着目前无连接 RTT样本
TCPTV_SRTTDFLT 6 3 连接无RTT样本时的默认值

图25-3 TCP实现中基本的定时器取值

图25-4列出了在代码中会遇到的其他定时器常量。

常 量 值 描 述

TCP_LINGERTIME 120 用于S O _ L I N G E R插口选项的最大时间,以秒为单位


TCP_MAXRXTSHIFT 12 等待某个 ACK的最大重传次数
TCPTV_KEEPCNT 8 对端无响应时,最大保活测试次数

图25-4 定时器常量

图2 5 - 5中定义的 T C P T _ R A N G E S E T宏,给定时器设定一个给定值,并确认该值在指定范
围内。

图25-5 TCPT_RANGESET 宏

从图2 5 - 3可知,重传定时器和持续定时器都有最大值和最小值限制,因为它们的取值都
是基于测量的往返时间动态计算得到的,其他定时器均设为常值。
本章中将不讨论图 2 5 - 4中列出的一个特殊定时器:插口的拖延定时器 (linger timer),这是
由插口选项 SO_LINGER设置的。这是一个插口级的定时器,由系统函数close使用(15.15节)。
在图3 0 - 1 2中读者将看到,插口关闭时, T C P会首先检查该选项是否置位,拖延时间是否为 0。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 657
如果上述条件满足,将不采用 TCP正常的关闭过程,连接直接被复位。

25.3 tcp_canceltimers函数

图25-6中定义了 t c p _ c a n c e l t i m e r s函数。连接进入 TIME_WAIT状态时,t c p _ i n p u t


在设定 2 M S L定时器之前,调用该函数。 4个定时计数器清零,相应地关闭了重传定时器、持
续定时器、保活定时器和 FIN_WAIT_2定时器。

图25-6 tcp_canceltimers 函数

25.4 tcp_fasttimo函数

图2 5 - 7定义了t c p _ f a s t t i m o函数。该函数每隔 200 ms被p r _ f a s t t i m o调用一次,用


于操作延迟 ACK定时器。

图25-7 tcp_fasttimo 函数,每 200 ms 调用一次

函数检查 T C P链表中每个具有对应 T C P控制块的 Internet PCB。如果 T C P _ D E L A C K标志置


位,清除该标志,并置位 T F _ A C K N O W标志。调用 t c p _ o u t p u t,由于T F _ A C K N O W标志已置
位,ACK被发送。
为什么TCP的PCB链表中的某个Internet PCB会没有相应的 TCP控制块(第50行的判断)?读
者将在图 3 0 - 11中看到,创建插口时 (P R U _ A T T A C H请求响应 s o c k e t系统调用 ),首先创建

Linux公社 www.linuxidc.com
bbs.theithome.com
658 计计TCP/IP详解 卷 2:实现

Inertnet PCB,之后才创建TCP控制块。两个操作间有可能会插入高优先级的时钟中断 (图1-13),


该中断有可能调用 tcp_fasttimo函数。

25.5 tcp_slowtimo函数

图25-8定义了 t c p _ s l o w t i m o函数,每隔 500ms被p r _ s l o w t i m o调用一次。它操作其他


6个定时器:连接建立定时器、重传定时器、持续定时器、保活定时器、 FIN_WAIT_2定时器
和2MSL定时器。

图25-8 tcp_slowtimo 函数,每隔 500 ms调用一次

71 tcp_maxidle初始化为10分钟,这是 TCP向对端发送连接探测报文段后,收到对端主机
响应前的最长等待时间。如图 25-6所示,FIN_WAIT_2定时器也使用了这一变量。它的初始化
语句可放到 tcp_init中,因为其值可在系统初启时设定 (见习题 25.2)。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 659
1. 检查所有TCP控制块中的所有定时器
72-89 检查T C P链表中每个具有对应 T C P控制块的 Internet PCB,测试每个连接的所有定时
计数器,如果非 0,计数器减 1。如果减为 0,则发送 P R U _ S L O W T I M O请求。后面会介绍该请
求将调用tcp_timers函数。
t c p _ u s r r e q的第四个入口参数是指向 m b u f的指针。不过,在不需要 m b u f指针的场合,
这个参数实际被用于完成其他功能。 t c p _ s l o w t i m o函数中利用它传递索引 i,指出超时的
是哪一个时钟。代码中把 i强制转换为 mbuf指针是为了避免编译错误。
2. 检查TCP控制块是否已被删除
90-93 在检查控制块中的定时器之前,先将指向下一个 Internet PCB的指针保存在ipnxt中。
每次P R U _ S L O W T I M O请求返回后, t c p _ s l o w t i m o会检查TCP链表中的下一个 PCB是否仍指
向当前正处理的 P C B。如果不是,则意味着控制块已被删除—也许2 M S L定时器超时或重传
定时器超时,并且TCP已放弃当前连接—控制转到tpgone,跳过当前控制块的其余定时器,
并移至下一个PCB。
3. 计算空闲时间
94 当一个报文段到达当前连接, t c p _ i n p u t清零控制块中的 t _ i d l e。从连接收到最后一
个报文段起,每隔 500ms t_idle递增一次。空闲时间统计主要有三个目的: (1)TCP在连接空
闲2小时后发送连接探测报文段; ( 2 )如果连接位于 FIN_WAIT_2状态,且空闲 1 0分钟后又空闲
75秒,TCP将关闭该连接; (3)连接空闲一段时间后, tcp_output将返回慢启动状态。
4. 增加RTT计数器
95-96 如果需要测量某个报文段的 RT T,t c p _ o u t p u t在发送该报文段时,初始化 t _ r t t
计数器为 1。它每 500 ms 递增一次,直至收到该报文段的确认。在 t c p _ s l o w t i m o函数中,
如果连接正对某个报文段计时,即 t_rtt计数器非零,则递增 t_rtt。
5. 递增初始发送序号
100 t c p _ i s s在t c p _ i n i t中初始化为 1。每 500 ms t c p _ i s s增加 64 000: 128 000
(T C P _ I S S I N C R) 除以2 ( P R _ S L O W H Z)。尽管看上去 t c p _ i s s每秒钟仅递增两次,但实际速
率可达每 8 微秒增加 1。后面将介绍,无论主动打开或被动打开,只要建立了一条连接,
tcp_iss就会增加64 000。
RFC 793规定初始发送序号应该约每 4微秒增加一次,或每秒钟 250 000次。Net/3
实现的增加速率只有规定的一半。
6. 递增RFC 1323规定的时间戳值
101 tcp_now在系统重启时初始化为 0,每500 ms递增一次,用于实现 RFC 1323中定义的时
间戳[Jacobson, Barden和Borman 1992]。26.6节中将详细介绍这一功能。
75-79 请注意,如果主机上没有打开的连接 (t c b . i n p _ n e x t为空 ),则 t c p _ i s s和则
t c p _ n o w的递增将停止。这种状况只可能发生在系统初启时,因为在一个联网的 U N I X系统
中几乎不可能没有若干活跃的 TCP服务器。

25.6 tcp_timers函数

t c p _ t i m e r s函数在4个T C P定时计数器中的任何一个减为 0时由T C P的P R U _ S L O W T I M O


请求处理代码调用 (图30-10):
Linux公社 www.linuxidc.com
bbs.theithome.com
660 计计TCP/IP详解 卷 2:实现

case PRU_SLOWTIMO:
tp = tcp_timers(tp, (int)nam);

整个函数的结构是一个 switch语句,每个定时器对应一个 case语句,如图 25-9所示。

图25-9 tcp_timers 函数:总体框架

下面我们介绍其中 3个定时计数器(5个TCP定时器),重传定时器留待 25.11节中再讨论。

25.6.1 FIN_WAIT_2和2MSL定时器

TCP的TCP2_2MSL定时计数器实现了两种定时器。
1) FIN_WAIT_2定时器。当 tcp_input从FIN_WAIT_1状态变迁到 FIN_WAIT_2状态,并
且插口不再接收任何新数据 (意味着应用进程调用了 c l o s e,而不是 s h u t d o w n,从而无法利
用T C P的半关闭功能 )时,FIN_WAIT_2定时器设定为 1 0分钟(t c p _ m a x i d l e)。这样可以防止
连接永远停留在 FIN_WAIT_2状态。
2) 2MSL定时器。当 TCP转移到TIME_WAIT状态,2MSL定时器设定为60秒。
图25-10列出了处理 2MSL定时器的 case语句—在该定时器减为 0时执行。

图25-10 tcp_timers 函数:2MSL定时器超时

1. 2MSL定时器
127-139 图2 5 - 1 0中的条件判断逻辑较为复杂,因为 T C P T _ 2 M S L计数器的两种不同用法混
在了一起 (习题25.4)。首先看TIME_WAIT状态,定时器 60秒超时后,将调用 t c p _ c l o s e并释

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 661
放控制块。图 2 5 - 11给出了典型的时间顺序,列出了 2 M S L定时器超时后的一系列函数调用。
从图中可看出,如果某个定时器被设定为 N秒(2×N滴答),由于定时计数器的第一次递减将发
生在其后的 0~500 ms之间,定时器将在其后 2×N-1和2×N个滴答之间的某个时刻超时。

119时钟滴答×500ms滴答=59.5秒

这个区间内,连接进 每滴答500 ms
入TIME_WAIT状态,
调用
2 M S L定时器设定为
60秒(120个滴答)
调用

调用

调用

图25-11 TIME_WAIT状态下2MSL定时器的设定与超时

2. FIN_WAIT_2定时器
127-139 如果连接状态不是 T I M E _ WA I T,T C P T _ 2 M S L计数器表示 F I N _ WA I T _ 2定时器。
只要连接的空闲时间超过 1 0分钟(t c p _ m a x i d l e),连接就会被关闭。但如果连接的空闲时间
小于或等于 10分钟,FIN_WAIT_2定时器将被设为 75秒。图25-12给出了典型的时间顺序。
1200滴答 150滴答
(10分钟) (75秒)

进入 F I N _ WA I T _ 2 状态 F I N _ WA I T _ 2定时器超时 FIN_WAIT_2定时器超时
FIN_WAIT_2定时器设定为 t _ i d l e =1198; t _ i d l e = 11 9 8 + 1 5 0 ;
1200(tcp_maxidle ); F I N _ WA I T _ 2定时器设定 tcp_close()
t_idle=0 为150 (tcp_keepintvl)

图25-12 FIN_WAIT_2定时器,避免永久滞留于 FIN_WAIT_2状态

连接接收到一个ACK后,从FIN_WAIT_1状态变迁到FIN_WAIT_2状态(图24-15),t_idle
被置为0,FIN_WAIT_2定时器设为1200(tcp_maxidle)。图25-12中,向上的箭头指着10分钟定
时起始时刻的右侧,强调定时计数器的第一次递减发生在定时器设定后的 0~500 ms之间。1199
个滴答后,定时器超时。从图25-8中可知,在四个定时计数器递减并做超时判定之后, t_idle
才会增加,因此t_idle等于1198(我们假定连接在10分钟内一直空闲)。因为条件表达式“ 1198
小于或等于1200”为真,FIN_WAIT_2定时器设为150 (tcp_keepintvl)。定时器75秒后再次
超时,假定连接一直空闲,t_idle应为1348,条件表达式为假,tcp_close被调用。
第一次 1 0分钟定时后加入另一个 7 5秒定时是因为除非持续空闲时间超过 1 0分钟,否则处
于FIN_WAIT_2状态的连接不会被关闭。如果第一个 1 0分钟定时器还未超时,测试 t _ i d l e值
是没有意义的,但只要过了这段时间,每隔 7 5秒就会进行一次测试。由于有可能收到重复的
报文段,即一个重复的 ACK使得连接从 FIN_WAIT_1状态变迁到 FIN_WAIT_2状态,因此每收
到一个报文段, 10分钟等待将重新开始 (因为t_idle重设为0)。

Linux公社 www.linuxidc.com
bbs.theithome.com
662 计计TCP/IP详解 卷 2:实现

处于FIN_WAIT_2状态的连接在 1 0分钟空闲后将被关闭,这一点并不符合协议规
范,但在实际中是可行的。处于 FIN_WAIT_2状态,应用进程调用 c l o s e,连接上的
所有数据都己发送并被确认,FIN已被对端确认,TCP等待对端应用进程调用 close。
如果对端进程永远不关闭它的连接,本地 T C P将一直滞留在 FIN_WAIT_2状态。应定
义计数器保存由于这种原因而终止的连接数,从而了解这种状况出现的频率。

25.6.2 持续定时器

图25-13给出了处理持续定时器超时的 case语句。

图25-13 tcp_timers 函数:持续定时器超时

强制发送窗口探测报文段
210-220 持续定时器超时后,由于对端已通告接收窗口为 0,TCP无法向对端发送数据。此
时, t c p _ s e t p e r s i s t计算持续定时器的下一个设定值,并存储在 T C P T _ P E R S I S T计数器
中。t_force标志置位,强制 tcp_output发送1字节数据。
图2 5 - 1 4给出了局域网环境下,持续定时器的典型值,假定连接的重传时限为 1 . 5秒(见卷1
的图22-1)。

60秒

图25-14 持续定时器取值的时间表:探测对端接收窗口

一旦持续定时器取值达到 6 0秒,T C P将每隔 6 0秒发送一次窗口探测报文段。由于持续定


时器取值的下限为 5秒,上限为 6 0秒,因此定时器头两次均设定为 5秒,而不是 1 . 5秒和 3秒。
从图中可知,定时器采用了指数退避策略,新的取值等于原有值乘以 2,2 5 . 9节中将介绍这一
算法的实现。

25.6.3 连接建立定时器和保活定时器

TCP的TCPT_KEEP计数器实现了两个定时器:
1) 当应用进程调用 c o n n e c t,连接转移到 S Y N _ S E N T状态 (主动打开 ),或者当连接从
LISTEN状态变迁到SYN_RCVD状态(被动打开 )时,SYN发送之后,将连接建立定时器设定为
75秒(TCPTV_KEEP_INIT)。如果75秒内连接未能进入ESTABLISHED状态,则该连接被丢弃。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 663
2) 收 到 一 个 报 文 段 后 , t c p _ i n p u t 将复 位 连 接 的 保 活 定 时 器 , 重 设为 2小时
(t c p _ k e e p i d l e),并清零连接的 t _ i d l e计数器。上述操作适用于系统中所有的 T C P连接,
无论是否置位了插口的保活选项。如果保活定时器超时 (收到最后一个报文段 2小时后),并且
置位了插口的保活选项,则 T C P将向对端发送连接探测报文段。如果定时器超时,且未置位
插口选项,则TCP将只复位定时器,重设为 2小时。
图25-15给出了处理 TCP的TCPT_KEEP计数器的case语句。

图25-15 tcp_timer 函数:保活时钟超时处理

1. 连接建立定时器 75秒后超时
221-228 如果状态小于ESTABLISHED(图24-16),TCPT_KEEP计数器代表连接建立定时器。
定时器超时后,控制转到 d r o p i t,调用 t c p _ d r o p终止连接,给出差错代码 E T I M E D O U T。
我们将看到, E T I M E D O U T是默认差错码—例如,连接收到了某个差错报告,比如 I C M P的
主机不可达,返回应用进程的差错码将变为 EHOSTUNREACH,而非默认差错码。
我们将在图 30-4中看到, TCP发送SYN的同时初始化了两个定时器:正在讨论的连接建立
定时器,设定为 7 5秒,和重传定时器,保证对端无响应时可重传 S Y N。图2 5 - 1 6给出了这两个

Linux公社 www.linuxidc.com
bbs.theithome.com
664 计计TCP/IP详解 卷 2:实现

75秒
连接建立
定时器:

48秒
重传
定时器:

发送 重传 重传
SYN SYN SYN

图25-16 SYN发送后:连接建立定时器和重传定时器

定时器。
对于一个新连接,重传定时器初始化为 6 秒(图25-19),后续值分别为24秒和48秒,25.7节
中将详细讨论定时器取值的计算方法。重传定时器使得 S Y N报文段在 0秒、6秒和3 0秒处连续
三次被重传。在 7 5秒处,也就是重传定时器再次超时之前 3秒钟,连接建立定时器超时,调用
tcp_drop终止连接。
2. 保活定时器在2小时空闲后超时
229-230 所有连接上的保活定时器在连续 2小时空闲后超时,无论连接是否选取了插口的
S O _ K E E P A L I V E 选项。如果插口选项置位,并且连接处于 E S TA B L I S H E D 状态或
CLOSE_WAIT状态(图24-15),TCP将发送连接探测报文段。但如果应用进程调用了 c l o s e(状
态大于CLOSE_WAIT),即使连接已空闲了 2小时,TCP也不会发送连接探测报文段。
3. 无响应时丢弃连接
231-232 如 果 连 接 总 的 空 闲 时 间 大 于 或 等于 2小时 (t c p _ k e e p i d l e )加 1 0分钟
(t c p _ m a x i d l e),连接将被丢弃。也就是说,对端无响应时, T C P最多发送 9个连接探测报
文段,间隔75秒(t c p _ k e e p i n t v l)。TCP在确认连接已死亡之前必须发送多个连接探测报文
段的一个原因是,对端的响应很可能是不带数据的纯 A C K报文段, T C P无法保证此类报文段
的可靠传输,因此,连接探测报文段的响应有可能丢失。
4. 进行保活测试
233-248 如果TCP进行保活测试的次数还在许可范围之内, tcp_respond将发送连接探测
报文段。报文段的确认字段 (t c p _ r e s p o n d的第四个参数 )填入r c v _ n x t,期待接收的下一
序号;序号字段填入 s n d _ u n a -1,即对端已确认过的序号 (图24-17)。由于这一特定序号落
在接收窗口之外,对端必然会发送 ACK,给定它所期待的下一序号。
图25-17小结了保活定时器的用法
2小时
(连接空闲)

收到报文
探测 探测 探测 探测 探测 探测 探测 探测 探测
1 2 3 4 5 6 7 8 9

图25-17 保活定时器小结:判定对端是否可达

从0秒起,每隔 7 5秒连续 9次发送连接探测报文段,直至 6 0 0秒。6 7 5秒时(定时器 2小时超


时后的11.25分钟)连接被丢弃。请注意,尽管常量 T C P T V _ K E E P C N T(图25-4)的值设为8,却发

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 665
送了9次报文段,这是因为代码首先完成定时器递减、与 图25-17中
探测
0比较并做可能的处理后才递增变量 t _ i d l e(图25-8)。当 次数 的时间

t c p _ i n p u t接收了一个报文段,就会复位保活定时器为
1 4 4 0 0 (t c p _ k e e p i d l e),并清零 t _ i d l e。下一次调用
t c p _ s l o w t i m o时,定时器减为 14339而t _ i d l e增为1。
约2小时后,定时器从 1减为0时将调用 t c p _ t i m e r s,而
此时 t _ i d l e的值将为 1 4 3 3 9。图 2 5 - 1 8列出了每次调用
tcp_timers时t_idle的取值。
图2 5 - 1 5中的代码一直等待 t _ i d l e大于或等于 1 5 6 0 0
(t c p _ k e e p i d l e+t c p _ m a x i d l e),这一事件只可能发 图25-18 调用tcp_timers 处理保活
生在图 2 5 - 1 7中的6 7 5秒处,即连续发送了 9次连接探测报 定时器时 t_idle 的取值

文段之后。
5. 复位保活定时器
249-250 如果插口选项未置位,或者连接状态大于 CLOSE_WAIT,连接的保活定时器将复
位,重设为 2小时(tcp_keepidle)。
遗憾的是,计数器 t c p _ k e e p d r o p s(253行)不加区分地统计 T C P T _ K E E P定时计
数器的两种不同用法所造成的连接丢弃:连接建立计数器和保活计数器。

25.7 重传定时器的计算

到目前为止,讨论过的定时器的取值都是固定的:延迟 ACK 200ms,连接建立定时器 7 5


秒,保活定时器 2小时等等。最后两个定时器—重传定时器和持续定时器—的取值依于连
接上测算得到的 RTT。在讨论实现定时器时限计算和设定的代码之前,首先应理解连接 RTT的
测算方法。
T C P的一个基本操作是在发送了需对端确认的报文段后,设置重传定时器。如果在定时
器时限范围内未收到 A C K,该报文段被重发。 T C P要求对端确认所有数据报文段,不携带数
据的报文段则无需确认 (例如纯A C K报文段)。如果估算的重传时间过小,响应到达前即超时,
造成不必要的重传;如果过大,在报文段丢失之后,发送重传报文段之前将等待一段额外的
时间,降低了系统的效率。更为复杂的是,主机间的往返时间动态改变,且变化范围显著。
N e t / 3中T C P计算重传时限 ( RTO )时不仅要测量数据报文段的往返时间 (n t i c k s),还要记录
已平滑的 RT T估计器 (s rt t)和已平滑的 RT T平均偏差估计器 (rt t v a r)。平均偏差是标准方差的良
好近似,计算较为容易,无需标准方差的求平方根运算。 [Jacobson 1988b]讨论了RT T测算的
其他细节,给出下面的公式:
delta=nticks-s rt t
s rt t←s rt t + g×delta
rttvar←rttvar+h(|delta|-rttvar)
RTO = s rt t +4×rttvar
delta是最新测量的往返时间 (nticks)与当前已平滑的 RTT估计器(s rt t)间的差值。g是用到RTT估
计器的增益,设为 1 / 8。h是用到平均偏差估计器的增益,设为 1 / 4。这两个增益和 RTO 计算中
的乘数4有意取为2的乘方,从而无需乘、除法,只需简单的移位操作就能够完成运算。
Linux公社 www.linuxidc.com
bbs.theithome.com
666 计计TCP/IP详解 卷 2:实现

[Jacobson 1988b]规定RTO算式应使用2×rttvar,但经过进一步的研究, [Jacobson


1990d]更正为 4×rttvar,即Net/1实现中采用的算式。
下面首先介绍TCP重传定时器计算中用到的各种变量和算式,它们在 TCP代码中出现的频
率很高。图 25-19列出了控制块中与重传定时器有关的变量。

t c p c b的成员 单 位 tcp_newtcpcb 秒 数 描 述
初始值

t_srtt 滴答×8 0 已平滑的R T T估计器: s rt t× 8


t_rttvar 滴答×4 24 3 已平滑的 R T T 平均偏差估计器: rt t v a r × 4
t_rxtcur 滴答 12 6 当前重传时限:RTO
t_rttmin 滴答 2 1 重传时限最小值
t_rxtshift 不用 0 t c p _b a c k o f f [ 数组索引(指数退避
] )

图25-19 用于重传定时器计算的控制块变量

t c p _ b a c k o f f数组将在 2 5 . 9节末尾定义。 t c p _ n e w t c p c b函数设定这些变量的初始值,


实现代码将在下一节详细讨论。对变量t_rxtshift 中的shift及其上限
T C P _ M A X R X T S H I F T的命名并不十分准确。它指的并不是比特移位,而是如图 25-19中所声明
的,指数组索引。
T C P时限计算中不易理解的地方是已平滑的 RT T估计器和已平滑的 RT T平均偏差估计器
(t _ r t t和t _ r t t v a r)在C代码中都定义为整型,而不是浮点型。这样可以避免内核中的浮点
运算,代价是增加了代码的复杂性。
为了区分缩放前和缩放后 ( s c a l e d )的变量,斜体变量 s rt t和rt t v a r表示前面公式中未缩放的
变量,t_srtt和t_rttvar表示TCP控制块中缩放后的变量。
图2 5 - 2 0列出了将遇到的四个常量,它们分别定义了 t _ s r t t的缩放因子和 t _ r t t v a r的
缩放因子,分别为 8和4。

常 量 值 描 述

TCP_RTT_SCALE 8 相乘:t _ s r t t =s rt t×8


TCP_RTT_SHIFT 3 移位:t _ s r t t =srtt<<3
TCP_RTTVAR_SCALE 4 相乘:t _ r t t v a r =rttvar×4
TCP_RTTVAR_SHIFT 2 移位:t _ r t t v a r =rttvar<<2

图25-20 RTT均值与偏差的乘法与移位

25.8 tcp_newtcpcb算法

图25-21定义了t c p _ n e w t c p c b,分配一个新的 TCP控制块并完成初始化。创建新的插口


时,T C P的P R U _ A T T A C H请求将调用它 (图3 0 - 2 )。调用者已事先为该连接分配了一个 I n t e r n e t
P C B,并在入口参数 i n p中包含指向该结构的指针。我们在这里给出函数代码,是因为它初
始化了TCP的定时器变量。
167-175 内核函数malloc分配控制块所需内存, bzero清零新分配的内存块。
176 变量seg_next和seg_prev指向未按正常次序到达当前连接的报文段的重组队列。我
们将在27.9节中详细讨论这一重组队列。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 667

图25-21 tcp_newtcpcb 函数:创建并初始化一个新的 TCP控制块

177-179 发送报文段的最大长度, t _ m a x s e q,默认为 5 1 2 (t c p _ m s s d f l t)。收到对端


M S S选项后,它将被 t c p _ m s s函数更改 (新连接建立后, T C P也会向对端发送 M S S选项)。如
果配置要求系统实现 RFC 1313 规定的可变窗口和时间戳功能 (图 2 4 - 3 中的全局变量
t c p _ d o _ r f c 1 3 1 3,默认值为 1 ),T F _ R E Q _ S C A L E和T F _ R E Q _ T S T M P两个标志将被置位。
TCP控制块中的 t_inpcb指针将指向由调用者传来的 Internet PCB。
180-185 初始化图 2 5 - 1 9中列出的四个变量 t _ s r t t 、 t _ r t t v a r 、 t _ r t t m i n和
t _ r x t c u r。首先,已平滑的 RT T估计器被设为 0 (T C P T V _ S R T T B A S E),这个取值非常特殊,
指明连接上还不存在 RT T估计器。首次进行 RT T测量时, t c p _ x m i t _ t i m e r函数将判定已平
滑的RTT估计器是否等于 0,以采取相应动作。
186-187 已平滑的 RT T平均偏差估计器 t _ r t t v a r定义为 2 4:3 (t c p _ r t t d f l t,图2 4 - 3 )
乘以2 (P R _ S L O W H Z)后左移2 bit(即乘以4 )。由于 t _ r t t v a r是变量rt t v a r的4倍,也就等于 6个
滴答,即3秒钟。RTO的最小值, t_rttmin,为2个滴答。
188-190 变 量 t _ r x t c u 保存了当前 RTO 值, 以滴答为单位,最小值为 2个滴答
(T C P T V _ M I N),最大值为128个滴答(T C P T V _ R E X M T M A X)。T C P T _ R A N G E S E T的第二个参数,
表达式计算后等于 12个滴答,即 6秒钟,是连接的第一个 RTO值。
理解上述 C表达式和 RT T缩放值的概念并不是一件容易的事,下面的讨论可能会对您有所

Linux公社 www.linuxidc.com
bbs.theithome.com
668 计计TCP/IP详解 卷 2:实现

帮助。首先从原始的计算公式开始,并将缩放后的变量替代其中缩放前的变量。下面的算式
用于计算第一个 RTO,以乘数2替代了乘数 4。
RTO=s rt t+2×rttvar
使用乘数2而非4是最初4.3BSD Tahoe实现的一个遗留问题 [Paxson 1994]。
把下面两个缩放后的变量代入上式:
t_srtt=8×s rt t
t_rttvar=4×rttvar
得到:
t_srtt
t_srtt t_rttvar + t_rttvar
RTO = +2× = 4
8 4 2
也就是图 2 5 - 2 1代码中 T C P T _ R A N G E S E T第二个参数的表达式,只不过用常量—值为6个滴
答的TCPTV_SRTTDFLT乘以4后(缩放运算)代替了变量 t_rttvar。
191-192 拥塞窗口(snd_cwnd)和慢起动门限(snd_ssthresh)初始化为1 073 725 440 (约
为1 G字节),如是配置了动态窗口选项,这已是 T C P窗口大小的上限 (卷1的2 1 . 6节详细讨论了
慢起动和避免拥塞策略 ),即TCP首部窗口字段的最大值 (65535,T C P _ M A X W I N)乘以214,14是
窗口缩放因子的最大值 (T C P _ M A X _ W I N S H I F T)。后面将看到,连接上发送或接收了一个 SYN
时,tcp_mss复位snd_cwnd为1。
1 9 3 - 1 9 4 Internet PCB中的IP TTL的默认值初始化为 6 4 (i p _ d e f t t l),而P C B则指向新的
TCP控制块。
代码中没有明确初始化的其他变量,如移位变量 t _ r x t s h i f t,均为0,这是因为控制块
内存分配后已由 bzero清零。

25.9 tcp_setpersist函数
接下来要讨论的函数是 t c p _ s e t p e r s i s t,它用到了 T C P的重传超时算法。从图 2 5 - 1 3
中可知,持续定时器超时后,将调用此函数。当 T C P有数据要发送,而连接对端通告接收窗
口为0时,持续定时器启动。图 25-22给出了函数实现代码,计算并存储定时器的下个取值。

图25-22 tcp_setpersist 函数:计算并存储持续定时器的下一次取值

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 669
1. 确认重传定时器未设定
493-499 持续定时器设定之前,首先检查确认重传定时器未启动,这是因为两个定时器彼
此互斥:如果数据已被发送,说明对端通告的接收窗口必然非零,但持续时钟仅当对端通告
零接收窗口时才会设定。
2. 计算RTO
500-505 函数起始处,计算 RTO值并存储到变量 t中。使用的计算公式为
RTO = s rt t +2×rttvar
与上小节结束时讨论过的公式相同。通过变量替换可得到
t_srtt
+ t_rttvar
RTO = 4
2
即变量t的计算式。
3. 指数退避算法
506-507 RTO计算中还用到了指数退避算法,将上式计算得到的 RTO与tcp_backoff数组
中的某个值相乘:
int tcp_backoff[ TCP_MAXRXTSHIFT + 1] =
{1, 2, 4, 8, 16, 32, 64, 64, 64, 64, 64, 64, 64 };

tcp_output第一次为连接设置持续定时器的代码是:
tp->t_rxtshift=0;
tcp_setpersist(tp);

因此,第一次调用 t c p _ s e t p e r s i s t时,t _ r x t s h i f t= 0。由于t c p _ b a c k o f f[0]=1,


持续时限等于t。TCPT _ R A N G E S E T宏确保RTO值位于5秒~60秒之间。t _ r x t s h i f t每次增加
1,直到最大值12(TCP_MAXRXTSHIFT),tcp_backoff[12]是数组的最后一个元素。

25.10 tcp_xmit_timer函数

下一个讨论的函数, t c p _ x m i t _ t i m e r,在得到了一个 RT T测量值,从而更新已平滑的


RTT估计器(s rt t)和平均偏差 (rttvar)时被调用。
参数r t t传递了得到的 RT T测量值。它的值为 n t i c k s+1 (与2 5 . 7节中的符号一致 ),可以通
过下面两种方法之一得到。
如果收到的报文段中存在时间戳选项, RT T测量值应等于当前时间 (t c p _ n o w)减去时间
戳值。我们将在26.6节中讨论时间戳选项,现在只需了解 tcp_now每500ms递增一次 (图25-8)。
发送报文段时, tcp_now做为时间戳被发送,连接对端在相应的 ACK中回显该时间戳。
如果未使用时间戳,可以对数据报文计时。从图 2 5 - 8可知,连接上的计数器 t _ r t t每5 0 0
ms递增一次。在 25.5节也曾提到,该计数器初始化为 1,因此收到 ACK时,该计数器中的值即
为RTT测量值加1(以滴答为单位)。
tcp_input中调用tcp_xmit_timer的典型代码如下:
if (ts_present)
tcp_xmit_timer(tp, tcp_now - ts_ecr + 1);
else if (tp->rtt && SEQ_GT(ti->ti_ack, tp->t_rtseq))
tcp_xmit_timer(tp, tp->t_rtt);

如果报文段中存在时间戳 (t s _ p r e s e n t),RTT测量值等于当前时间 (t c p _ n o w)减去回显

Linux公社 www.linuxidc.com
bbs.theithome.com
670 计计TCP/IP详解 卷 2:实现

的时间戳(ts_ecr)再加1,RTT估计器将被更新 (后面将介绍加1的原因)。
如果不存在时间戳,但收到的 A C K报文确认了一个正在计时的数据报文,这种情况下
RT T估计器也将被更新。每个 T C P控制块(t _ r t t)中只存在一个 RT T计数器,因此,在一条连
接上只可能对一个特定数据报文计时。这个报文发送时的起始序号存储在 t _ r t s e q中,与收
到的ACK比较,可以确定该报文对应 ACK返回的时间。如果收到的确认序号 (t i _ a c k)大于正
在计时的数据报文起始序号 (t_rtseq),t_rtt即为RTT新的样本,从而更新 RTT估计器。
在支持 RFC 1323 的时间戳功能之前, t _ r t t是T C P测量RT T的唯一方法。但这
个变量还用作确认报文段是否被计时的标志 ( 图 2 5 - 8 ) :如果 t _ r t t 大于 0 ,则
t c p _ s l o w t i m o每隔500ms完成t _ r t t的加1操作;因此, t _ r t t非零时,它等于所
用的滴答数再加 1。我们将看到,tcp_xmit_timer函数中对得到的第二个参数减 1,
以纠正上述偏差。因此,使用时间戳时,向 t c p _ x m i t _ t i m e r传送的第二个参数必
须加1,以保持一致。
序号的大于判定是因为 A C K 是累积的:如果 T C P 发送并计时的报文序号为 1 ~ 1 0 2 4
(t _ r t s e q等于 1 ),然后立即发送 (但未计时 )下一个报文序号为 1 0 2 5 ~ 2 0 4 8,接着收到一个
A C K报文,其 t i _ a c k等于2 0 4 9,它确认了序号 1 ~ 2 0 4 8,即同时确认了第一个计时报文和第
二个未计时报文。注意,如果使用了 RFC 1323定义的时间戳,则不存在序号比较问题。如果
对端发送了时间戳选项,意味着它填入了回应时间 (ts_ecr),从而可直接计算 RTT。
图25-23给出了函数更新 RTT估算值的部分代码。

图25-23 tcp_xmit_timer 函数:利用新的 RTT测量值计算已平滑的 RTT估计器

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计671

图25-23 (续)

1. 更新已平滑的RTT估计器
1310-1325 前面已介绍过, tcp_newtcpcb初始化已平滑的 RTT估计器(t_srtt)为0,指
明连接上不存在 RTT估计器。 d e l t a是RTT测量值与当前已平滑的 RTT估计器间的差值,以未
缩放的滴答为单位。 t_srtt除以8,单位从缩放后的滴答转换为未缩放的滴答。
1326-1327 已平滑的RTT估计器用以下公式进行更新:
s rt t←s rt t+g×delta
由于增益g=1/8,公式变为
8×s rt t←8×s rt t+ delta
也就是
t_srtt←t_srtt+delta
1328-1342 已平滑的RTT平均偏差估计器的计算公式如下:
rttvar←rttvar+h(| delta|-rttvar)
将h=1/4和缩放后的 t_rttvar=4×rttvar代入,得到:
t_rttvar
t_rttvar t_rttvar | delta | − 4
← +
4 4 4
也就是:

t_rttvar
t_rttvar ← t_rttvar+ | delta | −
4
最后一个表达式即为 C代码中的表达式。
2. 第一次测量 RTT时初始化平滑的估计器值
1343-1350 如果是首次测量某连接的 RT T值,已平滑的 RT T估计器初始化为测量得到的样
本值。下面的计算用到了参数 r t t,前面已介绍过 r t t等于测量到的 RT T值加1 (n t i c k s+ 1 ),而
前面公式中用到的 delta是从rtt中减1得到的。
s rt t=nticks+1

t_srtt
= nticks + 1
8

Linux公社 www.linuxidc.com
bbs.theithome.com
672 计计TCP/IP详解 卷 2:实现

也就是

t_srtt=(nticks+1)×8
平均偏差等于测量到的 RTT值的一半:
srtt
rttvar =
2
也就是
t_rttvar nticks + 1
=
4 2
或者
t_rttvar=(nticks+1)×2
代码中的注释指出,已平滑的平均偏差的这种初始取值使得 RTO的初始值等于3×s rt t。因为
RTO=s rt t+4×rttvar
替换掉rttvar,得到:
srtt
RTO = srtt + 4 ×
2
也就是:
RTO=3×s rt t
图25-24给出了tcp_xmit_timer函数最后一部分的代码。

图25-24 tcp_xmit_timer 函数:最后一部分

1352-1353 RTT计数器(t _ r t t)和重传移位计数器 (t _ r x t s h i f t)同时复位为 0,为下一个


报文的发送和计时做准备。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 673
1354-1366 连接的下一个RTO(t_rxtcur)计算用到宏
#define TCP_REXMTVAL(tp) \
(((tp)->t_srtt >> TCP_RTT_SHIFT) + (tp)->t_ttvar)

其实,这就是我们很熟悉的公式
RTO=s rt t+4×rttvar
用t c p _ x m i t _ t i m e r更新过的缩放后的变量替代上式中的 s rt t和rt t v a r,得到宏的表达
式:
t_srtt t_rttvar t_srtt
RTO = +4× = + t_rttvar
8 4 8
此外, RTO 取值应在规定范围之内,最小值为连接上设定的最小 RT O (t _ r t t m i n ,
t_newtcpcb初始化为2个滴答),最大值为 128个滴答(TCPTV_REXMTMAX)。
3. 清除软错误变量
1367-1374 由于只有当收到了已发送的数据报文的确认时,才会调用tcp_xmit_timer,如
果连接上发生了软错误 (t_softerror),该错误将被丢弃。下一节中将详细讨论软错误。

25.11 重传超时:tcp_timers函数

我们现在回到 t c p _ t i m e r s函数,讨论 2 5 . 6节中未涉及的最后一个 c a s e语句:处理重传


定时器。如果在 RTO内没有收到对端对一个已发送数据报的确认,则执行此段代码。
图2 5 - 2 5小结了重传定时器的操作。假定 t c p _ o u t p u t计算的报文首次重传时限为 1 . 5秒,
这是LAN的典型值(参见卷1的图21-1)。

t_rxtshift的新值

图25-25 发送数据时重传定时器小结

x轴为时间轴,以秒为单位,标注依次为: 0、1 . 5、4 . 5等等。这些数字的下方,给出了代


码中用到的t _ r x t s h i f t的值。连续12次重传后,总共为 542.5秒(约9分钟),TCP将放弃并丢
弃连接。
RFC 793建议在建立新连接时,无论主动打开或被动打开,应定义一个参数规定
TCP发送数据的总时限,也就是 TCP在放弃发送并丢弃连接之前试图传输给定数据报
文的总时间。推荐的默认值为 5分钟。
RFC 11 2 2要求应用程序必须为连接指定一个参数,限定 T C P总的重传次数或者
T C P试图发送数据的总时间。这个参数如果设为“无限”,那么 T C P永不会放弃,还
可能不允许终端用户终止连接。
在代码中可看到, N e t / 3不支持应用程序的上述控制权: T C P放弃传输之前的重
传次数是固定的 (12),所用的总时间取决于 RTT。

Linux公社 www.linuxidc.com
bbs.theithome.com
674 计计TCP/IP详解 卷 2:实现

图25-26给出了重传超时 case语句的前半部分。

图25-26 tcp_timers 函数:重传定时器超时,前半部分

1. 递增移位计数器
146 重传移位计数器 (t_rxtshift)在每次重传时递增,如果大于 12(TCP_MAXRXTSHIFT),
连接将被丢弃。图25-25给出了t_rxtshift每次重传时的取值。请注意两种丢弃连接的区别,
由于收不到对端对已发送数据报文的确认而造成的丢弃连接,和由于保活定时器的作用,在
长时间空闲且收不到对端响应时丢弃连接。两种情况下, T C P 都会向应用进程报告
ETIMEDOUT差错,除非连接收到了一个软错误。
2. 丢弃连接
147-152 软错误指不会导致 T C P终止已建立的连接或正试图建立的连接的错误,但系统会
记录出现的软错误,以备 TCP将来放弃连接时参考。例如,如果 TCP重传SYN报文段,试图建
立新的连接,但未收到响应, T C P将向应用进程报告 E T I M E D O U T差错。但如果在重传期间,
收到一个 I C M P“主机不可达”差错代码, t c p _ n o t i f y会在t _ s o f t e r r o r中存储这一软错
误。如果 T C P最终决定放弃重传,返回给应用进程的差错代码将为 E H O S T U N R E A C H,而不是

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 675
ETIMEDOUT,从而向应用进程提供了更多的信息。如果 TCP发送SYN后,对端的响应为 RST,
这是个硬错误,连接立即被终止,返回差错代码 ECONNRFUSED(图28-18)。
3. 计算新的RTO
153-157 利用TCP_ R E X M T V A L宏实现指数退避,计算新的 RTO值。代码中,给定报文第一
次重传时 t _ r x t s h i f t等于1,因此, RTO 值为T C P _ R E X M T V A L计算值的两倍。新的 RTO 值
存储在 t _ r x t c u r 中,供连接的重传定时器 — t _ t i m e r [ T C P T _ R E X M T ] — 使用,
tcp_input在启动重传定时器时会用到它 (图28-12和图29-6)。
4. 向IP询问更换路由
158-167 如果报文段已重传 4次以上, i n _ l o s i n g 将释放缓存中的路由 (如果存在 ),
t c p _ o u t p u t再次重传该报文时 (图25-27中c a s e语句的结尾处),将选择一条新的,也许好一
些的路由。从图 2 5 - 2 5可看到,每次重传定时器超时时,如果重传时限已超过 2 2 . 5秒,将调用
in_losing。
5. 清除RTT估计器
168-170 代码中,已平滑的 RTT估计器(t _ s r t t)被置为0(t _ n e w t c p c b中曾将其初始化为
0 ),强迫t c p _ x m i t _ t i m e r将下一个 RT T测量值做为已平滑的 RT T估计器,这是因为报文段
重传次数已超过 4次,意味着 TCP的已平滑的 RTT估计器可能已失效。若重传定时器再次超时,
进入 c a s e语句后,将利用 T C P _ R E X M T V A L计算新的 RTO 值。由于 t _ s r t t被置为 0,新的计
算值应与本次重传中的计算值相同,再利用指数退避算法加以修正 (图25-28中,在42.464秒处
的重传很好地说明了上面讨论的概念 )。
再次计算RTO时,利用公式
t_srtt
RTO = + t_rttvar
8
由于t _ s r t t等于0,RTO取值不变。如果报文的重传定时器再次超时 (图25-28中从84.064
秒到217.84秒),case语句再次被执行, t_srtt等于0,t_rttvar不变。
6. 强迫重传最早的未确认数据
171 下一个发送序号 (s n d _ n x t)被置为最早的未确认的序号 (s n d _ u n a)。回想图 2 4 - 1 7中,
snd_nxt大于snd_una。把snd_nxt回移,将重传最早的未确认过的报文。
7. Karn算法
172-175 RTT计数器,t_rtt,被置为0。Karn算法认为由于该报文即将重传,对该报文的
计时也就失去了意义。即使收到了 A C K,也无法区分它是对第一次报文,还是对第二次报文
的确认。 [Karn and Partridge 1987]和卷1的2 1 . 3节中都介绍了这一算法。因此, T C P只对未重
传报文计时,利用 t _ r t t计数器得到样本值,并据此修正 RT T估计器。在后面的图 2 9 - 6中将
看到,如何使用 RFC 1323的时间戳功能取代 Karn算法。

25.11.1 慢起动和避免拥塞

图2 5 - 2 7给出了 c a s e语句的后半部分,实现慢起动和避免拥塞,并重传最早的未确认过
的报文。
由于重传定时器超时,网络中很可能发生了拥塞。这种情况下,需要用到 T C P的拥塞避
免算法。如果最终收到了对端发送的确认, T C P采用慢起动算法以较慢的速率继续进行数据

Linux公社 www.linuxidc.com
bbs.theithome.com
676 计计TCP/IP详解 卷 2:实现

传输。卷1的20.6节和21.6节详细讨论了这两种算法。
176-205 win被置为现有窗口大小 (接收方通告的窗口大小 snd_wnd和发送方拥塞窗口大小
s n d _ c w n d,两者之中的较小值 )的一半,以报文为单位,而非字节 (因此除以 t _ m a x s e g),
最小值为2。它的值等于网络拥塞时现有窗口大小的一半,也就是慢起动门限,
t _ s s t h r e s h(以字节为单位,因此乘以 t _ m a x s e q)。拥塞窗口的大小, s n d _ c w n d,被置为
只容纳1个报文,强迫执行慢起动。上述做法假定造成网络拥塞的原因之一是本地数据发送太
快,因此在拥塞发生时,必须降低发送窗口大小。

这段代码放在一对括号中,是因为它是在 4 . 3 B S D和N e t / 1实现之间添加的,并要


求有自己的局部变量 (win)。

206 连续重复ACK计数器, t_dupacks (用于29.4节中将介绍的快速重传算法 )被置为0。我


们将在第29章中介绍它在TCP快速重传和快速恢复算法中的用途。
208 t c p _ o u t p u t重新发送包含最早的未确认序号的报文,即由于重传定时器超时引发了
报文重传。

图25-27 tcp_timer 函数:重传定时器超时,后半部分

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 677
25.11.2 精确性

TCP维护的这些估计器的精确性如何呢?首先应指出,因为 RTT以500 ms为测量单位,是


非常不精确的。已平滑的 RTT估计器和平均偏差的精确性要高一些 (缩放因子为8和4),但也不
够,L A N的RT T是毫秒级,横跨大陆的 RT T约为6 0 m s左右。这些估计器仅仅给出了 RT T的上
限,从而在设定重传定时器时,可以不考虑由于重传时限过小而造成不必要的重传。
[Brakmo, O’Malley, and Peterson 1994]描述的TCP实现,能够提供高精度的 RTT样本。他
们的做法是,发送报文段时记录系统时钟读数 (精度比以 500 ms 为测量单位要高得多 ),收到
ACK时再次读取系统时钟,从而得到高精度的 RTT。
N e t / 3支持的时间戳功能 ( 2 6 . 6节)本来可以提供较高精度的 RT T,但N e t / 3将时间戳的精度
也定为500 ms。

25.12 一个RTT的例子

下面讨论一个具体的例子,说明上述计算是如何进行的。我们从主机 bsdi向
v a n g o g h . c s . b e r k e l e y . e d u发送1 2 2 8 8字节的数据。在发送过程中,故意断开工作中的
PPP链路,之后再恢复,看看 TCP如何处理报文的超时与重传。为发送数据,我们运行自己的
sock程序(参见卷1的附录C),加-D选项,置位插口的 SO_DEBUG选项(27.10节)。传输结束后,
运行 t r p t ( 8 )程序检查留在内核的环形缓存中的调试记录,之后打印 T C P控制块中我们感兴
趣的时钟变量。
图2 5 - 2 8列出了各变量在不同时刻的值。我们用 M:N表示序号 M ~ N-1已被发送。本例中
的每个报文段都携带了 5 1 2字节的数据。符号“ ACK M ”表示 A C K报文的确认字段为 M。标
注“实际差值 ( m s )”栏列出了 RT T定时器打开时刻和关闭时刻间的时间差值。标注“ r t t (参
数)”栏列出了调用 t c p _ x m i t _ t i m e r时第二个参数的值: RT T定时器打开时刻和关闭时刻
间的滴答数再加 1。
t c p _ n e w t c p c b函数完成 t _ s r t t、t _ r t t v a r和t _ r x t c u r的初始化,时刻 0 . 0对应的
即为变量初始值。
第一个计时报文是最初的SYN报文,365 ms后收到了对端的ACK,调用tcp_xmit_timer,
r t t参数值为2。由于这是第一个 RTT测量值(t _ s r t t = 0),执行图25-23中的e l s e语句,计算
RTT估计器初始值。
携带1 ~ 5 1 2字节的数据报文是第二个计时报文, 1 . 2 5 9秒时收到对应的 A C K,RT T估计器
被更新。
从接下来的三个报文可看出,连续报文是如何被确认的。 1 . 2 6 0秒时发送携带 5 1 3 ~ 1 0 2 4字
节的报文,并启动定时器。之后又发送了携带 1 0 2 5 ~ 1 5 2 6字节的报文,在 2 . 2 0 6秒时收到了对
端的A C K,同时确认了已发送的两个报文。 RT T估计器被更新,因为 A C K确认了正计时报文
的起始序号 (513)。
2 . 2 0 6秒时发送携带 1 5 3 7 ~ 2 0 4 8字节的报文,并启动定时器。 3 . 1 3 2秒时收到对应的 A C K,
RTT估计器被更新。
对3 . 1 3 2秒时发送的报文段计时,重传定时器设为 5个滴答 (t _ r x t c u r的当前值 )。这时,
路由器 s u n和n e t b间的P P P链路中断,几分钟后恢复正常。重传定时器在 6 . 0 6 4秒超时,执行
图2 5 - 2 6中的代码更新 RT T变量。 t _ r x t s h i f t从0增至1,t _ r x t c u r置为1 0个滴答 (指数退
Linux公社 www.linuxidc.com
bbs.theithome.com
678 计计TCP/IP详解 卷 2:实现

避 ) ,重传最早的未确认过的序号 ( s n d _ u n a = 3 0 7 3 ) 。 5 秒钟后,定时器再次超时,
t_rxtshift递增为2,重传定时器设为 20个滴答。

发送 RTT 实际时间差
发送 接收 参数 (8个滴答) (4个滴答)
时间 定时器 (ms) (滴答)

图25-28 实例中的 RTT变量值和估计器

4 2 . 4 6 4秒时,重传定时器再次超时, t _ s r t t清零, t _ r t t v a r置为5。我们在图 2 5 - 2 6的


讨论中提到过,此时 t _ r x t c u r运算得到的结果相同 (因此,下一次运算的结果应为 1 6 0 )。但

Linux公社 www.linuxidc.com
bbs.theithome.com
第 25 章 TCP的定时器计计 679
由于t _ s r t t重置为 0,下一次更新 RT T估计器时 ( 2 1 8 . 8 3 4秒),与建立一条新的连接相类似,
得到的RTT测量值将成为新的已平滑的 RTT估计器。
之后继续进行数据传输,并且又多次更新了 RTT估计器。

25.13 小结

内核每隔200 ms和500 ms,分别调用 tcp_fasttimo函数和tcp_slowtimo函数。这两


个函数负责维护 TCP为连接建立的各种定时器。
TCP为每条连接维护下列 7个定时器:
• 连接建立定时器;
• 重传定时器;
• 延迟ACK定时器;
• 持续定时器;
• FIN_WAIT_2定时器;
• 2MSL 定时器;
延迟ACK定时器与其他6个定时器不同,设置它时意味着下一次 TCP200 ms定时器超时时,
延迟的 A C K报文必须被发送。其他 6个定时器都是计数器,每次 TCP 500 ms 定时器超时时,
计数器减 1。任何一个计数器减为 0时,触发 T C P完成相应动作:丢弃连接、重传报文、发送
连接探测报文等等,这些内容本章中都有详细讨论。由于某些定时器是彼此互斥的,代码用 4
个计数器实现了这 6个定时器,复杂性有所增加。
本章还介绍了重传定时器取值的标准计算方法。 TCP为每条连接维护两个 RTT估计器:已
平滑的 RT T估计器 (s rt t)和已平滑的 RT T平均偏差估计器 (rt t v a r)。尽管算法简单清楚,但由于
使用了缩放因子 (在不使用内核浮点运算的情况下保证足够的精度 ),使得代码较为复杂。

习题

25.1 T C P快速超时处理函数的效率如何? (提示:参考图 2 4 - 5中列出的延迟 A C K的次数 )


有没有另外的实现方式?
25.2 为什么在tcp_slowtimo函数,而不是在tcp_init函数中初始化tcp_maxidle?
25.3 t c p _ s l o w t i m o递增t _ i d l e,前面已介绍过 t _ i d l e用于计数从连接上收到最后
一个报文起到当前为止的滴答数。 T C P是否需要计数从连接上发送最后一个报文段起计时的
空闲时间?
25.4 重写图25-10中的代码,分离 TCPT_2MSL计数器两种不同用法的处理逻辑。
25.5 图25-12中,连接进入FIN_WIN_2状态75秒后收到一个重复的 ACK。会发生什么?
25.6 应用程序设置 S O _ K E E P A L I V E选项时连接已空闲了 1小时。第一次连接探测报文在
何时发送, 1小时后还是 2小时后?
25.7 为什么tcp_rttdflt是一个全局变量,而非常量?
25.8 重写与习题 25.6有关的代码,实现另一种结果。

Linux公社 www.linuxidc.com
bbs.theithome.com
第26章 TCP 输 出
26.1 引言

函数tcp_output负责发送报文段,代码中有很多地方都调用了它。
t c p _ u s r r e q 在多种请求处理中调用了这一函数:处理 P R U _ C O N N E C T,发送初始
S Y N;处理 P R U _ S H U T D O W N, 发送F I N;处理P R U _ R C V D,应用进程从插口接收缓存中读取
若干数据后可能需要发送新的窗口大小通告;处理 P R U _ S E N D ,发送数据;处理
PRU_SENDOOB,发送带外数据。
• tcp_fasttimo调用它发送延迟的 ACK;
• tcp_timers在重传定时器超时时,调用它重传报文段;
• tcp_timers在持续定时器超时时,调用它发送窗口探测报文段;
• tcp_drop调用它发送 RST;
• tcp_disconnect调用它发送 FIN;
• tcp_input在需要输出或需要立即发送 ACK时调用它;
• t c p _ i n p u t在收到一个纯ACK报文段且本地有数据发送时调用它 (纯ACK报文段指不携
带数据,只确认已接收数据的报文段 );
• t c p _ i n p u t 在连续收到 3个重复的 A C K时,调用它发送一个单一报文段 (快速重传算
法);
t c p _ i n p u t首先确定是否有报文段等待发送。除了存在需要发往连接对端的数据外,
TCP输出还受到其他许多因素的控制。例如,对端可能通告接收窗口为零,阻止 TCP发送任何
数据;Nagle算法阻止 TCP发送大量小报文段;慢启动和避免拥塞算法限制 TCP发送的数据量。
相反,有些函数置位一些特殊标志,强迫 t c p _ o u t p u t发送报文段,如 T F _ A C K N O W标志置位
意味着必须立即发送一个 A C K。如果 t c p _ o u t p u t确定不发送某个报文段,数据 (如果存在 )
将保留在插口的发送缓存中,等待下一次调用该函数。

26.2 tcp_output概述

tcp_output函数很大,我们将分 14个部分予以讨论。图 26-1给出了函数的框架结构。


1. 是否等待对端的 ACK?
61 如果发送的最大序号 (s n d _ m a x)等于最早的未确认过的序号 (s n d _ u n a),即不等待对端
发送A C K,i d l e为真。图 2 4 - 1 7中,i d l e应为假,因为序号 4 ~ 6己发送但还未被确认, T C P
在等待对端发送对上述序号的确认。
2. 返回慢启动
62-68 如果T C P不等待对端发送 A C K,而且在一个往返时间内也没有收到对端发送的其他
报文段,设置拥塞窗口为仅能容纳一个报文段 (t_maxseg字节),从而在发送下一个报文段时,

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计681
强迫执行慢启动算法。如果数据传输中出现了显著的停顿 (“显著”指停顿时间超过 RT T ),说
明与先前测量 RT T时相比,网络条件己发生了变化。 N e t / 3假定出现了最坏情况,因而返回慢
起动状态。

图26-1 tcp_output 函数:框架结构

3. 发送多个报文段
69-70 控制跳转至 send后,调用ip_output发送一个报文段。但如果 ip_output确定有

Linux公社 www.linuxidc.com
bbs.theithome.com
682 计计TCP/IP详解 卷 2:实现

多个报文段需要发送, sendalot置为1,函数将试图发送另一个报文段。因此, ip_output


的一次调用能够发送多个报文段。

26.3 决定是否应发送一个报文段

某些情况下,在报文段准备好之前已调用了 t c p _ o u t p u t。例如,当插口层从插口的接
收缓存中移走数据,传递给用户进程时,会生成 P R U _ R C V D请求。尽管不一定,但完全有可
能因为应用进程取走了大量数据,而使得 T C P有必要发送新的窗口通告。 t c p _ o u t p u t的前
半部分确定是否存在需要发往对端的报文段。如果没有,则函数返回,不执行发送操作。
图26-2给出了判定“是否有报文段发送”测试代码的第一部分。

图26-2 tcp_output 函数:强迫数据发送

71-72 off指从发送缓存起始处算起指向第一个待发送字节的偏移量,以字节为单位。它指
向的第一个字节为 snd_una(已发送但还未被确认的字节 )。
win是对端通告的接收窗口大小 (snd_wnd)与拥塞窗口大小 (snd_cwnd)间的最小值。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 683
73 图2 4 - 1 6给出了 t c p _ o u t f l a g s数组,数组值取决于连接的当前状态。 f l a g s包括下列
标志比特的组合: TH_ACK、TH_FIN、TH_RST和TH_SYN,分别表示需向对端发送的报文段
类型。其他两个标志比特, T H _ P U S H和T H _ U R G,如果需要,在报文段发送之前加入,与前 4
个标志比特是逻辑或的关系。
74-105 t _ f o r c e标志非零表示持续定时器超时,或者有带外数据需要发送。这两种条件
下,调用tcp_output的代码均为:
tp->t_force = 1;
error = tcp_output(tp);
tp->t_force = 0;

从而强迫TCP发送数据,尽管在正常情况下不会执行任何发送操作。
如果w i n等于0,连接处于持续状态 (因为t _ f o r c e非零)。如果此时插口的发送缓存中还
存在数据,则FIN标志被清除。win必须置为1,以强迫发送一个字节的数据。
如果 w i n非零,即有带外数据需要发送,则持续定时器复位,指数退避算法的索引,
t_rxtshift被置为0。
图26-3给出了tcp_output的下一模块,计算发送的数据量。

图26-3 tcp_output 函数:计算发送的数据量

1. 计算发送的数据量
106 l e n等于发送缓存中比特数和 w i n(对端通告的接收窗口与拥塞窗口间的最小值,强迫
TCP发送数据时也可能等于 1字节 ),两者间的最小值减去 off。减去off是因为发送缓存中的
许多字节已发送过,正等待对端的确认。
2. 窗口缩小检查

Linux公社 www.linuxidc.com
bbs.theithome.com
684 计计TCP/IP详解 卷 2:实现

107-117 造成l e n小于零的一种可能情况是接收方缩小了窗口,即接收方把窗口的右界移


向左侧,下面的例子说明了这种情况。开始时,接收方通告接收窗口大小为 6字节, T C P发送
报文段,携带字节 4、5和6。紧接着, T C P又发送一个报文段,携带字节 7、8和9。图2 6 - 4显
示了两个报文段发送后本地的状态。

s n d _ w n d=6:接收窗口
(由接收方通告)

发送,尚未确认

最早的未确认的 下一个发送序号
序号

图26-4 发送4~9字节后的本地发送缓存

之后,收到一个 A C K,确认序号字段为 7 (确认所有序号小于 7的数据,包括字节 6 ),但窗


口字段为1。接收方缩小了接收窗口,此时本地的状态如图 26-5所示。

发送,尚未确认

最早的未确认过 下一个发送序号
的序号

图26-5 收到4~7字节的确认后,本地发送缓存

窗口缩小后,执行图 26-2和图26-3中的计算,得到:
off = snd_nxt - snd_una = 10 - 7 = 3
win = 1
len = min(so_snd.sb_cc, win) -off = min(3, 1) - 3 = -2

假定发送缓存仅包含字节 7、8和9。
RFC 793和RFC 1122都非常不赞成缩小窗口。尽管如此,具体实现必须考虑这一
问题并加以处理。这种做法遵循了在 RFC 791 中首次提出的稳健性原则:“对接收报
文段的假设尽量少一些,对发送报文段的限制尽量多一些”。
造成 l e n小于 0的另一种可能情况是,已发送过 F I N,但还未收到确认 (见习题 2 6 . 2 )。图
26-6给出了这种情况。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 685

已发送和确认

最早的未确 下一个发送序号
认过的序号

图26-6 字节1~9已发送并收到对端确认,之后关闭连接

图26-6是图26-4的继续,假定字节 7~9已被确认, s n d _ u n a的当前值为 10。应用进程随后


关闭连接,向对端发送 F I N。在本章后续部分将看到, T C P发送F I N时,s n d _ n x t将增加1 (因
为FIN也需要序号 ),在本例中,s n d _ n x t将等于11,而FIN的序号为 10。执行图 26-2和图26-3
中的计算,得到:
off = snd_nxt - snd_una = 11 - 10 = 1
win = 6
len = min( so_snd.sb_cc, win ) - off = min(0, 6) - 1 = -1

我们假定接收方通告接收窗口大小为 6。这个假定无关紧要,因为发送缓存中待发送的字
节数(0)小于它。
3. 进入持续状态
118-122 len被置为0。如果对端通告的接收窗口大小为 0,则重传定时器将被置为 0,任何
等待的重传将被取消。令 s n d _ n x t等于s n d _ u n a,指针返回发送窗口的最左端,连接将进入
持续状态。如果接收方最终打开了接收窗口,则 TCP将从发送窗口的最左端开始重传。
4. 一次发送一个报文段
124-127 如果需要发送的数据超过了一个报文段的容量, l e n 置为最大报文段长度,
s e n d a l o t置为1。如图 2 6 - 1所示,这将使 t c p _ o u t p u t在报文段发送完毕后进入另一次循
环。
5. 如果发送缓存不空,关闭 FIN标志
128-129 如果本次输出操作未能清空发送缓存, FIN标志必须被清除 (防止该标志在 f l a g s
中被置位)。图26-7举例说明了这一情况。
(发送缓存)

一个报文 一个报文

最早的未确认过 下一个发送序号
的序号

图26-7 实例:FIN置位时,发送缓存不空

这个例子中,第一个 5 1 2字节的报文段已发送 (还未被确认 ),T C P正准备发送第二个报文


段( 5 1 2 ~ 1 0 2 4字节)。此时,发送缓存中仍有 1字节的数据 ( 1 0 2 5字节),应用进程关闭了连接。

Linux公社 www.linuxidc.com
bbs.theithome.com
686 计计TCP/IP详解 卷 2:实现

len=512(一个报文段 ),图26-3中的C表达式变为:
SEQ_LT (1025, 1026)

如果表达式为真,则 FIN标志被清除;否则, TCP无法向对端发送序号为 1025的字节。


6. 计算接收窗口大小
130 w i n设定为本地接收缓存中可用空间的大小,即 TCP向对端通告的接收窗口的大小。请
注意,这是第二次用到这个变量。在函数前一部分中,它等于允许 T C P发送的最大数据量,
但从现在起,它等于本地向对端通告的接收窗口的大小。
糊涂窗口综合症 (简写为 S W S,详见卷 1第2 2 . 3节)指连接上交换的都是短报文段,而不是
最大长度报文段。这种现象的出现是由于接收方通告的接收窗口过小,或者发送方传输了许
多小报文段,因此,避免糊涂窗口综合症,需要发送方和接收方的共同努力。图 2 6 - 8给出了
发送方避免糊涂窗口综合症的做法。

图26-8 tcp_output 函数:发送方避免糊涂窗口综合症

7. 发送方避免糊涂窗口综合症的方法
142-143 如果待发送报文段是最大长度报文段,则发送它。
144-146 如果无需等待对端的 ACK(idle为真),或者Nagle算法被取消 (TF_NODELAY为真),
并且T C P正在清空发送缓存,则发送数据。 N a g l e算法(详见卷1第1 9 . 4节)的思想是:如果某个
连接需要等待对端的确认,则不允许 T C P发送长度小于最大长度的报文段。通过设定插口选
项T C P _ N O D E L A Y,可以取消这个算法。对于正常的交互式连接 (如Te l n e t或R l o g i n ),即使连
接上存在未确认过的数据,代码中的 if语句也为假,因为默认条件下 TCP会采用Nagle算法。
147-148 如果由于持续定时器超时,或者有带外数据,强迫 T C P执行发送操作,则数据将
被发送。
149-150 如果接收方的接收窗口已至少打开了一半,则发送数据。这个限制条件是为了处

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计687
理对端一直发送小窗口通告,甚至小于报文段长度的情况。变量 m a x _ s n d w n d由t c p _ i n p u t
维护,等于连接对端发送的所有窗口通告中的最大值。实际上, T C P试图猜测对端接收缓存
的大小,并假定对端永远不会减小其接收缓存。
151-152 如果重传定时器超时,则必须发送一个报文段。 snd_max是已发送过的最高序号,
从图2 5 - 2 6可知,重传定时器超时后, s n d _ n x t将被设为 s n d _ u n a,即s n d _ n x t会指向窗口
的左侧,从而小于 snd_max。
图2 6 - 9给出了 t c p _ o u t p u t的下一部分,确定 T C P是否必须向对端发送新的窗口通告,
称之为“窗口更新”。

图26-9 tcp_output 函数:判定是否需要发送窗口更新报文

154-168 表达式
min (win, (long) TCP_MAXWIN << tp->rcv_scale)

等于插口接收缓存可用空间大小 (w i n)和连接上所允许的最大窗口大小之间的最小值,即 T C P
当前能够向对端发送的接收窗口的最大值。表达式
(tp->rcv_adv - tp->rcv_nxt)

等于TCP最后一次通告的接收窗口中剩余空间的大小,以字节为单位。两者相减得到 a d v,窗
口已打开的字节数。 t c p _ i n p u t顺序接收数据时,递增 r c v _ n x t。t c p _ o u t p u t在通告窗
口边界向右移动时,递增 rcv_adv(代码见图26-32)。
回想图 2 4 - 1 8 ,假定收到了字节 4、 5和 6,并提交给应用进程。图 2 6 - 1 0给出了此时
tcp_output中接收缓存的状态。
adv等于3,因为接收空间中还有 3个字节(字节10、11和12)等待对端填充。
169-170 如果剩余的接收空间能够容纳两个或两个以上的报文段,则发送窗口更新报文。
在收到最大长度报文段后, T C P将确认收到的所有其他报文段:“确认所有其他报文段 ( A C K -
every-other-segment)”的属性(马上就会看到具体的实例 )。

Linux公社 www.linuxidc.com
bbs.theithome.com
688 计计TCP/IP详解 卷 2:实现

接收缓存大小

下一个接收序号 通告的最高序号加1

图26-10 收到字节 4、5和6后,图24-18中连接的状态变化

171-172 如果可用空间大于插口接收缓存的一半,则发送窗口更新报文。
图2 6 - 11给出了t c p _ o u t p u t下一部分的代码,判定输出标志是否置位,要求 T C P发送相
应报文段。

图26-11 tcp_output 函数:是否需要发送特定报文段

174-178 如果T F _ A C K N O W置位,要求立即发送 A C K,则发送相应报文段。有多种情况可


导致T F _ A C K N O W置位: 200 ms延迟A C K定时器超时,报文段未按顺序到达 (用于快速重传算
法),三次握手时收到了 SYN,收到了窗口探测报文,收到了 FIN。
179-180 如果输出标志flags要求发送SYN或RST,则发送相应报文段。
181-182 如果紧急指针, s n d _ u p,超出了发送缓存的起始边界,则发送相应报文段。紧
急指针由PRU_SENDOOB请求处理代码(图30-9)负责维护。
183-190 如果输出标志 f l a g s要求发送F I N,并且满足下列条件: F I N未发送过或者 F I N等
待重传,则发送相应报文段。 FIN发送后,函数将置位 TF_SENTFIN标志。
到目前为止, t c p _ o u t p u t还没有真正发送报文段,图 2 6 - 1 2给出了函数返回前的最后一
段代码。
191-217 如果发送缓存中存在需要发送的数据 (s o _ s n d . s b _ c c非零),并且重传定时器和
持续定时器都未设定,则启动持续定时器。这是为了处理对端通告的接收窗口过小,无法接
收最大长度报文段,而且也没有特殊原因需要发送立即发送报文段的情况。
218-221 由于不需要发送报文段, tcp_output返回。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 689

图26-12 tcp_output 函数:进入持续状态

举例

应用进程向某个空闲的连接写入 1 0 0字节,接着又写入 5 0字节。假定报文段大小为 5 1 2字


节。在第一次写入操作时,由于连接空闲,且 T C P正在清空发送缓存,图 2 6 - 8中的代码
(144~146行)被执行,发送一个报文段,携带 100字节的数据。
在第二次写入 5 0字节时,图 2 6 - 8中的代码被执行,但未发送报文段:待发送数据不能构
成一个最大长度报文段,连接未空闲 (假定T C P正在等待第一个报文段的 A C K ),默认时采用
N a g l e算法, t _ f o r c e未置位,并且假定正常情况下接收窗口大小为 4 0 9 6,5 0不满足大于等
于2 0 4 8的条件。这 5 0字节的数据将暂留在发送缓存中,也许会一直等到第一个报文段的 A C K
到达。由于对端可能延迟发送 ACK,最后50字节数据发送前的延迟有可能会更长。
这个例子说明采用 N a g l e算法时,如果待发送数据无法构成最大长度报文段,如何计算它
的延时。参见习题 26.12。

举例

本例说明 T C P的“确认所有其他报文段”属性。假定连接的报文段大小为 1 0 2 4字节,接


收缓存大小为4096字节。本地不发送数据,只接收数据。

Linux公社 www.linuxidc.com
bbs.theithome.com
690 计计TCP/IP详解 卷 2:实现

发向对端的对 S Y N的A C K报文中,通告接收窗口大小为 4 0 9 6,图2 6 - 1 3给出了两个变量


rcv_nxt和rcv_adv的初始值。接收缓存为空。
接收缓存大小 =4096

下一个接收序号 通告的最高序号加1

图26-13 接收方通告接收窗口大小为 4096

对端发送 1 ~ 1 0 2 4字节的报文段, t c p _ i n p u t处理报文段后,设置连接的延迟 A C K标志,


把1024字节的数据放入插口的接收缓存中 (图28-13)。更新rcv_nxt,如图26-14所示。

接收缓存大小 =3072

下一个接收序号 通告的最高序号加1

图26-14 图26-13所示的连接在收到 1~1024字节后的状态变迁

应用进程从插口的接收缓存中读取 1 0 2 4 字节的数据。从图 3 0 - 6 中可看到,生成的


P R U _ R C V D请求在处理过程中会调用 t c p _ o u t p u t,因为应用进程从接收缓存读取数据后,
可能需要发送窗口更新报文。当 t c p _ o u t p u t被调用时, r c v _ n x t和r c v _ a d v的值与图 2 6 -
1 4相同,唯一的区别是接收缓存的可用空间增加至 4 0 9 6,因为应用进程从中读取了第一个
1024字节的数据。把上述具体数值代入图 26-9中的算式,得到:
adv = min(4096, 65535) - (4097 - 1025)
= 1024

T C P _ M A X W I N等于6 5 5 3 5,我们假定接收窗口大小偏移量为 0。由于窗口的增加值小于两


个最大报文段长度 (2048),无需发送窗口更新报文。但由于延迟 ACK标志置位,如果 200ms定
时器超时,将发送 ACK。
当T C P收到下一个 1 0 2 5 ~ 2 0 4 8字节的报文段时, t c p _ i n p u t处理后,设定连接的延迟
A C K标志(这个标志已置位 ),把1 0 2 4字节的数据放入插口的接收缓存中,更新 r c v _ n x t,如
图26-15所示。

接收缓存大小= 3072

下一个接收序号 通告的最高序号加1

图26-15 图26-14所示的连接收到 1025~2048字节后的状态变迁

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 691
应用进程读取 1 0 2 4 ~ 2 0 4 8字节的数据,调用 t c p _ o u t p u t。r c v _ n x t和r c v _ a d v的值与
图2 6 - 1 5相同,尽管应用进程读取 1 0 2 4字节的数据后,接收缓存的可用空间增加至 4 0 9 6。把上
述具体数值代入图 26-9的算式中,得到:
adv = min(4096, 65535) - (4097 - 2049)
= 2048

它等于两个报文段的长度,因此发送窗口更新报文,确认序号字段为 2 0 4 9,通告窗口字
段为4096,表示接收方希望接收序号 2049~6145的数据。我们在后面将看到,函数发送完窗口
更新报文后,将更新 rcv_adv的值为6145。
本例说明了如果数据接收时间少于 200ms延迟定时器时限,在有两个或两个以上报文段到
达,而且应用进程连续读取数据引起了接收窗口的不断变化时,将发送 A C K,确认所有接收
到的报文段。如果有数据到达,但应用进程没有从插口的接收缓存中读取数据,则“确认所
有其他报文段”的属性不会出现。相反,发送方只能看到多个延迟 A C K,每个A C K的窗口字
段均较前一个要小,直到接收缓存被填满,接收窗口缩小为 0。

26.4 TCP选项

T C P首部可以有任选项。由于 t c p _ o u t p u t的下一部分代码将试图确定哪些选项需要发
送,并据此组织将发送的报文段,下面我们将暂时离开函数代码,转而讨论这些选项。
图26-16列出了Net/3支持的选项格式。

选项表结束:

1字节

无操作:

1字节

最大报文段长
最大报文段长度:
度(MSS)
1字节 1字节 2字节

位移值
窗口缩放因子:
1字节 1字节 1字节

时间戳: 时间戳值 时间戳回显应答

1字节 1字节 4字节 4字节

图26-16 Net/3支持的TCP选项

所有选项以 1字节的k i n d字段开头,确定选项类型。头两个选项 (k i n d= 0或k i n d= 1 )只有1个


字节。其余3个选项都是多字节的,带有 len字段,位于kind字段之后,存储选项的长度。长度

Linux公社 www.linuxidc.com
bbs.theithome.com
692 计计TCP/IP详解 卷 2:实现

中包括kind字段和len字段。
多字节整数 —MSS和两个时间戳值—遵照网络字节序存储。
最后两个选项,窗口大小和时间戳,是新增的,因此许多系统都不支持。为了与以前的
系统兼容,应遵循下列原则:
1) TCP主动打开时 (发送不带 A C K的S Y N ),可以在初始 S Y N中同时发送这两个选项,或
发送其中的任何一个。如果全局变量 t c p _ d o _ r f c 1 3 2 3非零(默认值等于 1 ),则N e t / 3同时支
持这两个选项。此项功能由 tcp_newtcpcb函数实现。
2) 只有对端返回的 S Y N中包含同样的选项时,才可以使用这些选项。图 2 8 - 2 0和图2 9 - 2中
的代码实现此类处理。
3) TCP 被动打开时,如果收到的 S Y N中包含了这两个选项,而且也希望使用这些选项,
则发向对端的响应 (带有ACK的SYN)中必须包含它们,如图 26-23所示。
由于系统必须忽略它不了解的选项,因此新增的选项只有当连接双方都了解这一选项,
且同时希望支持它时才会被使用。
2 7 . 5节将讨论如何处理 M S S选项。下面两节将总结 N e t / 3处理两个新选项的做法:窗口大
小和时间戳。
还有其他可能的选项。 k i n d s等于4、5、6和7,称为选择性 A C K和回显选项,在
RFC 1072[Jacobson and Braden 1998]中定义。图 26-16中并未给出这些选项,因为回
显选项已被时间戳选项所代替,选择性 A C K选项目前还未形成正式标准,未在 R F C
1323中出现。此外,处理 TCP交易的T/TCP建议(RFC 1644[Braden 1994]和卷1的24.7
节)规定了其他 3个选项,kinds分别为11、12和13。

26.5 窗口大小选项

窗口大小选项,在 RFC 1323中定义,避免了TCP首部窗口大小字段只有16 bit的限制(图24-


1 0 )。如果网络带宽较高或延时较长 (如,RT T较长),则需要较大的窗口,称为长肥管道 ( l o n g
fat pipe)。卷1的第24.3节举例说明了现代网络需要较大的窗口,以获取最大的 TCP吞吐量。
图2 6 - 1 6中的偏移量最小值为 0 ( 无缩放 ),最大值为 1 4,即窗口最大可设定为 1 073 725
440 (65535×214)字节。Net/3内部实现时,利用 32 bit,而非16 bit整数表示窗口大小。
窗口大小选项只能出现在 S Y N中,因此,连接建立后,每个传输方向上的缩放因子是固
定不变的。
T C P控制块中的两个变量 s n d _ s c a l e和r c v _ s c a l e,分别规定了发送窗口和接收窗口
的偏移量。它们的默认值均为 0,无缩放。每次收到对端发送的窗口通告时, 16 bit的窗口大
小值被左移s n d _ s c a l e比特,得到真正的 32 bit的对端接收窗口大小 (图28-6)。每次准备向对
端发送窗口通告时,内部的 32 bit 窗口大小值被右移 r c v _ s c a l e比特,得到可填入 T C P首部
窗口字段的 16 bit值。
T C P发送 S Y N时,无论是主动打开或被动打开,都是根据本地插口接收缓存大小选取
rcv_scale值,填充窗口大小选项的偏移量字段。

26.6 时间戳选项
RFC 1323中还定义了时间戳选项。发送方在每个报文段中放入时间戳,接收方在 A C K中

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 693
将时间戳发回。对于每个收到的 ACK,发送方根据返回的时间戳计算相应的 RTT样本值。
图26-17总结了时间戳选项所用到的变量。
12字节时间戳选项

接收报文段
时间戳 时间戳回
显应答

传输报文段 时间戳回
时间戳
显应答

图26-17 时间戳选项中用到的变量小结

全局变量t c p _ n o w是一个时间戳时钟。内核初启时它初始化为 0,之后每500 ms增加1(图


25-8)。为实现时间戳选项, TCP控制块中定义了下面 3个变量:
• t s _ r e c e n t等于对端发送的最新的有效时间戳 (后面很快会介绍什么是“有效的”时间
戳)。
• ts_recent_age是最近一次 tcp_recent被更新时的 tcp_now值。
• l a s t _ a c k _ s e n t是最近一次发送报文段时确认字段 (t i _ a c k)的值 (图2 6 - 3 2 )。除非
ACK被延迟,正常情况下,它等于 rcv_nxt,下一个等待接收的序号。
tcp_input函数中的两个局部变量 ts_val和ts_ecr,保存时间戳选项的两个值:
• ts_val是对端发送的数据中携带的时间戳。
• ts_ecr是由收到的报文段确认的本地发送报文段中携带的时间戳。
发送报文段中,时间戳选项的前 4个字节为 0 x 0 1 0 1 0 8 0 a,这是RFC 1323附录A中建议的
填充值。第一和第二字节都等于 1,为 N O P;第三字节为 k i n d字段,等于8;第四字节为 l e n字
段,等于 10。在选项之前添加两个 NOP后,紧接着的两个 32 bit时间戳和后续数据都可按照 32
b i t边界对齐。此外,图 2 6 - 1 7中还给出了接收到的时间戳选项,同样采用了推荐的 1 2字节格式
( N e t / 3通常生成的格式 )。不过,处理接收选项的应用进程代码 (图2 8 - 1 0 ),并不要求必须使用
此格式。图 2 6 - 1 6中定义的 1 0字节格式中,没有两个前导的 N O P,对端接收处理代码一样工作
正常(参见习题28.4)。
从发送报文段至收到其 ACK间的RTT等于t c p _ n o w减t s _ e c r,单位为 500ms滴答,因为
这是Net/3时间戳的单位。
时间戳选项还可以支持 TCP执行PAWS:防止序号回绕(protection against wrapped sequence
number)。28.7节将详细讨论这一算法。 PAWS中会用到ts_recent_age变量。
t c p _ o u t p u t向输出报文段中填充时间戳选项时,复制 t c p _ n o w到时间戳字段,复制

Linux公社 www.linuxidc.com
bbs.theithome.com
694 计计TCP/IP详解 卷 2:实现

t s _ r e c e n t到时间戳回显字段 (图26-24)。如果连接采用了时间戳选项,则必须为所有输出报
文段执行这一操作,除非 RST标志置位。

26.6.1 哪个时间戳需要回显, RFC 1323算法

T C P通过时间戳的有效性测试决定是否更新 t s _ r e c e n t,因为这个变量会被填充到时间
戳回显字段中,也就决定了对端发送的哪个时间戳需要回显。 RFC 1323规定了下面的测试条
件:
ti_seq <= last_ack_sent < ti_seq + ti_len

图26-18中的C代码实现它。

图26-18 判定接收时间戳是否有效的典型代码

如果 收 到 的 报文 段 中 携 带时 间 戳 选 项, 则变量 t s _ p r e s e n t 为 真 。 我们在
t c p _ i n p u t 中两次遇到这段代码:图 2 8 - 11首部预测代码中的测试;和图 2 8 - 3 5正常输入处
理中的测试。
为了理解测试条件的具体含义,图 2 6 - 1 9给出了 5种不同的实例,分别对应于连接上收到
的5种不同的报文段。每个例子中, ti_len都等于3。

=接收窗口

实例1: 测试失败

实例2:

实例3: 测试成功

实例4:

实例5: 测试失败

图26-19 举例:收到 5个不同报文段时的接收窗口

接收窗口左边界的序号从 4开始。实例 1中,报文段中携带的全部是重复数据。图 2 8 - 11中


的S E Q _ L E Q测试为真,但 S E Q _ L T测试失败。对于实例 2、3和4,由于收到其中任何一个报文
段,接收窗口左边界都会增加, S E Q _ L E Q和S E Q _ L T测试都为真,尽管实例 2中包含 2个重复

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 695
数据,实例 3 中也包含 1 个重复数据。实例 5 ,由于它无法增加接收窗口左边界,所以
S E Q _ L E Q测试失败。这是一个未来报文段,而非等待的下一个报文段,意味着它前面的报文
段丢失或报文段序列错误。
不幸的是,这个用于判定是否更新 t s _ r e c e n t的测试条件存在问题 [Braden 1993],考虑
下面的例子。
1) 假定图 2 6 - 1 9中 的 连 接 开 始 时 收 到 了 一 个 报 文 段 , 携 带 字 节 1、 2和 3。因为
last_ack_sent等于1,报文段的时间戳被保存到ts_recent中。发送ACK,确认序号为4,
last_ack_sent设为4 (rcv_nxt的值),得到如图 26-19所示的接收窗口。
2) ACK丢失。
3) 对端超时后重传前一个报文段,携带字节 1、2和3,即为图26-19中实例1的报文段。由
于图26-18中的SEQ_LT测试失败, ts_recent不会更新为重传报文段中的值。
4) TCP 发送一个重复的 A C K,确认序号为 4,但时间戳回显字段填入的 t s _ r e c e n t,即
从步骤 1的原始报文段中获取的时间戳值。接收方利用这个值计算 RT T时,将 (不正确地 )计入
原始传输、丢失的 ACK、定时器超时、重传和重复 ACK,得到它们的总时延。
为了使对端能够正确地计算 RTT,重发ACK中应该携带重传报文中的时间戳值。
图2 6 - 1 8中的测试在收到的报文长度为 0时,由于无法移动接收窗口左边界,同样不能更
新r s _ r e c e n t。此外,这个错误的测试条件还会造成生存时间过长的 (大于24天,参见28.7节
中讨论的 PAW S限制)、单方向的 (数据流只在一个方向上存在,从而数据发送方总是输出相同
的ACK)连接。

26.6.2 哪个时间戳需要回显,正确的算法

N e t / 3源代码中使用了图 2 6 - 1 8所示的算法。 [Braden 1993]定义了正确的算法,如图 2 6 - 2 0


所示。

图26-20 判定接收时间戳是否有效的正确代码

它不关心接收窗口左侧是否移动,只确认新的时间戳 (t s _ v a l )大于等于前一个时间戳
(t s _ r e c e n t),并且接收到的报文段的起始序号不大于窗口的左边界。图 2 6 - 1 9中实例5的报
文仍旧无法通过新的测试,因为这是一个乱序报文。
宏T S T M P _ G E Q与图24-21中的S E Q _ G E Q相同。它用于处理时间戳,因为时间戳是 32 bit的
无符号整数,与序号一样存在回绕的问题。

26.6.3 时间戳与延迟ACK

正确理解延迟 A C K是如何影响时间戳和 RT T计算是很重要的。回想图 2 6 - 1 7,T C P 把


t s _ r e c e n t填入到发送报文段的时间戳回显字段中,对端据此计算新的 RT T样本值。如果
A C K被延迟,对端计算时应把延迟时间也考虑在内,否则会造成频繁重传。下面的例子中,
我们使用图 26-20中的代码,不过图 26-18的代码也能正确处理延迟 ACK。
考虑图26-21所示的接收窗口收到携带字节 4和5的报文段时的变化。

Linux公社 www.linuxidc.com
bbs.theithome.com
696 计计TCP/IP详解 卷 2:实现

r c v _ w n d =6:接收窗口

收到的报文段

图26-21 当字节4和5到达时的接收序号空间

由于ti_seq小于等于last_ack_sent,ts_recent被更新。rcv_nxt增加2。
假定对这两个字节的 A C K被延迟,而且在延迟 A C K发送之前,收到了下一个按序到达的
报文段,如图26-22所示。

r c v _ w n d =6:接收窗口

接收报文段

图26-22 当字节6和7到达时的接收序号空间

这一次t i _ s e q大于l a s t _ a c k _ s e n t,因此,不会更新 t s _ r e c e n t。这样做是有目的


的。假定 TCP现在发送确认序号 4~7的ACK,对端据此了解存在延迟 ACK,因为时间戳回显字
段填入的是携带序号 4和5的报文段的时间戳值 (图2 6 - 2 4 )。图 2 6 - 2 2还说明了除非使用了延迟
ACK,否则, rcv_nxt应该等于last_ack_sent。

26.7 发送一个报文段

t c p _ o u t p u t接下来的代码负责发送报文段—填充T C P报文首部的所有字段,并传递
给IP层准备发送。
图26-23给出了这段代码的第一部分,发送 SYN报文段,携带MSS选项和窗口大小选项。
223-234 T C P选项字段构建时用到数组 o p t,整数 o p t l e n记录累积的字节数 (因为一次可
发送多个选项 )。如果 S Y N标志置位, s n d _ n x t复位为初始发送序号 (i s s)。如果主动打开,
则创建 T C P控制块时在 P R U _ C O N N E C T请求处理中对 i s s赋值;如果被动打开,则 t c p _ i n p u t
创建TCP控制块的同时对 iss赋值。两种情况下, iss都等于全局变量 tcp_iss。
235 查看标志TF_NOOPT。但事实上,这个标志永远都不会置位,因为没有代码实现置位操

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 697
作。因此, SYN报文段中必然存在 MSS选项。
N e t / 1版的t c p _ n e w t c p c b中,初始化 t _ f l a g s为0的代码旁有一条注释“发送
选项!”。T F _ N O O P T标志很可能是从早期的 Net/1版本中遗留下来的问题。早期版本
发送MSS选项时与其他主机系统不兼容,只好默认设置不发送这一选项。

图26-23 tcp_output 函数:发送第一个 SYN时加入选项

1. 构造MSS选项
236-241 opt[0]等于2(TCPOPT_MAXSEG),opt[1]等于4,即MSS选项长度,以字节为单位。
函数t cp_mss计算准备向对端发送的 MSS值,27.5节将讨论这个函数。 b c o p y把16 bit的MSS
存储到opt[2]和opt[3]中(习题26.5)。注意,Net/3总是在建立连接的 SYN中发送MSS。
2. 是否发送窗口大小选项
242-244 即使TCP请求窗口大小功能,也只有在主动打开 (T H _ A C K未置位)时,或者被动打
开但对端 S Y N中已包含了窗口大小选项时,才会发送这一选项。回想图 2 5 - 2 1中T C P控制块创
建时,如果全局变量 t c p _ d o _ r f c 1 3 2 3 非零 ( 默认值 ) ,那么 t _ f l a g s 就等于
TF_REQ_SCALE|TF_REQ_TSTMP。
3. 构造窗口大小选项
245-249 由于窗口大小选项占用 3个字节(图26-16),在它前面加入 1字节的NOP,强迫其长

Linux公社 www.linuxidc.com
bbs.theithome.com
698 计计TCP/IP详解 卷 2:实现

度为4字节,从而后续数据都可以按照 4字节边界对齐。如果主动打开,则在 P R U _ C O N N E C T
请求处理代码中计算 r e q u e s t _ r _ s c a l e。如果被动打开,则 t c p _ i n p u t在收到 S Y N时计
算窗口大小因子。
RFC 1323规定如果TCP支持缩放窗口,即使自己的偏移量为 0,也应该发送窗口大小选项。
因为这个选项有两个目的:通知对端自己支持此选项;通告本地的偏移量。即使 T C P计算得
到的本地偏移量为 0,对端可能希望使用不同的值。
图26-24给出了tcp_output的下一部分,完成在外出报文段中构造选项。

图26-24 tcp_output 函数:完成发送选项构造

4. 是否需要发送时间戳
253-261 如果下列3个条件均为真,则发送时间戳选项: (1)TCP当前配置要求支持时间戳选
项;( 2 )正在构造的报文段不包含 R S T标志; ( 3 )主动打开 (f l a g s中S Y N标志置位, A C K标志
未置位 ),或者 T C P收到了对端发送的时间戳 (T F _ R C V S _ T S T M P)。与M S S和窗口大小选项不
同,只要连接双方都同意支持它,时间戳可加入到任意报文段中。
5. 构造时间戳选项
263-267 时间戳选项 ( 2 6 . 6节)占用 1 2字节 (T C P O L E N _ T S T A M P _ A P P A)。头 4个字节为
0 x 0 1 0 1 0 8 0 a(常量T C P O P T _ T S T A M P _ H D R),如图2 6 - 1 7所示。时间戳值等于 t c p _ n o w(系统
初启到现在的500ms滴答数)。时间戳回显字段值等于由 tcp_input设定的ts_recent。
6. 选项加入后是否会造成报文段长度越界
270-277 加入选项后,TCP首部长度会增加 optlen字节。如果发送数据的长度 (len)大于

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 699
M S S减去选项长度 (o p t l e n),则必须相应地减少数据量,并置位 s e n d a l o t标志,强迫函数
发送完当前报文段后进入另一个循环 (图26-1)。
M S S和窗口大小选项只出现在 S Y N报文段中。由于 N e t / 3不在S Y N中添加用户数据,因此
数据长度的调整对这两个选项不起作用。但如果存在时间戳选项,它可以出现在所有报文段
中,从而降低了一次可发送的数据量。最大长度报文段可携带的数据从通告的 M S S降至M S S
减去12字节。
图2 6 - 2 5给出了 t c p _ o u t p u t下一部分代码,更新部分统计值,并为 I P和T C P首部分配
mbuf。它在输出报文段携带有用户数据 (len大于0)时执行。

图26-25 tcp_output 函数:更新统计值,为 IP 和TCP 首部分配 mbuf

7. 更新统计值
284-292 如果t _ f o r c e非零,且用户数据只有 1字节,可知是一个窗口探测报文。如果
snd_nxt小于snd_max,则是一个重传报文。其他的都是正常的数据传输报文。

Linux公社 www.linuxidc.com
bbs.theithome.com
700 计计TCP/IP详解 卷 2:实现

8. 为IP和TCP首部分配mbuf
29 3 - 2 9 7 M G E T H D R为带有数据分组首部的 m b u f分配内存, m b u f中保存 I P和T C P的首部及
可能的数据 (若空间允许 )。尽管 t c p _ o u t p u t调用通常作为系统调用的一部分 (如,w r i t e),
它也可在软件中断级由 t c p _ i n p u t 调用,或作为定时器处理的一部分。因此,定义了
M _ D O N T W A I T。如果返回错误,控制跳转至“ o u t”处。它位于函数的末尾,如图 2 6 - 3 2所
示。
9. 向mbuf中复制数据
298-308 如果数据少于 44字节(100-40-16,假定没有TCP选项),数据由 m _ c o p y d a t a直接
从插口的发送缓存中复制到新的数据组首部 m b u f中。若数据量较大, m _ c o p y创建新的 m b u f
链表,复制插口发送缓存中的数据,最后与前面创建的数据组首部 m b u f链接。回想2 . 9节中介
绍过的m_copy函数,如果数据本身已是一个簇, m_copy将不复制,只引用这个簇。
10. 置位PSH标志
309-316 如果T C P发送了从发送缓存得到的所有数据,则 P S H标志被置位。如同注释中提
到的,这是因为有些接收系统只有在收到 P S H标志或者接收缓存已满时,才会向应用程序递
交收到的数据。我们在 t c p _ i n p u t中将看到, Net/3绝不会为了等待 PSH标志,而把数据滞留
在接收缓存中。
图2 6 - 2 6给出了 t c p _ o u t p u t下一部分的代码,从在 l e n等于0时执行的 e l s e语句开始,
处理不携带用户数据的 TCP报文段。

图26-26 tcp_output 函数:更新统计值,为 IP和TCP首部分配 mbuf

11. 更新统计值
318-325 需要更新的统计值有: T F _ A C K N O W和长度为 0说明是一个纯 A C K报文段。如果
S Y N、F I N或R S T中任何一个置位,即为控制报文段。如果紧急指针超过 s n d _ u n a,是为了

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 701
通知对端紧急指针的位置。如果上述条件均为假,则是窗口更新报文段。
12. 得到存储IP和TCP首部的mbuf
326-335 为带有数据包组首部的 mbuf分配内存,以保存 IP和TCP的首部。
13. 向mbuf中复制IP和TCP首部模板
336-338 b c o p y 把 I P和 T C P首部模板从 t _ t e m p l a t e 复制到 m b u f 中。这个模板由
tcp_template创建。
图26-27给出了tcp_output下一部分的代码,填充 TCP首部剩余的字段。

图26-27 tcp_output 函数:置位 ti_seq 、ti_ack 和ti_flags

14. 如果FIN将重传,递减snd_nxt
339-346 如果TCP已经发送过 FIN,则发送序列空间如图 26-28所示。因此,如果 TH_FIN置
位,则 T F _ S E N T F I N也置位,并且 s n d _ n x t等于s n d _ m a x,可知F I N等待重传。不久将看到
(图2 6 - 3 1 ),发送 F I N时, s n d _ n x t会递增 1 (由于F I N也要占用一个序号 ),因此,这里的代码
递减snd_nxt。
15. 设置报文段的序号字段
347-363 报文段的序号字段通常等于 s n d _ n x t,但在满足下列条件时,应等于 s n d _ m a x:
如果(1) 不传输数据 (len等于0);(2) SYN标志和FIN标志都未置位; (3) 持续定时器未置位。

Linux公社 www.linuxidc.com
bbs.theithome.com
702 计计TCP/IP详解 卷 2:实现

已发送和确认

图26-28 FIN发送后的发送序列空间

16. 设置报文段的确认字段
364 报文段的确认字段通常等于 rcv_nxt,期待接收的下一个序号。
17. 如果存在首部选项,设置首部长度字段
365-368 如果存在 T C P选项(o p t l e n大于0 ),代码把选项内容复制到 T C P首部,T C P首部4
b i t的首部长度字段 (图2 4 - 1 0的t h _ o f f)等于T C P首部的固定长度 ( 2 0字节)加上选项总长度后除
以4。这个字段是以 32 bit为单位的首部长度值,包括 TCP选项。
369 TCP首部的标志字段根据变量 flags设定。
图26-29给出了下一部分的代码,填充 TCP首部其他字段,并计算 TCP检验和。

图26-29 tcp_output 函数:填充其他 TCP首部字段并计算检验和

18. 通告的窗口大小应大于最大报文段长度

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 703
370-375 计算向对端通告的窗口大小 (ti_win)时,应考虑如何避免糊涂窗口综合症。回想
图 2 6 - 3 结 尾 处, w i n 等 于 插口 的 接 收缓 存 大小 。 如 果 w i n 小于 接 收 缓存 大 小的
1/4(so_rcv.sb_hiwa t),并且小于一个最大报文段长度,则通告的窗口大小设为 0,从而在
后续测试中防止窗口缩小。也就是说,如果可用空间已达到接收缓存大小的 1 / 4,或者等于最
大报文段长度,将向对端发送窗口更新通告。
19. 遵守连接的通告窗口大小的上限
376-377 如果win大于连接规定的最大值,应将其减少为最大值。
20. 不要缩小窗口
378-379 回想图2 6 - 1 0中,r c v _ a d v减去r c v _ n x t等于最近一次向发送方通告的窗口大小
中的剩余空间。如果 w i n小于它,应将其设定为该值,因为不允许缩小窗口。有时尽管剩余
的可用空间小于最大报文段长度 (因此,w i n在代码起始处被置为 0),但还可以容纳一些数据,
就会出现这种情况。卷 1中的图22-3举例说明了这一现象。
21. 设置紧急数据偏移量
381-383 如果紧急指针(snd_up)大于snd_nxt,则TCP处于紧急方式。 TCP首部的紧急数
据偏移量字段设定为以报文段起始序号为基准的紧急指针的 16 bit偏移量,并且置位URG标志。
无论所指向的紧急数据是否包含在当前处理的报文段中, T C P都会发送紧急数据偏移量和
URG标志。
图26-30举例说明了如何计算紧急数据偏移量,假定应用进程执行了
send(fd, buf, 3, MSG_OOB);

并且调用 s e n d时发送缓存为空。这种做法表明基于 B e r k e l e y的系统认为紧急指针应指向带


外数据后的第一个字节。回想图 2 4 - 1 0中,我们区分了数据流中 32 bit 的紧急指针 (s n d _ u p),
和TCP首部中的16 bit紧急数据偏移量 (ti_urp)。

这里有个小错误。无论是否采用窗口大小选项,如果发送缓存大于 6 5 5 3 5,并且
几乎为空,则应用进程发送带外数据时,从 s n d _ n x t算起的紧急指针的偏移量有可
能超过6 5 5 3 5。但偏移量是一个 16 bit的无符号整数,如果计算结果超过 6 5 5 3 5,高位
16 bit被丢弃,发送到对端的数据必然是错误的。解决办法参见习题 26.6。
384-391 如果TCP不处于紧急方式,则紧急指针移向窗口的最左端 (snd_una)。
392-399 T C P长度存储在伪首部中以计算 T C P检验和。
发送序列
到目前为止, T C P首部的所有字段已填充完毕,而且从
t _ t e m p l a t e复制I P和T C P首部模板时 (图2 6 - 2 6 ),对伪
首部中用到的 I P首部部分字段预先做了初始化 (见图2 3 - 1 9
中UDP检验和的计算)。
图2 6 - 3 1给出了 t c p _ o u t p u t下一部分的代码, S Y N s n d _ u p=7
或FIN标志置位时更新序号,并启动重传定时器。 由P R U _ S E N D O O B设置

22. 保存起始序号 紧急数据偏移量 =3

400-405 如果T C P不处于持续状态,则起始序号保存 由t c p _ o u t p u t设定


在s t a r t s e q中。图 2 6 - 3 1中的代码在对报文段计时时用 图26-30 紧急指针与紧急数据
到这一变量。 偏移量计算举例

Linux公社 www.linuxidc.com
bbs.theithome.com
704 计计TCP/IP详解 卷 2:实现

图26-31 tcp_output 函数:更新序号并启动重传定时器

23. 增加snd_nxt
406-417 由于SYN和FIN都占用一个序号,其中任一标志置位, snd_nxt都必须增加。FIN
发送过后, TF_SENTFIN将置位。之后, snd_nxt增加发送的数据字节数 (len),可以为0。
24. 更新snd_max
418-419 如果snd_nxt的最新值大于snd_max,则不是重传报文。 snd_max值被更新。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计705
420-428 如果连接目前还没有 RTT值(t_rtt=0),则定时器启动 (t_rtt=1),计时报文段的
起始序号保存在 t _ r t s e q中。t c p _ i n p u t利用它确定计时报文段 A C K的到达时间,从而更
新RTT。根据25.10节中的讨论,代码应为:
if (tp->t_rtt && SEQ_GT(ti->ti_ack, tp->t_rtseq))
tcp_xmit_timer(tp, tp->t_rtt);

25. 设定重传定时器
430-440 如果重传定时器还未启动,并且报文段中有数据,则重传定时器时限设定为
t_rxtcur。前面已经介绍过,通过测量 RTT样本值,tcp_xmit_timer将更新t_rxtcur。
但如果 s n d _ n x t等于s n d _ u n a(此时s n d _ n x t中已加入了 l e n),则是一个纯 ACK报文段,而
只有在发送数据报文段时才需要启动重传定时器。
441-444 如果持续定时器已启动,则关闭它。对于给定连接,可以在任何时候启动重传定
时器或者持续定时器,但两者不允许同时存在。
26. 持续状态
446-447 由于t_force非零,而且持续定时器已设定,可知连接处于持续状态 (与图26-31起
始处的if语句配对的else语句)。需要时,更新 snd_max。处于持续状态时,len应等于1。
t c p _ o u t p u t 的最后一部分,在图 2 6 - 3 2 中给出,输出报文段准备完毕,调用
ip_output发送数据报。

图26-32 tcp_output 函数:调用 ip_output 发送报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
706 计计TCP/IP详解 卷 2:实现

图26-32 (续)

27. 为插口调试添加路由记录
448-452 如果选用了 SO_DEBUG选项,tcp_trace会在TCP的循环路由缓存中添加一条记
录,27.10节将详细讨论这个函数。
28. 设置IP长度、TTL和TOS
453-462 IP首部的3个字段必须由传输层设置: IP长度、TTL和TOS,图23-19底部用星号强
调了这3个特殊字段。

注意,注释的内容为“ X X X”,这是因为尽管对于给定连接, T T L和TO S通常是


常量,可以保存在首部模板中,无需每次发送报文段时都明确赋值。只有当 TCP检验
和计算完毕后,这两个字段才能填入 IP首部,因此只能这样实现。

29. 向IP传递数据报
463-464 ip_output发送携带TCP报文段的数据报。TCP的插口选项和 SO_DONTROUTE逻
辑与,从而能向 IP层传送的插口选项只有一个: SO_DONTROUTE。尽管ip_output还测试另
一个选项 SO_BROADCAST,但即使设定了它,与 SO_DONTROUTE的逻辑与也会将其关闭。也
就是说,应用进程不允许向一个广播地址发送 connect,即使它设定了 SO_BROADCAST选项。
467-470 如果接口队列已满,或者 I P请求分配 m b u f失败,则返回差错码 E N O B U F S 。
t c p _ q u e n c h把拥塞窗口设定为只能容纳一个最大报文段长度,强迫连接执行慢起动。注意,
出现上述情况时, T C P 仍旧返回 0 ( O K ) ,而非错误,即使数据报实际已丢弃。这与
u d p _ o u t p u t(图2 3 - 2 0 )不同,后者返回一个错误。 T C P将通过超时重传该数据报 (数据报文
段),希望那时在接口输出队列中会有可用空间或者能申请到更多的 m b u f。如果T C P报文段不
包含数据,对端由于未收到 ACK而引发超时时,将重传由丢失的 ACK所确认的数据。
471-475 如果连接已收到一个 S Y N,但找不到至目的地的路由,则记录连接上出现了一个
软错误。
当t c p _ o u t p u t被t c p _ u s r r e q调用,做为应用进程系统调用的一部分时 (参见第 3 0章,
P R U _ C O N N E C T、P R U _ S E N D、P R U _ S E N D O O B和P R U _ S H U T D O W N请求),应用进程将接收
t c p _ o u t p u t的返回值。其他调用 t c p _ o u t p u t的函数,如 t c p _ i n p u t、快超时函数和慢
超时函数,忽略其返回值 (因为这些函数不向应用进程返回差错码 )。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 707
30. 更新rcv_adv和last_ack_sent
479-486 如果报文段中通告的最高序号 (rcv_nxt加上win)大于rcv_adv,则保存新的值。
回想图26-9中利用r c v _ a d v确定最后一个报文段发送后新增的可用空间,以及图 26-29中利用
它确定TCP没有缩小窗口。
4 8 7 报文段确认字段的值保存在 l a s t _ a c k _ s e n t中,t c p _ i n p u t利用它处理时间戳选项
(图26-6)。
488 由于所有延迟的 ACK都已被发送,TF_ACKNOW和TF_DELACK标志被清除。
31. 是否还有数据需要发送
489-490 如果sendalot标志置位,控制跳回到 again处(图26-1)。如果发送缓存中的数据
超过一个最大长度报文段的容量 (图2 6 - 3 ),或者由于加入 T C P选项,降低了最大长度报文段的
数据容量,无法在一个报文段中将缓存中的数据发送完毕时,控制将折回。

26.8 tcp_template函数
创建插口时,将调用 t c p _ n e w t c p c b(见前一章 )为T C P控制块分配内存,并完成部分初
始化。当在插口上发送或接收第一个报文段时 (主动打开, P R U _ C O N N E C T请求,或者在监听
的插口上收到了一个 SYN),t c p _ t e m p l a t e为连接的 IP和TCP的首部创建一个模坂,从而减
少了报文段发送时 tcp_output的工作量。
图26-33给出了tcp_template函数。

图26-33 tcp_template 函数:创建 IP和TCP首部的模板

Linux公社 www.linuxidc.com
bbs.theithome.com
708 计计TCP/IP详解 卷 2:实现

1. 分配mbuf
59-72 I P和T C P的首部模板在一个 m b u f中组建,指向这个 m b u f的指针存储在 T C P控制块的
t _ t e m p l a t e 成员变量中。由于这个函数可在软件中断级被 t c p _ i n p u t 调用,
M_DONTWAIT标志置位。
2. 初始化首部字段
73-88 除下列字段外, IP和TCP首部的其他字段均置为 0:t i _ p r等于TCP的IP协议值(6);
t i _ l e n等于20,TCP首部的默认值; t i _ o f f等于5,TCP首部长度,以32 bit为单位;此外,
还要从Internet PCB中把源IP地址、目的 IP地址和TCP端口号复制到TCP首部模板中。
3. 用于TCP检验和计算的伪首部
73-88 由于预先对 IP和TCP首部中许多字段做了初始化,简化了 TCP检验和的计算,方法与
2 3 . 6节中讨论过的 U D P首部检验和的计算方式相同。参考图 2 3 - 1 9中的u d p i p h d r结构,请读
者自己思考为什么 tcp_template将ti_next和ti_prev等字段初始化为 0。

26.9 tcp_respond函数

函数t c p _ r e s p o n d尽管也调用 i p _ o u t p u t发送I P数据报,但用途不同。主要在下面两


种情况下调用它:
1) tcp_input调用它生成 RST报文段,携带或不携带 ACK;
2) tcp_timers调用它发送保活探测报文。
在这两种特珠情况下, TCP调用t c p _ r e s p o n d,取代t c p _ o u t p u t中复杂的逻辑。但请
注意,下一章中讨论的 t c p _ d r o p函数调用 t c p _ o u t p u t来生成 R S T报文段。并非所有的
RST报文段都由 tcp_respond生成。
图26-34给出了tcp_respond的前半部分。

图26-34 tcp_respond 函数:前半部分

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 709

图26-34 (续)

104-110 图26-35列出了3种不同情况下调用 tcp_respond时其参数的变化。

参 数
tp ti m ack seq flags

生成不带ACK的RST tp ti m 0 ti_ack TH_RST


ti_seq + TH_RST|
tp ti m 0
生成带ACK的RST ti_len TH_ACK

生成保活探测 tp t_template NULL rcv_nxt snd_una 0

图26-35 tcp_respond 的参数

t p是指向 T C P控制块的指针 (可能为空 );t i是指向 I P和T C P首部模板的指针; m是指向


m b u f的指针,其中的报文段引发 R S T。最后3个参数是确认字段、序号字段和待生成报文段的
标志字段。
113-118 如果t c p _ i n p u t收到一个不属于任何连接的报文段,则有可能生成 R S T。例如,
收到的报文段中没有指明任何现存连接 (如,SYN指明的端口上没有正在监听的服务器 )。这种
情况下, t p为空,使用w i n和r o的初始值。如果 t p不空,则通告窗口大小将等于接收缓存中
的可用空间,指向缓存路由的指针保存在 ro中,在后面调用 tcp_input时会用到。
1. 保活定时器超时后发送保活探测
119-127 参数m是指向接收报文段的 m b u f链表的指针。但保活探测报文只有当保活定时器
超时时才会被发送,收到的 T C P报文段不可能引发此项操作,因此 m为空,由 m _ g e t h d r分配
保存I P和T C P首部的 m b u f。T C P数据长度 t l e n,设为 0,因为保活探测报文不包含任何用户
数据。
有些基于 4 . 2 B S D的较老的系统不响应保活探测报文,除非它携带数据。通过配
置,在编译内核时定义 T C P _ C O M P A T _ 4 2,N e t / 3能够在保活探测报文中携带一个字
节的无效数据,以引出这些系统的响应。这种情况下, t l e n设为1,而非0。无效字
节不会造成不良后果,因为它不是对方正等待 (而是一个对方已接收并确认过 )的字节,
对端将丢弃它。
利用赋值语句把 t i指向的T C P首部模板结构复制到 m b u f的数据部分,之后指针 t i将被重

Linux公社 www.linuxidc.com
bbs.theithome.com
710 计计TCP/IP详解 卷 2:实现

新设定,指向mbuf中的首部模板。
2. 发送RST报文段
128-138 接收到的报文段有可能会引发 t c p _ i n p u t发送RST。发送RST时,保存输入报文
段的mbuf可以重用。因为 t c p _ r e s p o n d生成的报文段中只包含 IP首部和TCP首部,因此,除
第一个 m b u f之外(数据分组首部 ),m _ f r e e将释放链表中其余的所有 m b u f。另外, I P首部和
TCP首部中的源 IP地址和目的 IP地址及端口号应互换。
图26-36给出了tcp_respond的后半部分。

图26-36 tcp_respond 函数:后半部分

139-157 为 计 算 T C P 检 验 和 , I P和 T C P 首 部 字 段 必 须 被 初 始 化 。 这 些 语 句 与
t c p _ t e m p l a t e初始化t _ t e m p l a t e字段的方式类似。序号和确认字段由调用者提供,最后
调用ip_output发送数据分组。

26.10 小结

本章讨论了生成大多数 T C P报文段的通用函数 (t c p _ o u t p u t)及生成R S T报文段和保活探


测的特殊函数(tcp_respond)。
T C P是否发送报文段取决于许多因素:报文段中的标志、对端通告的窗口大小、待发送
的数据量以及连接上是否存在未确认的数据等等。因此, t c p _ o u t p u t中的逻辑决定了是否
发送报文段 (函数的前半部分 ),如果需要发送,如何填充 T C P首部的字段 (函数后半部分 )。报
文段发送之后,还需要更新 TCP控制块中的相应变量。
t c p _ o u t p u t一次只生成一个报文段,但它在结尾处会测试是否还有剩余数据等待发送,
如果有,控制将折回,并试图发送下一个报文段。这样的循环会一直持续到数据全部发送完
毕,或者有其他停止传输的条件出现 (接收方的窗口通告 )。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 26章 TCP 输 出计计 711
T C P报文段中可以携带选项。 N e t / 3支持的选项规定了最大报文段长度、窗口大小缩放因
子和一对时间戳。头两个选项只能出现在 SYN报文段中,而时间戳选项 (如果连接双方都支持 )
能够出现在所有报文段中。因为窗口大小和时间戳是新增的选项,如果主动打开的一端希望
使用这些选项,则必须在自己发送的 S Y N中添加它们,并且只有在对端发回的 S Y N也包含了
同样的选项时才能使用。

习题

26.1 图2 6 - 1中,如果发送数据过程中出现停顿, T C P将返回慢启动状态,而空闲时间被


设定为从最后一次收到报文段到现在的时间。为什么 T C P不将空闲时间设定为从最
后一次发送报文到现在的时间?
26.2 图2 6 - 6中,我们说如果 F I N已发送,但还未被确认且没有重传,此时 l e n小于0。如
果FIN已重传,情况会怎样?
26.3 N e t / 3 总在主动打开时发送窗口大小和时间戳选项。为什么需要全局变量
t c p _ d o _ r f c 1 3 2?3
26.4 图2 5 - 2 8中的例子未使用时间戳, RT T估算值被更新了 8次。如果使用了时间戳,
RTT估算值会被更新几次?
26.5 图2 6 - 2 3中,调用 b c o p y把收到的 M S S存储在变量 m s s中。为什么不对指向 o p t[ 2 ]
的指针做强制转换,变为不带符号的短整型指针,并利用赋值语句完成这一操作?
26.6 在图 2 6 - 2 9后面,我们讨论了代码的一个错误,可能会导致发送一个错误的紧急数
据偏移量。提出你的解决方案。 (提示:一个TCP报文中能够发送的最大数据量是多
少?)
26.7 图2 6 - 3 2中,我们提到不会向应用进程返回差错代码 E N O B U F S,因为( 1 )如果丢弃的
是数据报文,重传定时器超时后数据将被重传; ( 2 )如果丢弃的是纯 A C K报文,对
端收不到ACK时会重传对应的数据报文。如果丢弃的是 RST报文,情况会怎样?
26.8 解释卷1图20-3中PSH标志的设定。
26.9 为什么图26-36使用ip_defttl作为TTL的值,而图 26-32却使用PCB?
26.10 如果应用进程规定的 I P选项是用于 T C P连接的,图 2 6 - 2 5中分配的 m b u f会出现什么
情况?实现一个更好的方案。
26.11 t c p _ o u t p ut函数很长 (包括注释约 5 0 0行),看上去效率不高,其中许多代码用于
处理特殊情况。假定函数只用于处理准备好的最大长度报文,且没有特殊情况:
无I P选项,无特殊标志如 S Y N、F I N或U R G。实际执行的约有多少行 C代码?报文
递交给ip_output之前会调用多少函数?
26.12 2 6 . 3节结尾的例子中,应用程序向连接写入 1 0 0字节,接着又写入 5 0字节。如果应
用程序为两个缓存各调用一次 w r i t e v,而不是调用 w r i t e两次,有何不同?如
果两个缓存大小分别为 200和300,而不是100和50,调用writev时又有何不同?
26.13 在时间戳选项中发送的时间戳来自于全局变量 t c p _ n o w,它每 5 0 0 m s递增一次。
修改TCP代码,使用更精确的时间戳值。

Linux公社 www.linuxidc.com
bbs.theithome.com
第27章 TCP的函数
27.1 引言

本章介绍多个TCP函数,它们为下两章进一步讨论 TCP的输入打下了基础:
• t c p _ d r a i n是协议的资源耗尽处理函数,当内核的 m b u f用完时被调用。实际上,不做
任何处理。
• tcp_drop发送RST来丢弃连接。
• t c p _ c l o s e执行正常的 T C P连接关闭操作:发送 F I N,并等待协议要求的 4次报文交换
以终止连接。卷 1的18.2节讨论了连接关闭时双方需要交换的 4个报文。
• tcp_mss处理收到的 MSS选项,并在 TCP发送自己的 MSS选项时计算应填入的 MSS值。
• t c p _ c t l i n p u t 在收到对应于某个 T C P报文段的 I C M P 差错时被调用,它接着调用
tcp_notify处理ICMP差错。tcp_quench专门负责处理ICMP的源站抑制差错。
• T C P _ R E A S S宏和t c p _ r e a s s函数管理连接重组队列中的报文段。重组队列处理收到的
乱序报文段,某些报文段还可能互相重复。
• t c p _ t r a c e向内核的 T C P调试循环缓存中添加记录 (插口选项 S O _ D E B U G)。运行 t r p t
(8)程序可以打印缓存内容。

27.2 tcp_drain函数

t c p _ d r a i n是所有T C P函数中最简单的。它是协议的 p r _ d r a i n函数,在内核的 m b u f用


完时,由 m _ r e c l a i m调用。图 1 0 - 3 2中,i p _ d r a i n丢弃其重组队列中的所有数据报分片,
而U D P则不定义自己的资源耗尽处理函数。尽管 T C P也占用 m b u f—位于接收窗口内的乱序
报文段 — 但 N e t / 3 实现的 T C P 并不丢弃这些 m b u f ,即使内核的 m b u f 已用完。相反,
tcp_drain不做任何处理,假定收到的 (但次序差错 )的TCP报文段比IP分片重要。

27.3 tcp_drop函数

t c p _ d r o p在整个系统中多次被调用,发送 RST报文段以丢弃连接,并向应用进程返回差
错。它与关闭连接 (t c p _ d i s c o n n e c t函数)不同,后者向对端发送 F I N,并遵守 T C P状态变
迁图所规定的连接终止步骤。
图27-1列出了调用 tcp_drop的7种情况和相应的 errno参数。
图27-2给出了tcp_drop函数。
202-213 如果TCP收到了一个SYN,连接被同步,则必须向对端发送RST。tcp_drop把状态
设为CLOSED,并调用tcp_output。从图24-16可知,CLOSED状态的tcp_outflags数组中
包含RST标志。
214-216 如果e r r n o等于E T I M E D O U T,且连接上曾收到过软差错 (如E H O S T U N R E A C H),

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 713
软差错代码将取代内容不确定的 ETIMEDOUT,做为返回的插口差错。
217 tcp_close结束插口关闭操作。

函 数 errno 描 述

tcp_input ENOBUFS 监听服务器收到 SYN,但内核无法为 t _ t e m p l a t e分配所需的


mbuf
tcp_input ECONNREFUSED 收到的RST是对本地发送的SYN的响应
tcp_input ECONNRESET 在现存连接上收到了 RST
tcp_timers ETIMEDOUT 重传定时器连续超时 13次,仍未收到对端的 ACK(图25-25)
tcp_timers ETIMEDOUT 连接建立定时器超时 (图25-16),或者保活定时器超时,且连续
9次发送窗口探测报文段,对方均无响应
tcp_usrreq ECONNABORTED P R U _ A B O R T请求
tcp_usrreq 0 关闭插口,设定 S O _ L I N G E R选项,且拖延时间为 0

图27-1 调用tcp_drop 函数和errno 参数

图27-2 tcp_drop 函数

27.4 tcp_close函数

通常情况下,如果应用进程被动关闭,且在 LAST_ACK状态时收到了ACK,tcp_input
将调用 t c p _ c l o s e关闭连接;或者当 2 M S L定时器超时,插口从 T I M E _ WA I T状态变迁到
CLOSED状态时, t c p _ t i m e r s也会调用 t c p _ c l o s e。它也可以在其他状态被调用,一种可
能是发生了差错,如上一小节讨论过的情况。 t c p _ c l o s e释放连接占用的内存 (IP和TCP首部
模板、TCP控制块、Internet PCB和保存在连接重组队列中的所有乱序报文段 ),并更新路由特
性。
我们分3部分讲解这个函数,前两部分讨论路由特性,最后一部分介绍资源释放。

27.4.1 路由特性

r t _ m e t r i c s结构 (图1 8 - 2 6 )中保存了 9个变量,有 6个用于 T C P。其中 8个变量可通过

Linux公社 www.linuxidc.com
bbs.theithome.com
714 计计TCP/IP详解 卷 2:实现

r o u t e ( 8 )命令读写 (第9个,r m x _ p k s e n t未使用 ):图 2 7 - 3列出了这些变量。此外,运行


r o u t e命令时,加入 - l o c k选项,可以设置 r m x _ l o c k s成员变量(图20-13)中对应的 R T V _xxx
比特,告诉内核不要更新对应的路由参数。
关闭TCP 插口时,如果下列条件满足:连接上传输的数据量足够生成有效的统计值,并
且变量未被锁定, tcp_close将更新3个路由参数 —已平滑的RTT估计器、已平滑的 RTT平
均偏差估计器和慢起动门限。

t c p _ c l o s e是否 t c p _ m s s是否
rt_metric s成员 r o u t e(8)附加参数
保存该成员 使用该成员

rmx_expire -expire
rmx_hopcount -hopcount
rmx_mtu • -mtu
rmx_recvpipe • -recvpipe
rmx_rtt • • -rtt
rmx_rttvar • • -rttvar
rmx_sendpipe • -sendpipe
rmx_ssthresh • • -ssthresh

图27-3 TCP用到的rt_metrics 结构中的变量

图27-4给出了tcp_close的第一部分。
1. 判断是否发送了足够的数据量
234-248 默认的发送缓存大小为 8 1 9 2字节(s b _ h i w a t),因此首先比较初始发送序号和连
接上已发送的最大序号,测试是否已传输了 131 072字节( 1 6个完整的缓存 )的数据。此外,插
口还必须有一条非默认路由的缓存路由 (参见习题19.2)。
请注意,如果传输的数据量在 N×232(N> 1 )和N×2 32+ 1 3 1 0 7 2 (N> 1 )之间,则因为
序号可能回绕,比较时也许会出现问题,尽管可能性不大。但目前很少有连接会传
输4 G的数据。
尽管I n t e r n e t上存在大量的默认路由,缓存路由对于维护有效的路由表还是很有
用的。如果主机长期与另外某个主机 (或网络)交换数据,即使默认路由可用,也应运
行r o u t e命令向路由表中添加源站选路和目的选路的路由,从而在整条连接上维护
有效的路由信息 (参见习题19.2)。这些信息在系统重启时丢失。
250 管理员可以锁定图 27-3中的变量,防止内核修改它们。因此,代码在更新这些变量之前,
必须先检查其锁定状态。
2. 更新RTT
251-264 t_srtt的单位为8个滴答(图25-19),而rmx_rtt的单位为微秒。因此,首先必须
实现单位换算, t _ s r t t乘以1 000 000(R T M _ R T T U N I T),除以2 (滴答/秒)再乘以8,得到RT T
的最新值。如果 rmx_rtt值已存在,它被更新为最新值与原有值和的一半,即两者的平均值。
如果不存在,最新值将直接赋给 rmx_rtt变量。
3. 更新平均偏差
265-273 更新平均偏差的算法与更新 RTT的类似,也需要把单位为 4个滴答的t_rttvar换
算为以微秒为单位。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 715

图27-4 tcp_close 函数:更新 RTT和平均偏差

图27-5给出了tcp_close的下一部分代码,更新路由的慢起动门限。
274-283 满足下列条件时,慢起动门限被更新: (1)它被更新过 (rmx_ssthresh非零);(2)
管理员规定了 r m x _ s e n d p i p e,而s n d _ s s t h r e s h的最新值小于 r m x _ s e n d p i p e的一半。
如同代码注释中指出的, T C P不会更新 r m x _ s s t h r e s h值,除非因为数据分组丢失而不得不

Linux公社 www.linuxidc.com
bbs.theithome.com
716 计计TCP/IP详解 卷 2:实现

这样做。从这个角度出发,除非十分必要, TCP不会修改门限值。

图27-5 tcp_close 函数:更新慢起动门限

284-290 变量s n d _ s s t h r e s h以字节为单位,除以 MSS(t _ m a x s e g)得到报文段数,加上


1 / 2t _ m a x s e g是为了保证总报文段容量必定大于 s n d _ s s t h r e s h字节。报文段数的下限为 2
个报文段。
291-297 M S S加上 I P 和T C P首部大小 ( 4 0 ) ,再乘以报文段数,利用得到的结果来更新
rmx_ssthresh,采用的算法与图 27-4中的相同(新值的1/2加上原有值的1/2)。

27.4.2 资源释放

图27-6给出了tcp_close的最后一部分,释放插口占用的内存资源。

图27-6 tcp_close 函数:释放连接资源

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 717

图27-6 (续)

1. 释放重组队列占用的 mbuf
299-306 如果连接重组队列中还有报文段,则丢弃它们。重组队列用于存放收到位于接收
窗口内、但次序差错的报文段。在等待接收的正常序列报文段到达之前,它们会一直保存在
重组队列中;之后,报文段被重组并递交给应用程序。 27.9节会详细讨论这一过程。
2. 释放首部模板和 TCP控制块
307-309 调用 m _ f r e e 释放 I P 和 T C P首部模板,调用 f r e e 释放 T C P控制块,调用
sodisconnected发送PRU_DISCONNECT请求,标记插口已断开连接。
3. 释放PCB
310-318 如果插口的Internet PCB保存在TCP的高速缓存中,则把 TCP的PCB链表表头赋给
tcp_last_inpcb,以清空缓存。接着调用 in_pcbdetach释放PCB占用的内存。

27.5 tcp_mss函数

tcp_mss被两个函数调用:
1) tcp_output,准备发送 SYN时调用,以添加 MSS选项;
2) tcp_input,收到的SYN报文段中包含MSS选项时调用;
tcp_mss函数检查到达目的地的缓存路由,计算用于该连接的 MSS。
图2 7 - 7给出了 t c p _ m s s第一部分的代码,如果 P C B中没有到达目的地的路由,则设法得
到所需的路由。

图27-7 tcp_mss 函数:如果 PCB中没有路由,则设法得到所需路由

Linux公社 www.linuxidc.com
bbs.theithome.com
718 计计TCP/IP详解 卷 2:实现

图27-7 (续)

1. 如果需要,就获取路由
1391-1417 如果插口没有高速缓存路由,则调用 rtalloc得到一条。与外出路由相关的接
口指针存储在 i f p中。外出接口是非常重要的,因为其 M T U会影响 T C P通告的 M S S。如果无
法得到所需路由,函数就立即返回默认值 512 ( tcp_mssdflt)。
图27-8给出了t c p _ m s s的下一部分代码,判断得到的路由是否有相应的参数表。如果有,
则变量t_rttmin、t_srtt和t_rttvar将初始化为参数表中的对应值。

图27-8 tcp_mss 函数:判断路由是否有相应的 RTT参数表

2. 初始化已平滑的 RTT估计器
1420-1432 如果连接上不存在 RT T样本值 (t _ s r t t = 0),并且r m x _ r t t非零,则将后者赋

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 719
给已平滑的 RT T估计器 t _ s r t t。如果路由参数表锁定标志的 R T V _ R T T比特置位,表明连接
的最小 RT T (t _ r t t m i n )也应初始化为 r m x _ r t t 。前面介绍过, t c p _ n e w t c p c b 把
t_rttmin初始化为2个滴答。
rmx_rtt(以微秒为单位)转换为t_srtt(以8个滴答为单位),这是图27-4的反变换。注意,
t_rttmin等于t_srtt的1/8,因为前者没有除以缩放因子 TCP_RTT_SCALE。
3. 初始化已平滑的 RTT平均偏差估计器
1433-1439 如果存储的 rmx_rttvar(以微秒为单位)值非零,将其转换为 t_rttvar (以4
个滴答为单位 )。但如果为零,则 t _ r t t v a r等于t _ r t t,即偏差等于均值。已平滑的 RT T平
均偏差估计器默认设置为 ±1 RT T。由于t _ r t t v a r的单位为 4个滴答,而 t _ r t t的单位为 8个
滴答,t_srtt值也必须做相应转换。
4. 计算初始RTO
1440-1442 计算当前的 RTO,并存储在 t_rxtcur中,采用下列算式更新:
RTO=s rt t+2×rttvar
计算第一个 RTO 时,乘数取 2,而非 4,上式与图 2 5 - 2 1中用到的算式相同。将缩放关系代
入,得到:
t_srtt
t_srtt t_rttvar + t_rttvar
RTO = +2× = 4
8 4 2
即为TCPT_RANGESET的第二个参数。
图27-9给出了tcp_mss的下一部分,计算 MSS。

图27-9 tcp_mss 函数:计算 mss

5. 从路由表中的MTU得到MSS
1444-1450 如果路由表中的 MTU有值,则将其赋给 m s s。如果没有,则 m s s初始值等于外
出接口的MTU值减去40(IP和TCP首部默认值 )。对于以太网, MSS初始值应为 1460。
6. 减小MSS,令其等于 MCLBYTES的倍数
1451-1457 如 果 m s s 大 于 M C L B Y T E S , 则 减 小 m s s的 值 , 令 其 等 于 最 接 近 的

Linux公社 www.linuxidc.com
bbs.theithome.com
720 计计TCP/IP详解 卷 2:实现

M C L B Y T E S( m b u f簇大小)的整数倍。如果 M C L B Y T E S值(通常等于 1 0 2 4或2 0 4 8 )与M C L B Y T E S值


减1逻辑与后等于 0,说明M C L B Y T E S等于2的倍数。例如, 1 0 2 4 (0 x 4 0 0)逻辑与 1 0 2 3 (0 x 3 f f)
等于0。
代码通过清零 m s s的若干低位比特,将 m s s减小到最接近的 M C L B Y T E S的倍数:如果
m b u f簇大小为 1 0 2 4,m s s与1 0 2 3的二进制补码 (0 x f f f f f c 0 0)逻辑与,低位的 10 bit被清零。
对于以太网, m s s 将从 1 4 6 0 减至 1 0 2 4 。如果 m b u f簇大小为 2 0 4 8 ,与 2 0 4 7 的二进制补码
(0 x f f f f 8 0 0 0)逻辑与,低位的 11 bit 被清零。对于令牌环, M T U大小为 4 4 6 4,上述运算将
m s s从4 4 2 4减为4 0 9 6。如果 M C L B Y T E S不是2的倍数,代码用 m s s整数除以 M C L B Y T E S后,再
乘上MCLBYTES,从而将mss减小到最接近的 MCLBYTES的倍数。
7. 判断目的地是本地地址还是远端地址
1458-1459 如 果 目 的 I P不 是 本 地 地 址 (i n _ l o c a l a d d r 返 回 零 ), 且 m s s 大 于
512(tcp_mssdflt),则将mss设为512。

I P地址是否为本地地址取决于全局变量 s u b n e t s a r e l o c a l,内核编译时把符
号变量S U B N E T S A R E L O C A L的值赋给它。默认值为 1,意味着如果给定 I P地址与主机
任一接口的 I P地址具有相同的网络 I D,则被认为是一个本地地址。如果为 0,则给定
IP地址必须与主机任一接口的 IP地址具有相同的网络号和子网号,才会被认为是一个
本地地址。
对于非本地地址,将 M S S最小化是为了避免 I P数据报经广域网时被分片。绝大多
数WA N链路的 M T U只有1 0 0 6,这是从 ARPANET遗留下来的一个问题。在卷 1的11 . 7
节中讨论过,现代的多数 WA N支持1 5 0 0,甚至更大的 M T U。感兴趣的读者还可阅读
卷1的2 4 . 2节中讨论的路由 M T U发现特性 (RFC 11 9 1,[Mogul and Deering 1990]) 。
Net/3不支持路由 MTU发现。

图27-10给出了tcp_mss最后一部分的代码。
8. 对端的MSS用作上限
1461-1472 如果tcp_mss被tcp_input调用,参数 offer非零,等于对端通告的 mss值。
如果m s s大于对端通告的值,则将 o f f e r赋给它。例如,如果函数计算得到的 m s s等于1 0 2 4,
但对端通告的值只有 5 1 2,则 m s s必须被设定为 5 1 2。相反,如果 m s s等于5 3 6 (即输出 M T U等
于5 7 6 ),而对端通告的值为 1 4 6 0,T C P仍旧使用 5 3 6。只要不超过对端通告的值, m s s可以取
小于它的任何一个值。如果 t c p _ m s s被t c p _ o u t p u t调用,o f f e r等于0,用于发送 M S S选
项。注意,尽管 mss的上限可变,其下限固定为 32。
1473-1483 如果mss小于tcp_newtcpcb中设定的默认值t_maxseg(512),或者如果TCP正
在处理收到的MSS选项(offer非零),则需执行下列步骤。首先,如果路由的 rmx_sendpipe
有值,则采用它做为发送缓存的高端 (high-water)标志(图16-4)。如果缓存小于 m s s,则使用较
小的值。除非是应用程序有意把发送缓存定得很小,或者管理员将 rmx_sendpipe定得很小,
这种情况一般不会发生,因为发送缓存的上限默认值为 8192,大于绝大多数的 mss。
9. 增加缓存大小,令其等于最近的 MSS整数倍
1484-1489 增加缓存大小,令其等于最近的 m s s整数倍,上限为 s b _ m a x( N e t / 3中定义为
262 144 ,即2 5 6×1 0 2 4 )。插口发送缓存的上限设定为 s b r e s e r v e。例如,上限默认值等于

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计721
8192,但对于以太网上的本地 TCP传输,其 mbuf簇大小为2048(假定m s s等于1460),代码把上
限值增加到 8760(等于6×1460)。但对于非本地的连接, mss等于512,上限值保持8192不变。

图27-10 tcp_mss 函数:结束处理

1490 由于t_maxseg已小于默认值(512),或者由于收到了对端发送的 MSS选项,所以应更

Linux公社 www.linuxidc.com
bbs.theithome.com
722 计计TCP/IP详解 卷 2:实现

新它。
1491-1499 对接收缓存的处理与发送缓存相同。
10. 初始化拥塞窗口和慢起动门限
1500-1509 拥塞窗口的值, s n d _ c w n d,等于一个最大报文段长度。如果路由表中的
r m x _ s s t h r e s h非零,慢起动门限 (s n d _ s s t h r e s h)初始化为该值,但应保证其下限为两个
最大报文段长度。
1510 函数最后返回mss。tcp_input忽略这一返回值 (图28-10,因为它已收到对端的 MSS
选项),但图26-23中,tcp_output将它用作MSS通告。

举例

下面通过一个连接建立的实例说明 t c p _ m s s的操作过程。连接建立过程中,它会被调用
两次:发送 SYN时和收到对端带有 MSS选项的SYN时。
1) 创建插口, tcp_newtcpcb初始化t_maxseg为512。
2) 应用进程调用 c o n n e c t。为了在 S Y N报文段中加入 M S S选项, t c p _ o u t p u t调用
t c p _ m s s,参数o f f e r等于零。假定目的 I P为本地以太网地址, m b u f簇大小为 2 0 4 8,执行图
2 7 - 9中的代码后, m s s等于1 4 6 0。由于o f f e r等于零,图 2 7 - 1 0中的代码不修改 m s s值,函数
返回 1 4 6 0 。因为 1 4 6 0 大于默认值 ( 5 1 2 ) 而且未收到对端的 M S S 选项,缓存大小不变。
tcp_output发送MSS选项,通告 MSS大小为1460。
3) 对端发送响应 S Y N,通告 m s s大小为1 0 2 4。t c p _ i n p u t调用t c p _ m s s,参数 o f f e r
等于1 0 2 4。图2 7 - 9的代码逻辑仍旧设定 m s s为1 4 6 0,但在图 2 7 - 1 0起始处的 m i n语句将 m s s减
小为1 0 2 4。因为 o f f e r非零,缓存大小增加至最近的 1 0 2 4的整数倍 (等于8 1 9 2 )。t _ m a x s e g
更新为1024。
初看上去,t c p _ m s s的逻辑存在问题: TCP向对端通告 mss大小为1460,之后从
对端收到的 m s s只有1 0 2 4。尽管 T C P只能发送 1 0 2 4字节的报文段,对端却能够发送
1 4 6 0字节的报文段。读者可能会认为发送缓存应等于 1 0 2 4的倍数,而接收缓存则应
等于1 4 6 0的倍数。但图 2 7 - 1 0中的代码却将两个缓存大小都设为对端通告的 m s s的倍
数。这是因为尽管 T C P通告m s s为1 4 6 0,但对端通告的 m s s仅为1 0 2 4,对端有可能不
会发送1460字节的报文段,而将发送报文段限制为 1024字节。

27.6 tcp_ctlinput函数

回想图22-32中,t c p _ c t l i n p u t处理5种类型的ICMP差错:目的地不可达、数据报参数
错、源站抑制、数据报超时和重定向。所有重定向差错会上交给相应的 TCP或UDP进行处理。
对于其他 4种差错,仅当它们是被 T C P报文段引发的,才会调用 t c p _ c t l i n p u t进行处
理。
图27-11给出了tcp_ctlinput函数,它与图23-30的udp_ctlinput函数类似。
365-366 在逻辑上, t c p _ c t l i n p u t与u d p _ c t l i n p u t的唯一区别是如何处理 ICMP源站
抑制差错。因为 i n e t c t l e r r m a p等于0,U D P忽略源站抑制差错。 T C P检查源站抑制差错,
并把notify函数的默认值tcp_notify改为tcp_quench。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 723

图27-11 tcp_ctlinput 函数

27.7 tcp_notify函数

t c p _ n o t i f y被t c p _ c t l i n p u t调用,处理目的地不可达、数据报参数错、数据报超时
和重定向差错。与 U D P的差错处理函数相比,它要复杂得多,因为 T C P必须灵活地处理连接
上收到的各种软差错。图 27-12给出了tcp_motify函数。

图27-12 tcp_notify 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
724 计计TCP/IP详解 卷 2:实现

图27-12 (续)

328-345 如果连接状态为 E S TA B L I S H E D,则忽略 E H O S T U N R E A C H、E N E T U N R E A C H和


EHOSTDOWN差错代码。
处理这 3个差错是 4 . 4 B S D中新增的功能。 N e t / 2及早期版本在连接的软差错变量
(t _ s o f t e r r o r)中记录这些差错,如果连接最终失败,则向应用进程返回相应的差
错码。回想一下, t c p _ x m i t _ t i m e r在收到一个 A C K,确认未发送过的报文段时,
复位t_softerror为零。
346-353 如果连接还未建立,而且 TCP已经至少4次重传了当前报文段, t _ s o f t e r r o r中
已存在差错记录,则最新的差错将被保存在插口的 s o _ e r r o r变量中,从而应用进程可以调
用select对插口进行读写。如果上述条件不满足,当前差错将仍旧保存在 t_softerror中。
我们在 t c p _ d r o p 函数中讨论过,如果连接最终由于超时而被丢弃, t c p _ d r o p 会把
t _ s o f t e r r o r赋给插口差错变量 e r r n o。任何在插口上等待接收或发送数据的应用进程会
被唤醒,并得到相应的差错代码。

27.8 tcp_quench函数

t c p _ q u e n c h的函数代码在图 2 7 - 1 3中给出。 T C P在两种情况下调用它:当连接上收到源


站抑制差错时,由 t c p _ i n p u t 调用。当 i p _ o u t p u t 返回 E N O B U F S 差错代码时,由
tp_output调用。

图27-13 tcp_quench 函数

拥塞窗口设定为最大报文段长度,强迫 T C P 执行慢起动。慢起动门限不变 (与
t c p _ t i m e r s 处理重传超时的思想相同 ),因此,窗口大小将成指数地增加,直至达到
snd_ssthresh门限或发生拥塞。

27.9 TCP_REASS宏和tcp_reass函数

TCP报文段有可能乱序到达,因此,在数据上交给应用进程之前, TCP必须设法恢复正确

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 725
的报文段次序。例如,如果接收方的接收窗口
大小为 4 0 9 6,等待接收的下一个序号为 0。收
到的第一个报文段携带 0 ~ 1023字节的数据(次
序正确 ),第二个报文段携带了 2048 ~ 3071 字
节的数据,很明显,第二个报文段到达的次序
差错。如果乱序报文段位于接收窗口内, T C P
并不丢弃它,而是将其保存在连接的重组队列
中,继续等待中间缺失的报文段 (携带 1024 ~ 16字节(未用)
2 0 4 7字节的报文段 )。这一节我们将讨论处理
T C P重组队列的代码,为后两章讨论 t c p _
input打下基础。
(20字节)
如果假定某个 m b u f中包含 I P首部、 T C P首
部和 4字节的用户数据 ( 回想图 2 - 1 4 的左半部
分),如图 2 7 - 1 4所示。此外还假定数据的序号
依次为7、8、9和10。 (20字节)
图2 4 - 1 2中定义的 t c p i p h d r结构里包含
了i p o v l y和t c p h d r两个结构, t c p h d r结构 4字节数据
在图2 4 - 1 2中给出。图 2 7 - 1 4只列出了与重组有
关的一些变量: t i _ n e x t 、 t i _ p r e v 、
40字节(未用)
t i _ l e n、t i _ d p o r t和t i _ s e q。头两个指
针指向由给定连接所有乱序报文段组成的双向
链表。链表头保存在连接的 T C P控制块中:结
构的头两个成员变量为seg_next和 图27-14 举例:带有 4字节数据的 IP和TCP首部

s e g _ p r e v。t i _ n e x t和t i _ p r e v指针与I P首部的头 8个字节重复,只要数据报到达了 T C P,


就不再需要这些内容。 t i _ l e n等于T C P数据的长度, T C P计算检验和之前首先计算并存储这
个字段。

27.9.1 TCP_REASS宏

t c p _ i n p u t收到数据后,就调用图 2 7 - 1 5中的宏 T C P _ R E A S S,把数据放入连接的重组队


列。TCP_REASS只在一种情况下被调用:参见图 29-22。
54-63 t p是指向连接 T C P控制块的指针, t i是指向接收报文段的 t c p i p h d r结构的指针。
如果下列3个条件均为真:
1) 报文段到达次序正确 (序号t i _ s e q等于连接上等待接收的下一序号, r c v _ n x t);并

2) 连接的重组队列为空 (seg_next指向自己,而不是某个 mbuf);并且
3) 连接处于ESTABLISHED状态。
则执行下列步骤:设定延迟 A C K标志;更新 r c v _ n x t,增加报文段携带的数据长度;如果报
文段T C P首部中F I N标志置位,则 f l a g s参数中增加 T H _ F I N标志;更新两个统计值;数据放
入插口的接收缓存;唤醒所有在插口上等待接收的应用进程。

Linux公社 www.linuxidc.com
bbs.theithome.com
726 计计TCP/IP详解 卷 2:实现

图27-15 TCP_REASS 宏:向连接的重组队列中添加数据

必须满足前述 3个条件的原因是:首先,如果数据次序差错,则必须将其放入重组队列,
直至收到了中间缺失的报文段,才能把数据提交给应用进程。第二,即使当前数据到达次序
正确,但如果重组队列中已存在乱序数据,则新的数据有可能就是所需的缺失数据,从而能
够向应用进程同时提交多个报文段中的数据;第三,尽管允许请求建立连接的 S Y N报文段中
携带数据,但这些数据在连接进入 ESTABLISHED状态之前,必须保存在重组队列中,不允许
直接提交给应用进程。
64-67 如果这3个条件不是同时满足,则 TCP_REASS宏调用TCP_REASS函数,向重组队列
中添加数据。由于收到的报文段如果不是乱序报文段,就有可能是所需的缺失报文段,因此,
置位T F _ A C K N O W,要求立即发送 A C K。T C P的一个重要特性是收到乱序报文段时,必须立即
发送ACK,这有助于快速重传算法 (29.4节)的实现。
在讨论 T C P _ R E A S S 函数代码之前,需要先了解图 2 7 - 1 4中T C P首部的两个端口号,
t i_sport和ti_dpor t,所起的作用。其实,只要找到了 TCP控制块并调用了 T C P _ R E A S S,
就不再需要它们了。因此, T C P报文段放入重组队列时,可以把对应 m b u f的地址存储在这两
个端口号变量中。对于图 2 7 - 1 4中的报文段,无需这样做,因为 I P和T C P的首部都存储在 m b u f
的数据部分,可直接使用 d t o m宏。但我们在 2 . 6节讨论m _ p u l l u p时曾指出,如果 I P和T C P的
首部保存在簇中 (如图2 - 1 6所示,对于最大长度报文这是很正常的 ),d t o m宏将无法使用。我
们在该节中曾提到, TCP把从TCP首部指向mbuf的后向指针 (back pointer)存储在TCP的两个端
口号字段中。
图2 7 - 1 6举例说明了这一技术的用法,利用它处理连接上的两个乱序报文段,每个报文段
都存储在一个 m b u f簇中。乱序报文段双向链表的表头是连接的 T C P控制块中的 s e g _ n e x t成
员变量。为简化起见,图中未标出 s e g _ p r e v指针和指向链表最后一个报文段的 t i _ n e x t指
针。
接收窗口等待接收的下一个序号为 1 (r c v _ n x t),但我们假定这个报文段丢失了。接着又
收到了两个报文段,携带 1 4 6 1 ~ 4 3 8 0字节的数据,这是两个乱序报文段。 T C P调用m _ d e v g e t
把它们放入 mbuf簇中,如图 2-16所示。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 727

(未用) (未用)

2048字节簇 2048字节簇

(20字节)

后向指针 后向指针

(20字节)

1460字节数据 1460字节数据

548字节(未用) 548字节(未用)

图27-16 两个乱序TCP报文段存储在mbuf簇中

T C P首部的头 32 bit存储指向对应 m b u f的指针,下面介绍的 T C P _ R E A S S函数将用到这个


后向指针。

27.9.2 TCP_REASS函数

图2 7 - 1 7给出了 T C P _ R E A S S函数的第一部分。参数包括: t p,指向 T C P控制块的指针;


t i,指向接收报文段 I P和T C P首部的指针; m,指向存储接收报文段的 m b u f链表的指针。前

Linux公社 www.linuxidc.com
bbs.theithome.com
728 计计TCP/IP详解 卷 2:实现

面曾提到过,ti既可以指向由m所指向的mbuf的数据区,也可以指向一个簇。

图27-17 TCP_REASS 函数:第一部分


6 9 - 8 3 后面将看到, T C P收到一个对 S Y N的确认时, t c p _ i n p u t将调用 T C P _ R E A S S,并
传递一个空的 t i指针(图2 8 - 2 0和图2 9 - 2 )。这意味着连接已建立,可以把 S Y N报文段中携带的
数据(T C P _ R E A S S已将其放入重组队列 )提交给应用程序。连接未建立之前,不允许这样做。
标志“present”位于图27-23中。
8 4 - 9 0 遍历从 s e g _ n e x t 开始的乱序报文段双向链表,寻找序号大于接收报文段序号
(ti_seq)的第一个报文段。注意, for循环体中只包含一个 if语句。
图2 7 - 1 8的例子中,新报文段到达时重组队列中已有两个报文段。图中标出了指针 q,指
向链表的下一个报文段,带有字节 1 0 ~ 1 5 。此外,图中还标出了两个指针 t i _ n e x t 和
t i _ p r e v,起始序号 (t i _ s e q)、长度 (t i _ l e n)和数据字节的序号。由于这些报文段较小,
每个报文段很可能存储在单一的 mbuf中,如图27-14所示。

链表中的前
一报文段

新报文段

图27-18 存储重复报文段的重组队列举例

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计729
图27-19给出了TCP_REASS下一部分的代码

图27-19 TCP_REASS 函数:第二部分

91-107 如果双向链表中 q指向的报文段前还存在报文段,则该报文段有可能与新报文段重


复,因此,挪动指针 q,令其指向 q的前一个报文段 (图2 7 - 1 8中携带字节 4 ~ 8的报文段 ),计算
重复的字节数,并存储在变量 i中:
i = q->ti_seq + q->ti_len - ti->ti_seq;
= 4 + 5 -7
= 2

如果i大于0,则链表中原有报文段与新报文段携带的数据间存在重复,如例子中给出的
报文段。如果重复的字节数 (i)大于或等于新报文段的大小,即新报文段中所有的数据都已包
含在原有报文段中,新报文段是重复报文段,应予以丢弃。
108-112 如果只有部分数据重复 (如图27-18所示),m_adj丢弃新报文段起始 i字节的数据,
并相应更新新报文段的序号和长度。挪动 q指针,指向链表中的下一个报文段。图 2 7 - 2 0给出
了图27-18中各报文段和变量此时的状态。
116 mbuf的地址m存储在TCP首部的源端口号和目的端口号中,也就是我们在本节前面曾提
到的后向指针,防止 TCP首部被存放在 mbuf簇中,而无法使用 d t o m宏。宏R E A S S _ M B U F定义
为:
#define REASS_MBUF(ti) (*(struct mbuf **)&((ti)->ti_t))

t i _ t是一个t c p h d r结构(图2 4 - 1 2 ),最初的两个成员变量是两个 1 6 b i t的端口号。请注意


图2 7 - 1 9中的注释“ X X X”,其中隐含了这样一个假定,指针能够存放在两个端口号占用的 3 2
bit空间中。

Linux公社 www.linuxidc.com
bbs.theithome.com
730 计计TCP/IP详解 卷 2:实现

链表中的前
一报文段

新报文段

图27-20 删除新报文段中的字节 7和8后,更新图 27-18

图27-21给出了tcp_reass的第三部分,删除重组队列下一报文段中可能的重复字节。
117-135 如果还有后续报文段,则计算新报文段与下一报文段间重复的字节数,并存储在
变量i中。还是以图27-18中的报文段为例,得到:
i = 9 + 2 - 10
= 1

因为序号10的字节同时存在于两个报文段中。
根据i值的大小,有可能出现 3种情况:
1) 如果i小于等于0,无重复。
2) 如果i小于下一报文段的字节数 (q - > t i _ l e n),则有部分重复,调用 m _ a d j,从该报
文段中丢弃起始的 i字节。
3) 如果i大于等于下一报文段的字节数,则出现完全重复,从链表中删除该报文段。
136-139 代码最后调用 i n s q u e,把新报文段插入连接的重组双向链表中。图 27-22给出了
图27-18中各报文段和变量此时的状态。

图27-21 TCP_REASS 函数:第三部分

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 731

图27-21 (续)

链表中的前
一报文段

新报文段

图27-22 丢弃所有重复字节后,更新图 27-20

图27-23给出了tcp_reass最后一部分的代码,如果可能,向应用进程递交数据。

图27-23 tcp_reass 函数:第四部分

Linux公社 www.linuxidc.com
bbs.theithome.com
732 计计TCP/IP详解 卷 2:实现

145-146 如果连接还没有收到 SYN(连接处于 LISTEN状态或SYN_SENT状态),不允许向应


用进程提交数据,函数返回。当函数被宏 T C P _ R E A S S 调用时,返回值 0被赋给宏的参数
f l a g s。这种做法带来的副作用是可能会清除 FIN标志。当宏 T C P _ R E A S S被图29-22的代码调
用时,如果接收报文段包含了 S Y N、F I N和数据(尽管不常见,但却是有效的报文段 ),会出现
这种情况。
147-149 t i设定为链表的第一个报文段。如果链表为空,或者第一个报文段的起始序号
(t i - > t i _ s e q)不等于连接等待接收的下一序号 (r c v _ n x t),则函数返回 0。如果第二个条件
为真,说明在等待接收的下一序号与已收到的数据之间仍然存在缺失报文段。例如,图 2 7 - 2 2
中,如果携带 4 ~ 8 字节的报文段是链表的起始报文段,但 r c v _ n x t等于2,字节 2和3仍旧缺
失,因此,不能把 4 ~ 15字节提交给应用进程。返回值 0将清除FIN标志(如果该标志设定 ),这
是因为还有未收到的数据,所以暂时不能处理 FIN。
150-151 如果连接处于 S Y N _ R C V D状态,且报文段长度非零,则函数返回 0。如果两个条
件均为真,说明插口在监听过程中收到了携带数据的 S Y N报文段。数据将保存在连接队列中,
等待三次握手过程结束。
152-164 循环从链表的第一个报文段开始 (从前面的测试条件可知,它携带数据的次序已经
正确),把数据放入插口的接收缓存,并更新 r c v _ n x t。当链表为空,或者链表下一报文段的
序号又出现差错,即当前处理报文段与下一报文段间存在缺失报文段时,循环结束。此时,
f l a g s变量(函数的返回值 )等于0或者为 T H _ F I N,取决于放入插口接收缓存的最后一个报文
段中是否带有FIN标志。
在所有mbuf都放入插口的接收缓存后, s o r w a k e u p唤醒所有在插口上等待接收数据的应
用进程。

27.10 tcp_trace函数

图26-32中,在向IP递交报文段之前, tcp_output调用了tcp_trace函数:
if (so->so_options & SO_DEBUG)
tcp_trace(TA_OUTPUT, tp->t_state, tp, ti, 0);

在内核的环形缓存中添加一条记录,这些记录可通过 trpt (8)程序读取。此外,如果内核


编译时定义了符号 TCPDEBUG,并且变量tcpconsdebug非零,则信息将输出到系统控制台。
任何进程都可以设定 T C P的插口选项 S O _ D E B U G,要求T C P把信息存储到内核的
环形缓存中。但只有特权进程或系统管理员才能运行 t r p t,因为它必须读取系统内
存才能获取这些信息。
尽管可以为任何类型的插口设定 S O _ D U B U G选项(如U D P或原始I P ),但只有 T C P
才会处理它。
这些信息被保存在 tcp_debug结构中,如图27-24所示。
35-43 t c p _ d e b u g很大 ( 1 9 6字节 ),因为它包含了其他两个结构:保存 I P和T C P首部的
t c p i p h d r和完整的 T C P控制块 t c p c b。由于保存了 T C P控制块,其中的任何变量都可通过
t r p t打印出来。也就是说,如果 t r p t标准输出中没有包含读者感兴趣的信息,可修改源代
码以打印控制块中任何想要的信息 ( N e t / 3版支持这种修改 )。图2 5 - 2 8中的RT T变量就是通过这
种方式得到的。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 733

图27-24 tcp_debug 结构

53-55 图2 7 - 2 4 还定义了数组 t c p _ d e b u g ,也就是前面提到的环形缓存。数组指针


(tcp_debx)初始化为零,该数组约占 20 000字节。
内核只调用了 t c p _ t r a c e 4次,每次调用都会在结构的 t d _ a c t变量中存入一个不同的
值,如图27-25所示。

td_act 描 述 参 考

TA_DROP 当输入报文段被丢弃时,被 t c p _ i n p u t调用 图29-27


TA_INPUT 输入处理完毕后,调用 t c p _ o u t p u t之前 图29-26
TA_OUTPUT 调用i p _ o u t p u t发送报文段之前 图26-32
TA_USER R P U _xxx请求处理完毕后,被 t c p _ u s r r e q调用 图30-1

图27-25 td_act 值及相应的 tcp_trace 调用

图2 7 - 2 6给出了 t c p _ t r a c e函数的主要部分,我们忽略了直接输出到控制台的那部分代
码。
48-133 在函数被调用时, ostate中保存了连接的前一个状态,与连接的当前状态 (保存在
控制块中 )相比较,可了解连接的状态变迁状况。图 2 7 - 2 5中,T A _ O U T P U T不改变连接状态,
但其他3个调用则会导致状态的转移。

图27-26 tcp_trace 函数:在内核的环形缓存中保存信息

Linux公社 www.linuxidc.com
bbs.theithome.com
734 计计TCP/IP详解 卷 2:实现

图27-26 (续)

输出举例

图2 7 - 2 7列出了t c p d u m p输出的前 4行,反映 2 5 . 1 2节例子中的三次握手过程和发送的第一


个数据报文段(卷1附录A提供了tcpdump输出格式的细节 )。

图27-27 反映图25-28实例的tcpdump 输出

图27-28列出了与之对应的 trpt的输出。
图2 7 - 2 8的输出与正常的 t r p t输出相比略有一些不同: 32 bit的数字序号显示为
无符号整数(t r p t将其差错地打印为有符号整数 );有些t r p t按1 6进制输出的值被改
为10进制;为了编制图 25-28,作者人为地把从 t _ r t t到t _ r x t c u r的值加入到 t r p t
中。

图27-28 反映图25-28实例的trpt 输出

Linux公社 www.linuxidc.com
bbs.theithome.com
第 27章 TCP的函数计计 735

图27-28 (续)

在时刻 953 738 ,发送 S Y N。注意,代码中的时间变量有 8位数字,以毫秒为单位,这里


只输出了低6位。输出的结束序号 (20 288 005)是差错的。 SYN中确实携带了 4字节的内容,但
并非数据,而是 M S S选项。重传定时器设定为 6秒(R E X M T),保活定时器为 7 5秒(K E E P),这些
定时器值均以500 ms滴答为单位。t_rtt等于1,意味对该报文段计时,测量 RTT样本值。
发送S Y N是为了响应应用进程的 c o n n e c t调用。一毫秒后,这次系统调用的信息被写入
内核的环形缓存。尽管是因为应用进程调用了 c o n n e c t,才导致发送 S Y N报文段,但 T C P在
处理完PRU_CONNECT请求后,才调用 tcp_trace,环形缓存中实际写入了两条记录。此外,
应用进程调用 c o n n e c t时,连接状态为 C L O S E D,发送完 S Y N后,状态变迁至 S Y N _ S E N T,
这也是两条记录仅有的不同之处。
第三条记录,时刻 954 103,与第一条记录相隔 365 ms (tcpdump显示时间差为362.7ms),
即为图2 5 - 2 8中“实际时间差 ( m s )”一栏的填充值。收到带有 S Y N和A C K的报文段后,连接状
态从SYN_SENT转移到ESTABLISHED。因为计时报文段已得到确认,更新 RTT估计器值。
第四条记录反映了三次握手过程中的第三个报文段:确认对端的 S Y N。因为是纯 A C K报
文段,不用对它计时 (rtt等于0),它在时刻 954 103被发送。connect系统调用返回,应用进
程接着调用 write发送数据,产生 TCP输出。
第五条记录反映了这个数据报文段,在时刻 954 153,三次握手结束后 50 ms ,被发送。它
携带50字节 的数据,起始序号为 20 288 002。重传定时器设为 3秒,需要计时。
应用进程继续调用 w r i t e发送数据。尽管不再显示更多记录,但很明显,接下来的 3条记

Linux公社 www.linuxidc.com
bbs.theithome.com
736 计计TCP/IP详解 卷 2:实现

录也都是在TCP处理完P R U _ S E N D请求后写入环形缓存的。第一次 P R U _ S E N D请求,生成我们


已看到的第一个 5 1 2字节的输出报文段,其他 3次请求不会引发 T C P输出报文段,此时连接正
处于慢起动状态。只生成 4条记录是因为,图 2 5 - 2 8的例子中的 T C P发送缓存大小只有 4 0 9 6,
mbuf簇大小为1024。一旦发送缓存被占满,应用进程就进入休眠状态。

27.11 小结

本章介绍了各种 TCP函数,为后续章节打下基础。
T C P连接正常关闭时,向对端发送 F I N,并等待 4次报文交换过程结束。它被丢弃时,只
需发送RST。
路由表中的每条记录都包含 8个变量,其中有 3个在连接关闭时更新,有 6个用于新连接的
建立,从而内核能够跟踪与同一目标之间建立的正常连接的某些特性,如 RT T估计器值和慢
起动门限。系统管理员可以设置或锁定部分变量,如 M T U、接收管道大小和发送管道大小,
这些特性会影响到达该目标的连接的性能。
T C P对收到的 I C M P差错有一定的容错性—不会导致 T C P终止已建立的连接。 N e t / 3处理
ICMP差错的方式与早期的 Berkeley版本不同。
T C P报文段可能乱序到达,并包含重复数据, T C P必须处理这些异常现象。 T C P为每条连
接维护一个重组队列,保存乱序报文段,处理之后再提交给应用进程。
最后介绍了选定插口选项 S O _ D E B U G时,内核中保存的信息。除某些程序如 t c p d u m p之
外,这些内容也是很有用的调试工具。

习题

27.1 为什么图27-1中最后一行的errno等于0?
27.2 rmx_rtt中存储的最大值是多少?
27.3 为了保存某个给定主机的路由信息 (图2 7 - 3 ),我们用手工在本地的路由表中添加一
条到达该主机的路由。之后,运行 F T P客户程序,向这台主机发送足够多的数据,
如图2 7 - 4所要求的。但终止 F T P客户程序后,检查路由表,到达该主机的所有变量
依旧为0。出了什么问题?

Linux公社 www.linuxidc.com
bbs.theithome.com
第28章 TCP的输入
28.1 引言

T C P输入处理是系统中最长的一部分代码,函数 t c p _ i n p u t约有11 0 0行代码。输入报文


段的处理并不复杂,但非常繁琐。许多实现,包括 N e t / 3,都完全遵循 RFC 793中定义的输入
事件处理步骤,它详细定义了如何根据连接的当前状态,处理不同的输入报文段。
当收到的数据报的协议字段指明这是一个 T C P报文段时, i p i n t r(通过协议转换表中的
pr_input函数)会调用tcp_input进行处理。 tcp_input在软件中断一级执行。
函数非常长,我们将分两章讨论。图 2 8 - 1列出了 t c p _ i n p u t中的处理框架。本章将结束
对RST报文段处理的讲解,从下一章开始介绍 ACK报文段的处理。
头几个步骤是非常典型的:对输入报文段做有效性验证 (检验和、长度等 ),以及寻找连接
的P C B。尽管后面还有大量代码,但通过“首部预测 (header prediction)”( 2 8 . 4节),算法却有
可能完全跳过后续的逻辑。首部预测算法是基于这样的假定:一般情况下,报文段既不会丢
失,次序也不会错误,因此,对于给定连接, T C P总能猜到下一个接收报文段的内容。如果
算法起作用,函数直接返回,这是 tcp_input中最快的一条执行路径。
如果算法不起作用,函数在“ dodata”处结束,测试几个标志,并且若需要对接收报文段
作出响应,则调用 tcp_output。

图28-1 TCP输入处理步骤小结

Linux公社 www.linuxidc.com
bbs.theithome.com
738 计计TCP/IP详解 卷 2:实现

图28-1 (续)

函数结尾处有 3个标注,处理出现差错时控制会跳转到这些地方: d r o p a f t e r a c k、
d r o p w i t h r e s e t和d r o p。标注中出现的“ d r o p”指丢弃当前处理的报文段,而非丢弃连
接。不过,当控制跳转到 dropwithreset时,将发送 RST,从而丢弃连接。
函数仅有的另一个分支是首部预测算法后的 s w i t c h语句,如果连接处于 L I S T E N或
S Y N _ S E N T状态时收到了一个有效的 S Y N报文段,它负责分别进行处理。 t r i m t h e n s t e p 6

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 739
处的代码结束后,跳转到 step 6,继续执行正常的流程。

28.2 预处理

图28-2的代码包含一些声明,并对收到的 TCP报文段进行预处理。
1. 从第一个mbuf中获取IP和TCP首部
170-204 参数i p h l e n等于I P首部长度,包括可能的 I P选项。如果长度大于 2 0字节,可知存
在I P选项,调用 i p _ s t r i p o p t i o n s丢弃这些选项。 T C P忽略除源选路之外的所有 I P选项,
源选路选项由 I P特别保存 ( 9 . 6节),T C P能够读取其内容 (图2 8 - 7 )。如果簇中第一个 m b u f的容
量小于 I P / T C P首部大小 ( 4 0字节 ),则调用 m _ p u l l u p,试着把最初的 4 0字节移入第一个
m b u f中。

图28-2 tcp_input 函数:变量声明及预处理

图28-3给出了函数下一部分的代码,验证 TCP检验和及偏移字段。
2. 验证TCP检验和
205-217 t l e n 指T C P报文段的长度,即 I P 首部后的字节数。前面介绍过, I P 已经从

Linux公社 www.linuxidc.com
bbs.theithome.com
740 计计TCP/IP详解 卷 2:实现

i p _ l e n中减去了 I P的首部长度,因此,变量 l e n就等于整个 I P数据报的长度,即包括伪首部


在内的需要计算检验和的数据长度。根据 T C P检验和计算的要求,填充伪首部中的各个字段,
如图23-19所示。

图28-3 tcp_input 函数:验证 TCP检验和及偏移字段

3. 验证TCP偏移字段
218-228 T C P的偏移字段, t i _ o f f,是以 32 bit 为单位的 T C P首部长度值,包括所有的
T C P选项。把它乘以 4 (得到T C P报文段中第一个数据字节所在位置的偏移量 ),并验证其有效
性。偏移量必须大于等于标准 TCP首部的大小 (20字节),并且小于等于 TCP报文段的长度。
从T C P长度变量 t l e n中减去首部长度,得到报文段中携带的数据字节数 (可能为 0 ),并把
这个值赋给 tlen,以及TCP首部的变量 ti_len。函数中会多次用到这个值。
图28-4给出了函数下一部分的代码:处理特定的 TCP选项。

图28-4 tcp_input 函数:处理特定的 TCP选项

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 741

图28-4 (续)

4. 把IP和TCP首部及选项放入第一个 mbuf
230-236 如果首部长度大于 2 0,说明存在 T C P选项。必要时调用 m _ p u l l u p,把标准 I P首
部、标准 T C P首部的所有T C P选项放入簇中的第一个 m b u f中。因为 3部分数据最大只能为 8 0字
节(20+20+40),因此,必定能够放入第一个存储数据分组首部的 mbuf中。
此处能够造成 m _ p u l l u p失败的惟一原因是 I P数据分组的字节数小于 2 0加上T C P
首部长度,而且已通过 TCP检验和的验证,我们认为 m _ p u l l u p不可能失败。但有一
点,图 2 8 - 2中调用的 m _ p u l l u p,将共享计数器 t c p s _ r c v s h o r t,因此,查看
t cps_rcvshort并不能说明哪一个调用失败。不管怎样,从图 24-5可知,即使收到
九百万个TCP报文段之后,这个计数器仍旧为 0。
5. 快速处理时间戳选项
237-255 o p t l e n等于首部中 T C P选项的长度, o p t p是指向第一个选项字节的指针。如果
下列3个条件均为真,说明只存在时间戳选项,而且格式正确:
1) TCP 选项长度等于 1 2 ( T C P O L E N _ T S T A M P _ A P P A );或 T C P选项长度大于 1 2 ,但
optp[12]等于选项结束字节。
2) 选项的头4个字节等于 0x0101080a(TCPOPT_TSTAMP_HDR,在26.6节曾讨论过 )。
3) SYN标志未置位(说明连接已建立,如果报文段中出现时间戳选项,意味着连接双方都
同意使用这一选项 )。
如果上述条件全部满足,则 t s _ p r e s e n t置为1;从接收报文段首部获取两个时间戳值,
分别赋给 t s _ v a l和t s _ e c r;o p t p置为空,因为所有选项已处理完毕。这种辨认时间戳的
方法可以避免调用通用选项处理函数 t c p _ d o o p t i o n s,从而使后者能够专门处理只出现在
SYN报文段中的各种选项 (MSS和窗口大小选项 )。如果连接双方同意使用时间戳,那么在建立
的连接上交换的几乎所有报文段中都可能带有时间戳选项,因此,必须加快其处理速度。
图28-5给出了函数下一部分的代码,寻找报文段的 Internet PCB。
6. 保存输入标志,把字段转换为主机字节序
257-264 接收报文段中的标志 (SYN、FIN等)被保存在本地变量 t i f l a g s中,因为函数在
处理过程中会多次引用这些标志。 TCP首部的两个 16 bit字段和两个 32 bit序号被转换回主机字

Linux公社 www.linuxidc.com
bbs.theithome.com
742 计计TCP/IP详解 卷 2:实现

节序,而两个 16 bit端口号则不做处理,依旧为网络字节序,因为 Internet PCB中的端口号是


依照网络字节序存储的。

图28-5 tcp_input 函数:寻找报文段的 Internet PCB

7. 寻找Internet PCB
265-279 TCP的缓存(t c p _ l a s t _ i n p c b)中保存了收到的最后一个报文段的 PCB地址,采
用的技术与 U D P相同。T C P使用一对插口来识别连接,寻找 P C B时插口对中 4个元素的比较次
序与 u d p _ i n p u t相同。如果与 T C P缓存中的记录不匹配,则调用 i n _ p c b l o o k u p,把新的
PCB放入缓存。
T C P中不会出现我们在 U D P 中曾遇到过的问题:由于高速缓存中存在通配项 ( w i l d c a r d
e n t r y ),导致匹配成功率很低。因为只有处于监听状态的服务器,才可能在其插口中保存通配
项。连接一旦建立,插口对的 4个元素将全部填入确定值。从图 2 4 - 5可知,高速缓存命中率能
够达到80%。
图28-6给出了函数下一部分的代码。

图28-6 tcp_input 函数:判断是否应丢弃报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计743

图28-6 (续)

8. 丢弃报文段并生成 RST
280-287 如果没有找到PCB,则丢弃输入报文段,并发送 RST作为响应。例如, TCP收到了
一个S Y N,但报文段指定的服务器并不存在,则直接向对端发送 R S T。回想一下,出现这种
情况时UDP的处理方式,它将发送一个 ICMP端口不可达差错。
288-290 如果P C B存在,但对应的 T C P控制块不存在,可能插口已关闭 (t c p _ c l o s e释放
TCP之后,才释放PCB),则丢弃输入报文段,并发送 RST作为响应。
9. 丢弃报文段且不发送响应
291-292 如果T C P控制块存在,但连接状态为 C L O S E D,说明插口已创建,且得到了本地
地址和本地端口号,但还未调用 c o n n e c t或者l i s t e n。报文段被丢弃,且不发送任何响应。
举例来说,如果客户向服务器发送的连接请求报文段到达时,服务器已调用了 b i n d,但还未
调用listen。这种情况下,客户连接请求将超时,导致重传 SYN。
10. 不改变通告窗口大小
293-297 如果需要支持窗口大小选项,连接双方都必须在连接建立时通过窗口大小选项规
定窗口缩放因子。如果报文段中包含 S Y N,说明此时窗口缩放因子还未定义,因此,直接把
T C P首部的窗口字段值复制给 t i w i n;否则,首部中的 16 bit 数值应根据窗口缩放因子左移,
得到32 bit的数值。
图28-7给出了函数的下一部分代码,如果选取了插口的 S O _ D E B U G选项,或者插口正处于
监听状态,则完成一些相应的预处理工作。

图28-7 tcp_input 函数:处理调试选项和监听状态的插口

Linux公社 www.linuxidc.com
bbs.theithome.com
744 计计TCP/IP详解 卷 2:实现

图28-7 (续)

11. 如果选定了插口调试选项,则保存连接状态及 IP和TCP首部


300-303 如果 S O _ D E B U G 选项置位,则保存当前连接状态 (o s t a t e )及I P和T C P首部
(tcp_saveti)。函数结束时,这些信息将作为参数传给 tcp_trace(图29-26)。
12. 如果监听插口收到了报文段,则创建新的插口
304-319 如果有报文段到达处于监听状态的插口 (listen置位SO_ACCEPTCONN),则调用
s o n e w c o n n创建新的插口。发出 P R U _ A T T A C H协议请求 (图30-2),分配Internet PCB和TCP控
制块。在 T C P最终接受连接请求之前,还需做更多的处理 (如一个最基本的问题,报文段中是
否包含 S Y N ) ,如果发现差错,置位 d r o p s o c k e t 标志,控制跳转至标注“ d r o p ”和
“dropwithreset”,丢弃新的插口。
320-326 i n p和t p将指向新建的插口。本地地址和本地端口号直接从接收报文段 T C P首部
的目的地址和目的端口号字段中复制。如果输入数据报中有源选路的路由, T C P 调用
i p _ s r c r o u t e,得到指向保存数据报源选路选项的 m b u f的指针,并赋给 i n p _ o p t i o n s。
TCP向连接发送数据时, t c p _ o u t p u t会把源选路选项传给 i p _ o u t p u t,使用与之相同的逆
向路由。
327 新插口的状态设为 LISTEN。如果接收报文段中包含SYN,控制将转到图28-16中的代码,
完成连接建立请求的处理。
13. 计算窗口缩放因子
328-331 窗口缩放因子取决于接收缓存的大小。如果接收缓存大于通告窗口的最大值
( 6 5 5 3 5,T C P _ M A X W I N),则左移 6 5 5 3 5,直到结果大于接收缓存大小,或者窗口缩放因子已
等于最大值(14,T C P _ M A X _ W I N S H I F T)。注意,窗口缩放因子的选取基于监听插口的接收缓
存,也就是说,应用进程调用 l i s t e n进入监听状态之前,应首先设定 S O _ R C V B U F插口选项,
或者继承tcp_recvspace中的默认值。
窗口缩放因子最大值等于 14,而65535×214等于1 073 725 440,已远远大于接收

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 745
缓存的最大值(Net/3中为2626 144),因此,在窗口缩放因子远小于14时,循环即终止。
参见习题28.1和28.2。
图28-8给出了TCP输入处理下一部分的代码。

图28-8 tcp_inpu t函数:复位空闲时间和保活定时器,处理应用进程选项

14. 复位空闲时间和保活定时器
334-339 由于连接上收到了报文段, t_idle重设为0。保活定时器复位为 2小时。
15. 如果不处于监听状态,处理 TCP选项
340-346 如果TCP首部中有选项,并且连接状态不等于 LISTEN,调用tcp_dooptions进
行处理。前面介绍过,如果连接已建立,接收报文段中只存在时间戳选项,并且时间戳选项
格式符合 RFC 1323附录A的建议,这种情况在图 2 8 - 4中已处理过,而且 o p t p被置为空。如果
插口处于监听状态, T C P把对端地址保存在 P C B中之后,才会调用 t c p _ d o o p t i o n s,这是
因为处理MSS选项时需要了解到达对端的路由,具体代码如图 28-17所示。

28.3 tcp_dooptions函数

函数处理 N e t / 3支持的5个T C P选项(图2 6 - 4 ):E O L、N O P、M S S、窗口大小和时间戳。图


28-9给出了函数的第一部分。

图28-9 tcp_dooptions 函数:处理 EOL和NOP选项

Linux公社 www.linuxidc.com
bbs.theithome.com
746 计计TCP/IP详解 卷 2:实现

图28-9 (续)

1. 获取选项类型的长度
1213-1229 代码遍历 T C P首部选项,遇到 E O L (选项终止 )时终止循环,函数返回;遇到
N O P 时,将其长度置为 1,因为它后面不带长度字段 (图2 6 - 1 6 ),控制转到 s w i t c h语句的
default子句,对其不做处理。
1230-1234 所有其他选项的长度保存在 optlen中。
所有新增的 Net/3不支持的TCP选项都被忽略。这是因为:
1) 将来定义的所有新选项都将带有长度字段 ( N O P和E O L是仅有的两个不带长度字段的选
项),而for语句的每次循环都跳过 optlen字节。
2) switch语句的default子句忽略所有未知选项。
图2 8 - 1 0给出了 t c p _ d o o p t i o n s最后一部分的代码,处理 M S S、窗口大小和时间戳选
项。
2. MSS选项
1238-1246 如果长度不等于 4(T C P O L E N _ M A X S E G),或者报文段不带 SYN标志,则忽略该
选项。否则,复制两个 M S S字节到本地变量,转换为主机字节序,调用 t c p _ m s s完成处理。
t c p _ m s s负责更新 T C P控制块中的变量 t _ m a x s e g,即发向对端的报文段中允许携带的最大
字节数。
3. 窗口大小选项
1247-1254 如果长度不等于 4(TCPOLEN_WINDOW),或者报文段不带 SYN标志,则忽略该
选项。 N e t / 3 置位 T F _ R C V D _ S C A L E ,说明收到了一个窗口大小选项请求,并在
r e q u e s t e d _ s _ s c a l e中保存缩放因子。由于 c p[ 2 ]只有一个字节,因此,不存在边界问题。
当连接转移到ESTABLISHED状态时,如果连接双方都同意支持窗口大小选项,则使用这一功
能。
4. 时间戳选项
1255-1273 如果长度不等于 1 0 ( T C P O L E N _ T I M E S T A M P ),则忽略该选项。否则,
t s _ p r e s e n t指向的标志被置位 1,两个时间戳值分别保存在 t s _ v a l和t s _ e c r所指向的变
量中。如果收到的报文段带有 S Y N标志,N e t / 3置位T F _ R C V D _ T S T M P,说明收到了一个时间
戳请求。 t s _ r e c e n t等于收到的时间戳值, t s _ r e c e n t _ a g e等于t c p _ n o w,从系统初启
到目前的时间,以 500ms滴答为单位。

Linux公社 www.linuxidc.com
bbs.theithome.com
748 计计TCP/IP详解 卷 2:实现

[Partridge 1993]介绍了一种基于 Van Jacobson的研究成果,速度更快的 T C P首部


预测算法实现。
图28-11给出了首部预测的第一部分代码。

图28-11 tcp_input 函数:首部预测,第一部分

1. 判定收到的报文段是否是等待接收的报文段
347-366 下列6个条件必须全真,才能说明收到的报文段是连接正等待接收的数据报文段或
ACK报文段:
1) 连接状态等于ESTABLISHED。
2) 下列4个控制标志必须不设定: S Y N、F I N、R S T、或U R G。但A C K标志必须置位。换
言之, T C P的6个控制标志中, A C K标志必须置位,前面列出的 4个标志必须清除, P S H标志
置位与否无关紧要 (连接处于 E S TA B L I S H E D状态时,除非 R S T标志置位,一般情况下, A C K
都会置位)。
3) 如果报文段带有时间戳选项,则最新时间戳值 (t s _ v a l)必须大于或等于连接上以前收
到的时间戳值 (t s _ r e c e n t)。本质上说,这就是 PAW S测试, 2 8 . 7节将详细介绍 PAW S。如果
t s _ v a l小于t s _ r e c e n t,则新报文段是乱序报文段,因为它的发送时间早于连接上收到的
上一个报文段。由于对端通常把时钟值填充到时间戳字段 ( N e t / 3的全局变量 t c p _ n o w),收到
的时间戳正常情况下应该是一个单调递增的序列。
并非每个顺序到达报文段中的时间戳都会增加。事实上, N e t / 3系统每500 ms增加一次时

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 749
钟值(t c p _ n o w),在这段时间间隔中,完全可能发送多个报文段。假定利用时间戳和序号构
成一个64 bit的数值,序号放在低 32 bit,时间戳放在高 32 bit,对于每个顺序报文段,这个 64
bit的值都至少会增加 1(应考虑取模算法 )。
4) 报文段的起始序号 (t i _ s e q)必须等于连接上等待接收的下一个序号 (r c v _ n x t)。如果
这个条件为假,那么收到的报文段是重传报文段或是乱序报文段。
5) 报文段通告的窗口大小必须非零 (t i w i n),必须等于当前发送窗口 (s n d _ w n d)。也就
是说,无需更新当前发送窗口。
6) 下一个发送序号 (s n d _ n x t)必须等于已发送的最大序号 (s n d _ m a x),也就是说,上一
个发送报文段不是重传报文段。
2. 根据接收的时间戳更新 ts_recent
367-375 如果存在时间戳选项,并且时间戳值满足图 26-18中的测试条件,则把收到的时间
戳值(ts_val)赋给ts_recent,并在ts_recent_age中保存当前时钟 (tcp_now)。
前面讨论过图 26-18中的时间戳有效性测试条件所存在的问题,并在图 26-20中给
出了正确的测试条件。但在首部预测算法的实现中,图 2 6 - 2 0中的T S T M P _ G E Q测试
是多余的,因为图 28-11起始处的 if语句己完成了这一测试
图2 8 - 1 2 给出了首部预测的下一部分代码,用于单向数据的发送方:处理输出数据的
ACK。

图28-12 tcp_input 函数:首部预测,发送方处理

Linux公社 www.linuxidc.com
bbs.theithome.com
750 计计TCP/IP详解 卷 2:实现

图28-12 (续)

3. 纯ACK测试
376-379 如果下列4个条件全真,则收到的是一个纯 ACK报文段。
1) 报文段不携带数据 (ti_len等于0)。
2) 报文段的确认字段 (t i _ a c k)大于最大的未确认序号 (s n d _ u n a)。由于测试条件是“大
于”,而非“大于等于”,也就是要求收到的 ACK必须确认未曾确认过的数据。
3) 报文段的确认字段 (ti_ack)小于等于已发送的最大序号 (snd_max)。
4) 拥塞窗口大于等于当前发送窗口 (s n d _ w n d),要求窗口完全打开,连接不处于慢起动
或拥塞避免状态。
4. 更新RTT值
384-388 如果报文段携带时间戳选项,或者报文段中的确认序号大于某个计时报文段的起
始序号,则调用 tcp_xmit_timer更新RTT值。
5. 从发送缓存中删除被确认的字节
389-394 a c k e d等于接收报文段确认字段所确认的字节数,调用 s b d r o p从发送缓存中删
除这些字节。更新最大的未确认过的序号 (s n d _ u n a)为报文段的确认字段值,释放保存接收
报文段的mbuf链表(由于数据长度等于 0,实际只有一个保存首部的 mbuf)。
6. 终止重传定时器
395-407 如果接收报文段确认了所有已发送数据 (snd_una等于snd_max),则关闭重传定
时器。若条件不满足,且持续定时器未设定,则重启重传定时器,时限设为 t_rxtcur。
前面介绍过, t c p _ o u t p u t发送报文段时,只有重传定时器未启动,才会设定它。如果
连续发送两个报文段,发送第一个报文段时定时器启动,发送第二个报文段时定时器不变。
但如果只收到第一个报文段的确认,则重传定时器必须重启,防止第二个报文段丢失。
7. 唤醒等待进程
408-409 如果发送缓存修改后,有必要唤醒等待的应用进程,则调用 s o w w a k e u p。从图
16-5可知,如果有应用进程正在等待缓存空间,或者设定了与缓存有关的 s e l e c t选项,或者
正等待插口上的 SIGIO,则SB_NOTIFY为真。
8. 生成更多的输出
410-411 如果发送缓存中有数据,则调用 t c p _ o u t p u t,因为发送窗口已经向右移动。
snd_una已增加,但 snd_wnd未变化,因此,图 24-17中的整个窗口将向右移动。
图2 8 - 1 3给出了首部预测的下一部分代码,接收方收到顺序到达的数据时进行的各种处
理。
9. 测试收到报文段是否是连接等待接收的下一个报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 751
414-416 如果下列4个条件均为真,则收到的报文段是连接上等待接收的下一报文段,并且
插口缓存中的剩余空间能够容纳到达的数据。
1) 报文段的数据量 (ti_len)大于0,即图28-12起始处if语句的else子句。
2) 确认字段(ti_ack)等于最大的未确认序号,即报文段未确认任何数据。
3) 连接乱序报文段的重组队列为空 (seq_next等于tp)。
4) 接收缓存能够容纳报文段数据。
10. 完成接收数据的处理
423-435 等待接收序号 (r c v _ n x t)递增收到的数据字节数。从 m b u f链中丢弃 I P首部、T C P
首部和所有TCP选项,将剩余的 mbuf链附加到插口的接收缓存,调用 s o r w a k e u p唤醒接收进
程。注意,代码没有调用 T C P _ R E A S S宏,因为宏代码中的条件判定已经包含在首部预测的测
试条件中。设定延迟 ACK标志,输入处理结束。

图28-13 tcp_input 函数:首部预测的接收方处理

统计量

首部预测能在多大程度上改善系统性能?让我们做个简单的实验,跨越 L A N (b d s i和
s v r 4 间的双向通信 ) 的数据传输,和跨越 WA N ( v a n g o g h . c s . b e r k e l e y . e d u 和
f t p . u u . n e t之间的双向通信 )的数据传输。运行 n e t s t a t,得到类似于图 24-5的输出,列出
了两种情况下的首部预测寄存器的值。
跨越L A N传输时,无数据分组丢失,只有一些重复的 A C K。利用首部预测处理的报文段
可占到97%~100%。跨越WAN时,比例有所降低,约为 83%~99%之间。
请注意,首部预测的应用限定于单独的连接,无论主机是否收到了额外的 TCP流量,PCB

Linux公社 www.linuxidc.com
bbs.theithome.com
752 计计TCP/IP详解 卷 2:实现

缓存必须在主机范围内共享。即使 T C P流量的丢失造成了 P C B缓存缺失,但如果给定连接上


的数据分组未丢失,这条连接上的首部预测仍能工作。

28.5 TCP输入:缓慢的执行路径

下面讨论首部预测失败时的处理代码, t c p _ i n p u t中较慢的一条执行路径。图 2 8 - 1 4给
出了下一部分代码,为输入报文段的处理完成一些准备工作。

图28-14 tcp_input 函数:丢弃 IP和TCP首部

1. 丢弃IP和TCP首部,包括 TCP选项
438-442 更新数据指针和 mbuf链表中的第一个 mbuf的长度,以跳过 IP首部、TCP首部和所
有T C P选项。因为 o f f等于T C P首部长度,包括 T C P选项,因此,表达式中减去了标准 T C P首
部的大小(20字节)。
2. 计算接收窗口
443-455 win等于插口接收缓存中可用的字节数, rcv_adv减去rcv_nxt等于当前通告的
窗口大小,接收窗口等于上述两个值中较大的一个,这是为了保证接收窗口不小于当前通告
窗口的大小。此外,如果最后一次窗口更新后,应用进程从插口接收缓存中取走了数据,
w i n可能大于通告窗口,因此, T C P最多能够接收 w i n字节的数据 (即使对端不会发送超过通
告窗口大小的数据 )。
因为函数后面的代码必须确定通告窗口中能放入多少数据 (如果有),所以现在必须计算通
告窗口的大小。落在通告窗口之外的接收数据被丢弃:落在窗口左侧的数据是已接收并确认
过的数据,落在窗口右侧的数据是暂不允许对端发送的数据。

28.6 完成被动打开或主动打开

如果连接状态等于 L I S T E N或者S Y N _ S E N T,则执行本节给出的代码。连接处于这两个状


态时,等待接收的报文段为 SYN,任何其他报文段将被丢弃。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 753
28.6.1 完成被动打开

连接状态等于 L I S T E N时,执行图 2 8 - 1 5中的代码,其中变量 t p和i n p指向图 2 8 - 7所创建


的新的插口,而非服务器的监听插口。

图28-15 tcp_input 函数:检测监听插口上是否收到了 SYN

1. 丢弃RST、ACK或非SYN
473-478 如果接收报文段中带有 R S T标志,则丢弃它。如果带有 A C K,则丢弃它并发送
RST作为响应(建立连接的最初的 SYN报文段是少数几个不允许携带 ACK的报文段之一)。如果
未带有 S Y N,则丢弃它。 c a s e子句的后续代码处理连接处于 L I S T E N状态时收到了 S Y N的状
况。新的连接状态等于 SYN_RCVD。
图28-16给出了case语句接下来的代码。

图28-16 tcp_input 函数:处理监听插口上收到的 SYN报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
754 计计TCP/IP详解 卷 2:实现

图28-16 (续)

2. 如果是广播报文段或多播报文段,则丢弃它
479-486 如果数据报被发送到广播地址或多播地址,则丢弃它, TCP只支持点到点的应用。
前面介绍过,根据数据帧携带的目的硬件地址, e t h e r _ i n p u t置位M _ B C A S T和M _ M C A S T标
志,IN_MULTICAST宏可判定IP地址是否为 D类地址。
注释引用了 i n _ b r o a d c a s t,因为 N e t / 1代码(它不支持多播 )在此处调用了这个
函数,以检测目的 I P地址是否为广播地址。 N e t / 2中改为根据目的硬件地址,通过
ether_input设定M_BCAST和M_MCAST标志。
N e t / 3只测试目的硬件地址是否为广播地址,而且不调用 i n _ b r o a d c a s t测试目
的IP地址是否为广播地址。它假定除非目的硬件地址是广播地址,否则,目的 IP地址
绝不可能是广播地址,从而避免调用 i n _ b r o a d c a s t。另外,如果 N e t / 3真的收到了
一个数据帧,其目的硬件地址为单播地址,而目的 I P地址为广播地址,将执行图 2 8 -
16中的代码处理此种报文段。
目的地址参数IN_MULTICAST需要被转换为主机字节序。
3. 为客户端的 IP地址和端口号分配 mbuf
487-496 分配一个 m b u f,保存 s o c k a d d r _ i n结构,其中带有客户端的 I P地址和端口号。
IP地址从IP首部的源地址字段中复制,端口号从 TCP首部的源端口号字段中复制。这个结构用
于把服务器的PCB连到客户,之后 mbuf被释放。
注释中的“XXX”,是因为获取mbuf的消耗等同于之后调用 i n _ p c b c o n n e c t的
消耗。不过此处的代码位于 t c p _ i n p u t中较慢的一条执行路径。从图 2 4 - 5可知,不
足%2的接收报文段的处理中会用到这段处理代码。
4. 设定PCB中的本地地址
497-499 laddr是绑定在插口上的本地地址。如果服务器没有为插口绑定一个确定地址 (正
常情况下 ),I P首部的目的地址将成为 P C B中的本地地址。注意,不管数据报是在哪个端口收
到的,都将保存 IP首部中的目的地址。
注意,l a d d r不会是通配地址,因为图 28-7中的代码已将收到报文段中的目的 IP

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 755
地址赋给了它。
5. 填充PCB中的对端地址
500-505 调用i n _ p c b c o n n e c t,把服务器的 P C B与客户相连,填充 P C B中的对端地址和
对端端口号。之后,释放 mbuf。
图28-17给出了函数下一部分的代码,结束 case语句的处理。

图28-17 tcp_input 函数:完成 LISTEN状态下收到 SYN报文段的处理

6. 分配并初始化IP和TCP首部模板
506-511 调用tcp_template创建IP和TCP首部的模板。图 28-7中调用sonewconn时,为
新连接分配了PCB和TCP控制块,但未分配首部模板。
7. 处理所有的 TCP选项
512-514 如果存在 T C P选项,则调用 t c p _ d o o p t i o n s进行处理。图 2 8 - 8中曾调用过一次
t c p _ d o o p t i o n s,但只处理非 L I S T E N状态时的 T C P选项。现在,插口处于监听状态, P C B
中的对端地址已填入 (t c p _ m s s函数中会用到对端地址 ),调用 t c p _ d o o p t i o n s:获取到达
对端的路由;查看对端主机是本地结点还是远端结点 (选择M S S时,需考虑到对端的网络 I D和
子网ID)。
8. 初始化ISS
515-519 通常情况下,初始发送序号复制自全局变量 t c p _ i s s ,之后增加 64 000
(T C P _ I S S I N C R除以2 )。如果局部变量 i s s非零,则使用 i s s取代t c p _ i s s,初始化连接的
发送序号。
出现以下事件序列时,会用到 iss:
• 服务器的IP地址为128.1.2.3,端口号为 27。

Linux公社 www.linuxidc.com
bbs.theithome.com
756 计计TCP/IP详解 卷 2:实现

• I P地址等于 1 9 2 . 3 . 4 . 5的客户与前述服务器建立了连接,客户端口号等于 3 0 0 0。服务器的


插口对为{128.1.2.3, 27, 192.3.4.5, 3000}。
• 服务器主动关闭了连接,上述插口对的状态转移到 TIME_WAIT。连接处于这种状态时,
最后收到的序号保存在 TCP控制块中。假设序号等于 100 000。
• 连接离开 T I M E _ WA I T状态之前,收到来自于同一客户主机、同一端口号 ( 1 9 2 . 3 . 4 . 5 ,
3 0 0 0 )的新的 S Y N,T C P寻找处于 TIME_WAIT状态的连接所对应的 P C B,而不是监听服
务器的PCB。假定新SYN报文段的序号等于 200 000。
• 因为连接状态不等于 L I S T E N,所以将不执行刚讨论过的图 2 8 - 1 7中的代码,而是执行图
2 8 - 2 8中的代码。我们将看到,其中包含了下列处理逻辑:如果新 S Y N报文段的序号
(200 000)大于客户最后发来的序号 (100 000),那么: ( 1 )局部变量 i s s等于100 000加上
128 000;( 2 )处于TIME_WAIT状态的连接被完全关闭 ( P C B和T C P控制块被删除 );( 3 )控
制跳转到 findpcb(图28-5)。
• 寻找服务器监听插口的 P C B (假定监听服务器还在运行 ),执行本节中介绍的代码。图 2 8 -
17中的代码将使用局部变量 iss(现在等于228 000)初始化新连接的 tcp_iss。
RFC 11 2 2中定义的这种处理逻辑,允许同一个客户和服务器重用同样的插口连接对,只
要服务器主动关闭原有连接。它也解释了为什么只要有进程调用 c o n n e c t ,全局变量
tcp_iss就递增64 000(图30-4):为了确保在某个客户不断地重建与同一个服务器的连接的情
况下,即使前一次连接上没有传输数据,甚至 500 ms定时器都未超时 (定时器超时处理代码会
增加tcp_iss),新建连接仍可以使用较大的 ISS。
9. 初始化控制块中的序号变量
520-522 图2 8 - 1 7中,初始接收序号复制自 S Y N报文段中的序号字段 (i r s)。下面两个宏初
始化了TCP控制块中的相关变量。
#define tcp_rcvseqinit(tp) \
(tp)->rcv_adv = (tp)->rcv_nxt = (tp)->irs + 1

#define tcp_sendseqinit(tp) \
(tp)->snd_una = (tp)->snd_nxt = (tp)->snd_max = (tp)->snd_up = \
(tp)->iss

因为SYN占据一个序号,所以第一个宏表达式需加 1。
10. 确认SYN并更新状态
523-525 因为对于SYN的确认必须立即发送,所以置位 TF_ACKNOW标志。连接状态转移到
S Y N _ R C V D,连接建立定时器设为 7 5秒(T C P T V _ K E E P _ I N I T)。因为 T F _ A C K N O W置位,函
数结束时将调用 t c p _ o u t p u t。从图 2 4 - 1 6可知,此种 t c p _ o u t f l a g s会导致发送携带 S Y N
和ACK的报文段。
526-528 现在,TCP结束了从图 28-7开始的新插口的创建, d r o p插口标志被清除。控制跳
转到t r i m t h e n s t e p 6处,完成 SYN报文段的处理。前面介绍过, SYN报文段能够携带数据,
尽管只有等连接进入 ESTABLISHED状态后,数据才会被提交给应用程序。

28.6.2 完成主动打开

图28-18给出了连接进入 SYN_SENT状态后,处理代码的第一部分。 TCP等待接收SYN。


Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 757

图28-18 tcp_input 函数:判定收到的 SYN是否是所需的响应

1. 验证收到的 ACK
530-546 当应用进程主动打开, T C P发送S Y N时,从
图3 0 - 4可知,连接的 i s s将等于全局变量 t c p _ i s s,宏
t c p _ s e n d s e q i n i t(前一节结尾给出了定义 )被执行。
假设ISS等于365,图28-19给出了t c p _ o u t p u t发送SYN
后的发送序号变量。
t c p _ s e n d s e q i n i t初始化图 2 8 - 1 9中的4个变量为
图28-19 ISS等于365的SYN发送后
3 6 5,接着图 2 6 - 3 1中的代码在发送 S Y N之后,把其中两 的发送序号变量
个增至 3 6 6。因此,如果图 2 8 - 1 8 中的接收报文段包含
A C K,并且确认字段小于等于 i s s( 3 6 5 ),或者大于 s n d _ m a x( 3 6 6 ),A C K无效,丢弃报文段
并发送 R S T作为响应。注意,连接处于 S Y N _ S E N T状态时,收到的报文段中无需携带 A C K。
它可以只包括SYN,这种情况称为同时打开 (simultaneous open)(图24-15)。
2. 处理并丢弃 RST报文段
547-551 如果接收报文段中带有 R S T,则丢弃它。但首先应查看 A C K标志,因为如果报文
段同时携带了有效的 ACK(已验证过)和RST,则说明对端拒绝本次连接请求,通常是因为服务
器进程未运行。这种情况下, t c p _ d r o p设定插口的 s o _ e r r o r变量,并向调用 c o n n e c t的
应用进程返回差错。
3. 判定SYN标志是否置位
552-553 如果收到报文段中的 SYN标志未置位,则丢弃它。
这个c a s e语句的其余代码用于处理本地发送连接请求后,收到对端响应的 S Y N 报文段
(及可选的ACK)的情况。图 28-20给出了tcp_input下一部分的代码,继续处理 SYN。

Linux公社 www.linuxidc.com
bbs.theithome.com
758 计计TCP/IP详解 卷 2:实现

图28-20 tcp_input 函数:发送连接请求后,收到对端响应的 SYN

4. 处理ACK
554-558 如果报文段中有 A C K,令 s n d _ u n a 等于报文段的确认字段。以图 2 8 - 1 9为例,
s n d _ u n a 应更新为 3 6 6 ,因为确认字段惟一有效的值就是 3 6 6 。如果 s n d _ n x t 小于
snd_una(在图28-19的例子中不可能发生 ),令snd_nxt等于snd_una。
5. 关闭连接建立定时器
559 连接建立定时器被关闭。
此处代码有错误。连接建立定时器只有在ACK标志置位时才能被关闭,因为收到一
个不带ACK的SYN报文段,只是说明连接双方同时打开,而不意味着对端已收到了SYN。
6. 初始化接收序号
560-562 初始接收序号从接收报文段的序号字段中复制。 t c p _ r c v s e q i n i t宏(上一节结
束时给出了定义 )初始化 r c v _ a d v和r c v _ n x t为接收序号加 1。置位 T F _ A C K N O W标志,从而
在函数结尾处调用 t c p _ o u t p u t,发送报文段携带的确认字段应等于 r c v _ n x t(图26-27),确
认刚收到的 SYN。
563-564 如果接收报文段带有 A C K,并且 s n d _ u n a大于连接的 I S S,主动打开处理完毕,
连接进入ESTABLISHED状态。
第二个测试条件其实是多余的。图 28-20起始处,如果 ACK标志置位, s n d _ u n a
将等于接收报文段的确认字段值。另外,图 28-18中紧跟着 case语句的if语句验证了
收到的确认字段大于 ISS。所以,此处只要 ACK置位,就可以确保 snd_una大于ISS。
Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 759
7. 连接建立
565-566 s o i s c o n n e c t e d 设 定 插 口 进 入 连 接 状 态 , T C P连 接 的 状 态 转 移 到
ESTABLISHED。
8. 查看窗口大小选项
567-572 如果TCP在本地SYN中加入窗口大小选项,并且收到的 SYN中也包含了这一选项,
使用窗口缩放功能,设定 s n d _ s c a l e和r c v _ s c a l e。因为t c p _ n e w t c p c b初始化T C P控制
块为0,所以,如果不使用窗口大小选项,这两个变量的默认值为 0。
9. 向应用进程提交队列中的数据
573-574 由于数据可能在连接未建立之前到达,调用 tcp_reass把数据放入接收缓存,第
二个参数为空。
测试条件其实不必要的。因为 T C P 刚收到带有 A C K 的S Y N 报文段,状态从
SYN_SENT转移到ESTABLISHED。即使有数据出现在 SYN中,也会被暂时搁置,直
到函数快结束,控制转到 d o d a t a标注时才会被处理。如果 T C P 收到不带 A C K 的
SYN(同时打开),即使报文段携带数据,也会被暂时搁置,等到收到了 ACK,连接从
SYN_RCVD转移到ESTABLISHED之后,才会被处理。
尽管S Y N中可以携带数据,并且 N e t / 3能够正确处理这样的报文段,但 N e t / 3自己
不会产生这样的报文段。
10. 更新RTT估计器值
575-580 如果确认的 S Y N正被计时, t c p _ x m i t _ t i m e r将根据得到的对 S Y N报文段的测
量值初始化 RTT估计器值。
TCP在此处忽略收到的时间戳选项,只查看 t _ r t t计数器。TCP主动打开时,在
第一个 S Y N中加入时间戳选项 (图2 6 - 2 4 ),如果对端也同意采用时间戳,就会在它响
应的S Y N中回应收到的时间戳 (参见图 2 8 - 1 0,N e t / 3在S Y N中回应收到的时间戳 )。因
此, T C P 在此处可以使用收到的时间戳,而不用 t _ r t t,但因为两者的精度相同
( 5 0 0 m s ),在这一点上时间戳并无优势可言。使用时间戳,而非 t _ r t t计数器的真正
好处在于高速网络中同时发送大量数据时,能提供更多的 RTT测量值和更好的估计器
值(希望如此)。
11. 同时打开
581-582 如果TCP在SYN_SENT状态收到不带 ACK的SYN,则称为同时打开,连接转移到
SYN_RCVD状态。
图28-21给出了函数的下一部分代码,处理 SYN中可能携带的数据。图 28-17结尾处,代码
跳转至trimthenstep6标注处,这里也有类似的情况。

图28-21 tcp_input 函数:接收 SYN的通用处理

Linux公社 www.linuxidc.com
bbs.theithome.com
760 计计TCP/IP详解 卷 2:实现

图28-21 (续)

584-589 报文段序号加1,以计入SYN。如果SYN带有数据, t i _ s e q现在应等于数据第一


个字节的序号。
12. 丢弃落在接收窗口外的数据
590-597 t i _ l e n等于报文段中的数据字节数。如果它大于接收窗口,超出部分的数据
(t i _ l e n减去r c v _ w n d)将被 m _ a d j丢弃。函数参数为负值,所以,将从 m b u f链尾部起逆向
删除数据 (图2 - 2 0 )。更新t i _ l e n,等于数据删除后 m b u f中剩余的数据量。清除 F I N标志,这
是因为FIN可能跟在最后一个数据字节之后,落在接收窗口外而被丢弃。
如果 S Y N是对本地连接请求的响应,且携带的数据过多,则说明对端收到的
S Y N报文段中带有窗口通告,但对端忽略了通告的窗口大小,并禁止不规范的行为。
但如果主动打开的 S Y N报文段中带有大量数据,则说明对端还未收到窗口通告,所
以不得不猜测SYN中能够携带多少数据。
13. 强制更新窗口变量
598-599 s n d _ w l 1等于接收序号减 1。从图 2 9 - 1 5中可看到,这将强制更新 3个窗口变量:
s n d _ w n d、s n d _ w l 1和s n d _ w l 2。接收紧急指针 (r c v _ u p)等于接收序号。控制跳转到标注
step6处,与RFC 793定义的步骤相对应,我们将在图 29-15中详细讨论。

28.7 PAWS:防止序号回绕

图2 8 - 2 2给出了 t c p _ i n p u t下一部分的代码,处理可能出现的序号回绕: RFC 1323中定


义的PAWS算法。请回想一下我们在 26.6节关于时间戳的讨论。

图28-22 tcp_input 函数:处理时间戳选项

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 761

图28-22 (续)

1. 基本PAWS测试
602-613 如果存在时间戳,则调用 tcp_dooptions设定ts_present。如果下列 3个条件
全真,则丢弃报文段:
1) RST 标志未置位 (参见习题28.8)。
2) TCP曾收到过对端发送的有效的时间戳 (ts_recent非零);并且
3) 当前报文段中的时间戳 (ts_val)小于原先收到的时间戳。
PAWS算法基于这样的假定:对于高速连接, 32 bit时间戳值回绕的速度远小于 32 bit序号
回绕的速度。习题 2 8 . 6说明,即使是最高的时钟计数器更新频率 (每毫秒加 1 ),时间戳的符号
位也要 2 4天才会回绕一次。而在千兆级网络中,序号可能 1 7秒就回绕一次 (卷1的2 4 . 3节)。因
此,如果报文段时间戳小于从同一个连接收到的最近一次的时间戳,说明是个重复报文段,
应被丢弃 (还需进行后续的时间戳过期测试 )。尽管因为序号已过时, t c p _ i n p u t也可将其丢
弃,但PAWS算法能够有效地处理序号回绕速率很高的高速网。
注意,PAW S算法是对称的:它不仅丢弃重复的数据报文段,也丢弃重复的 A C K。PAW S
处理所有收到的报文段,前面介绍过,首部预测代码也采用了 PAWS测试(图28-11)。
2. 检查过期时间戳
614-627 尽管可能性不大, PAWS测试还是会失败,因为连接有可能长时间空闲。收到的报
文段并非重复报文段,但连接空闲时间过长,造成时间戳值回绕,从而小于从同一个连接收
到的最近一次的时间戳。
无论何时,t s _ r e c e n t保存接收报文段中的时间戳值, t s _ r e c e n t _ a g e记录当前时间
(t c p _ n o w)。如果t s _ r e c e n t的最后一次更新发生在 24天之前,则将其清零,不是一个有效
的时间戳值。常量 T C P _ P A W S _ I D L E定义为( 2 4×2 4×6 0×6 0×2 ),最后的乘数 2指每秒钟 2个
滴答。这种情况下,不丢弃接收报文段,因为问题是时间戳过期,而非重复报文段。参见习

Linux公社 www.linuxidc.com
bbs.theithome.com
762 计计TCP/IP详解 卷 2:实现

题28.6和28.7。
图2 8 - 2 3举例说明了时间戳过期问题。连接左侧的系统是一个非 N e t / 3的T C P实现,以 R F C
1323中规定的最高速度,每毫秒更新一次时钟。连接右侧是 Net/3实现。

数据,时间戳=1
时间戳

连接空闲25天=
滴答
时间戳
时间戳符号位变化
时间戳

数据、时间戳
时间戳

图28-23 过期时间戳举例

第一个数据报文段中携带的时间戳值等于 1,所以ts_recent等于1,ts_recent_age等
于当前时间(tcp_now),如图28-11和图28-35所示。连接空闲25天,在此期间 tcp_now增加了
4 320 000 (25×24×60×60×1000),对端的时间戳值增加了 2 160 000 000 (25×24×60×60×
1000),时间戳值的符号位改变,即 2 147 483 649大于1,而2 147 483 650小于1(回想图24-26)。
因此,当收到的数据报文段中的时间戳等于 2 160 000 001 时,调用 T S T M P _ L T宏进行比较,
收到的时间戳小于 t s _ r e c e n t(1),PAWS测试失败。但因为 t c p _ n o w减去t s _ r e c e n t _ a g e
大于24天,说明造成失败的原因是连接空闲时间过长,报文段被接受。
3. 丢弃重复报文段
628-633 如果PAW S算法测试说明收到的是一个重复报文段,确认之后丢弃该报文段 (所有
重复报文段都必须被确认 ),不更新本地时间戳变量。
图2 4 - 5中,t c p _ p a w s d r o p ( 2 2 )远小于t c p s _ r c v d u p p a c k (46 953)。这可能
是因为目前只有很少的系统支持时间戳,导致绝大多数重复报文段直到 TCP输出处理
中才被发现和丢弃,而非 PAWS。

28.8 裁剪报文段使数据在窗口内

本节讨论如何调整收到的报文段,确保它只携带能够放入接收窗口内的数据:
• 丢弃接收报文段起始处的重复数据;并且
• 从报文段尾部起,丢弃超出接收窗口的数据。
从而只剩下可放入接收窗口的新数据。图 2 8 - 2 4给出的代码,用于判定报文段起始处是否
存在重复数据。
1. 查看报文段前部是否存在重复数据
635-636 如果接收报文段的起始序号 (t i _ s e q)小于等待接收的下一序号 (r c v _ n x t),则

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计763
todrop大于0,报文段前部有重复数据。这些数据己被确认并提交给应用进程 (图24-18)。

图28-24 tcp_input 函数:查看报文段起始处的重复数据

2. 丢弃重复SYN
637-645 如果S Y N标志置位,它必然指向报文段的第一个数据序号,现已知是重复数据。
清除SYN,报文段的起始序号加 1,以越过重复的 SYN。此外,如果接收报文段中的紧急指针
大于1 (ti_urp),则必须将其减 1,因为紧急数据偏移量以报文段起始序号为基准。如果紧急
指针等于 0或者1,则不做处理,为防止出现等于 1的情况,清除 U R G标志。最后, t o d r o p减
1(因为SYN占用一个序号)。
图28-25继续处理报文段前部的重复数据。

图28-25 tcp_input 函数:处理完全重复的报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
764 计计TCP/IP详解 卷 2:实现

图28-25 (续)

3. 判定报文段数据是否完全重复
646-648 如果报文段前部重复的数据字节数大于等于报文段大小,则是一个完全重复的报
文段。
4. 判定重复FIN
649-663 接下来测试 FIN是否重复,图28-26举例说明了这一情况。

TCP已确认并提交给插口层的数据序号

接收报文段 标志置位
下一接收序号

图28-26 举例:带有 FIN标志的重复报文段

图2 8 - 2 6的例子中, t o d r o p等于5,大于等于 t i _ l e n( 4 )。因为 F I N置位,并且 t o d r o p等


于t i _ l e n加1,所以清除FIN标志,t o d r o p重设为4,置位T F _ A C K N O W,函数结束时立即发
送ACK。这个例子也适用于其他报文段,如果 ti_seq加上ti_len等于10。
代码的注释提到了 4 . 2 B S D实现中的保活定时器, N e t / 3省略了相关处理 ( i f语句中
的另一项测试)。
5. 生成重复ACK
664-672 如果todrop非零(报文段携带的全部是重复数据 ),或者ACK标志未置位,则丢弃
报文段,调用 d r o p a f t e r a c k生成ACK。出现这种情况,一般是因为对端未收到 ACK,导致
报文段重发。TCP生成新的ACK。
6. 处理同时打开或半连接
664-672 代码还处理同时打开,以及插口与自己建立连接的情况,将在下一节中详细讨论。
如果 t o d r o p等于 0 (完全重复报文段中不包含数据 ),且 A C K标志置位,则继续下一步的处
理。
i f语句是 4 . 4 B S D版中新加的。早期的基于 B e r k e l e y的系统只是简单地跳转到
dropafterack,即不处理同时打开,也不处理与自己建立连接的情况。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计765
即使做了改进,这段代码仍有错误,我们在本节结束时将谈到这一点。
7. 收到部分重复报文段时,更新统计值
673-676 当t o d r o p小于报文段长度时,执行 e l s e语句:报文段携带数据中只有部分重
复。
8. 删除重复数据,更新紧急指针
677-685 调用m_adj,从mbuf链的首部开始删除重复数据,并相应地调整起始序号和长度。
如果紧急指针指向的数据仍在 m b u f中,也需做相应的调整。否则,紧急指针清零,并清除
URG标志。
图28-27给出了函数下一部分的代码,处理应用进程终止后到达的数据。

图28-27 tcp_input 函数:处理应用进程终止后到达的数据

687-696 如果找不到插口的描述符,说明应用进程已关闭了连接 (连接状态等于图 2 4 - 1 6中


大于C L O S E _ WA I T的5个状态中的任何一个 ),若接收报文段中有数据,则连接被关闭。报文
段被丢弃,输出 RST做为响应。
因为T C P支持半关闭功能,如果应用进程意外终止 (也许被某个信号量终止 ),做为进程终
止的一部分,内核将关闭所有打开的描述符, TCP将发送FIN。连接转移到FIN_WAIT_1状态。
因为FIN的接收者无法知道对端执行的是完全关闭,还是半关闭。如果它假定是半关闭,并继
续发送数据,那么将收到图 28-27中发送的FIN。
图28-28给出了函数下一部分的代码,从接收报文段中删除落在通告窗口右侧的数据。

图28-28 tcp_input 函数:删除落在窗口右侧的数据

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com
766 计计TCP/IP详解 卷 2:实现

图28-28 (续)

9. 计算落在通告窗口右侧的字节数
697-703 t o d r o p等于接收报文段中落在通告窗口右侧的字节数。例如,在图 2 8 - 2 9中,
todrop等于(6+5)减去(4+6),即等于1。
接收窗口
向发送方通告

已确认过的序号

接收报文段

图28-29 举例:接收报文段部分数据落在窗口右侧

10. 如果连接处于TIME_WAIT状态,查看有无新的连接请求
704-718 如果todrop大于等于报文段长度,则丢弃整个报文段。如果下列3个条件全真:
1) SYN标志置位;并且
2) 连接处于TIME_WAIT状态;并且
3) 新的起始序号大于连接上最后收到的序号;
说明对端要求在已被关闭且正处于 TIME_WAIT状态的连接上重建连接。 RFC 1122允许这种情
况,但要求新连接的 ISS必须大于最后收到的序号 (r c v _ n x t)。TCP在r c v _ n x t的基础上增加

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 767
128 000(T C P _ I S S I N C R),得到执行图 2 8 - 1 7中的代码时所使用的 I S S。调用 t c p _ c l o s e释放
处于TIME_WAIT状态的原有连接的 P C B和T C P控制块。控制跳转到 f i n d p c b(图2 8 - 5 ),寻找
监听服务器的PCB(假定服务器仍在运行 )。然后执行图28-7中的代码,为新连接创建新的插口,
最后执行图 28-16和图28-17中的代码,完成新连接请求的处理。
11. 判定是否为窗口探测报文段
719-728 如果接收窗口已关闭 (rcv_wnd等于0),且接收报文段中的数据从窗口最左端开始
(r c v _ n x t),说明是对端发送的窗口探测报文段。 T C P立即发送响应 A C K,其中包含等待接
收的序号。
12. 丢弃完全落在窗口之外的其他报文段
729-730 如果报文段整个落在窗口之外,且并非窗口探测报文段,则丢弃该报文段,并发
送携带等待接收序号的 ACK,作为响应。
13. 处理携带部分有效数据的报文段
731-735 通过m _ a d j,从m b u f链中删除落在窗口右侧的数据,并更新 t i _ l e n。如果接收
报文段是对端发送的窗口探测报文段, m _ a d j将丢弃 m b u f链中的所有数据,并将 t i _ l e n设
为0,最后清除 FIN和PSH标志。

何时丢弃ACK

图2 8 - 2 5 中的代码有错误,在几种情况下,本应继续进行报文段处理,控制却跳转到
d r o p a f t e r a c k[Carlson 1993; Lanciani 1993]。系统实际运行时,如果连接双方重组队列中
都存在缺失报文段,并都进入持续状态,将造成死锁,因为双方都将丢弃正常的 ACK。
纠正的方法是简化图28-25起始处的代码。控制不再跳转到dropafterack,如果收到了完全
重复报文段,则关闭FIN标志,并在函数结束时强迫立即发送ACK。删除图28-25中的646~676行的
代码,而代之以图28-30中的代码。此外,新代码还更正了原代码中的另一个错误(习题28.9)。

图28-30 图28-28中646~676行代码的修正

Linux公社 www.linuxidc.com
bbs.theithome.com
768 计计TCP/IP详解 卷 2:实现

28.9 自连接和同时打开

读者应首先理解插口与自己建立连接的步骤。接着会看到在 4 . 4 B S D中,如何巧妙地通过
一行代码修正图 28-25中的错误,从而不仅能够处理自连接,还能处理 4.4BSD以前的版本中都
无法正确处理的同时打开。
应用进程创建一个插口,并通过下列系统调用建立自连接: s o c k e t,b i n d绑定到一个
本地端口 (假定为 3 0 0 0 ),之后 c o n n e c t试图与同一个本地地址和同一个端口号建立连接。如
果c o n n e c t成功,则插口已建立了与自己的连接:向这个插口写入的所有数据,都可以在同
一插口上读出。这有点类似于全双工的管道,但只有一个,而非两个标识符。尽管很少有应
用进程会这样做,但实际上它是一种特殊的同时打开,两者的状态变迁图相同。如果系统不
允许插口建立自连接,那么它也很可能无法正确处理同时打开,而后者是 RFC 1122所要求的。
有些人对于自连接能成功感到非常惊诧,因为只用了一个 Internet PCB和一个T C P控制块。不
过,TCP是全双工的、对称的协议,它为每个方向上的数据流保留一份专有数据。
图2 8 - 3 1给出了应用进程调用 c o n n e c t时的发送序号空间, S Y N已发送,连接状态为
SYN_SENT。
插口收到 S Y N后,执行图 2 8 - 1 8和图2 8 - 2 0中的代码,但因为 S Y N中未包含 A C K,连接状
态转移到 S Y N _ R C V D。从状态变迁图 (图2 4 - 1 5 )可知,与同时打开类似。图 2 8 - 3 2给出了接收
序号空间。图 2 8 - 2 0置位T F _ A C K N O W,t c p _ o u t p u t生成的报文段将包含 S Y N和A C K (图2 4 -
16中的tcp_outflags)。SYN序号等于153,而确认序号等于 154。

发送 状态

图28-31 自连接:SYN发送后的发送序号空间

接收 状态

图28-32 自连接:收到的 SYN处理完毕后的接收序号空间

与图28-20处理的正常情况相比,发送序号空间没有变化,只是连接状态等于 SYN_SENT。
图28-33给出了收到同时带有 SYN和ACK的报文段时,接收序号空间的状态。

接收

图28-33 收到带有 SYN和ACK报文段时,接收序号空间的状态

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 769
因为连接状态等于 S Y N _ R C V D,将执行图 2 9 - 2中的代码处理收到的报文段,而不用我们
在本章前面讨论过的处理主动打开或被动打开的代码。但在此之前,首先遇到的是图 2 8 - 2 4中
的代码,而且从测试结果看似乎是一个重复 SYN:
todrop = rcv_nxt - rcv_seq
= 154 - 153
= 1

因为S Y N标志置位,清除该标志, t i _ s e q等于1 5 4,t o d r o p等于0。但因为 t o d r o p等于报


文段长度 (0),图28-25开始处的测试条件为真,从而判定是一个重复报文段,执行注释为“处
理绑定插口自连接的情况”的代码。早期的 T C P实现直接跳到 d r o p a f t e r a c k ,略过了
SYN_RCVD状态的处理逻辑,不可能建立连接。相反,即使 t o d r o p等于0,且ACK标志置位
( 本例中两个条件都成立 ) , N e t / 3 仍旧继续处理收到的报文段,从而进入函数后面对
SYN_RCVD状态的处理,连接转移到 ESTABLISHED状态。
图28-34给出了自连接处理中函数调用的情况,是非常有意思的。

系统调用

软中断 软中断

添加到 添加到

动作 发送 处理 发送 处理
起始状态
结束状态

图28-34 自连接处理中的函数调用序列

操作顺序从左至右,首先应用进程调用 c o n n e c t,发出 P R U _ C O N N E C T请求,经协议栈


发送S Y N。因为报文段发向主机自己的 I P地址,直接通过环回接口加入到 i p i n t r q,并生成
一个软中断。
系统在软中断处理中调用 i p i n t r,i p i n t r调用 t c p _ i n p u t ,t c p _ i n p u t再调用

Linux公社 www.linuxidc.com
bbs.theithome.com
770 计计TCP/IP详解 卷 2:实现

tcp_output,经协议栈发送带有ACK的SYN。这个报文段也经由环回接口加入到 ipintrq,
并生成一个软中断。系统调用 i p i n t r处理软中断, i p i n t r调用 t c p _ i n p u t,连接进入
ESTABLISHED状态。

28.10 记录时间戳

图28-35给出了tcp_input下一部分的代码,处理收到的时间戳选项。

图28-35 tcp_input 函数:记录时间戳

737-746 如果收到的报文段中带有时间戳,时间戳值保存在 ts_recent中。我们在 26.6节


曾讨论过Net/3的处理代码有错误。如果 FIN和SYN标志均未置位,表达式
((tiflags & (TH_SYN|TH_FIN)) != 0)

等于0;如果有一个置位,则等于 1。

28.11 RST处理

图28-36给出了处理 RST标志的switch语句,取决于当前的连接状态。
1. SYN_RCVD状态
759-761 插口差错代码设定为 ECONNREFUSED,控制向前跳转若干行,关闭插口。在两种
状况下,连接进入此状态。一般地讲,连接收到 S Y N后,从L I S T E N转移到S Y N _ R C V D状态。
T C P发送带有 A C K的S Y N做为响应,但接着却收到了对端的 R S T。此时, s o引用的插口是在
图28-7中调用s o n e w c o n n新创建的。因为 d r o p s o c k e t为真,在标注 d r o p处,插口被丢弃,
监听插口不受影响。这也是图 24-15中状态从SYN_RCVD转回LISTEN的原因。
另一种情况是,应用进程调用 c o n n e c t后,出现同时打开,状态也转移到 S Y N _ R C V D。
收到RST后,向应用进程返回插口差错。

图28-36 tcp_input 函数:处理 RST标志

Linux公社 www.linuxidc.com
bbs.theithome.com
第 28 章 TCP的输入计计 771

图28-36 (续)

2. 其他状态
762-777 如果在E S TA B L I S H E D、F I N _ WA I T _ 1、F I N _ WA I T _ 2或C L O S E _ WA I T状态收到
R S T,则返回差错代码 E C O N N R E S E T。如果状态为 C L O S I N G、L A S T _ A C K或T I M E _ WA I T,
由于应用进程已关闭插口,无需返回差错代码。
如果允许RST终止处于 T I M E _ W A I T状态的连接,那么 T I M E _ W A I T状态也就没有
存在的必要。 RFC 1337 [Braden 1992] 讨论了这一点,及其他取消 TIME_WAIT状态
的可能状况,建议不允许 RST永久终止处于 T I M E _ W A I T状态的连接。参见习题 28.10
中的例子。
图28-37给出了函数下一部分的代码,验证 SYN是否出错, ACK是否存在。

图28-37 tcp_input 函数:处理带有多余 SYN或者缺少 ACK的报文段

778-785 如果 S Y N标志依旧置位,说明出现了差错,连接被丢弃,返回差错代码
ECONNRESET。

Linux公社 www.linuxidc.com
bbs.theithome.com
772 计计TCP/IP详解 卷 2:实现

786-790 如果ACK标志未置位,则报文段被丢弃。我们将在下一章讨论函数剩余部分的代
码,其中假定ACK标志均置位。

28.12 小结

本章详细介绍了 TCP输入处理的前半部分,下一章将继续讨论函数剩余的部分。
本章介绍了如何验证报文段检验和,处理各种 T C P选项,处理发起和结束连接建立的
SYN报文段,从报文段头尾两个方向删除无效数据,及处理 RST标志。
首部预测算法处理正常情况的数据流是非常有效的,执行速度最快。尽管我们讨论的多
数处理逻辑用于覆盖所有可能发生的情况,但多数报文段都是正常的,只需很少的处理步骤。

习题

28.1 假定 N e t / 3中插口缓存最大等于 262 444,基于图 2 8 - 7的算法,得到的窗口缩放因子


是多少?
28.2 假定N e t / 3中插口缓存最大等于 262 444,如果往返时间等于 6 0 m s,可能的最大吞吐
量是多少? (提示:见卷 1的图24-5及带宽的解 )
28.3 为什么图28-10中,调用 bcopy获取时间戳值?
28.4 我们在26.6节中提到, TCP要求的时间戳选项格式与 RFC 1323附录A中定义的不同。
尽管TCP能够正确处理时间戳,但由于采用与标准不同的格式,会付出什么代价?
28.5 处理 P R U _ A T T A C H 请求时会分配 P C B 和 T C P 控制块,为什么不接着调用
tcp_template分配首部模板?而是直至收到了SYN,在图28-17中才进行这一操作。
28.6 阅读RFC 1323,理解为什么图 28-22中选取24天做为空闲时间的界限?
28.7 在图 2 8 - 2 2中,如果连接空闲时间超过 2 4天, t c p _ n o w - t s _ r e c e n t _ a g e 与
T C P _ P A W S _ I D L E的比较,会出现符号位回绕的问题。 N e t / 3中采取5 0 0 m s做为时间
戳单位,会在什么时间出现问题?
28.8 阅读RFC 1323,回答为什么图 28-22中PAWS测试不包括 RST报文段?
28.9 客户发送了 SYN,服务器响应SYN/ACK。客户转移到ESTABLISHED状态,并发送
响应 A C K。但这个 A C K丢失,服务器重发 S Y N / A C K。描述一下客户收到重发的
SYN/ACK时的处理步骤。
28.10 客户和服务器已建立了连接,服务器主动关闭。连接正常终止,服务器上的插口
对转移到 TIME_WAIT状态。在服务器的 2 M S L定时器超时前,同一客户 (客户端的
同一个插口对 )向服务器发送 S Y N,但起始序号小于连接上最后收到的序号。会发
生什么?

Linux公社 www.linuxidc.com
bbs.theithome.com
第29章 TCP的输入(续)
29.1 引言

本章从前一章结束的地方开始,继续介绍 T C P输入处理。回想一下图 2 8 - 3 7中最后的测试


条件,如果 ACK未置位,输入报文段被丢弃。
本章处理 A C K标志,更新窗口信息,处理 U R G标志及报文段中携带的所有数据,最后处
理FIN标志,如果需要,则调用 tcp_output。

29.2 ACK处理概述

在本章中,我们首先讨论 A C K的处理,图 2 9 - 1给出了A C K处理的框架。 S Y N _ R C V D状态


需要特殊处理,紧跟着是其他状态的通用处理代码 (前一章已讨论过在 L I S T E N和S Y N _ S E N T
状态下收到 A C K 时的处理逻辑 ) 。接着是对 T C P S _ F I N _ WA I T _ 1 、 T C P S _ C L O S I N G 和
T C P S _ L A S T _ A C K状态的一些特殊处理,因为在这些状态下收到 A C K会导致状态的转移。此
外,在TIME_WAIT状态下收到 ACK还会导致 2MSL定时器的重启。

图29-1 ACK处理框架

Linux公社 www.linuxidc.com
bbs.theithome.com
774 计计TCP/IP详解 卷 2:实现

图29-1 (续)

29.3 完成被动打开和同时打开

图2 9 - 2给出了如何处理 S Y N _ R C V D状态下收到的 A C K报文段。如前一章中提到过的,这


也将完成被动打开 (一般情况),或者是同时打开及自连接 (特殊情况)的连接建立过程。
1. 验证收到的 ACK
801-806 如果收到的ACK确认了已发送的SYN,它必须大于snd_una (tcp_sendseqinit将
s n d _ u n a设定为连接的 I S S,S Y N报文段的序号 ),且小于等于 s n d _ m a x。如果条件满足,则
插口进入连接状态 ESTABLISHED。

图29-2 tcp_input 函数:在SYN_RCVD 状态收到 ACK

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 775
在收到三次握手的最后一个报文段后,调用 s o i s c o n n e c t e d唤醒被动打开的应用进程
(一般为服务器 )。如果服务器在调用 a c c e p t上阻塞,则该调用现在返回。如果服务器调用
select等待连接可读,则连接现在已经可读。
2. 查看窗口大小选项
807-812 如果T C P曾发送窗口大小选项,并且收到了对方的窗口大小选项,则在 T C P控制
块中保存发送缩放因子和接收缩放因子。另外, T C P控制块中的 s n d _ s c a l e和r c v _ s c a l e
的默认值为 0 (无缩放)。
3. 向应用进程提交队列中的数据
813 现在可以向应用进程提交连接重组队列中的数据,调用 tcp_reass,第二个参数为空。
重组队列中的数据可能是 SYN报文段中携带的,它同时将连接状态变迁为 SYN_RCVD。
814 snd_wl1等于收到的序号减 1,从图29-15可知,这样将导致更新 3个窗口变量。

29.4 快速重传和快速恢复的算法

图2 9 - 3给出了 A C K处理的下一部分代码,处理重复 A C K,并决定是否起用 T C P的快速重


传和快速恢复算法 [Jacobson 1990c]。两个算法各自独立,但一般都在一起实现 [Floyd 1994]。
• 快速重传算法用于连续出现几次 (一般为 3次)重复A C K时,T C P认为某个报文段已丢失
并且从中推断出丢失报文段的起始序号,丢失报文段被重传。 RFC 11 2 2中的4 . 2 . 2 . 2 1节
提到了这一算法,建议 T C P收到乱序报文段后,立即发送 A C K。我们看到,在图 2 7 - 1 5
中,N e t / 3正是这样做的。这个算法最早出现在 4.3BSD Ta h o e版及后续的 N e t / 1实现中,
丢失报文段被重传之后,连接执行慢起动。
• 快速恢复算法认为采用快速重传算法之后 (即丢失报文段已重传 ),应执行拥塞避免算法,
而非慢起动。这样,如果拥塞不严重,还能保证较大的吞吐量,尤其窗口较大时。这个
算法最早出现在 4.3BSD Reno版和后续的 Net/2实现中。
Net/3同时实现了快速重传和快速恢复算法,下面将做简单介绍。
在图24-17节中,我们提到有效的 ACK必须满足下面的不等式:
snd_una < 确认字段 <= snd_max

第一步只与 s n d _ u n a做比较,之后在图 2 9 - 5中再进行不等式第二部分的比较。分开比较


的原因是为了能对收到的 ACK完成下列5项测试:
1) 如果确认字段小于等于 snd_una;并且
2) 接收报文段长度为 0;并且
3) 窗口通告大小未变;并且
4) 连接上部分发送数据未被确认 (重传定时器非零 );并且
5) 接收报文段的确认字段是 TCP收到的最大的确认序号 (确认字段等于snd_una)。
之后可确认报文段是完全重复的 A C K (测试项 1、2和3在图2 9 - 3中,测试 4和5在图2 9 - 4的起始
处)。
T C P 统计连续收到的重复 A C K 的个数,保存在变量 t _ d u p a c k s 中,次数超过门限
(t c p r e x m t t h r e s h,3 )时,丢失报文段被重传。这也就是卷 1第2 1 . 7节中介绍的快速重传算
法。它与图 27- 1 5中的代码互相配合:当 T C P收到乱序报文段时,立即生成一个重复的 A C K,

Linux公社 www.linuxidc.com
bbs.theithome.com
776 计计TCP/IP详解 卷 2:实现

告诉对端报文段有可能丢失和等待接收的下一个序号值。快速重传算法是为了让 T C P立即重
传看上去已经丢失的报文段,而不是被动地等待重传定时器超时。卷 1第2 1 . 7节举例详细说明
了这个算法是如何工作的。

图29-3 tcp_input 函数:判定完全重复的 ACK报文段

另一方面,重复 ACK的接收方也能确认某个数据分组已“离开了网络”,因为对端已收到
了一个乱序报文段,从而开始发送重复的 ACK。快速恢复算法要求连续收到几个重复 ACK后,
TCP应该执行拥塞避免算法 (如降低速度 ),而不一定必需等待连接两端间的管道清空 (慢起动)。
“离开了网络”指数据分组已被对端接收,并加入到连接的重组队列中,不再滞留在传输途
中。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 777
如果前述5项测试条件只有前3项为真,说明ACK是重复报文段,统计值tcps_rcvdupack
加1,而连续重复 A C K计数器(t _ d u p a c k s)复位为0。如果仅有第一项测试条件为真,则计数
器t_dupacks复位为0。
图2 9 - 4给出了快速重传算法其余的代码,当所有 5个测试条件全部满足时,根据已连续收
到的重复ACK数目的不同,运用快速重传算法处理收到的报文段。
1) t _ d u p a c k s等于 3 (t c p r e x m t t h r e s h ),则执行拥塞避免算法,并重传丢失报文
段。
2) t_dupacks大于3,则增大拥塞窗口,执行正常的 TCP输出。
3) t_dupacks小于3,不做处理。

图29-4 tcp_input 函数:处理重复的 ACK

1. 连续收到的重复 ACK次数己达到门限值 3
861-868 t _ d u p a c k s等于3(t c p r e x m t t h r e s h)时,在变量 o n x t中保存s n d _ n x t值,令
慢起动门限(s s t h r e s h)等于当前拥塞窗口大小的一半,最小值为两个最大报文段长度。这与
图2 5 - 2 7中重传定时器超时处理中的慢起动门限设定操作类似,但我们将看到,超时处理中把
拥塞窗口设定为一个最大报文段长度,快速重传算法并不这样做。
2. 关闭重传定时器
869-870 关闭重传定时器。为防止 TCP正对某个报文段计时, t_rtt清零。

Linux公社 www.linuxidc.com
bbs.theithome.com
778 计计TCP/IP详解 卷 2:实现

3. 重传缺失报文段
871-873 从连续收到的重复ACK报文段中可判断出丢失报文段的起始序号 (重复ACK的确认
字段),将其赋给snd_nxt,并将拥塞窗口设定为一个最大报文段长度,从而tcp_output将只
发送丢失报文段 (参见卷1的图21-7中的63号报文段)。
4. 设定拥塞窗口
874-875 拥塞窗口等于慢起动门限加上对端高速缓存的报文段数。“高速缓存”指对端已收
到的乱序报文段数,且为这些报文段发送了重复的 A C K。除非对端收到了丢失的报文段 (刚刚
发送),这些缓存报文段中的数据不会被提交给应用进程。卷 1的图21-10和图21-11给出了快速
重传算法起作用时 ,拥塞窗口和慢起动门限的变化情况。
5. 设定snd_nxt
876-878 比较下一发送序号 (s n d _ n x t)的先前值(o n x t)和当前值,将两者中最大的一个重
新赋还给 s n d _ n x t,因为重传报文段时, t c p _ o u t p u t 会改变 s n d _ n x t 。一般情况下,
snd_nxt将等于原来保存的值,意味着只有丢失报文段被重传,下一次调用 tcp_output时,
将继续发送序列中的下一报文段。
6. 连续收到的重复 ACK数超过门限 3
879-883 因为t _ d u p a c k s等于3时,已重传了丢失的报文段,再次收到重复 ACK说明又有
另一个报文段离开了网络。拥塞窗口大小加 1,调用t c p _ o u t p u t发送序列中的下一报文段,
并丢弃重复的ACK(参见卷1的图21-7的67号、69号和71号报文段)。
884-885 如果收到的报文段中带有重复的 A C K,且长度非零或者通告窗口大小发生变化,
则执行这些语句。此时,前面提到的 5个测试条件中只有第一个为真,连续收到的重复 ACK数
被清零。
7. 略过ACK处理的其余部分
886 b r e a k语句在下列3种情况下被执行: (1)前述5个测试条件中只有第一个条件为真; (2)
只有前 3个条件为真; ( 3 )重复A C K次数小于门限值 3。任何一种情况下,尽管收到的是重复
A C K,将执行 b r e a k语句,控制跳到图 2 9 - 2中s w i t c h语句的结尾处,在标注 s t e p 6处继续
执行。
为了理解前面的窗口操作步骤,请看下面的例子。假定对端接收窗口只能容纳 8个报文段,
而本地报文段 1 ~ 8已发送。报文段 1丢失,其余报文段均正常到达且被确认。收到对报文段 2、
3和4的确认后,重传丢失的报文段 (1)。尽管在收到后续的对报文段 5~8的确认后,TCP希望能
够发送报文段 9,以保证高的吞吐率。但窗口大小等于 8,禁止发送报文段 9及其后续报文段。
因此,每当再次收到一个重复的 ACK,就暂时把拥塞窗口加 1,因为收到重复的 ACK告诉TCP
又有一个报文段已在对端离开了网络。最终收到对报文段 1的确认后,下面将介绍的代码会减
少拥塞窗口大小,令其等于慢起动门限。卷 1的图2 1 - 1 0举例说明了这一过程,重复 A C K到达
时,增加拥塞窗口大小,之后收到新的 ACK时,再相应地减少拥塞窗口。

29.5 ACK处理

图29-5中的代码继续处理 ACK。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 779

图29-5 tcp_input 函数:继续 ACK处理

1. 调整拥塞窗口
888-895 如果连续收到的重复 A C K数超过了门限值 3,说明这是在收到了 4个或4个以上的
重复 A C K 后,收到的第一个非重复的 A C K。快速重传算法结束。因为从收到的第 4个重复
A C K开始,每收到一个重复 A C K就会导致拥塞窗口加 1,如果它已超过了慢起动门限,令其
等于慢起动门限。连续收到的重复 ACK计数器清零。
2. 检查ACK的有效性
896-899 前面介绍过,有效的 ACK必须满足下列不等式:
snd_una < 确认字段 <= snd_max

如果确认字段大于 s n d _ m a x,可对端正在确认了 T C P尚未发送的数据。可能的原因是,


对于高速连接,某个失踪的 A C K再次出现时,序号已回绕,从图 2 4 - 5可知,这是极为罕见的
(因为实际的网络不可能那么快 )。
3. 计算确认的字节数
900-902 经过前面的测试,已知这是一个有效的 ACK。acked等于确认的字节数。
图29-6给出了ACK处理的下一部分代码,完成 RTT测算和重传定时器的操作。
4. 更新RTT测算值
903-915 如果(1)时间戳选项存在;或者 (2)TCP对某个报文段计时,且收到的确认字段大于
该报文段的起始序号,则调用 t c p _ x m i t _ t i m e r更新 RT T测算值。注意,使用时间戳时,
t c p _ x m i t _ t i m e r的第二个参数等于当前时间 (t c p _ n o w)减去收到的时间戳回显 (t s _ e c r)
加1(因为函数处理中减了 1)。
由于延迟 A C K的存在,在前面的测试不等式中应采用大于号。例如,假定 T C P发送了一
个报文段,携带字节 1 ~ 1 0 2 4,并对其计时,接着又发送了一个报文段,携带字节 1 0 2 5 ~ 2 0 4 8。
如果收到的确认字段等于 2 0 4 9,因为 2 0 4 9大于1 (计时报文段的起始序号 ),T C P将更新 RT T测
算值。
5. 是否确认了所有己发送数据
916-924 如果收到报文段的确认字段 (t i _ a c k)等于TCP的最大发送序号 (s n d _ m a x),说明
所有已发送数据都已被确认。关闭重传定时器,并置位 n e e d o u t p u t标志,从而在函数结束

Linux公社 www.linuxidc.com
bbs.theithome.com
780 计计TCP/IP详解 卷 2:实现

时强迫调用 t c p _ o u t p u t。这是因为在此之前,有可能因为发送窗口已满, T C P拒绝了等待


发送的数据,而现在收到了新的 A C K,确认了全部已发送数据,发送窗口能够向右移动 (图
29-8中的snd_una被更新),允许发送更多的数据。

图29-6 tcp_input 函数:RTT测算值和重传定时器

6. 存在未确认的数据
925-926 由于发送缓存中还存在未被确认的数据,如果持续定时器未设定,则启动重传定
时器,时限等于 t_rxtcur的当前值。

Karn算法和时间戳

注意,时间戳的运用取消了 K a r n算法的部分规定 (卷1的2 1 . 3节):如果重传定时器超时,


则报文段被重传,收到对重传报文段的确认时,不应据此更新 RT T测算值 (重传确认的二义性
问题)。在图2 5 - 2 6中,我们看到当发生重传时,遵从 K a r n算法,t _ r t t被设为0。如果时间戳
不存在,且收到的是对重传报文段的确认,则图 29-6中的代码不会更新 RTT测算值,因为此时
t _ r t t等于0。但如果时间戳存在,则不查看 t _ r t t值,允许利用收到的时间戳回显字段更新
RT T测算值。根据 RFC 1323,时间戳的运用不存在二义性,因为 t s _ e c r的值复制自被确认
的报文段。 Karn算法中关于重传报文段时应采用指数退避的策略依旧有效。
图29-7给出了ACK处理的下一部分代码,更新拥塞窗口。

图29-7 tcp_input 函数:响应收到的 ACK,打开拥塞窗口

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计781

图29-7 (续)

1. 更新拥塞窗口
927-942 慢起动和拥塞避免的一条原则是收到 ACK后将增大拥塞窗口。默认情况下,每收
到一个 A C K (慢起动 ),拥塞窗口将加 1。但如果当前拥塞窗口大于慢起动门限,增加值等于 1
除以拥塞窗口大小,并加上一个常量。表达式
incr * incr / cw

等于
t_maxseg * t_maxseg / snd_cwnd

即1除以拥塞窗口,因为 s n d _ c w n d的单位为字节,而非报文段。表达式的常量部分等于最大
报文段长度的 1 / 8。此外,拥塞窗口的上限等于连接发送窗口的最大值。算法的举例参见卷 1
的21.8节。
添加一个常量 (最大报文段长度的 1 / 8 )是错误的 [Floyd 1994] 。但它一直存在于
BSD源码中,从 4.3BSD到4.4BSD和Net/3,应将其删除。
图29-8给出了tcp_input下一部分的代码,从发送缓存中删除已确认的数据。

图29-8 tcp_input 函数:从发送缓存中删除已确认的数据

2. 从发送缓存中删除已确认的字节
943-946 如果确认字节数超过发送缓存中的字节数,则从 snd_wnd中减去发送缓存中的字

Linux公社 www.linuxidc.com
bbs.theithome.com
782 计计TCP/IP详解 卷 2:实现

节数,并且可知本地发送的 F I N已被确认。调用 s b d r o p从发送缓存中删除所有字节。能够以


这种方式检查对 FIN报文段的确认,是因为 FIN在序号空间中只占一个字节。
947-951 如果确认字节数小于或等于发送缓存中的字节数, ourfinisacked等于0,并从
发送缓存中丢弃 acked字节的数据。
3. 唤醒等待发送缓存的进程
951-956 调用s o w w a k e u p唤醒所有等待发送缓存的应用进程,更新 s n d _ u n a保存最老的
未被确认的序号。如果 s n d _ u n a的新值超过了 s n d _ n x t,则更新后者,因为这说明中间的数
据也被确认。
图2 9 - 9举例说明了为什么 s n d _ n x t保存的序号有可能小于 s n d _ u n a。假定传输了两个报
文段,第一个携带字节 1~512,而第二个携带字节 513~1024。

一个报文段 一个报文段

图29-9 连接上发送了两个报文段

确认返回前,重传定时器超时。图 2 5 - 2 6中的代码将 s n d _ n x t设定为 s n d _ u n a,进入慢


起动状态,调用 t c p _ o u t p u t重传携带 1 ~ 5 1 2字节的报文段。 t c p _ o u t p u t将s n d _ n x t增加
为513,如图29-10所示。

报文段重传

图29-10 重传定时器超时后的连接 (接图29-9)

此时,确认字段等于 1025的ACK到达(或者是最初发送的两个报文段或者是 ACK在网络中


被延迟 )。这个 A C K是有效的,因为它小于等于 s n d _ m a x,但它也将小于更新后的 s n d _ u n a
值。
一般性的ACK处理现在已结束,图 29-11中的switch语句接着处理了 4种特殊情况。

图29-11 tcp_input 函数:在 FIN_WAIT_1状态时收到了 ACK

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计783

图29-11 ( 续)

4. 在FIN_WAIT_1状态时收到了ACK
958-971 此时,应用进程已关闭了连接, TCP已发送了 FIN,但还有可能收到对在 FIN之前
发送的报文段的确认。因此,只有在收到 F I N的确认后,连接才会转移到 F I N _ WA I T _ 2状态。
图2 9 - 8中,o u r f i n i s a c k e d标志已置位,这取决于确认的字节数是否超过发送缓存中的数
据量。
5. 设定FIN_WAIT_2定时器
972-975 我们在25.6节中介绍了 Net/3如何设定FIN_WAIT_2定时器,以防止在 FIN_WAIT_2
状态无限等待。只有当应用进程完全关闭了连接 (如c l o s e系统调用,或者在应用进程被某个
信号量终止时与 c l o s e类似的内核调用 ),而不是半关闭时 (如已发送了 F I N,但应用进程仍在
连接上接收数据 ),定时器才会启动。
图29-12给出了在CLOSING状态收到ACK时的处理代码。

图29-12 tcp_input 函数:在CLOSING状态收到ACK

6. 在CLOSING状态收到 ACK
979-992 如果收到的 A C K是对 F I N的确认 (而非之前发送的数据报文段 ),则连接转移到
TIME_WAIT状态。所有等待的定时器都被清除 (如等待的重传定时器 ),TIME_WAIT定时器被
启动,时限等于两倍的 MSL。
图29-13给出了在LAST_ACK状态收到 ACK的处理代码。

Linux公社 www.linuxidc.com
bbs.theithome.com
784 计计TCP/IP详解 卷 2:实现

图29-13 tcp_input 函数:在 LAST_ACK状态收到 ACK

7. 在LAST_ACK状态收到 ACK
993-1004 如果FIN已确认,连接将转移到 CLOSED状态。t c p _ c l o s e将负责这一状态变
迁,并同时释放 Internet PCB和TCP控制块。
图29-14给出了在TIME_WAIT状态收到ACK的处理代码。

图29-14 tcp_input 函数:在 TIME_WAIT状态收到ACK

8. 在TIME_WAIT状态收到ACK
1005-1014 此时,连接两端都已发送过 F I N,且两个 F I N都已被确认。但如果 T C P对远端
F I N 的确认丢失,对端将重传 F I N ( 带有 A C K ) 。 T C P 丢弃报文段并重传 A C K 。此外,
TIME_WAIT定时器必须被重传,时限等于两倍的 MSL。

29.6 更新窗口信息

TCP控制块中还有两个窗口变量我们未曾提及: snd_wl1和snd_wl2。
• nd_wl1记录最后接收报文段的序号,用于更新发送窗口 (snd_wnd)。
• snd_wl2记录最后接收报文段的确认序号,用于更新发送窗口。
到目前为止,只在连接建立时 ( 主动打开、被动打开或同时打开 )遇到过这两个变量,
s n d _ w l 1 被设定为 t i _ s e q 减1。当时说是为了保证窗口更新,下面的代码将证明这一
点。
如果下列 3个条件中的任一个被满足,则应根据接收报文段中的通告窗口值 (t i w i n)更新
发送窗口(snd_wnd):
1) 报文段携带了新数据。因为 s n d _ w l 1保存了用于更新窗口的最后接收报文段的起始序

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 785
号,如果snd_wl1<ti_seq,说明此条件为真。
2) 报文段未携带新数据(snd_wl1等于ti_seq),但报文段确认了新数据。因为 snd_wl2
保存了用于更新窗口的最后接收报文段的确认序号,如果 s n d _ w l 2 < t i _ a c k,说明此条件为
真。
3) 报文段未携带新数据,也未确认新数据,但通告窗口大于当前发送窗口。
这些测试条件的目的是为了防止旧的报文段影响发送窗口,因为发送窗口并非绝对的序
号序列,而是从 snd_una算起的偏移量。
图29-15给出了更新发送窗口的代码。

图29-15 tcp_input 函数:更新窗口信息

1. 是否需要更新发送窗口
1015-1023 if语句检查报文段的 ACK标志是否置位,且前述 3个条件中是否有一个被满足。
前面介绍过,在 L I S T E N状态或 S Y N _ S E N T状态收到 S Y N后,控制将跳转到 s t e p 6,而在
LISTEN状态收到的 SYN不带ACK。
注释中的 TA C指“终端接入控制器 (terminal access controller)”,是ARPANET上
的Telnet客户。
1024-1027 如果收到一个纯窗口更新报文段 (长度为0,ACK未确认新数据,但通告窗口增
加),统计值 tcps_rcvwinupd递增。
2. 更新变量
1028-1033 更新发送窗口,保存新的 s n d _ w l 1和s n d _ w l 2值。此外,如果新的通告窗口
是T C P从对端收到的所有窗口通告中的最大值,则新值被保存在 m a x _ s n d w n d中。这是为了
猜测对端接收缓存的大小,在图 2 6 - 8中用到了此变量。更新 s n d _ w n d后,发送窗口可用空间
增加,从而能够发送新的报文段,因此, needoutput标志置位。

Linux公社 www.linuxidc.com
bbs.theithome.com
786 计计TCP/IP详解 卷 2:实现

29.7 紧急方式处理

TCP输入处理的下一部分是 URG标志置位时的报文段。如图 29-16所示。

图29-16 tcp_input 函数:紧急方式的处理

1. 是否需要处理URG标志
1035-1039 只有满足下列条件的报文段才会被处理: U R G标志置位,紧急数据偏移量
( t i _ u r p ) 非零,连接还未收到 F I N 。只有当连接的状态等于 T I M E _ WA I T 时,宏
T C P S _ H A V E R C V D F I N才会为真,因此,连接处于任何其他状态时, U R G都会被处理。在后
面的注释中提到,连接处于 CLOSE_WAIT、C L O S I N G、L A S T _ A C K和TIME_WAIT等几个状
态时,URG标志会被忽略,这种说法是错误的。
2. 忽略超出的紧急指针
1040-1050 如果紧急数据偏移量加上接收缓存中已有的数据超过了插口缓存可容纳的数据
量,则忽略紧急标志。紧急数据偏移量被清零, U R G标志被清除,剩余的紧急方式处理逻辑
被忽略。
图29-17给出了tcp_input下一部分的代码,处理紧急指针。

图29-17 tcp_input 函数:处理收到的紧急指针

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 787

图29-17 (续)

1051-1065 如果接收报文段的起始序号加上紧急数据偏移量超过了当前接收紧急指针,说
明已收到了一个新的紧急指针。例如,图 2 6 - 3 0中的携带 3字节的报文段到达接收方,如图 2 9 -
18所示。
一般情况下,收到的紧急指针 (r c v _ u p) 接收报文段
等于 r c v _ n x t。这个例子中,因为 i f 语句
为真(4加3大于4),rcv_up的新值等于 7。
3. 计算收到的紧急指针
1066-1070 计算插口接收缓存中带外数据
的分界点,应计入接收缓存中已有的数据
(s o _ r c v . s b _ c c)。在上面的例子中,假定
接收缓存为空,so_oobmark等于2:序号为 紧急数据偏移量
6的字节被认为是带外数据。如果这个带外数
图29-18 图26-30中发送的报文段到达接收方
据标记等于 0,说明插口正处在带外数据分界
点上。如果发送带外数据的 send系统调用给定长度为 1,并且这个报文段到达对端时接收缓存
为空,就会发生这一现象,同时也再次重申了 Berkeley系统认为紧急指针应指向带外数据后的
第一字节。
4. 向应用进程通告 TCP的紧急方式
1071-1072 调用s o h a s o u t o f b a n d告知应用进程有带外数据到达了插口,清除两个标志

Linux公社 www.linuxidc.com
bbs.theithome.com
788 计计TCP/IP详解 卷 2:实现

TCPOOB_HAVEDATA和TCPOOB_HADDATA,它们用于图30-8中的PRU_RCVOOB请求处理。
5. 从正常的数据流中提取带外数据
1074-1085 如果紧急数据偏移量小于等于接收报文段中的字节数,说明带外数据包含在报
文段中。 T C P 的紧急方式允许紧急数据偏移量指向尚未收到的数据。如果定义了
S O _ O O B I N L I N E常量(正常情况下, N e t / 3定义了此常量 ),而且未选用对应的插口选项,则接
收进程将从正常的数据流中提取带外数据,并保存在 t _ i o b c变量中。完成这一功能的函数,
是我们将在下一节介绍的 tcp_pulloutofband。
注意,无论紧急指针指向的字节是否可读, T C P都将通知接收进程发送方已进入紧急方
式。这是TCP紧急方式的一个特性。
6. 如果不处于紧急方式,调整接收紧急指针
1086-1093 在接收方未处理紧急指针时,如果 r c v _ n x t大于接收紧急指针,则 r c v _ u p向
右移动,并等于 r c v _ n x t。这使接收紧急指针一直指向接收窗口的左侧,确保在收到 U R G标
志时,图29-17起始处的宏 SEQ_GT能够得出正确的结果。
如果要实现习题 2 6 . 6中提出的方案,也必须相应修改图 2 9 - 1 6和图 2 9 - 1 7中的代
码。

29.8 tcp_pulloutofband函数

图29-17中的代码调用了这个函数,如果:
1) 接收报文段中带有紧急方式标志;并且
2) 带外数据包含在接收报文段中 (如,紧急指针指向接收报文段 );并且
3) 未选用SO_OOBINLINE选项。
函数从正常的数据流 (保存接收报文段的 mbuf链)中提取带外字节,并保存在连接 TCP控制
块中的 t _ i o b c变量中。应用进程通过 r e c v系统调用,置位 M S G _ O O B标志,读取这个变量:
图30-8中的PRU_RCVOOB请求。图29-19给出了函数代码。

图29-19 tcp_pulloutofband 函数:将带外数据保存在 t_iobc 变量中

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 789

图29-19 (续)

1282-1289 考虑图29-20中的例子。紧急数据偏移量等于 3,因此紧急指针等于 7,带外字节


的序号等于 6。接收报文段携带了 5字节的数据,全部保存在一个 mbuf中。
变量cnt等于2,因为m_len(等于5)大于2,执行if语句为真部分的代码。
1290-1298 c p 指向序号为 6的字节,它被放入保存带外字节的变量 t _ i o b c 中。置位
T C P O O B _ H A V E D A T A标志,调用 b c o p y将接下来的两个字节 (序号7和8 )左移1字节,如图 2 9 -
21所示。

接收报文段

带外字节

紧急数据偏移量

图29-20 携带带外字节的报文段 图29-21 移走带外数据后的结果 (接图29-20)

注意,数字 7和8指数据字节的序号,而不是其内容。 m b u f的长度从 5减为4,但t i _ l e n


仍等于5不变,这是为了按序把报文段放入插口的接收缓存。 T C P _ R E A S S宏和t c p _ r e a s s函
数(在下一节调用 )都会给 r c v _ n x t增加t i _ l e n,本例中 t i _ l e n必须等于 5,因为下一个等
待接收的序号等于 9 。还请注意,函数没有对第一个 m b u f 中的数据分组首部长度
(m _ p k t h d r . l e n)减1,这是因为负责把数据添加到插口接收缓存的 s b a p p e n d不使用此长度
值。
跳至链中的下一个 mbuf
1299-1302 如果带外数据未保存在此 mbuf中,则从cnt中减去mbuf中的字节数,处理链中
的下一个 m b u f。因为只有当紧急数据移量指向接收报文段时,才会调用此函数,所以,如果
链已结束,不存在下一个 mbuf,则执行 break语句,跳转到标注 panic处。

29.9 处理已接收的数据

t c p _ i n p u t接着提取收到的数据 (如果存在 ),将其添加到插口接收缓存,或者放入插口


的乱序重组队列中。图 29-22给出了完成此项功能的代码。

Linux公社 www.linuxidc.com
bbs.theithome.com
790 计计TCP/IP详解 卷 2:实现

图29-22 tcp_input 函数:把收到的数据放入插口接收队列

1094-1105 报文段数据将被处理,如果:
1) 接收数据的长度大于 0,或者FIN标志置位;并且
2) 连接还未收到FIN;
则调用宏 T C P _ R E A S S处理数据。如果数据次序正确 (如,连接等待接收的下一序号 ),置位延
迟A C K标志,增加 r c v _ n x t,并把数据添加到插口的接收缓存中。如果数据次序错误,宏会
调用t c p _ r e a s s函数,把数据加入到连接的重组队列中 (新到数据有可能填充队列中的缺口,
从而将已排队的数据添加到插口的接收缓存中 )。
前面介绍过,宏的最后一个参数 (t i f l a g s)是可修改的。特别地,如果数据次序错误,
t c p _ r e a s s令t i f l a g s等于0,清除FIN标志(如果它已置位)。这也就是为什么即使报文段中
没有数据,只要 FIN置位,if语句也为真。
考虑下面的例子。连接建立后,发送方立即发送报文段:一个携带字节 1 ~ 1 0 2 4,另一个
携带字节 1 0 2 5 ~ 2 0 4 8,还有一个未带数据的 F I N。第一个报文段丢失,因此,第二个报文段到
达时(字节1 0 2 5 ~ 2 0 4 8 ),接收方将其放入乱序重组队列,并立即发送 A C K。当第三个带有 F I N
标志的报文段到达时,图 29-22中的代码被执行。即使数据长度等于 0,因为FIN置位,导致调
用T C P _ R E A S S,它接着调用 t c p _ r e a s s。因为t i _ s e q( 2 0 4 9,F I N的序号)不等于r c v _ n x t
( 1 ),t c p _ r e a s s返回0 (图27- 2 3 )。在T C P _ R E A S S宏中, t i f l a g s被设为 0,从而清除了
FIN标志,阻止后续代码 (图29-10)继续处理FIN。
猜测对端发送缓存大小
1106-1111 计算l e n,实际上是在猜测对端发送缓存的大小。考虑下面的例子。插口接收
缓存大小等于 8 1 9 2 ( N e t / 3的默认值 ),因此, T C P在S Y N中通告窗口大小为 8 1 9 2。之后收到第
一个报文段,携带字节 1 ~ 1 0 2 4。图2 9 - 2 3给出了在 T C P _ R E A S S增加r c v _ n x t以反应收到的数
据后接收空间的状态。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 791

图29-23 大小为8192的接收窗口收到字节 1~1024后的状态

此时,经计算, l e n等于1024。对端向接收窗口发送更多数据后, l e n值将增加,但绝不


会超过对端发送缓存的大小。前面介绍过,图 29-15中对变量 m a x _ s n d w n d的计算,是在猜测
对端接收缓存的大小。
事实上,变量 l e n从未被使用。它是从 N e t / 1遗留下来的, l e n计算后被存储到
TCP控制块的max_rcvd变量中:
if (len > tp->max_rcvd)
tp->max_rcvd = len;

但即使在Net/1中,变量max_rcvd也未被使用。
1112-1115 如果l e n等于0,且F I N标志未置位,或者连接上已收到了 F I N,则丢弃保存接
收报文段的 mbuf链,并清除 FIN。

29.10 FIN处理

tcp_input的下一步,在图 29-24中给出,处理FIN标志。

图29-24 tcp_input 函数:FIN处理,前半部分

1. 处理收到的第一个 FIN
1116-1125 如果接收报文段 F I N 置位,并且是连接上收到的第一个 F I N,则调用
s o c a n t r c v m o r e,把插口设为只读,置位 T F _ A C K N O W ,从而立即发送 A C K (无延迟 )。
Linux公社 www.linuxidc.com
bbs.theithome.com
792 计计TCP/IP详解 卷 2:实现

rcv_nxt加1,越过FIN占用的序号。
1126 FIN处理的其余部分是一个大的 switch语句,根据连接的状态进行转换。注意,连接
处于CLOSED、LISTEN和SYN_SENT状态时,不处理 FIN,因为处于这 3个状态时,还未收到
对端发送的 S Y N ,无法同步接收序号,也就无法验证 F I N序号的有效性。此外,连接处于
C L O S I N G、C L O S E _ WA I T和L A S T _ A C K状态时,也不处理 F I N,因为在这 3个状态下收到的
FIN必然是一个重复报文段。
2. SYN_RCVD和ESTABLISHED状态
1127-1134 如果连接处于 S Y N _ R C V D或E S TA B L I S H E D状态,收到 F I N后,新的状态为
CLOSE_WAIT。
尽管在 S Y N _ R C V D状态下收到 F I N是合法的,但却极为罕见。图 2 4 - 1 5的状态图
未列出这一状态变迁。它意味着处于 L I S T E N状态的插口收到一个同时带有 S Y N和
F I N的报文段。或者,正在监听的插口收到了 S Y N,连接转移到 S Y N _ R C V D状态,
但在收到 A C K之前,先收到了 F I N (从分析可知, F I N未携带有效的 A C K,否则,图
29-2中的代码会使连接转移到 ESTABLISHED状态)。
图29-25给出了FIN处理的下一部分。

图29-25 tcp_input 函数:FIN处理,后半部分

3. FIN_WAIT_1状态
1135-1141 因为报文段的ACK处理已结束,如果处理 FIN时,连接处于FIN_WAIT_1状态,
意味着连接两端同时关闭连接—两端发送的两个FIN在网络中交错。连接进入CLOSING状态。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 793
4. FIN_WAIT_2状态
1142-1148 收到FIN将使连接进入TIME_WAIT状态。当在 FIN_WAIT_1状态收到携带ACK
和F I N的报文段时 (典型情况 ),尽管图 2 4 - 1 5显示连接直接从 FIN_WAIT_1转移到 T I M E _ WA I T
状态,但在图 2 9 - 11中处理 A C K时,连接实际已进入 F I N _ WA I T _ 2状态。此处的 F I N处理再将
连接转到 TIME_WAIT状态。因为ACK在FIN之前处理,所以连接总会经过 FIN_WAIT_2状态,
尽管是暂时性的。
5. 启动TIME_WAIT定时器
1149-1152 关闭所有等待的 TCP定时器,并启动 TIME_WAIT定时器,时限等于 MSL(如果
接收报文段中包含 ACK和FIN,图29-11中的代码会启动 FIN_WAIT_2定时器)。插口断开连接。
6. TIME_WAIT状态
1153-1159 如果在TIME_WAIT状态时收到 F I N,说明这是一个重复报文段。与图 2 9 - 1 4中
的处理类似,启动 TIME_WAIT定时器,时限等于两倍的 MSL。

29.11 最后的处理

图2 9 - 2 6给出了 t c p _ i n p u t函数中首部预测失败时,较慢的执行路径中最后一部分的代
码,以及标注dropafterack。

图29-26 tcp_input 函数:最后的处理

1. SO_DEBUG 插口选项
1161-1162 如果选用了 S O _ D E B U G插口选项,则调用 t c p _ t r a c e向内核的环形缓存中添
加记录。回想一下,图 28-7中的代码同时保存了原有连接状态, IP和TCP的首部,因为函数有
可能改变这些值。
2. 调用tcp_output
1163-1168 如果needoutput标志置位(图29-6和图29-15),或者需要立即发送 ACK,则调
用tcp_output。

Linux公社 www.linuxidc.com
bbs.theithome.com
794 计计TCP/IP详解 卷 2:实现

3. dropafterack
1169-1179 只有当R S T标志未置位时,才会生成 A C K (带有R S T的报文段不会被确认 ),释
放保存接收报文段的 mbuf链,调用tcp_output立即发送ACK。
图29-27结束tcp_input函数。

图29-27 tcp_input 函数:最后的处理

4. dropwithreset
1180-1188 除了接收报文段也有RST,或者接收报文段是多播和广播报文段的情况之外,应发送
RST。绝不允许因为响应RST而发送新的RST,这将引起RST风暴(两个端点间连续不断地交换RST)。
此处的代码存在与图 2 8 - 1 6同样的错误:它不检查接收报文段的目的地址是否为
广播地址。
类似地,IN_MULTICAST的目的地址参数应转换为主机字节序。
5. RST报文段的序号和确认序号
1189-1196 RST报文段的序号字段值、确认字段值和 ACK标志取决于接收报文段中是否带
有ACK。
图29-28总结了生成 RST报文段中的这些字段。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 795
生成的RST报文段
接收到的报文段 序 号 值 确认序号 输出标志

带有ACK 接收到的确认字段 0 TH_RST


不带ACK 0 接收到的序号字段 TH_RST|TH_ACK

图29-28 生成RST报文段各字段的值

正常情况下,除了起始的 S Y N (图2 4 - 1 6 ),所有报文段都带有 A C K。t c p _ r e s p o n d的第


四个参数是确认序号,第五个参数是序号。
6. 拒绝连接
1192-1193 如果SYN置位,则 ti_len必须加1,从而生成 RST的确认字段比收到的 SYN报
文段的起始序号大 1。如果到达的 S Y N请求与不存在的服务器建立连接,会执行这一段代码。
此时,由于图 2 8 - 6中的代码找不到请求的 Internet PCB,控制跳转到 d r o p w i t h r e s e t。但为
了使发送的 R S T能被对端接受,报文段必须确认 S Y N (图2 8 - 1 8 )。卷1的1 8 . 1 4节举例说明了这
种类型的RST。
最后请注意,t c p _ r e s p o n d利用保存接收报文段的第一个 mbuf构造RST,并且释放链上
的其他mbuf。当第一个 mbuf最终到达设备驱动程序后,它也会被丢弃。
7. 释放临时创建的插口
1197-1199 如果在图28-7中为监听的服务器创建了临时的插口,但图 28-16中的代码发现接
收报文段有错误,它会置位 drop socket。如果出现了这种情况,插口在此处被释放。
8. 丢弃(不带ACK或RST)
1201-1206 如果接收报文段被丢弃,且不生成 A C K或R S T,则调用 t c p _ t r a c e。如果
S O _ D E B U G置位且生成了 A C K,则t c p _ o u t p u t将向内核的环形缓存中添加一条跟踪记录。
如果SO_DEBUG置位且生成了RST,系统不会为RST添加新的跟踪记录。
1207-1211 释放保存接收报文段的 mbuf链。如果dropsocket非零,则释放临时创建的插
口。

29.12 实现求精

为了加速 T C P处理而进行的优化与 U D P类似( 2 3 . 1 2节)。应利用复制数据计算检验和,并


避免在处理中多次遍历数据。 [Dalton et al. 1993]讨论了这些修订。
连接数增加时,对 TCP PCB的线性搜索也是一个处理瓶颈。 [McKenney and Dove 1992]讨
论了这个问题,利用哈希表替代了线性搜索。
[Partridge 1993]介绍了Van Jacobson开发的一个用于研究目的的协议实现,极大地减少了
T C P的输入处理。接收数据分组首先由 I P进行处理 ( R I S C系统中约有 2 5条指令 ),之后由分用
器(demultiplexer)寻找PCB(约10条指令),最后由TCP处理(约30条指令)。这30条指令完成了首
部预测,并计算伪首部检验和。如果数据报文段通过了首部预测,且应用进程正等待接收数
据,则复制数据到应用进程缓存,计算 T C P检验和并完成验证 (一次遍历中完成数据复制和检
验和计算)。如果TCP首部预测失败,则执行 TCP输入处理中较慢的路径。

29.13 首部压缩

下面介绍 T C P首部压缩。尽管首部压缩不是 T C P输入处理的一部分,但需要彻底了解 T C P


Linux公社 www.linuxidc.com
bbs.theithome.com
796 计计TCP/IP详解 卷 2:实现

的工作机制后,才能很好地理解首部压缩。 RFC 1144[Jacobson 1994a]中详细定义了首部压缩,


因为Van Jacobson首先提出了这一算法,通常也称为 VJ 首部压缩。本节的目的不是详细讨论
首部压缩的源代码 (RFC 1144给出了实现代码,其中有很好的注释,程序量与 tcp_output差
不多),而是概括性地介绍一下算法的思想。请注意区分首部预测 (28.4节)和首部压缩。

29.13.1 引言

多数的S L I P和P P P实现支持首部压缩。尽管首部压缩,在理论上,适用于任何数据链路,


但主要还是面向慢速串行链路。首部压缩只处理 TCP报文段—与其他的IP协议无关 (如ICMP、
I G M P、U D P等等)。它能够把I P / T C P组合首部从正常的 4 0字节压缩到只有 3字节,从而降低了
交互性应用,如远程登录或 Te l n e t中T C P报文段的大小,从典型的 4 1字节减少到只剩 4字节
—大大提高了慢速串行链路的效率。
串行链路的两端,每端都维护着两个连接状态表,一个用于数据报的发送,另一个用于
数据报的接收。每张表最多保存 256条记录,但典型的只有 16条,即同一时间内最多允许 16条
不同的 T C P连接执行首部压缩算法。每条记录中保存一个 8 bit的连接I D (限制记录数最多只能
为256)、某些标志和最近接收 /发送的数据报的未被压缩的首部。 96 bit的插口对可惟一确定一
条连接—源端IP地址和TCP端口、目的IP地址和TCP端口—这些信息都保存在未压缩的首
部中。图29-29举例说明了这些表的结构。
因为T C P连接是全双工的,在两个方向的数据流上都可执行首部压缩算法。连接两端必
须同时实现压缩和解压缩。同一条连接在两端的表中都会出现,如图 2 9 - 2 9所示。在这个例子
上部的两张表中,连接 I D等于1的表项的源端 I P地址都等于 1 2 8 . 1 . 2 . 3,源端 T C P端口号都等于
1 5 0 0,目的 I P地址等于 1 9 2 . 3 . 4 . 5,目的 T C P端口号都等于 2 5。在底部的两张表中,连接 I D等
于2的记录保存了同一条连接反方向数据流的信息。
发送连接状态表 接收连接状态表
标号 标志 最近的IP/TCP首部 标号 标志 最近的IP/TCP首部

接收连接状态表 发送连接状态表
标号 标志 最近的IP/TCP首部 标号 标志 最近的IP/TCP首部

图29-29 链路(如SLIP链路)两端的一组连接状态表

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 797
我们在图 2 9 - 2 9中利用数组表示这些表,但在源代码中,表项定义为一个结构,连接状态
表定义为这些结构组成的环形链表,最近一次用过的结构位于表头。
因为连接两端都保存了最近用过的未压缩的数据报首部,所以只需在链路上传送当前
数据报与前一数据报不同的字段 (及一个特殊的前导字节,指明后续的是哪一个字段 )。因
为某些首部字段在相邻的数据报之间不会变化,而其他的首部字段变化也很小,这种差分
处理是压缩算法的核心。首部压缩只适用于 I P 和T C P首部 — T C P报文段的数据部分不
变。
图29-30给出了发送方利用首部压缩算法,在串行链路上发送 IP数据报时采取的步骤。

待发送的 IP
数据报

非T C P报文或不可 I P型
检查数据报
压缩的 T C P报文
其他 TCP

找到 在连接表相应记
在连接表中寻找匹 C O M P R E S S E D _ T C P型
录中保存未压缩 压缩
配的96-bit插口对
的I P / T C P首部
无法 压缩
未找到

在连接表记录中
使用表中最老的一 U N C O M P R E S S E D _ T C P型
保存未压缩的
条记录
I P / T C P首部

图29-30 发送方采用首部压缩时的步骤

接收方必须能够识别下面 3种类型的数据报:
1) I P型数据报,前导字节的高位 4比特等于 4。这也是 I P首部中正常的 I P版本号 (图8 - 8 ),
说明链路上发送的是正常的、未压缩的数据报。
2) COMPRESSED_TCP型数据报,前导字节的最高位置为 1,类似于IP版本号介于 8和15之
间(剩余的 7 b i t由压缩算法使用 ),说明链路上发送的是压缩过的首部和未压缩的数据,接下来
我们还会谈到这种类型的数据报。
3) U N C O M P R E S S E D _ T C P型数据报,前导字节的高位 4比特等于 7,说明链路上发送的是
正常的、未压缩的数据报,但 I P的协议字段 (等于6,对T C P )被替换为连接 I D,接收方可据此
从连接状态表中找到正确的记录。
接收方查看数据报的第一个字节,即前导字节,确定其类型,实现代码参见图 5-13。图5-
1 6中,发送方调用 s l _ c o m p r e s s _ t c p确认T C P报文段是可压缩的,函数返回值与数据报首
字节逻辑或后,结果依然保存在首字节中。
图2 9 - 3 1列出了链路上传送的前导字节,其中 4位“ -”表示正常的 I P首部长度字段。 7位
“C、I、P、S、A、W和E”指明后续的是哪些可选字段,后面会简单地介绍这些字母的含
义。

Linux公社 www.linuxidc.com
bbs.theithome.com
798 计计TCP/IP详解 卷 2:实现

4 bit 版本号 4 bit 首部长度


链路上传送
的首字节

图29-31 链路上传送的前导字节

图29-32给出了使用压缩算法之后,不同类型的完整的 IP数据报。
IP数据报的头4bit

协议=TCP 0-65515字节的IP数据
非TCP报文

20-60字节的IP首部

TCP报文 协议=TCP 20-60字节的TCP首部 0-65495字节的TCP数据

20-60字节的IP首部

协议=连接 ID 20-60字节的TCP首部 0-65495字节的TCP数据

0-65495字节的TCP数据

3-16字节
压缩的TCP
首部

图29-32 采用首部压缩后的不同类型的 IP数据报

图中给出了两个 I P型数据报:一个携带了非 T C P报文段(如U D P、I C M P或I G M P协议报文


段),另一个携带了 T C P报文段。这是为了说明做为 I P型数据报发送的 T C P 报文段与做为
U N C O M P R E S S E D _ T C P型数据报发送的 TCP报文段间的差异:前导字节的高位 4比特互不相同,
类似于IP首部的协议字段。
如果IP数据报的协议字段不等于 TCP,或者协议是 TCP,但下列条件之一为真,都不会采
用首部压缩算法。
• 数据报是一个IP分片:分片偏移量非零或者分片标志置位;
• SYN 、FIN或RST中的任何一个置位;
• ACK 标志未置位。
上述3个条件中只要有一个为真,都将作为 IP型数据报发送。
此外 , 即 使 数据 报 携 带 了可 压缩的 T C P 报 文段 , 压 缩 算法 也 可 能 失败 ,生成
U N C O M P R E S S E D _ T C P型的数据报。可能因为当前数据报与连接上发送的上一个数据报比较
时,有些特殊字段发生了变化,而正常情况下,对于给定的连接,它们应该不变,从而导致
压缩算法无法反映存在的变化。例如, TO S字段,分片标志位。此外,如果某些字段数值的

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 799
差异超过65535,压缩算法也会失败。

29.13.2 首部字段的压缩

下面介绍如何压缩图 2 9 - 3 3中给出的 I P和T C P的首部字段,阴影字段指对于给定连接,正


常情况下不会发生变化的字段。

0 15 16 31

4-bit 4-bit 8-bit


16-bit总长(字节)
版本号 首部长度 服务类型(TOS)

3-bit
16-bit标识符 13-bit分片偏移量
标志

8-bit存活时间( TTL) 8-bit协议 16-bit首部检验和 20字节

32-bit源IP地址

32-bit目的IP地址

16-bit源端口号 16-bit目的端口号

32-bit序号

20字节
32-bit确认序号

4-bit首部 U A P P S F
保留(6 bit) R C S S Y I 16-bit窗口大小
长度 G K H T N N

16-bit TCP检验和 16-bit紧急指针

图29-33 组合的IP和TCP首部:阴影字段通常不变化

如果连接上发送的前一个报文段与当前报文段之间,有阴影字段发生变化,则压缩算法
失败,报文段被直接发送。图中未列出 I P和T C P选项,但如果它们存在,且这些选项字段发
生了变化,则报文段也不压缩,而被直接发送 (习题29.7)。
如果阴影字段均未变化,即使算法只传输非阴影字段,也会节省 5 0 %的传输容量。 V J首
部压缩甚至做得更好,图 29-34给出了压缩后的 IP/TCP首部格式。
最小的压缩后的 IP/TCP首部只有 3个字节:第一个字节 (标志比特 ),加上16 bit的TCP检验
和。为了防止可能的链路错误,一般不改动 T C P检验和 ( S L I P不提供链路层的检验和,尽管
PPP提供一个)。

Linux公社 www.linuxidc.com
bbs.theithome.com
800 计计TCP/IP详解 卷 2:实现

字节数 标志位:链路上发送的第一个字节

如果C=1:连接ID

TCP检验和不变 16-bit TCP检验和,必选

如果U=1:TCP紧急数据偏移量

如果W=1:TCP窗口大小差值

如果A=1:TCP确认序号差值

如果S=1:TCP序号差值

如果I=1:IP标识符差值

数据不变

图29-34 压缩后的 IP/TCP首部格式

其他的6个字段: connid、u rg o f f、 win、 ack、 seq和 ipid,都是可选的。图 29-34的最


左侧列出了各字段压缩后所需的字节数。读者可能认为压缩后的首部最大应占用 1 9字节,但
实际上压缩后的首部中 4 bit的SAWU绝不可能同时置位,因此,压缩首部最大为 16字节,后面
我们还会详细讨论这个问题。
第一个字节的最高位比特必须设为 1,说明这是 C O M P R E S S E D _ T C P型的数据报。其余 7
bit中的6个规定了后续首部中存在哪些可选字段,图 29-35小结了这7 bit的用法。

标志比特 描 述 结构变量 标志等于 0说明 标志等于1说明

C 连接ID 连接ID不变 connid=连接ID


I IP标识符 ip_id i p _ i d已加1 i p i d=IP标识符差值
P TCP推标志 P S H标志清除 P S H标志置位
S TCP序号 th_seq t h _ s e q不变 s e q=TCP序号差值
A TCP确认序号 th_ack t h _ a c k不变 a c k=TCP确认序号差值
W TCP窗口 th_win t h _ w i n不变 w i n=TCP窗口字段差值
U TCP紧急数据偏移量 th_urg URG标志未置位 u r g o f f=紧急数据偏移量

图29-35 压缩首部中的7个标志比特

C 如果C比特等于 0,则当前报文段与前一报文段 (无论是压缩的或非压缩的 )具有相同的


连接ID。如果等于 1,则connid将等于连接 ID,其值位于 0~255之间。
I 如果 I比特等于 0,当前报文段的 I P标识符较前一报文段加 1 (典型情况 )。如果等于 1,
ipid等于ip_id的当前值减去它的前一个值。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 801
P 这个比特复制自 T C P报文段中的 P S H标志位。因为 P S H标志不同于其他的正常方式,
必须在每个报文段中明确地定义这一标志。
S 如果S比特等于 0,T C P序号不变。如果等于 1, s e q等于t h _ s e q的当前值减去它的前
一 个值。
A 如果A比特等于 0,T C P确认序号不变 (典型情况 )。如果等于 1,∆a c k等于t h _ a c k的当
前值减去它的前一个值。
W 如果W比特等于 0,T C P窗口大小不变。如果等于 1, w i n等于t h _ w i n的当前值减去
它的前一个值。
U 如果U比特等于 0,报文段的U R G标志未置位,紧急数据偏移量不变 (典型情况)。如果
等于1,说明URG标志置位,urgoff等于th_urg的当前值。如果URG标志未置位时,
紧急数据偏移量发生改变,报文段将被直接发送 (这种现象通常发生在紧急数据传送
完毕后的第一个报文段 )。
通过字段的当前值减去它的前一个值,得到需传输的差值。正常情况下,得到的是一个
小正数( win是个例外)。
请注意,图 29-34中有5个字段的长度可变,可占用 0、1或3字节。
0字节:对应标志未置位,此字段不存在;
1字节:发送值在 1~255之间,只需占用 1字节;
3字节:如果发送值等于 0或者在256~65535之间,则需要用 3个字节才能表示:第一个字节
全0,后两个字节保存实际值。这种方法一般用于 3个16 bit的值:u rg o f f、∆w i n和∆i p i d。但如
果两个32 bit字段 ack和∆seq的差值小于 0或者大于65535,报文段将被直接发送。
如果把图29-33中不带阴影的字段与图 29-34中可能的传输字段进行比较,会发现有些字段
永远不会被传输。
• IP总长度字段不会被传输,因为绝大多数链路层向接收方提供接收数据分组的长度。
• 因为I P首部中被传输的惟一字段是 16 bit的I P标识符, I P检验和被忽略。因为它只在一
段链路上保护IP首部,每次转发都会被重新计算。

29.13.3 特殊情况

算法检查输入报文段,如果出现两种特定情况,则用前导字节的低位 4比特 — S AW U
—的两种特殊组合,分别加以表示。因为紧急数据很少出现,如果报文段中 URG标志置位,
并且与前一报文段相比,序号与窗口字段都发生了变化 (意味着低位 4比特应为 1 0 11或1111 ),
此种报文段会跳过压缩算法,被直接发送。因此,如果低位 4比特等于 1 0 11 ( 称为 *S A)或
1111(称为*S),就说明出现了下面两种特定情况:
*SA 序号与确认序号都增加,差值等于前一报文段的数据量,窗口大小与紧急数据偏移
量不变,URG标志未置位。采用这种表示法可以避免传送 seq和 ack。
如果对端回送终端数据,那么两个传输方向上的数据报文段中都会经常出现这一现
象。卷1的图19-3和图19-4,举例说明了远程登录应用中出现的这种类型的数据。
*S 序号增加,差值等于前一报文段的数据量,确认序号、窗口大小与紧急数据偏移量
均不变,URG标志未置位。采用这种表示法可以避免传送 ∆seq。
这种类型的数据通常出现在单向数据传输 (如F T P )的发送方。卷 1的图2 0 - 1、图2 0 - 2

Linux公社 www.linuxidc.com
bbs.theithome.com
802 计计TCP/IP详解 卷 2:实现

和图2 0 - 3举例说明了这种类型的数据传输。此外,如果对端不回送终端数据,那么
在数据发送方的数据报文段中也会出现这种现象。

29.13.4 实例

下面的两个例子,在图 1 - 1 7中的b s d i和s l i p两个系统间,利用 S L I P链路传输数据。这


条SLIP链路在两个传输方向上都采用了首部压缩算法。在主机 b s d i上运行t c p d u m p程序(卷1
的附录 A ),保存所有数据帧的备份。这个程序还支持一个选项,能够输出压缩后的首部,列
出图29-34中的所有字段。
在主机间已建立了两条连接:一条远程登录连接,另一条是从 b s d i到s l i p的文件传输
(FTP)。图29-36列出了两条连接上不同类型数据帧出现的次数。

远程登录 FTP
帧 类 型
输入 输出 输入 输出

IP 1 1 5 5
UNCOMPRESSED_TCP 3 2 2 3
COMPRESSED_TCP
特殊情况*SA 75 75 0 0
特殊情况*S 25 1 1 325
一般情况 9 93 337 13
总数 113 172 345 346

图29-36 远程登录和 FTP连接上,不同类型数据帧出现的次数

远程登录连接中,在两个传输方向上, *SA都出现了 75次,从而证明了在对端回显终端流


量时,这一特定情况在两个传输方向上都会经常出现。 F T P连接中,在数据的发送方, *S出
现了325次,也证明了对于单向数据传输,这一特定情况会经常出现在数据的发送方。
F T P连接中, I P型的数据帧出现了 1 0次,对应于 4个带有 S Y N的报文段,和 6个带有F I N的
报文段。FTP使用了两条连接:一条用于传输交互式命令,另一条用于文件传输。
U N C O M P R E S S E D _ T C P型数据帧一般对应于连接建立后的第一个报文段,即同步连接 I D
的报文段。这两个例子中还有少量的其他类型的报文段,主要用于服务类型设定 (Net/3中的远
程登录及FTP客户及服务器都是在连接建立后才设定 TOS字段)。

远程登录 FTP

字节数 输入 输出 输入 输出
3 102 44 2 250
4 94 78
5 7 12 5 2
6 6 325 5
7 13 2 1
8 1
9 4 1
总数 109 169 338 338

图29-37 压缩首部大小的分布

Linux公社 www.linuxidc.com
bbs.theithome.com
第 29章 TCP的输入(续)计计 803
图2 9 - 3 7给出了压缩首部大小的分布情况,后 4栏中压缩首部的平均大小为分别等于 3 . 1、
4 . 1、6 . 0和3 . 3字节,与原来的 4 0字节相比,大大提高了系统的传输效率,尤其对于交互式连
接,效果更加明显。
在FTP输入一栏中,压缩首部大小为 6字节的报文段有 325个,其中绝大多数只携带了值等
于2 5 6的 a c k字段,因为 2 5 6大于2 5 5,所以必须用 3个字节表示。 S L I P M T U等于2 9 6,因此,
T C P采用了2 5 6的M S S。在F T P输出一栏中,压缩首部大小为 3字节的报文段有 2 5 0个,其中绝
大多数都代表 *S类的特定情况 (只有序号发生变化 ),差值等于2 5 6。但因为*S的序号差值默认
为前一报文段的数据量,所以只需传输前导字节和 TCP检验和。在FTP输出一栏中, 78个压缩
首部大小为 4字节的报文段也属于同一情况,只不过 IP标识符也发生了变化 (习题29.8)。

29.13.5 配置

对给定的 S L I P或P P P链路,首部压缩必须被选定后才能起作用。配置 S L I P链路接口时,


一般可设定两个标志:首部压缩标志和自动首部压缩标志。配置命令是 i f c o n f i g,分别带
选项l i n k 0和l i n k 2。正常情况下,由客户端 (拨号主机 )决定是否采用首部压缩算法,服务
器(客户通过拨号接入的主机或终端服务器 )只选择是否置位自动首部压缩标志。如果客户选用
了首部压缩算法,它的 T C P首先发送一个 U N C O M P R E S S E D _ T C P型的数据报,规定连接 I D。
如果服务器收到这个数据报,它也开始采用首部压缩算法 (服务器处于自动方式 );如果未收到
这个数据报,服务器绝不会在这条链路上采用首部压缩。
PPP允许在链路建立时,连接双方共同协商传输选项,其中的一个选项即是否支持首部压
缩算法。

29.14 小结

本章结束了我们对 T C P输入处理的详细介绍。首先介绍了如果连接在 S Y N _ R C V D状态时


收到了ACK,该如何处理,即如何完成被动打开、同时打开或自连接。
快速重传算法指 T C P在连续收到的重复 A C K数超过规定的门限值后,能够检测到丢失的
报文段并进行重发,即使重传定时器还未超时。 N e t / 3结合了快速重传算法与快速恢复算法,
执行拥塞避免算法而非慢起动,尽量保证发送方到接收方的数据流不中断。
A C K处理负责从插口的发送缓存中丢弃已确认的数据,并且在收到的 A C K会改变连接当
前状态时,对一些 TCP状态做特殊处理。
处理接收报文段的 U R G标志,如果置位,则通过 T C P紧急方式的处理,提取带外数据。
这一操作是非常复杂的,因为应用进程可以利用正常的数据流缓存,或者特殊的带外数据缓
存接收带外数据,而且 TCP收到URG时,紧急指针所指向的数据可能还未到达。
T C P输入处理结束时,会调用 T C P _ R E A S S,提取报文段中的数据放入插口的接收缓存或
重组队列,处理 F I N标志,并且在接收报文段需要响应时,调用 t c p _ o u t p u t输入响应报文
段。
T C P首部压缩是用于 S L I P和P P P链路的一种技术,能够把 I P和T C P首部长度从 4 0字节减少
到约为 3 ~ 6字节(典型情况 )。这是因为对于给定连接,相邻两个报文段之间,首部的多数字段
不会改变,即使有些字段的值发生了变化,其差值也很小,从而可以通过前导字节中的标志比
特,指明哪些字段发生了变化,在后续部分只传输这些字段的当前值与前一报文段间的差值。

Linux公社 www.linuxidc.com
bbs.theithome.com
804 计计TCP/IP详解 卷 2:实现

习题

29.1 客户与服务器建立连接,不考虑报文段丢失,哪一个应用进程,客户或服务器,首
先完成连接建立过程?
29.2 N e t / 3系统中,监听服务器收到了一个 S Y N,它同时携带了 5 0字节的数据。会发生
什么?
29.3 继续前一个习题,假定客户没有重传 5 0字节的数据,而是在对服务器 S Y N / A C K报
文段的确认中置位 FIN标志,会发生什么?
29.4 N e t / 3客户向服务器发送 S Y N,服务器响应 S Y N / A C K,其中还携带了 5 0字节的数据
和FIN标志。列出客户端 TCP的处理步骤。
29.5 卷1的图18-19和RFC 793的图14,都给出了出现同时关闭时,连接双方交换的 4个报
文段。但如果连接两端都是 N e t / 3系统,出现同时关闭时,或者一个 N e t / 3系统的自
连接关闭时,彼此将交换 6个报文段,而不是 4个,多余出两个报文段是因为连接两
端各自收到对端的 FIN后,将向对端重发 FIN。问题出在什么地方,如何解决?
29.6 RFC 793第7 2页建议,如果发送缓存中的数据已被对端确认,“应给用户一个确认,
指明缓存中已发送且被确认的数据 (例如,发送缓存返回时应带有‘ O K’响应 )”。
Net/3是否提供了这种机制?
29.7 RFC 1323中定义的选项对 TCP首部压缩有何影响?
29.8 Net/3对IP标识符字段的赋值方式,对 TCP首部压缩有何影响?

Linux公社 www.linuxidc.com
bbs.theithome.com
第30章 TCP的用户需求
30.1 引言

本章介绍 T C P的用户请求处理函数 t c p _ u s r r e g,它被协议的 p r _ u s r r e q函数调用,处


理各种与 T C P插口有关的系统调用。此外,还将介绍 t c p _ c t l o u t p u t ,应用进程调用
setsockopt设定TCP 插口选项时,会用到它。

30.2 tcp_usrreq函数

T C P的用户请求函数用于处理多种操作。图 3 0 - 1给出了 t c p _ u s r r e q函数的基本框架,


其中s w i t c h的语句体部分将在后续部分逐一展开。图 1 5 - 1 7中列出了函数的参数,其具体含
义取决于所处理的用户请求。

图30-1 tcp_usrreq 函数体

Linux公社 www.linuxidc.com
bbs.theithome.com
806 计计TCP/IP详解 卷 2:实现

图30-1 (续)

1. in_control处理ioctl请求
45-58 PRU_CONTROL请求来自于 ioctl系统调用,函数 in_control负责处理这一请求。
2. 控制信息无效
59-64 如果试图调用 s e n d m s g,为 TCP 插口配置控制信息,代码将释放 m b u f,并返回
EINVAL差错代码,声明这一操作无效。
65-66 函数接着执行splnet。这种做法极为保守,因为并非在所有情况下都需要锁定,只
是为了防止在 c a s e语句中单个地调用 s p l n e t。我们在图 2 3 - 1 5中曾提到,调用 s p l n e t设定
处理器的优先级,唯一的作用是阻止软中断执行 I P输入处理 (它会接着调用 t c p _ i n p u t),但
却无法阻止接口层接收输入数据分组并放入到 IP的输入队列中。
通过指向插口结构的指针,可得到指向 Internet PCB 的指针。只有在应用进程调用
socket系统调用,发出 PRU_ATTACH请求时,该指针才允许为空。
67-81 如果 i n p非空,当前连接状态将保存在 o s t a t e中,以备函数结束时可能会调用
tcp_trace。
下面我们开始讨论单独的 c a s e语句。应用进程调用 s o c k e t系统调用,或者监听服务器
收到连接请求 (图2 8 - 7 ),调用 s o n e w c o n n函数时,都会发出 P R U _ A T T A C H请求,图 3 0 - 2给出
了这一请求的处理代码。

图30-2 tcp_usrreq 函数:PRU_ATTACH 和PRU_DETACH 请求

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 807

图30-2 (续)

3. PRU_ATTACH请求
83-94 如果插口结构已经指向某个 PCB,则返回EISCONN差错代码。调用 t c p _ a t t a c h完
成处理:分配并初始化 Internet PCB和TCP控制块。
95-96 如 果 选 用 了 S O _ L I N G E R 插 口 选 项 , 且 拖 延 时 间 为 0, 则 将 其 设 为 1 2 0
(TCP_LINGERTIME)。
为什么在 P R U _ A T T A C H请求发出之前,就可以设定插口选项?尽管不可能在调
用s o c k e t之前就设定插口选项,但 s o n e w c o n n也会发送 P R U _ A T T A C H请求。它在
把监听插口的 s o _ o p t i o n s复制到新建插口之后,才会发送 P R U _ A T T A C H请求。此
处的代码防止新建连接从监听插口中继承拖延时间为 0的SO_LINGER选项。
请注意,此处的代码有错误。常量 T C P _ L I N G E R T I M E在t c p _ t i m e r.h中初始
化为120,该行的注释为“最多等待 2分钟”。但S O _ L I N G E R值也是内核 t s l e e p函数
(由s o c l o s e调用)的最后一个参数,从而成为内核的 t i m e o u t函数的最后一个参数,
单位为滴答,而非秒。如果系统的滴答频率 ( h z )等于1 0 0,则拖延时间将变为 1 . 2秒,
而非2分钟。
97 现在,t p已指向插口的 T C P控制块。这样,如果选定了 S O _ D E B U G插口选项,函数结束
时就可以输出所需信息。
4. PRU_DETACH请求
99-111 c l o s e系统调用在 P R U _ D I S C O N N E C T请求失败后,将发送 P R U _ D E T A C H请求。如
果连接尚未建立 (连接状态小于 ESTABLISHED),则无需向对端发送任何信息。但如果连接已
建立,则调用 t c p _ d i s c o n n e c t初始化 T C P的连接关闭过程 (发送所有缓存中的数据,之后
发送FIN)。
代码if语句的测试条件要求状态大于LISTEN,这是不正确的。因为如果连接状态
等于SYN_SENT或者SYN_RCVD,两者都大于LISTEN,此时tcp_disconnect会直
接调用tcp_close。实际上,这个case语句可以简化为直接调用tcp_disconnect。

Linux公社 www.linuxidc.com
bbs.theithome.com
808 计计TCP/IP详解 卷 2:实现

图30-3给出了bind和listen系统调用的处理代码。

图30-3 tcp_usrreq 函数:PRU_BIND 和PRU_LISTEN 请求

112-119 PRU_BIND请求的处理只是简单地调用 in_pcbbind。


120-128 对于 P R U _ L I S T E N 请求,如果插口还未绑定在某个本地端口上,则调用
i n _ p c b b i n d自动为其分配一个。这种情况十分少见,因为多数服务器会明确地绑定一个知
名端口,尽管 R P C (远端过程调用 )服务器一般是绑定在一个临时端口上,并通过 Port Mapper
向系统注册该端口 (卷1的2 9 . 4节介绍了 Port Mapper)。连接状态变迁到 L I S T E N,完成了 l i s t e n
调用的主要目的:设定插口的状态,以便接受到达的连接请求 (被动打开)。
图30-4给出了connect系统调用的处理代码:客户发起的主动打开。

图30-4 tcp_usrreq 函数:PRU_CONNECT 请求

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 809

图30-4 (续)

5. 分配临时端口
129-141 如果插口还未绑定在某个本地端口上,调用 i p _ p c b b i n d自动为其分配一个。对
于客户端,这是很常见的,因为客户一般不关心本地端口值。
6. 连接PCB
142-144 调用i n _ p c b c o n n e c t,获取到达目的地的路由,确定外出接口,验证插口对不
重复。
7. 初始化IP和TCP首部
145-150 调用tcp_template分配mbuf,保存IP和TCP的首部,并初始化两个首部,填入
尽可能多的信息。会造成函数失败的唯一原因是内核耗尽了 mbuf。
8. 计算窗口缩放因子
151-154 计算用于接收缓存的窗口缩放因子:左移 65535(TCP_MAXWIN),直到它大于或等
于接收缓存的大小 (s o _ r c v . s b _ h i w a t)。得到的位移次数 (0~14之间),就是需要在SYN中发
送的缩放因子值 (图2 8 - 7处理被动打开时,有相同的代码 )。应用进程必须在调用 c o n n e c t之
前,设定 S O _ R C V B U F 插口选项, T C P才会在 S Y N中添加窗口大小选项,否则,将使用接收
缓存大小的默认值 (图24-3中的tcp_recvspace)。
9. 设定插口和连接的状态
155-158 调用soisconnecting,置位插口状态变量中恰当的比特,设定 TCP连接状态为
SYN_SENT,从而在后续的tcp_output调用中发送SYN(参见图24-16的tcp_outlags值)。连接
建立定时器启动,时限初始化为75秒。tcp_output还会启动SYN的重传定时器,如图25-16所示。
10. 初始化序号
159-161 令 初 始 序 号 等 于 全 局 变 量 t c p _ i s s , 之 后 令 t c p _ i s s 增 加 64 000
(T C P _ I S S I N C R除以2 )。在监听服务器收到 S Y N并初始化 I S S时(图2 8 - 1 7 ),对t c p _ i s s的相
同的操作。接着调用 tcp_sendseqinit初始化发送序号。
11. 发送初始SYN
162 调用tcp_output发送初始SYN,以建立连接。如果tcp_output返回错误(例如,mbuf
耗尽或没有到达目的地的路由),该差错代码将成为 tcp_usrreq的返回值,报告给应用进程。

Linux公社 www.linuxidc.com
bbs.theithome.com
810 计计TCP/IP详解 卷 2:实现

图30-5给出了PRU_CONNECT2、PRU_DISCONNECT和PRU_ACCEPT请求的处理代码。
164-169 PRU_CONNECT2请求,来自于socketpair系统调用,对TCP协议无效。
170-183 c l o s e系统调用会发送 P R U _ D I S C O N N E C T请求。如果连接已建立,应调用
tcp_disconnect,发送FIN,执行正常的TCP关闭操作。

图30-5 tcp_usrreq 函数:PRU_CONNECT2 、PRU_DISCONNECT 和PRU_ACCEPT 请求

请注意以“应该实现”起头的注释,这是因为无法接着使用出现错误的插口。
例如,客户调用 c o n n e c t,并得到一个错误,它就无法在同一个插口上再次调用
c o n n e c t,而必须首先关闭插口,调用 s o c k e t创建新的插口,在新的插口上才能
再次调用connect。
184-191 与a c c e p t系统调用有关的工作全部由插口层和协议层完成。 P R U _ A C C E P T请求
只简单地向应用进程返回对端的 IP地址和端口号。
图30-6给出了PRU_SHUTDOWN、PRU_RCVD和PRU_SEND请求的处理代码。
12. PRU_SHUTDOWN请求
192-200 应用进程调用 s h u t d o w n ,禁止更多的输出时, s o s h u t d o w n 会发送
P R U _ S H U T D O W N请求。调用 s o c a n t s e n d m o r e置位插口的标志,禁止继续发送报文段。接
着调用 t c p _ u s r c l o s e d,根据图 24-15的状态变迁图,设定正确的连接状态。 t c p _ o u t p u t
发送FIN之前,如果发送缓存中仍有数据,会首先发送等待数据。

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 811

图30-6 tcp_usrreq 函数:PRU_SHUTDOWN 、PRU_RCVD 和PRU_SEND 请求

13. PRU_RCVD请求
201-206 应用进程从插口的接收缓存中读取数据后, soreceive会发送这个请求。此时接
收缓存已扩大,也许会有足够的空间,让 T C P发送更大的窗口通告。 t c p _ o u t p u t会决定是
否需要发送窗口更新报文段。
14. PRU_SEND请求
207-214 图2 3 - 1 4中给出的 5个写函数,都以这一请求结束。调用 s b a p p e n d,向插口的发
送缓存中添加数据 (它将一直保存在缓存中,直到被确认 ),并调用 t c p _ o u t p u t发送新报文
段(如果条件允许)。
图30-7给出了PRU_ABORT和PRU_SENSE请求的处理代码。

图30-7 tcp_usrreq 函数:PRU_ABORT 和PRU_SENSE 请求

15. PRU_ABORT请求
215-220 如果插口是监听插口 (如服务器 ),并且存在等待建立的连接,例如已发送初始

Linux公社 www.linuxidc.com
bbs.theithome.com
812 计计TCP/IP详解 卷 2:实现

S Y N或已完成三次握手过程,但还未被服务器 a c c e p t的连接,调用 s o c l o s e会导致发送


PRU_ABORT请求。如果连接已同步, tcp_drop将发送RST。
16. PRU_SENSE请求
221-224 f s t a t系统调用会生成 P R U _ S E N S E请求。 T C P返回发送缓存的大小,保存在
stat结构的成员变量 st_blksize中。
图3 0 - 8给出了 P R U _ R C V O O B的处理代码。当应用进程置位 M S G _ O O B标志,试图读取带外
数据时,soreceive会发送这一请求。

图30-8 tcp_usrreq 函数:PRU_RCVOOB 请求

17. 能否读取带外数据
225-232 如果下列3个条件有一个为真,应用进程读取带外数据的努力就会失败。
1) 如果插口的带外数据分界点 (s o _ o o b m a r k)等于0,并且插口的 S S _ R C V A T M A R K标志
未设定;或者
2) 如果SO_OOBINLINE插口选项设定;或者
3) 如果连接的 TCPOOB_HADDATA标志设定(例如,连接的带外数据已被读取 )。
如果上述3个条件中任何一个为真,则返回差错代码 EINVAL。
18. 是否有带外数据到达
233-236 如果上述3个条件全假,但 TCPOOB_HAVEDATA标志置位,说明尽管 TCP已收到了
对端发送的紧急方式通告,但尚未收到序号等于紧急指针减 1的字节(图29-17),此时返回差错
代码E W O U L D B L O C K,有可能因为发送方发送紧急数据通告时,紧急数据偏移量指向了尚未
发送的字节。卷 1的图2 6 - 7举例说明了这种情况,发送方的数据传输被对端的零窗口通告停止
时,常出现这种现象。
19. 返回带外数据字节
237-238 tcp_pulloutofband向应用进程返回存储在 t_iobc中的一个字节的带外数据。
20. 更新标志
239-241 如果应用进程已读取了带外数据(而不是仅大致了解带外数据的情况, MSG_PEEK标
志置位),TCP清除HAVE标志,并置位 HAD标志。case语句执行到此处时,通过前面的代码可

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 813
以肯定,HAVE标志已置位,而 HAD标志被清除。置位HAD标志的目的是防止应用进程试图再次
读取带外数据。一旦HAD标志置位,在收到新的紧急指针之前,它不会被清除 (图29-17)。
代码使用了让人费解的异或运算,而不是简单的
tp->t_oobflags = TCPOOB_HADDATA;

是为了能够在 t _ o o b f l a g s中定义更多的比特。但 N e t / 3中,实际只用到了上面


提及的两个标志比特。
图30-9中的P R U _ S E N D O O B请求,是在应用进程写入数据并置位 M S G _ O O B时,由s o s e n d
发送的。

图30-9 tcp_usrreq 函数:PRU_SENDOOB 请求

21. 确认发送缓存中有足够空间并添加新数据
242-247 发送带外数据时,允许应用进程写入数据后,待发送数据量超过发送缓存大小,
超出量最多为 5 1 2字节。插口层的限制要宽松一些,写入带外数据后,最多可超出发送缓存
1024字节(图16-24)。调用sbappend向发送缓存末端添加数据。
22. 计算紧急指针
248-257 紧急指针 (s n d _ u p)指向写入的最后一个字节之后的字节。图 2 6 - 3 0举例说明了这
一点,假定发送缓存为空,应用进程写入 3字节的数据,且置位了 M S G _ O O B标志。这是考虑
到若应用进程置位 M S G _ O O B标志,且写入的数据量超过 1字节,如果接收方为伯克利系统,
则只有最后一个字节会被认为是带外数据。
23. 强制TCP输出
258-261 令t _ f o r c e等于1,并调用 t c p _ o u t p u t。即使收到了对端的零窗口通告, T C P
也会发送报文段, U R G标志置位,紧急指针偏移量非零。卷 1的图2 6 - 7说明了如何向一个关闭
的接收窗口发送紧急报文段。
图30-10给出了最后 3个请求的处理。

Linux公社 www.linuxidc.com
bbs.theithome.com
814 计计TCP/IP详解 卷 2:实现

图30-10 tcp_usrreq 函数:PRU_SOCKADDR 、PRU_PEERADDR 和PRU_SLOWTIMO 请求

262-267 g e t s o c k n a m e 和g e t p e e r n a m e 系统调用分别发送 P R U _ S O C K A D D R 和
P R U _ P E E R A D D R请求。调用i n _ s e t s o c k a d d r和i n _ s e t p e e r a d d r函数,从 PCB中获取需
要信息,存储在 addr参数中。
268-275 执行 t c p _ s l o w t i m o函数会发送 P R U _ S L O W T I M O函数。如同注释所指出的,
t c p _ s l o w t i m o 不直接调用 t c p _ t i m e r s 的唯一原因是为了能够在函数结尾处调用
t c p _ t r a c e,跟踪记录定时器超时事件 (图3 0 - 1 )。为了在记录中指明是 4个T C P定时器中的哪
一个超时,t c p _ s l o w t i m o通过n a m参数传递了 t _ t i m e r数组(图25-1)的指针,并左移 8位后
与请求值(req)逻辑或。trpt程序了解这种做法,并据此完成相应的处理。

30.3 tcp_attach函数

t c p _ a t t a c h函数,在处理P R U _ A T T A C H请求(例如,插口系统调用,或者监听插口上收
到了新的连接请求 )时,由tcp_usrreq调用。图30-11给出了它的代码。
1. 为发送缓存和接收缓存分配资源
361-372 如果还未给插口的发送和接收缓存分配空间, sbreserve将两者都设为8192,即
全局变量 tcp_sendspace和tcp_recvspace的默认值(图24-3)。
这些默认值是否够用,取决于连接两个传输方向上的 MSS,后者又取决于 MTU。
例如,[Comer and lin 1994]论证了,如果发送缓存小于 3倍的M S S,则会出现异常,
严重降低系统性能。某些实现定义的默认值很大,如 61 444字节,已考虑到这些默认
值对性能的影响,尤其对较大的 MTU(如FDDI和ATM)更是如此。
2. 分配Internet PCB和TCP控制块
373-377 in_pcballoc分配Internet PCB,而tcp_newtcpcb分配TCP控制块,并将其与
对应的PCB相连。
3 7 8 - 3 8 4 如果t c p _ n e w t c p c b调用m a l l o c时失败,则执行注释为“ XXX”的代码。前面
已介绍过, P R U _ A T T A C H请求是插口系统调用或监听插口收到新的连接请求 (s o n e w c o n n)的
结果。对于后一种情况,插口标志 S S _ N O F D R E F置位。如果此标志置位, i n _ p c b a l l o c调
用s o f r e e时会释放插口结构。但我们在 t c p _ i n p u t中看到,除非该函数已完成接收报文段

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 815
的处理 ( 图 2 9 - 2 7 中的 d r o p s o c k e t 标志 ) ,否则,不应释放插口结构。因此,调用
i n _ p c b d e t a c h 时,应将 S S _ N O F D R E F 标志的当前值保存在变量 n o f d 中,并在
tcp_attach返回前重设该标志。
385-386 TCP连接状态初始化为 CLOSED。

图30-11 tcp_attach 函数:创建新的 TCP插口

30.4 tcp_disconnect函数

图30-12给出的tcp_disconnect函数,准备断开 TCP连接。
1. 连接未同步
396-402 如果连接还未进入 ESTABLISHED状态(如L I S T E N、S Y N _ S E N T或S Y N _ R C V D ),
tcp_close只释放PCB和TCP控制块。无需向对端发送任何报文段,因为连接尚未同步。
2. 硬性断开
403-404 如果连接已同步,且 S O _ L I N G E R插口选项置位,拖延时间 (S O _ L I N G E R)设为零,
则调用 t c p _ d r o p丢弃连接。连接不经过 T I M E _ W A I T,直接更新为 C L O S E D,向对端发送
R S T,释放P C B和T C P控制块。调用 c l o s e会发送P R U _ D I S C O N N E C T请求,丢弃仍在发送或
接收缓存中的任何数据。
如果SO_LINGER插口选项置位,且拖延时间非零,则调用 soclose进行处理。
3. 平滑断开
405-406 如果连接已同步,且 S O _ L I N G E R选项未设定,或者选项设定且拖延时间不为零,

Linux公社 www.linuxidc.com
bbs.theithome.com
816 计计TCP/IP详解 卷 2:实现

则执行TCP正常的连接终止步骤。 soisdisconnecting设定插口状态。

图30-12 tcp_disconnect 函数:准备断开 TCP连接

4. 丢弃滞留的接收数据
407 调用s b f l u s h,丢弃所有滞留在接收缓存中的数据,因为应用进程已关闭了插口。发
送缓存中的数据仍保留, t c p _ o u t p u t将试图发送剩余的数据。我们说“试图”,因为不能保
证数据还能成功地被发送。在收到并确认这些数据之前,对端可能已崩溃,即使对端的 T C P
模块能够接收并确认这些数据,在应用程序读取数据之前,系统也可能崩溃。因为本地进程
已关闭了插口,即使 TCP放弃发送仍滞留在发送缓存中的数据 (因为重传定时器最终超时 ),也
无法向应用进程通告错误。
5. 改变连接状态
408-410 t c p _ u s r c l o s e d基于连接的当前状态,促使其进入下一状态。通常情况下,连
接将转移到 FIN_WAIT_1状态,因为连接关闭时一般都处于 ESTABLIDHED状态。后面会看到,
t c p _ u s r c l o s e d通常返回当前控制块的指针 (t p)。因为状态必须先同步才会执行此处的代
码,所以总需要调用 t c p _ o u t p u t 发送报文段。如果连接从 E S TA B L I S H E D 转移到
FIN_WAIT_2,将发送 FIN。

30.5 tcp_usrclosed函数

图30-13给出的这个函数,在 PRU_SHUTDOWN处理中,由 tcp_disconnect调用。


1. 未收到SYN时的简单关闭
429-434 如果连接上还未收到 S Y N ,则无需发送 F I N 。新的状态等于 C L O S E D,
tcp_close将释放Internet PCB和TCP控制块。
2. 转移到FIN_WAIT_1状态
435-438 如果连接当前状态等于 S Y N _ R C V D 和 E S TA B L I S H E D,新的状态将等于
FIN_WAIT_1,再次调用 tcp_output时,将发送 FIN(图24-16中的tcp_outflags值)。
3. 转移到LAST_ACK状态

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 817
439-441 如果连接当前状态等于 C L O S E _ WA I T,新状态等于 L A S T _ A C K,则再次调用
tcp_output时,将发送 FIN。
443-444 如果连接当前状态等于 FIN_WAIT_2或TIME_WAIT,s o i s d i s c o n n e c t e d将正
确地标注插口的状态。

图30-13 tcp_usrclosed 函数:基于连接关闭的处理进程,将连接转移到下一状态

30.6 tcp_ctloutput函数

t c p _ c t l o u t p u t函数被 g e t s o c k o p t和s e t s o c k o p t函数调用,如果它们的描述符参


数指明了一个 TCP插口,且level不是SOL_SOCKET。图30-14列出了TCP支持的两个插口选项。

选 项 名 变 量 存 取 描 述

TCP_NODELAY t_flags 读、写 Nagel算法(图26-8)


TCP_MAXSEG t_maxseg 读、写 TCP将发送的最大报文段长度

图30-14 TCP支持的插口选项

图30-15给出了函数的第一部分。

图30-15 tcp_ctloutput 函数:第一部分

Linux公社 www.linuxidc.com
bbs.theithome.com
818 计计TCP/IP详解 卷 2:实现

图30-15 (续)

296-303 函数执行时,处理器优先级设为 splnet,inp指向插口的 Internet PCB。如果inp


为空,且操作类型是设定插口选项,则释放 mbuf并返回错误。
304-308 如果 l e v e l(g e t s o c k o p t 和 s e t s o c k o p t 系统调用的第二个参数 )不等于
I P P R O T O _ T C P,说明操作的是其他协议 (如I P )。例如,可以创建一个 T C P插口,并设定其 I P
源选路插口选项。此时应由 IP处理这个插口选项,而不是 TCP。ip_ctloutput处理命令。
309 如果是对TCP选项进行操作, tp将指向TCP控制块。
函数的剩余部分是一个 s w i t c h语句,有两个分支:一个处理 P R C O _ S E T O P T(图3 0 - 1 6中
给出),另一个处理PRCO_GETOPT(图30-17中给出)。

图30-16 tcp_ctloutput 函数:设定插口选项

Linux公社 www.linuxidc.com
bbs.theithome.com
第 30 章 TCP的用户需求 计计 819

图30-16 (续)

315-316 m是一个 m b u f,保存了 s e t s o c k o p t的第四个参数。对于两个 T C P插口选项,


m b u f中都必须是整数。如果任何一个 m b u f指针为空,或者 m b u f中的数据长度小于整数大小,
则返回错误。
1. TCP_NODELAY选项
317-321 如果整数值非零,则置位 TF_NODELAY标志,从而取消图 26-8中的Negal算法。如
果整数值等于0,则使用Negal算法(默认值),并清除TF_NODELAY标志。
2. TCP_MAXSEG选项
322-327 应用进程只能减少MSS。TCP插口创建时,tcp_newtcpcb初始化t_maxseg为默
认值5 1 2。当收到对端 S Y N中包含的 M S S选项时, t c p _ i n p u t调用t c p _ m s s,t _ m a x s e g最
高可等于外出接口的 M T U (减去 4 0字节, I P和T C P首部的默认值 ),以太网等于 1 4 6 0。因此,
调用插口之后,连接建立之前,应用进程只能以默认值 5 1 2为起点,减少 M S S。连接建立后,
应用进程可以从 tcp_mss选取的任何值起,减少 MSS。

4 . 4 B S D是伯克利版本中第一次支持 M S S做为插口选项,以前的版本只允许利用
getsockopt读取MSS值。
3. 释放mbuf
332-333 释放mbuf链。
图30-17给出了PRCO_GETOPT命令的处理。

图30-17 tcp_ctloutput 函数:读取插口选项

Linux公社 www.linuxidc.com
bbs.theithome.com
820 计计TCP/IP详解 卷 2:实现

335-337 两个T C P插口选项都向应用进程返回一个整数值,因此,调用 m _ g e t得到一个


mbuf,其长度等于整数长度。
339-341 T C P _ N O D E L A Y返回T F _ N O D E L A Y标志的当前状态:如果标志未置位 (使用N a g e l
算法),则等于0;如果标志置位,则等于 TF_NODELAY。
342-344 T C P _ M A X S E G选项返回 t _ m a x s e g的当前值。前面讨论 P R C O _ S E T O P T命令时曾
提到,返回值取决于插口是否已进入连接状态。

30.7 小结

t c p _ u s r r e q函数处理逻辑很简单,因为绝大多数处理都由其他函数完成。 P R U _x x x请
求是独立于协议的系统调用与 TCP协议处理间的桥梁。
t c p _ c t l s o c k o p t函数也很简单,因为 T C P只支持两个插口选项:使用或取消 N a g e l算
法,设置或读取最大报文段长度。

习题

30.1 现在,我们已经结束了对 T C P 的讨论,如果某个客户执行了正常的 s o c k e t 、


c o n n e c t、 w r i t e (向服务器请求 )和r e a d (读取服务器响应 ),分别列出客户端和
服务器端的处理步骤及 TCP状态变迁。
30.2 如果应用进程设定 S O _ L I N G E R插口选项,且拖延时间等于 0,之后调用c l o s e,我
们给出了如何调用 t c p _ d i s c o n n e c t,从而发送 R S T。如果应用进程设定了这个
插口选项,且拖延时间等于 0,之后进程被某个信号杀死 ( k i l l ),而非调用 c l o s e,
会发生什么?还会发送 RST报文段吗?
30.3 图25-4中描述T C P _ L I N G E R T I M E时,称之为“ S O _ L I N G E R插口选项的最大秒数”。
根据图30-2中的代码,这个说法正确吗?
30.4 某个 N e t / 3客户调用 s o c k e t和c o n n e c t,主动与服务器建立连接,使用了客户的
默认路由。客户主机向服务器发送了 1 129个报文段。假定到达目的地的路由未变,
为了这条连接,客户主机需要搜索多少次路由表?解释你的结论。
30.5 找到卷 1的附录 C中提到的 s o c k程序。把该程序做为服务器运行,读取数据前有停
顿(-p),且有较大的接收缓存。之后在另一个系统中运行同一个程序,但做为客户。
通过t c p d u m p查看数据。确认 TCP“确认所有其他报文段”的属性未出现,服务器
送出的ACK全部是延迟 ACK。
30.6 修改SO_KEEPALIVE插口选项,从而能够配置每个连接的参数。
30.7 阅读RFC 1122,了解为什么它建议 TCP应该允许RST报文段携带数据。修改 Net/3代
码以实现此功能。

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD 分组过滤程序
31.1 引言

B S D分组过滤程序( B P F)是一种软件设备,用于过滤网络接口的数据流,即给网络接口加
上“开关”。应用进程打开 / d e v / b p f 0、/ d e v / b p f 1等等后,可以读取 B P F设备。每个应用
进程一次只能打开一个 BPF设备。
因为每个 B P F设备需要 8 1 9 2字节的缓存,系统管理员一般限制 B P F设备的数目。
如果o p e n返回E B U S Y,说明该设备已被使用,应用进程应该试着打开下一 BPF设备,
直到open成功为止。
通过若干 i o c t l命令,可以配置 B P F设备,把它与某个网络接口相关连,并安装过滤程
序,从而能够选择性地接收输入的分组。 B P F设备打开后,应用进程通过读写设备来接收分
组,或将分组放入网络接口队列中。
我们将一直使用“分组”,尽管“帧”可能更准确一些,因为 B P F工作在数据链
路层,在发送和接收的数据帧中包含了链路层的首部。
B P F设备工作的前提是网络接口必须能够支持 B P F。第3章中提到以太网、 S L I P和环回接
口的驱动程序都调用了 b p f a t t a c h,用于配置读取 B P F设备的接口。本节中,我们将介绍
BPF设备驱动程序是如何组织的,以及数据分组在驱动程序和网络接口之间是如何传递的。
BPF一般情况下用作诊断工具,查看某个本地网络上的流量,卷 1附录A 介绍的t c p d u m p
程序是此类工具中最好的一个。通常情况下,用户感兴趣的是一组指定主机间交互的分组,
或者某个特定协议,甚至某个特定 TCP连接上的数据流。 BPF设备经过适当配置,能够根据过
滤程序的定义丢弃或接受输入的分组。过滤程序的定义类似于伪机器指令, B P F的细节超出
了本书的讨论范围,感兴趣的读者请参阅 bpf(4)和[McCanne and Jacobson 1993]。

31.2 代码介绍

下面将要介绍的有关 B P F设备驱动程序的代码,包括两个头文件和一个 C文件,在图 3 1 - 1


中给出。

文 件 描 述

net/bpf.h BPF常量
net/bpfdesc.h BPF结构
net/bpf.c BPF设备支持

图31-1 本章讨论的文件

31.2.1 全局变量

本章用到的全局变量在图 31-2中给出。

Linux公社 www.linuxidc.com
bbs.theithome.com
822 计计TCP/IP详解 卷 2:实现

变 量 数 据 类 型 描 述

bpf_iflist struct bpf_if * 支持BPF的接口组成的链表


bpf_dtab struct bpf_d [] BPF描述符数组
bpf_bufsize int BPF缓存大小默认值

图31-2 本章用到的全局变量

31.2.2 统计量

图31-3列出了bpf_d结构中为每个活动的 BPF设备维护的两个统计量。

bpf_d成员变量 描 述

bd_rcount 从网络接口接收的分组的数目
bd_dcount 由于缓存空间不足而丢弃的分组的数目

图31-3 本章讨论的统计值

本章的其余内容分为 4个部分:
• BPF 接口结构;
• BPF 设备描述符;
• BPF 输入处理;和
• BPF 输出处理。

31.3 bpf_if结构

BPF维护一个链表,包括所有支持 BPF的网络接口。每个接口都由一个 bpf_if结构描述,


全局指针bpf_iflist指向表中的第一个结构。图 31-4给出了BPF接口结构。

图31-4 bpf_if 结构

67-79 b i f _ n e x t指向链表中的下一个 B P F接口结构。b i f _ d l i s t指向另一个链表,包括


所有已打开并配置过的 BPF设备。
70 如果某个网络接口已配置了 BPF设备,即被加上了开关,则 bif_driverp将指向ifnet
结构中的 b p f _ i f指针。如果网络接口还未加上开关, * b i f _ d r i v e r p为空。为某个网络接
口配置BPF设备时, * b i f _ d r i v e r p将指向b i f _ i f结构,从而告诉接口可以开始向 BPF传递
分组。
71 接口类型保存在 bif_dlt中。图31-5中列出了前面提到的几个接口所分别对应的常量值。

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 823
bif_dlt 描 述

DLT_EN10MB 10 Mb以太网接口
DLT_SLIP SLIP接口
DLT_NULL 环回接口

图31-5 bif_dlt 值

72-74 BPF接受的所有分组都有一个附加的 BPF首部。b i f _ h d r l e n等于首部大小。最后,


bif_ifp指向对应接口的 ifnet结构。
图31-6给出了每个输入分组中附加的 bpf_hdr结构。

图31-6 bpf_hdr 结构

122-128 b h _ t s t a m p记录了分组被捕捉的时间。 b h _ c a p l e n 等于 B P F保存的字节数,


b h _ d a t a l e n等于原始分组中的字节数。 b h _ h e a d l e n等于b p f _ h d r的大小加上所需填充字
节的长度。它用于解释从 BPF设备中读取的分组,应该等同于接收接口的 bif_hdrlen。
图3 1 - 7给出了 b p f _ i f结构是如何与前述 3个接口 (l e _ s o f t c [ 0 ]、s l _ s o f t c [ 0 ]和
loif)的ifnet结构建立连接的。

图31-7 bpf_if 和ifnet 结构

注意,bif_driverp指向网络接口的 if_bpf和sc_bpf指针,而不是接口结构。
S L I P设备使用 s c _ b p f,而不是 if _ b p f。这可能是因为 SLIP BPF 代码完成时,

Linux公社 www.linuxidc.com
bbs.theithome.com
824 计计TCP/IP详解 卷 2:实现

i f _ b p f成员变量还未加入到 i f n e t结构中。 Net/2中的i f n e t结构不包括 i f _ b p f成


员。
按照各接口驱动程序调用 b p f a t t a c h时给出的信息,对 3个接口初始化链路类型和首部
长度成员变量。
第3章介绍了 b p f a t t a c h被以太网、 S L I P和环回接口的驱动程序调用。每个设备驱动程
序初始化调用bpfattach时,将构建 BPF接口结构链表。图 31-8给出了该函数。

图31-8 bpfattach 函数

1053-1063 每个支持 B P F的设备驱动程序都将调用 b p f a t t a c h。第一个参数是保存在


b i f _ d r i v e r p的指针(图3 1 - 4给出),第二个参数指向接口的 i f n e t结构,第三个参数确认数
据链路层类型,第四个参数传递分组中的数据链路首部大小,为接口分配一个新的 b p f _ i f结
构。
1. 初始化bpf_if结构

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 825
1064-1070 b p f _ i f 结构根据函数的参数进行初始化,并插入到 B P F 接口链表,
bpf_iflist,的表头。
2. 计算BPF首部大小
1071-1077 设定b i f _ h d r l e n大小,强迫网络层首部 (如I P首部)从一个长字的边界开始。
这样可以提高性能,避免为 B P F加入不必要的对齐限制。图 3 1 - 9列出了在前述 3个接口上,各
自捕捉到的 BPF分组的总体结构。

IP分组

18字节 14字节

填充

IP分组

18字节 2字节 16字节

填充
环回伪链路首部

IP分组

18字节 2字节 4字节

图31-9 BPF分组结构

e t h e r _ h e a d e r结构在图 4 - 1 0中给出, S L I P伪链路首部在图 5 - 1 4中给出,而环回接口伪


链路首部在图5-28中给出。
请注意,SLIP和环回接口分组需要填充 2字节,以强迫IP首部按4字节对齐。
3. 初始化bpf_dtab表
1078-1083 代码初始化图 3 1 - 1 0 中给出的 B P F 描述符表。注意,仅在第一次调用
bpfattach时进行初始化,后续调用将跳过初始化过程。
4. 打印控制台信息
1084-1085 系统向控制台输出一条短信息,宣告接口已配置完毕,可以支持 BPF。

31.4 bpf_d结构

为了能够选择性地接收输入报文,应用进程首先打开一个 B P F设备,调用若干 i c t l命令


规定BPF过滤程序的条件,指明接口、读缓存大小和超时时限。每个 BPF设备都有一个相关的
bpf_d结构,如图 31-10所示。
45-46 如果同一网络接口上配置了多个 BPF设备,与之相应的 b p f _ d结构将组成一个链表。
bd_next指向链表中的下一个结构。
分组缓存
47-52 每个bpf_d结构都有两个分组缓存。输入分组通常保存在 bd_sbuf所对应的缓存(存
储缓存 )中。另一个缓存要么对应于 b d _ f b u f (空闲缓存 ),意味着缓存为空;或者对应于
b d _ h b u f(暂留缓存 ),意味着缓存中有分组等待应用进程读取。 b d _ s l e n和b d _ h l e n分别记

Linux公社 www.linuxidc.com
bbs.theithome.com
826 计计TCP/IP详解 卷 2:实现

录了保存在存储缓存和暂留缓存中的字节数。

图31-10 bpf_d 结构

如果存储缓存已满,它将被连接到 b d _ h b u f,而空闲缓存将被连接到 b d _ s b u f。当暂留


缓存清空时,它会被连接到 b d _ f b u f 。宏 R O T A T E _ B U F F E R S 负责把存储缓存连接到
b d _ h b u f,空闲缓存连接到 b d _ s b u f,并清空 b d _ f b u f。存储缓存满或者应用进程不想再
等待更多的分组时调用该宏。
b d _ b u f s i z e记录与设备相连的两个缓存的大小,其默认值等于 4096(B P F _ B U F S I Z E)字
节。修改内核代码可以改变默认值大小,或者通过 B I O C S B L E Ni o c t l命令改变某个特定
B P F设备的 b d _ b u f s i z e。B I O C G B L E N命令返回 b d _ b u f s i z e的当前值,其最大值不超过
32768 (BPF_MAXBUFSIZE)字节,最小值为 32 (BPF_MINBUFSIZE)字节。
53-57 b d _ b i f 指向 B P F 设备所对应的 b p f _ i f 结构。 B I O C S E T I F命令可指明设备。
b d _ r t o u t是等待分组时,延迟的滴答数。 b d _ f i l t e r指向B P F设备的过滤程序代码。两个
统计值,应用进程可通过 B I O C G S TAT S命令读取,分别保存在 b d _ r c o u n t和b d _ d c o u n t
中。
58-63 bd_promisc通 过 BIOCPROMISC命 令 置 位 , 从 而 使 接 口 工 作 在 混 杂
(promiscuous)状态。bd_state未使用。bd_immediate通过BIOCIMMEDIATE命令置位,
促使驱动程序收到分组后即返回,不再等待暂留缓存填满。 b d _ p a d填弃b p f _ d结构,从而
与长字边界对齐。 b d _ s e l保存的s e l i n f o结构,可用于s e l e c t系统调用。我们不准备介绍
如何对BPF设备使用select系统调用, 16.13节已介绍了 select的一般用法。

31.4.1 bpfopen函数

应用进程调用open,试图打开一个 BPF设备时,该调用将被转到 bpfopen(图31-11)。


256-263 系统编译时,BPF设备的数目受到 NBPFILTER的限制。如果设备的最小设备号大

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 827
于 N B P F I L T E R ,则返回 E N X I O ,这是因为系统管理员创建的 / d e v / b p f x 项数大于
NBPFILTER的值。

图31-11 bpfopen 函数

分配bpf_d结构
264-275 同一时间内,一个应用进程只能访问一个 B P F设备。如果 b p f _ d结构已被激活,
则返回 E B U S Y。应用程序,如 t c p d u m p,收到此返回值时,会自动寻找下一个设备。如果该
设备已存在,最小设备号所指定的 bpf_dtab表中的项被清除,分组缓存大小复位为默认值。

31.4.2 bpfioctl函数

设备打开后,可通过 ioctl命令进行配置。图 31-12总结了与BPF设备有关的 ioctl命令。


图3 1 - 1 3给出了 b p f i o c t l函数,只列出 B I O C S E T F和B I O C S E T I F的处理代码,其他未涉及
到的ioctl命令则被忽略。
命 令 第三个参数 函 数 描 述

FIONREAD u_int bpfioctl 返回暂留缓存和存储缓存中的字节数


BIOCGBLEN u_int bpfioctl 返回分组缓存大小
BIOCSBLEN u_int bpfioctl 设定分组缓存大小
BIOCSETF struct bpf_program bpf_setf 安装BPF程序
BIOCFLUSH reset_d 丢弃挂起分组
BIOCPROMISC ifpromisc 设定混杂方式
BIOCGDLT u_int bpfioctl 返回b i f _ d l t
BIOCGETIF struct ifreq bpf_ifname 返回所属接口的名称
BIOCSETIF struct ifreq bpf_setif 为网络接口添加设备
BIOCSRTIMEOUT struct timeval bpfioctl 设定“读”操作的超时时限
BIOCGRTIMEOUT struct timeval bpfioctl 返回“读”操作的超时时限
BIOCGSTATS struct bpf_stat b p f i o c tl 返回BPF统计值
BIOCIMMEDIATE u_int bpfioctl 设定立即方式
BIOCVERSION struct b p f _ v e r s i o n bpfioctl 返回BPF版本信息

图31-12 BPF ioctl 命令

Linux公社 www.linuxidc.com
bbs.theithome.com
828 计计TCP/IP详解 卷 2:实现

图31-13 bpfioctl 函数

501-509 与bpfopen类似,通过最小设备号从bpf_dtab表中选取相应的bpf_d结构。整个
命令处理是一个大的switch/case语句。我们给出了两个命令,BIOCSETF和BIOCSETIF,以
及default子句。
510-522 b p f _ s e t f函数安装由a d d r指向的过滤程序, b p f _ s e t i f建立起指定名称接口
与bpf_d结构间的对应关系。本书中没有给出 bpf_setf的实现代码。
668-673 如果命令未知,则返回 EINVAL。
图31-14的例子中, bpf_setif已把bpf_d结构连接到 LANCE接口上。
图中,b i f _ d l i s t指向b p f _ d t a b[0],以太网接口描述符链表中的第一个也是仅有的一
个描述符。在 bpf_dta b[0]中,b d _ s b u f和b d _ h b u f成员分别指向存储缓存和暂留缓存。两
个缓存大小都等于 4096(bd_bufsize)字节。bd_bif回指接口的 bpf_if结构。
i f n e t结构(l e _ s o f t c[ 0 ] )中的i f _ b p f也指回 b p f _ i f结构。如图 4 - 1 9和图4 - 11所示,
如果if_bpf非空,则驱动程序开始调用 bpf_tap,向BPF设备传递分组。
图3 1 - 1 5接着图3 1 - 1 0,给出了打开第二个 B P F设备,并连接到同一个以太网网络接口后的
各结构变量的状态。

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 829

到其他b p f _ i f结构

存储缓存

空闲缓存

b p f _ d t a b [ 2 ]至
bpf_dtab[NBPFILTER-1]

图31-14 连接到以太网接口的 BPF设备

到其他b p f _ i f结构

存储缓存

空闲缓存

存储缓存

空闲缓存

b p f _ d t a b [ 2 ]至
bpf_dtab[NBPFILTER-1]

图31-15 连接到以太网接口的两个 BPF设备

Linux公社 www.linuxidc.com
bbs.theithome.com
830 计计TCP/IP详解 卷 2:实现

第二个 B P F 设备打开时,在 b p f _ d t a b 表中分配一个新的 b p f _ d 结构,本例中为


b p f _ d t a b [ 1 ]。因为第二个 B P F 设备也连接到同一个以太网接口, b i f _ d l i s t 指向
b p f _ d t a b[ 1 ],并且 b p f _ d t a b[ 1 ] .b d _ n e x t指向b p f _ d t a b[ 0 ],即以太网上对应的第一个
BPF描述符。系统为新的描述符结构分别分配存储缓存和暂留缓存。

31.4.3 bpf_setif函数

bpf_setif函数,负责建立 BPF描述符与网络接口间的连接,如图 31-16所示。

图31-16 bpf_setif 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 831

图31-16 (续)

721-746 b p f _ s e t i f的第一部分完成 i f r e q结构(图4-23)中接口名的正文与数字部分的分


离,数字部分保存在 u n i t中。例如,如果 i f r _ n a m e的头4字节为“ s l 1 \ 0”,代码执行完毕
后,将等于“sl\0\0”,且unit等于1。
1. 寻找匹配的 ifnet结构
747-754 for循环用于在支持 BPF的接口(bpf_iflist中)中查找符合 ifreq定义的接口。
755-768 如果未找到匹配的接口,则返回 ENETDOWN。如果接口存在, bpf_allocate为
bpf_d分配空闲缓存和存储缓存,如果它们还未被分配的话。
2. 连接bpf_d结构
769-777 如果B P F设备还未与网络接口建立连接关系,或者连接的网络接口不是 i f r e q中
指定的接口,则调用 b p f _ d e t a c h d丢弃原先的接口 (如果存在 ),并调用 b p f _ a t t a c h d将其
连接到新的接口上。
778-784 r e s e t _ d复位分组缓存,丢弃所有在应用进程中等待的分组。函数返回 0,说明
处理成功;或者 ENXIO,说明未找到指定接口。

31.4.4 bpf_attachd函数

图31-17给出的bpf_attachd函数,建立起BPF描述符与BPF设备和网络接口间的对应关系。

图31-17 bpf_attachd 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
832 计计TCP/IP详解 卷 2:实现

图31-17 (续)

189-203 首先,令bd_bif指向网络接口的 BPF接口结构。接着, bpf_d结构被插入到与设


备对应的 b p f _ d结构链表的头部。最后,改变网络接口中的 BPF指针,指向当前 BPF结构,从
而促使接口向BPF设备传递分组。

31.5 BPF的输入

一旦B P F设备打开并配置完毕,应用进程就通过 r e a d系统调用从接口中接收分组。 B P F


过滤程序复制输入分组,因此,不会干扰正常的网络处理。输入分组保存在与 B P F设备相连
的存储缓存和暂留缓存中。

31.5.1 bpf_tap函数

下面列出了图 4 - 11中L A N C E设备驱动程序调用 b p f _ t a p的代码,并利用这一调用介绍


bpf_tap函数。图4-11中的调用如下:
bpf_tap(le->sc_if.if_bpf, buf, len + sizeof(struct ether_header));

图31-18给出了bpf_tap函数。

图31-18 bpf_tap 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 833
869-882 第一个参数是指向 b p f _ i f结构的指针,由 b p f a t t a c h设定。第二个参数是指向
进入分组的指针,包括以太网首部。第三个参数等于缓存中包含的字节数,本例中,等于以
太网首部(14字节)大小加上以太网帧的数据部分。
向一个或多个BPF设备传递分组
883-890 f o r循环遍历连接到网络接口的 B P F设备链表。对每个设备,分组被递交给
b p f _ f i l t e r。如果过滤程序接受了分组,它返回捕捉到的字节数,并调用 c a t c h p a c k e t
复制分组。如果过滤程序拒绝了分组, slen等于0,循环继续。循环终止时, bpf_tap返回。
这一机制确保了同一网络接口上对应了多个 B P F设备时,每个设备都能拥有一个独立的过滤
程序。
环回驱动程序调用 b p f _ m t a p,向B P F传递分组。这个函数与 b p f _ t a p类似,然而是在
mbuf链,而不是在一个内存的连续区域中复制分组。本书中不介绍这个函数。

31.5.2 catchpacket函数

图31-18中,过滤程序接受了分组后,将调用 catchpacket,图31-19给出了这个函数。

图31-19 catchpacket 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
834 计计TCP/IP详解 卷 2:实现

图31-19 (续)

946-955 c a t c h p a c k e t的参数包括: d,指向B P F设备结构的指针; p k t,指向进入分组


的通用指针; p k t l e n,分组被接收时的长度; s n a p l e n ,从分组中保存下来的字节数;
c p f n,函数指针,把分组从 p k t中复制到一块连续内存中。如果分组已经保存连续内存中,
则c p t n等于b c o p y。如果分组被保存在 m b u f中(p k t指向m b u f链表中的第一个 m b u f,如环回
驱动程序),则cptn等于bpf_mcopy。
956-964 除了链路层首部和分组, c a t c h p a c k e t为每个分组添加 b p f _ h d r。从分组中保
存的字节数等于 s n a p l e n和p k t l e n中较小的一个。处理过的分组和 b p f _ h d r必须能放入分
组缓存中(bd_bufsize字节)。
1. 分组能否放入缓存
965-985 c u r l e n等于存储缓存中已有的字节数加上所需的填充字节,以保证下一分组能
从长字边界处开始存放。如果进入分组无法放入剩余的缓存空间,说明存储缓存已满。如果
空闲缓存不可用 (如应用进程正从暂留缓存中读取数据 ),则进入分组被丢弃。如是空闲缓存可
用,则调用 ROTATE_BUFFERS宏轮转缓存,并通过 bpf_wakeup唤醒所有等待输入数据的应
用进程。
2. 立即方式处理
986-991 如果设备处于立即方式,则唤醒所有等待进程以处理进入分组—内核中没有分
组的缓存。
3. 添加BPF 首部
992-1004 当前时间(microtime)、分组长度和首部长度均保存在 bpf_hdr中。调用cptf
所指的函数,把分组复制到存储缓存,并更新存储缓存的长度。因为在把分组从设备缓存传
送到某个 m b u f链表之前, b p f _ t a b已由l e r e a d直接调用,接收时间戳近似等于实际的接收

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 835
时间。

31.5.3 bpfread函数

内核把针对B P F设备的r e a d转交给b p f r e a d处理。通过 B I O C S R T I M E O U T命令,B P F支


持限时读取。这个“特性”也可通过 s e l e c t系统调用来实现,但至少 t c p d u m p还是采用了
B I O C S R T I M E O U T,而非s e l e c t。应用进程提供一个读缓存,能够与设备的暂留缓存大小相
匹配。 B I C O G B L E N命令返回缓存大小。一般情况下,读操作在存储缓存已满时返回。内核轮
转缓存,把存储缓存转给暂留缓存,后者在 read系统调用时被复制到应用进程提供的读缓存,
同时BPF设备继续向存储缓存中存放进入分组。图 31-20给出了bpfread。

图31-20 bpfread 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
836 计计TCP/IP详解 卷 2:实现

图31-20 (续)

344-357 通过最小设备号在 bpf_dtab中寻找相应的BPF设备。如果读缓存不能匹配 BPF设


备缓存的大小,则返回 EINVAL。
1. 等待数据
358-364 因为多个应用进程能够从同一个 B P F设备中读取数据,如果有某个进程已先读取
了数据, w h i l e循环将强迫读操作继续。如果暂留缓存中存在数据,循环被跳过。这与两个
应用进程通过两个不同的 BPF设备过滤同一个网络接口的情况 (见习题31.2)是不同的。
2. 立即方式
365-373 如果设备处于立即方式,且存储缓存中有数据,则轮回缓存, while循环被终止。
3. 无可用的分组
374-384 如果设备不处于立即方式,或者存储缓存中没有数据,则应用进程进入休眠状态,
直到某个信号到达,读定时器超时,或者有数据到达暂留缓存。如果有信号到达,则返回
EINTR或ERESTART。
记住,应用进程不会见到 E R E S T A R T,因为 s y s c a l l函数将处理这一错误,且
不会向应用进程返回这一错误。
4. 查看暂留缓存

Linux公社 www.linuxidc.com
bbs.theithome.com
第31章 BPF:BSD分组过滤程序计计 837
385-391 如果定时器超时,且暂留缓存中存在数据,则循环终止。
5. 查看存储缓存
3 9 2 - 3 9 9 如果定时器超时,且存储缓存中没有数据,则 r e a d返回0。应用进程执行限时读
取时,必须考虑到这种情况。如果定时器超时,且存储缓存中存在数据,则把存储缓存转给
暂留缓存,循环终止。
如果tsleep返回正常且存在数据,同时 while循环测试失败,则循环终止。
6. 分组可用
400-416 循环终止时,暂留缓存中已有数据。uiomove从暂留缓存中移出 bd_hlen个字节,
交给应用进程。把暂留缓存转给空闲缓存,清除缓存计数器,函数返回。 u i o m o v e调用前的
注释指出,u i o m o v e通常能向应用进程复制 b d _ h l e n字节的数据,因为前面已检查过读缓存
大小,确保它大于 BPF设备缓存的最大值,即 bd_bufsize。

31.6 BPF的输出
最后,我们讨论如何向带有 BPF设备的网络接口输出队列中添加分组。首先,应用进程必
须构造完整的数据链路帧。对以太网而言,包括源和目的主机的硬件地址和数据帧类型 (图4-8)。
内核在把它放入接口的输出队列前不会修改链路帧。

bpfwrite函数

内核把应用进程的 w r i t e系统调用转给图 3 1 - 2 1给出的 b p f w r i t e处理,数据帧被传给


BPF设备。

图31-21 bpfwrite 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
838 计计TCP/IP详解 卷 2:实现

图31-21 (续)

1. 检查设备号
4 3 7 - 4 4 9 通过最小的设备号选择 B P F设备,它必须已连接到某个网络接口。如果还没有,
则返回ENXIO。
2. 向mbuf链中复制数据
4 5 0 - 4 5 7 如果w r i t e给出的写入数据长度等于 0,则立即返回0。b p f _ m o v e i n从应用进程
复制数据到一个 m b u f链表,并基于由 b i f _ d l t传递的接口类型计算去除了链路层首部后的分
组长度,并在 d a t l e n中返回该值。它还在 d s t中返回一个已初始化过的 s o c k a d d r结构。对
以太网而言,这个地址结构的类型应该等于 A F _ U N S P E C,说明mbuf链中保存了外出数据帧的
数据链路层首部。如果分组大于接口的 MTU,则返回EMSGSIZE。
3. 分组排队
4 5 8 - 4 6 5 调用i f n e t结构中指定的 i f _ o u t p u t函数,得到的 m b u f链被提交给网络接口。
对于以太网,if_output等于ether_output。

31.7 小结
本章中,我们讨论了如何配置 B P F设备,如何向 B P F设备递交进入数据帧,及如何在一个
BPF设备上传送外出数据帧。
一个网络接口可以有多个 B P F设备,每个B P F设备都有自己的过滤程序。存储缓存和暂留
缓存最大限度地减少了应用进程为了处理进入数据帧而调用 read的次数。
本章中只介绍了 B P F的一些主要特性。有关 B P F设备过滤程序代码的详细情况和其他一些
特性,感兴趣的读者请参阅源代码和 Net/3手册。

习题
31.1 为什么在分组存入 BPF缓存之前,就能在 catchpacket中调用bpf_wakeup?
31.2 图3 1 - 2 0中,我们提到可能会有两个进程在同一 B P F设备上等待数据。图 3 1 - 11中,
我们指出同一时间只能有一个应用进程可以打开一个特定的 B P F设备。为什么这两
种说法都正确呢?
31.3 如果BIOCSETIF命令中指定的设备不支持 BPF,会发生什么现象?

Linux公社 www.linuxidc.com
bbs.theithome.com
第32章 原 始 IP
32.1 引言

应用进程在 I n t e r n e t域中创建一个 S O C K _ R A W类型的插口,就可以利用原始 I P层。一般有


下列3种用法:
1) 应用进程可利用原始插口发送和接收 ICMP和IGMP报文。
Ping程序利用这种类型的插口,发送 ICMP回显请求和接收 ICMP回显应答。
有些选路守护程序,利用这一特性跟踪通常由内核处理的 I C M P重定向报文段。我们在
1 9 . 7 节 中 提到 , N e t / 3 处 理 重 定向 报 文 段 时, 会 在 需重 定 向 的插 口 上 生成
RTM_REDIRECT消息,从而无需利用原始插口的这一功能。
这个特性还用于实现基于 I C M P的协议,如路由通告和路由请求 (卷1的9 . 6节),它们需
用到ICMP,不过最好由应用进程,而不是内核完成相应处理。
多播路由守护程序利用原始 IGMP插口,发送和接收 IGMP报文。
2) 应用进程可利用原始插口构造自己的 I P首部。路由跟踪程序利用这一特性生成自己的
UDP数据报,包括IP和UDP首部。
3) 应用进程可利用原始插口读写内核不支持的 IP协议的IP数据报。
gated程序利用这一特性支持基于 IP的路由协议:EGP、HELLO和OSPF。
这种类型的原始插口还可用于设计基于 I P的新的运输层协议,而无需增加对内核的支
持。调试应用进程代码比调试内核代码容易得多。
本章介绍原始IP插口的实现。

32.2 代码介绍

图32-1给出的C文件中包含了5个原始IP处理函数。

文 件 描 述

netinet/raw_ip.c 原始IP处理函数

图32-1 本章讨论的文件

图32-2给出了5个原始IP函数与其他内核函数间的关系。
带阴影的椭圆表示我们在本章中将要讨论的 5个函数。请注意,原始 I P函数名中的前缀
“rip”表示“原始IP (Raw IP)”,而不是“选路信息协议 (Routing Information Protocol)”,后者
的缩写也是 RIP。

32.2.1 全局变量

本章中用到 4个全局变量,如图 32-3所示。

Linux公社 www.linuxidc.com
bbs.theithome.com
840 计计TCP/IP详解 卷 2:实现

插口接收缓存 多种系统调用
系统初始化

软中断

图32-2 原始IP函数与其他内核函数间的关系

变 量 数据类型 描 述

rawinpcb struct inpcb 原始IP的Internet PCB链表表头


ripsrc s t r u c t s o c k a d d r _ i n在输入中包含发送方的 IP地址
rip_recvspace u_long 插口接收缓存大小默认值, 8192字节
rip_sendspace u_long 插口发送缓存大小默认值, 8192字节

图32-3 本章介绍的全局变量

32.2.2 统计量

原始IP在ipstat结构(图8-4)中维护两个计数器,如图 32-4所示。

ipstat成员变量 描 述 SNMP变量使用

ips_noproto 协议类型未知或不支持的数据报数目 •
ips_rawout 生成的原始 IP数据报总数

图32-4 ipstat 结构中维护的原始 IP统计量

图8 - 6给出了如何在 S N M P中使用i p s _ n o p r o t o计数器。图 8 - 5给出了这两个计数器输出


值的例子。

32.3 原始IP的protosw结构

与所有其他协议不同, i n e t s w数组有多条记录都可以读写原始 IP。i n e t s w结构中有4个


记录的插口类型都等于 SOCK_RAW,但协议类型则各不相同:
• IPPROTO_ICMP(协议值1);
• IPPROTO_IGMP(协议值2);
• IPPROTO_RAW(协议值255);和

Linux公社 www.linuxidc.com
bbs.theithome.com
第 32章 原 始 IP计计 841
• 原始IP通配记录 (协议值0)。
其中ICMP和IGMP,前面已介绍过 (图11-12和图13-9)。四项记录间的区别总结如下:
• 如果应用进程创建了一个原始插口 (S O C K _ R A W),协议值非零 (s o c k e t的第三个参数 ),
并且如果协议值等于 I P P R O T O _ I C M P、I P P R O T O _ I G M P或I P P R O T O _ R A W,则会使用
对应的protosw记录。
• 如果应用进程创建了一个原始插口 (S O C K _ R A W),协议值非零,但内核不支持该协议,
pffindproto会返回协议值为 0的通配记录,从而允许应用进程处理内核不支持的 IP协
议,而无需修改内核代码。
我们在7.8节中提到,i p _ p r o t o x数组中的所有未知记录都指向 I P P R O T O _ R A W,它的协
议转换类型如图 32-5所示。

成 员 i n e t s w[3] 描 述

pr_type SOCK_RAW 原始插口


pr_domain & inetdomain 属于Internet域的原始IP
pr_protocol I P P R O T O _ R A W(255) 出现在IP首部的i p _ p字段
pr_flags PR_ATOMIC|PR_ADDR 插口层标志,不用于协议处理
pr_input rip_input 从IP层接收报文段
pr_output 0 原始IP不使用
pr_ctlinput 0 原始IP不使用
pr_ctloutput rip_ctlinput 响应应用进程的管理请求
pr_usrreq rip_usrreq 响应应用进程的通信请求
pr_init 0 原始IP不使用
pr_fasttimo 0 原始IP不使用
pr_slowtimo 0 原始IP不使用
pr_drain 0 原始IP不使用
pr_sysctl 0 原始IP不使用

图32-5 原始IP的protosw 结构

本章中我们将介绍 3个以r i p _开头的函数,此外还大致提一下 r i p _ o u t p u t函数,它没


有出现在协议转换记录中,但输出原始 IP报文段时, rip_usrreq将会调用它。
第五个原始 I P函数, r i p _ i n i t,只出现在通配处理记录中。初始化函数只能调用一次,
所以它既可以出现在 IPPROTP_RAW记录中,也可以放在通配记录中。
不过,图 3 2 - 5中并没有说明其他协议 ( I C M P和I G M P ),在它们自己的 p r o t o s w结构中也用
到了一些原始 IP函数。图 32-6对4个S O C K _ R A W协议各自 p r o t o s w结构的相关成员变量做了一
个比较。为了强调指出彼此间的区别,不同之处都用黑体字标出。

协议类型
记录 通配(0)

图32-6 原始插口的协议散转值的比较

Linux公社 www.linuxidc.com
bbs.theithome.com
842 计计TCP/IP详解 卷 2:实现

不同 B S D 版本中,原始 I P的实现各有不同。 i p _ p r o t o x 表中,协议号等于


I P P R O T O _ R A W记录通常都用做通配记录以支持未知的 I P协议,而协议号等于 0的记
录通常做为默认记录,从而允许应用进程读写内核不支持的 IP协议数据报。
应用进程使用 I P P R O T O _ R A W记录,最早见于 Van Jacobson开发的 Tr a c e o u t,这
是第一个需要自己写 I P首部(改变T T L字段)的应用进程。为了支持 Tr a c e o u t,修订了
4 . 3 B S D和N e t / 1,包括修改 r i p _ o u t p u t,在收到协议号等于 I P P R O T O _ R A W的数据
报时,假定应用进程提交了一个完整的 I P数据报,包括 I P首部。在 N e t / 2中,引入了
I P _ H D R I N C L插口选项,简化了 I P P R O T O _ R A W的用法,允许应用进程利用通配记
录发送自己的IP首部。

32.4 rip_init函数

系统初始化时, domaininit函数调用原始IP初始化函数 rip_init (图32-7)。

图32-7 rip_init 函数

这个函数执行的唯一操作是令 P C B首部(r a w i n p c b)中的前向和后向指针都指向自己,实


现一个空的双向链表。
只要某个 s o c k e t 系统调用创建了 S O C K _ R A W 类型的插口,下面将介绍的原始 IP
PRU_ATTACH函数就创建一个 Internet PCB,并插入到 rawinpcb链表中。

32.5 rip_input函数

因为i p _ p r o t o x数组中保存的所有关于未知协议记录都指向 I P P R O T O _ R A W(图7 - 8 ),且


后者的p r _ i n p u t函数指向 r i p _ i n p u t (图3 2 - 6 ),所以只要某个接收 I P数据报的协议号内核
无法识别,就会调用此函数。但从图 3 2 - 2可看出, I C M P和I G M P都可能调用 r i p _ i n p u t,只
要满足下列条件:
• icmp_input调用rip_input处理所有未知的ICMP报文类型和所有非响应的ICMP报文。
• igmp_input调用rip_input处理所有IGMP分组。
上述两种情况下,调用 r i p _ i n p u t的一个原因是允许创建了原始插口的应用进程处理新
增的ICMP和IGMP报文,内核可能不支持它们。
图32-8给出了rip_input函数。

图32-8 rip_input 函数

Linux公社 www.linuxidc.com
bbs.theithome.com
第 32章 原 始 IP计计 843

图32-8 (续)

59-66 I P 数据报中的源地址被保存在全局变量 r i p s r c 中,只要找到了匹配的 P C B,


r i p s r c 将做为参数传给 s b a p p e n d a d d r。与U D P不同,原始 I P没有端口号的概念,因此
sockaddr_in结构中的sin_port总等于0。
2. 在所有原始 IP PCB 中寻找一个或多个匹配的记录
67-88 原始IP处理PCB表的方式与 UDP和TCP不同。前面介绍过,这两个协议维护一个指针,
总是指向最近收到的报文段 (单报文段缓存 ),并调用通用函数 i n _ p c b l o o k u p寻找一个最佳
匹配(如果收到的数据报不同于缓存中的记录 )。由于原始 IP数据报可能发送到多个插口上,所
以无法使用 i n _ p c b l o o k u p,因此,必须遍历原始 P C B链表中的所有 P C B。这一点类似于
UDP处理广播报文段和多播报文段的方式 (图23-26)。
3. 协议比较
68-69 如果PCB中的协议字段非零,并且与 IP首部的协议字段不匹配,则 PCB被忽略。也说
明协议值等于0(socket的第三个参数)的原始插口能够匹配所有收到的原始 IP报文段。
4. 比较本地和远端 IP地址

Linux公社 www.linuxidc.com
bbs.theithome.com
844 计计TCP/IP详解 卷 2:实现

70-75 如果PCB中的本地地址非零,并且与 IP首部的目的 IP地址不匹配,则 PCB被忽略。如


果PCB中的远端地址非零,并且与 IP首部的源IP地址不匹配,PCB被忽略。
上述3种测试说明应用进程能够创建一个协议号等于 0的原始插口,即不绑定到本地地址,
也不与远端地址建立连接,可以接收经 rip_input处理的所有数据报。
代码71行和74行都有同样的错误:相等测试,实际应为不相等测试。
5. 递交接收数据报的复制报文段以备处理
76-94 sbappendaddr向应用进程提交一个接收数据报的复制报文段。变量 last的使用与
图2 3 - 2 6中的用法类似:因为 s b a p p e n d a d d r把报文段放入到适当队列中后将释放所有 m b u f,
如果有多个进程接收数据报的复制报文段, r i p _ i n p u t必须调用 m _ c o p y保存一份复制报文
段。但如果只有一个应用进程接收数据报,则无需复制。
6. 无法上交的数据报
95-99 如果无法为数据报找到相匹配的插口,则释放 m b u f,递增 i p s _ n o p r o t o,递减
i p s _ d e l i v e r e d。I P在调用r i p _ i n p u t之前已经递增过后一个计数器 (图8 - 1 5 )。由于数据
报实际上没有上交给运输层,因此,必须递减 i p s _ d e l i v e r e d,确保两个 S N M P计数器,
ipInDiscards和 ipInDelivers (图8-16),的正确性。
本节开始时,我们提到, i c m p _ i n p u t会为未知报文类型或非响应报文调用
r i p _ i n p u t,意味着如果收到 I C M P主机不可达报文,且 r i p _ i n p u t找不到可匹配
的原始插口P C B,i p s _ n o p r o t o会递增。这也说明为什么图 8 - 5中的计数器值较大。
在前面对该计数器的描述中提到“未知或不支持的协议”,这种说法是不正确的。
如果收到的 IP数据报带有的协议字段,既无法为内核辩识,也无法由某个应用进
程通过原始插口处理, N e t / 3不会生成差错代码等于 2 (协议不可达 )的I C M P目的不可
达报文。RFC 1 122建议出现此种情况时应该生成 ICMP差错报文(参见习题32.4)。

32.6 rip_output函数

图3 2 - 6中,I C M P、I G M P和原始I P都调用r i p _ o u t p u t实现原始 I P输出。应用进程调用 5


个写函数之一: s e n d、s e n d t o、s e n d m s g、w r i t e和w r i t e v,系统将输出报文段。如果
插口已建立连接,就可以任意调用上述 5个函数,尽管 s e n d t o和s e n d m s g中不能规定目的地
址。如果插口没有建立连接,则只能调用 sendto和sendmsg,且必须规定目的地址。
图32-9给出了rip_output函数。
1. 内核填充IP首部
119-128 如果IP_HDRINCR插口选项未定义, M_PREPEND为IP首部分配空间,并填充 IP首
部各字段。此处未填充的字段留待 i p _ o u t p u t初始化 (图8 - 2 2 )。协议字段等于 P C B中保存的
值,并且是图32-10中socket系统调用的第三个参数。
TOS等于0,TTL等于255。内核为原始IP插口填充各首部字段时,通常都使用这些固定值。
这与UDP和TCP不同,应用进程能够通过插口选项设定 IP_TTL和IP_TOS值。
129 应用程序通过 I P _ O P T I O N S 插口选项设定的所有 I P 选项,都通过 o p t s 变量传给
ip_output函数。
2. 调用者填充 IP首部:IP_HDRINCR插口选项

Linux公社 www.linuxidc.com
bbs.theithome.com
第 32章 原 始 IP计计 845
130-133 如果选用了 I P _ H D R I N C R插口选项,调用者在数据报前提供完整的 IP首部。如果
应用进程提供的 ID字段等于 0,对此类IP首部需做的唯一修改是 ID字段。 IP数据服的 ID字段可
以等于 0。此处, r i p _ o u t p u t对I D字段的赋值可以简化应用进程的处理,直接设 I D字段等
于0,rip_output向内核请求内核变量 ip_id的当前值,做为 IP报文段的ID值。
134-136 令opts为空,忽略应用进程通过 IP_OPTIONS可能设定的任何 IP选项。如果调用
者构建了自己的 I P首部,其中肯定已包括了调用者希望加入的 I P选项。 f l a g s变量中必须有
IP_RAWOUTPUT标志,告诉 ip_output不要修改IP首部。

图32-9 rip_output 函数

137 计数器ips_rawout递增。执行 Traceroute时,Traceroute每发送一个变量就会导致此变


量加1。
r i p _ o u t p u t的操作在不同版本中也有所变化。在 N e t / 3中使用I P _ H D R I N C L插
口选项时, r i p _ o u t p u t对I P首部所做的唯一修改就是填充 I D字段,如果应用进程
将其定为0。因为I P _ R A W O U T P U T标志置位, N e t / 3中的i p _ o u t p u t函数不改动I P首

Linux公社 www.linuxidc.com
bbs.theithome.com
846 计计TCP/IP详解 卷 2:实现

部。但在 N e t / 2中,即使 I P _ H D R I N C L插口选项设定时,它也会修改 I P首部中特定字


段:IP版本号等于 4,分片偏移量等于 0,分片标志被清除。

32.7 rip_usrreq函数

协议的用户请求处理函数能够完成多种操作。与 U D P和T C P的用户请求处理函数类似,


rip_usrreq是一个很大的switch语句,每个 PRU_xxx请求,都有一个对应的 case子句。
图32-10给出的PRU_ATTACH请求,来自 socket系统调用。

图32-10 rip_usrreq 函数:PRU_ATTACH 请求

194-206 每次s o c k e t函数被调用时,都会创建新的 s o c k e t结构,此时还没有指向某个


Internet PCB。
1. 确认超级用户
207-210 只有超级用户才能创建原始插口,这是为了防止普通用户向网络发送自己的 I P数
据报。
2. 创建Internet PCB,保留缓存空间
211-215 为输入和输出队列保留所需空间,调用 in_pcballoc分配新的Internet PCB,添
加到原始 IP PCB链表中(r a w i n p c b),并与s o c k e t结构建立对应关系。 r i p _ u s r r e q的n a m
参数就是 s o c k e t系统调用的第三个参数:协议。它被保存在 P C B中,因为 r i p _ i n p u t需用
它上交收到的数据报, r i p _ o u t p u t 也要把它填入到外出数据报的协议字段中 ( 如果
IP_HDRINCL未设定)。
原始IP插口与远端 IP地址建立的连接,与 UDP插口和远端 IP地址建立的连接相类似。它固
定了原始插口只能接收来自于特定地址的数据报,如我们在 r i p _ i n p u t中所看到的。原始 I P

Linux公社 www.linuxidc.com
bbs.theithome.com
第 32章 原 始 IP计计 847
与UDP一样,是一个无连接协议,下面两种情况下会发送 PRU_DISCONNECT请求:
1) 关闭建立连接的原始插口时,在 P R U _ D E T A C H之前会先发送 P R U _ D I S C O N N E C T请
求。
2) 如果对一个已建立连接的原始插口调用 connect,soconnect在发送PRU_CONNECT
请求前会先发送 PRU_DISCONNECT请求。
图32-11给出了PRU_DISCONNECT、PRU_ABORT和PRU_DETACH请求。

图32-11 rip_usrreq 函数:PRU_DISCONNECT 、PRU_ABORT 和PRU_DETACH 请求

217-222 如果处理PRU_DISCONNECT请求的插口没有进入连接状态,则返回错误。
223-225 尽管禁止在一个原始插口上发送 P R U _ A B O R T请求,这个 c a s e 语句实际上是
PRU_DISCONNECT请求处理的延续。插口转入断开状态。
226-230 close系 统 调 用 发 送 PRU_DETACH 请 求 , 这 个 case语 句 还 将 结 束
P R U _ D I S C O N N E C T请求的处理。如果 s o c k e t结构用于多播选路 (i p _ m r o u t e r),则调用
i p _ m r o u t e r _ d o n e 取 消 多 播 选 路 。 一 般 情 况下, m r o u t e d ( 8 )守 护 程 序 会通过
D V M P R _ D O N E插口选项取消多播选路,因此,这个条件用于防止 m r o u t e d (8) 在没有正确设
定插口选项之前就异常终止了。
231 调用in_pcbdetach释放Internet PCB,并从原始 IP PCB表(rawinpcb)中删除。
通过P R U _ B I N D请求,可以把原始 I P插口绑定到某个本地 I P地址上,如图 3 2 - 1 2所示。我
们在rip_input中指出,插口将只能接收发向该地址的数据报。
233-250 应用进程向s o c k a d d r _ i n结构填充本地IP地址。下列 3个条件必须全真,否则将
返回差错代码EADDRNOTAVAIL:
1) 至少配置了一个 IP接口;
2) 地址族应等于AF_INET (或者AF_IMPLINK,历史上人为造成的不一致 );和
3) 如果绑定的IP地址不等于0.0.0.0,它必须对应于某个本地接口。调用者的 sockaddr_in
中的端口号必须等于 0,否则,ifa_ifwithaddr将返回错误。
本地IP地址保存在 PCB中。

Linux公社 www.linuxidc.com
bbs.theithome.com
848 计计TCP/IP详解 卷 2:实现

图32-12 rip_usrreq 函数:PRU_BIND 请求

应用进程还可以在原始 I P插口与某个特定远端 I P地址间建立连接。我们在 r i p _ i n p u t中


指出,这样可以限制应用进程只能接收源 I P地址等于连接对端 I P地址的数据报。应用进程可
以同时调用 b i n d和c o n n e c t,或者两者都不调用,取决于它希望 r i p _ i n p u t对接收数据报
采用的过滤方式。图 32-13给出了PRU_CONNECT请求的处理逻辑。
251-270 如果调用者的 s o c k a d d r _ i n初始化正确,且至少配置了一个 I P接口,则指定的
远端地址将存储在 P C B中。注意,这一处理和 U D P插口建立与远端 I P地址的连接有所不同。
对于U D P,i n _ p c b c o n n e c t申请到达远端地址的一条路由,并把外出接口视为本地地址 (图
2 2 - 9 ) 。对于原始 I P ,只有远端 I P 地址存储到 P C B 中,除非应用进程还调用了 b i n d ,
rip_input将只比较远端地址。

图32-13 rip_usrreq 函数:PRU_CONNECT 请求

Linux公社 www.linuxidc.com
bbs.theithome.com
第 32章 原 始 IP计计 849
应用进程结束发送数据后,调用 s h u t d o w n,生成P R U _ S H U T D O W N请求,尽管应用进程
很少为原始 I P插口调用 s h u t d o w n。图3 2 - 1 4给出了 P R U _ C O N N E C T 2和P R U _ S H U T D O W N请求
的处理逻辑。

图32-14 PRU_CONNECT2 和PRU_SHUTDOWN 请求

271-273 原始IP插口不支持 PRU_CONNECT2请求。


274-279 socantsendmore置位插口标志,禁止所有输出。
图2 3 - 1 4中,我们给出了 5个写函数如何调用协议的 p r _ u s r r e q函数,发送 P R U _ S E N D请
求。图32-15给出了这个请求的处理逻辑。

图32-15 rip_usrreq 函数:PRU_SEND 请求

280-303 如果插口处于连接状态,则调用者不能指定目的地址 (nam参数)。如果插口未建立


连接,则需要指明目的地址。不管哪种情况,只要条件满足, d s t 将等于目的 I P 地址。
r i p _ o u t p u t发送数据报。令 m b u f指针m为空,防止函数结束时释放 m b u f链。因为接口输出

Linux公社 www.linuxidc.com
bbs.theithome.com
850 计计TCP/IP详解 卷 2:实现

例程发送数据报之后会释放 m b u f链( 记住, r i p _ o u t p u t 向i p _ o u t p u t 提交 m b u f链,


ip_output把它加入到接口的输出队列中 )。
图3 2 - 1 6给出了 r i p _ u s r r e q的最后一部分代码。由 f s t a t系统调用生成的 P R U _ S E N S E
请求,没有返回值。 P R U _ S O C K A D D R 和P R U _ P E E R A D D R 请求分别由 g e t s o c k n a m e 和
getpeername系统调用生成。原始 IP插口不支持其余请求。
319-324 函数i n _ s e t s o c k a d d r和i n _ s e t p e e r a d d r能够从P C B中读取信息,在 n a m参
数中返回结果。

图32-16 rip_usrreq 函数:剩余的请求

32.8 rip_ctloutput函数

s e t s o c k o p t和g e t s o c k o p t函数会调用 r i p _ c t l o u t p u t,它处理一个IP插口选项和 8


个用于多播选路的插口选项。
图32-17给出了rip_ctloutput函数的第一部分。

图32-17 rip_usrreq 函数:处理 IP_HDRINCL 插口选项

Linux公社 www.linuxidc.com
bbs.theithome.com
第 32章 原 始 IP计计 851

图32-17 (续)

144-172 保存新选项值或者选项当前值的 mbuf至少要能容纳一个整数。对于 setsockopt


系统调用,如果 m b u f中的整数值非零,则设定该标志,否则清除它。对于 g e t s o c k o p t系统
调用,m b u f中的返回值要么等于 0,要么是非零的选项值。函数返回,以避免 s w i t c h语句结
束时处理其他IP选项。
图32-18给出了rip_ctloutput函数的最后一部分,处理 8个多播选路插口选项。

图32-18 rip_usrreq 函数:处理多播选路插口选项

173-188 这 8个插口选项只对 s e t s o c k o p t 系统调用有效,它们由图 1 4 - 9 讨论的

Linux公社 www.linuxidc.com
bbs.theithome.com
852 计计TCP/IP详解 卷 2:实现

ip_mrouter_cmd函数处理。
189 所有其他IP插口选项,如设定 IP选项的IP_OPTIONS,则由ip_ctloutput处理。

32.9 小结

原始插口为 IP主机提供 3种功能。


1) 用于发送和接收 ICMP和IGMP报文。
2) 支持应用进程构建自己的 IP首部。
3) 允许应用进程支持基于 IP的其他协议。
原始IP较为简单—只填充IP首部的有限几个字段—但它允许应用进程提供自己的 IP首
部。例如,调试程序就能发送任何类型的 IP数据报。
原始I P输入提供了 3种处理方式,能够选择性地接收进入的 I P数据报。应用进程基于下列
因素选择接收数据报: (1) 协议字段; (2) 源I P地址 (由c o n n e c t指明 );(3) 目的I P地址 (由
bind指明)。应用进程可以任意组合上述 3种过滤条件。

习题

32.1 假定 I P _ H D R I N C L 插口选项未设定。如果 s o c k e t 的第三个参数等于 0 ,


r i p _ o u t p u t填入I P首部协议字段 (i p _ p)的值是多少?如果 s o c k e t的第三个参数
等于IPPROTO_RAW (255),rip_output填入该段(ip_p)的值又是多少?
32.2 应用进程创建了一个原始插口,协议值等于 I P P R O T O _ R A W ( 2 5 5 )。应用进程在这
个插口上将收到什么类型的 IP数据报?
32.3 应用进程创建了一个原始插口,协议值等于 0。应用进程在这个插口上将收到什么
类型的IP数据报?
32.4 修改 r i p _ i n p u t,在适当情况下发送代码等于 2 ( 协议不可达 )的I C M P目的不可达
报文。请注意,不要为 rip_input正处理的ICMP或IGMP数据报生成一个差错。
32.5 如果应用进程希望生成自己的 IP数据报,自己填充IP首部字段,可使用IP_HDRINCL
选项置位的原始 IP插口,或者采用 BPF(第31章),两种方法的区别是什么?
32.6 什么时候应用进程应该读取原始 IP插口?什么时候读取 BPF?

Linux公社 www.linuxidc.com
bbs.theithome.com
附录A 部分习题的解答
第1章

1.2 S L I P驱动程序执行 s p l t t y(图1 - 1 3 ),其优先级必须低于或等于 s p l i m p,且高于


splnet。因此,SLIP驱动程序由于中断而被阻塞。

第2章

2.1 M_EXT标志是mbuf自身的一个属性,而不是 mbuf中保存的数据报的属性。


2.2 调用者请求大于 100字节(MHLEN)的连续空间。
2.3 不可行,因为多个 m b u f都可指向簇 ( 2 . 9节)。此外,簇中也没有用于后向指针的空间
(习题2.4)。
2.4 在< s y s / m b u f . h >定义的宏M C L A L L O C和M C L F R E E中,我们看到引用计数器是一个
名为mclrefcnt的数组。它在内核初始化时被分配,代码文件为 machdep.c。

第3章

3.3 采用很大的交互式队列,并不符合建立队列的目的,新的交互式流量跟在原有流量
之后,会造成附加时延。
3.4 因为sl_softc结构都是全局变量,内核初始化时都被置为 0。
3.5

9字节
3字节

环回

9字节
3字节

图 A-1

第4章
4.1 l e r e a d必须查看数据报,确认把数据报提交给 B P F之后,是否需要将其丢弃。因为

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 855
B P F开关会造成接口处于一种混杂模式,数据报的目的地有可能是以太网中的其他
主机,BPF处理完毕后,必须将其丢弃。
如果接口没有加开关,则必须在 ether_input中完成这一测试。
4.2 如果测试反过来,广播标志永远不会置位。
如果第二个 if前没有else,所有广播分组都会带上多播标志。

第5章

5.1 环回接口不需要输入函数,因为它接收的所有分组都直接来自于 l o o u t p u t,后者


实际完成了输入功能。
5.2 堆栈分配快于动态存储器分配。对 B P F处理,性能是首要考虑的因素,因为对每个
进入数据报都会执行该代码。
5.5 缓存溢出的第一个字节被丢弃, S C _ E R R O R置位, s l i n p u t重设簇指针,从缓存起
始处开始收集字符。因为 SC_ERROR置位,slinput收到SLIP END字符后,丢弃当
前接收的数据帧。
5.6 如果检验和无效或者 IP首部长度与实际数据报长度不匹配,数据报被丢弃。
5.7 因为ifp指向le_softc结构的第一个成员,
sc = (struct le_softc*) ifp;

sc初始化正确。
5.8 这是非常困难的。某些路由器在开始丢弃数据报时,可能会发送 I C M P源站抑制报文
段,但N e t / 3实现中的 U D P插口丢弃这些报文段 (图2 3 - 3 0 )。应用程序可以使用与 T C P
所采用的相同技术:根据确认的数据报估算往返时间,确认可用的带宽和时延。

第6章

6.1 I P子网出现之前 (RFC 950 [Mogul和Postel 1985]),I P地址的网络和主机部分都以字


节为界。 in_addr结构的定义如下:

图 A-2

I n t e r n e t地址读写单位既可以是 8 bit 字节,也可以是 16 bit 单字,或 32 bit 双字。宏


s_host、s_net、s_imp等等,它们的名字明确地反应出早期 TCP/IP网络的结构。
子网和超网概念的引入,淘汰了这种以字节和单字区分的做法。
6.2 返回指向结构sl_softc[0]的指针。

Linux公社 www.linuxidc.com
bbs.theithome.com
856 计计TCP/IP详解 卷 2:实现

6.3 接口输出函数,如 e t h e r _ o u t p u t,只有一个指向接口 i f n e t结构的指针,而没有


指向i f a d d r的指针。在 a r p c o m结构(最后一次为接口设定的 I P地址)中使用 I P地址
可以避免从 ifaddr地址链表中寻找所需地址。
6.4 只有超级用户进程才能创建原始 I P插口。通过 U D P插口,任何用户进程能够查看接
口配置,但内核仍拥有超级用户特权,能够修改接口地址。
6.5 有3个函数循环处理网络掩码,一次处理一个字节。它们是 i f a _ i f w i t h n e t 、
ifaof_ifpforaddr和rt_maskedcopy。较短的网络掩码能够提高这些函数的性能。
6.6 与远端系统建立 Te l n e t连接。 N e t / 2系统不应该转交这些数据报,而其他系统不会接
受到达环回接口之外的非环回接口的环回数据报。

第7章
7.1 下列调用返回指向 inetsw[6]的指针:
pffindproto(PF_INET, 0, SOCK_RAW);

第8章
8.1 可能不会。系统不可能响应任意的广播报文,因为没有可供响应的源地址。
8.4 因为数据报已经损坏,无法知道首部中的地址是否正确。
8.5 如果应用程序选取的源地址与指定的外出接口的地址不同,则无法发送到下一跳路由
器。如果下一跳路由器发现数据报源地址与其到达的子网地址不符,则不会执行下一
步的转发操作。这是尽量减少终端系统复杂性带来的后果,RFC 1122指出了这一问题。
8.6 新主机认为广播报文来自于某个没有划分子网的网络中的主机,并试图将数据报发
回给源主机。网络接口开始广播 A R P请求,向网络请求该广播地址,当然,这一请
求永远不会收到响应。
8.7 减少T T L的操作出现在小于等于 1的测试之后,是为了避免收到的 T T L等于0,减1后
将等于255,从而引起操作差错。
8.8 如果两个路由器彼此认为对方是某个数据报的下一跳路由器,则形成环路。除非该
环路被打破,原始数据报在两个路由器间来回传递,并且每个路由器都向源主机发
送I C M P重定向报文段,如果该主机与路由器处于同一个网络中。路由更新时,不同
路由器中的路由表暂时存在的不一致现象,会造成这种环路。
原始报文段的TTL最终减为0,数据报被丢弃。这是 TTL存在的一个主要原因。
8.9 不会检查4个以太网广播地址,因为它们不属于接收接口。但应检查有限的广播地址,说
明带有SLIP链路的系统采用有限的广播地址,即使不知道对端地址,也能与对端通信。
8.10 只对数据报的第一个分片 (分片偏移量等于 0 )生成I C M P差错报文。无论是主机字节
序,还是网络字节序, 0的表示都相同,因此无需转换。

第9章
9.1 RFC 11 2 2建议如果数据报中的选项彼此冲突,处理方式由各实现代码自己决定。
N e t / 3能正确处理第一个源路由选项,但因为它会更新数据报首部的 i p _ d s t,第二
条源路由处理将出现差错。
9.2 网络中的主机也可以用做到达网络其他主机的中继。如果目的主机不可直接到达,

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 857
源主机可在数据报中加入路由,首先到达中继主机,接着到达最终的目的主机。路
由器不会丢弃数据报,因为目的地址指向中继主机,后者将处理路由并把数据报转
发给最终目的主机。目的主机把路由反转,同样利用中继主机转发响应。
9.3 采用与前一个习题同样的原则。我们选取一个能够同时与源主机和目的主机通信的
中继路由器,并构造源路由,穿过中继路由器到达目的地址。中继路由器必须与目
的地址处于同一个网络,通信中无需默认路由。
9.4 如果源路由是仅有的 I P选项,N O P选项使得所有 I P地址以4字节边界对齐,从而能够
优化存储器中的地址读取操作。这种对齐技术也适用于多个 I P选项,如果每个 I P选
项都通过NOP填充,保证按4字边界对齐。
9.5 不应混淆非标准时间值和标准时间值,最大的标准时间值等于86 399 399(24×60×60×
1000-1),需要28 bit才能表示。由于时间值有32 bit,从而避免了高位比特的混淆问题。
9.6 源路由选项代码在处理过程中可能会改变 i p _ d s t。保存目的地址,从保证时间戳处
理使用原始目的地址。

第10章

10.2 重装后,只有第一个分片的选项上交给运输层协议。
10.3 因为数据长度(204 + 20 )大于208(图2-16)。
图10-11中的m_pullup把头40字节复制到一个单独的 mbuf中,如图2-18所示。

id=6的
ipq

ipq首部

数据报起始
20字节

接下来的184字节数据报

2048字节簇

图 A-3

Linux公社 www.linuxidc.com
bbs.theithome.com
858 计计TCP/IP详解 卷 2:实现

10.5 平均每个数据报收到的分片数等于
7 2 7 8 6− 349
= 4.4
16 557
平均每个输出数据报新建的平均分片数等于
796 084
= 3.1
2 6 0484
10.6 图10-11中,数据报最初被做为分片处理。当 ip_off左移时,保留的比特位被丢弃。
得到的数据报被视为分片或一个完整的数据报,取决于 MF和分片偏移量的值。

第11章

11.1 输出响应使用收到请求的接口的源地址。主机可能无法辩识 0 . 0 . 0 . 0是一个有效的广


播地址,因此,有可能忽略请求。推荐的广播地址等于 255.255.255.255。
11.2 假定主机发送了一个链路层的广播数据报,其源 I P地址是另一台主机的地址,且数
据报有差错,如内容差错的选项。所有主机都能接收并检测出差错,因为这是一个
链路层的广播报文,而且选项的处理先于最终目的地的检测。许多发现差错的主机
会向数据报的源 I P地址发送 I C M P报文,即使原数据报属于链路层广播。另一台主
机将收到大量假的 I C M P差错。这就是为什么不允许为链路层广播而发送 I C M P差错
报文。
11.3 第一个例子中,这种重定向报文不会诱骗主机向另一个子网中的某个主机发送报文
段。这台主机可能被误认为是路由器,但它确实记录收到的流量。 RFC 1009规定路
由器只能向位于同一个子网的其他路由器发送重定向报文。即使主机忽略了这些要
求把数据报转发到另一个子网的报文段,但如果报文段发送者与主机处于同一个子
网中,它们就会被接受。第二个例子,为了防止出现上述现象,要求主机只接受它
(错误地 )选定的原始路由器的重定向报文,即假定这个错误的路由器是管理员指定
的默认路由器。
11.4 通过向 r i p _ i n p u t传递报文段,进程级的守护程序能够正确响应,一些依赖于这
种行为的老系统能够继续得到支持。
11.5 I C M P差错只针对 I P数据报的第一个分片。因为第一个分片的偏移量值必等于 0,字
段的字节表示顺序是无关紧要的。
11.6 如果收到ICMP请求的接口还未配置 IP地址,则ia将为空,且不生成响应。
11.7 Net/3处理与时间戳响应一起到达的数据。
11.10 高位比特被保留,并必须设为 0。如果它必须被发送,则 i c m p _ e r r o r将丢弃数据
报。
11.11 返回值被丢弃,因为 i c m p _ s e n d不返回差错。更重要的是, I C M P报文处理过程
中生成的差错将被丢弃,以避免进入死循环,不断生成差错报文。

第12章

12.1 以太网中, I P广播地址 2 5 5 . 2 5 5 . 2 5 5 . 2 5 5转换为以太网的广播地址 ff : ff : ff : ff : ff : ff,网


络中的所有以太网接口都会接收这样的数据帧。没有运行 I P软件的系统必须主动接

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 859
收并丢弃这种广播报文。
数据报需发送给多播组 2 2 4 . 0 . 0 . 1 中的所有主机,转换后的以太网多播地址为
0 1 : 0 0 : 5 e : 0 0 : 0 0 : 0 1,只有明确要求其接口接收 I P多播报文的系统才会收到它。
没有运行 I P或者在链路层不兼容的系统不会收到这些报文段,因为以太网接口的硬
件已直接丢弃了这些报文段。
12.2 一种替代方案是通过文本名规定接口,如同 i f r e q结构和i o c t l命令存取接口信息
采取的方式一样。 i p _ s e t m o p t i o n s和i p _ g e t m o p t i o n s可以调用 i f u n i t,取
代INADDR_TO_IFP,寻找指向接口 ifnet结构的指针。
12.3 多播组高位 4 bit通常为1110,因此,只有5个有意义的比特被匹配函数丢弃。
12.4 完整的 i p _ m o p t i o n s结构必须能放入单个 m b u f中,从而限制结构最大只能等于
1 0 8字节(记住2 0字节的 m b u f首部)。I P _ M A X _ M E M B E R S H I P S可以大一些,但必须
小于等于25(4+1+1+2+(4×25)=108)。
12.5 数据报重复,在 I P输入队列中有两份复制的数据报。多播应用程序必须能识别并丢
弃重复的数据报。
12.6

多播路
主机
由器

图 A-4

12.8 应用进程可以创建第二个插口,并通过第二个插口请求 IP_MAX_MEMBERSHIPS。


12.9 为mbuf首部的m_flags成员变量定义一个新的 mbuf标志M_LOCAL。ip_output处
理环回数据报时置位该标志,从而取代检验和。如果该标置位, i p i n t r就跳过检
验和验证。SunOS 5.X提供完成此功能的选项 (ip_local_cksum,卷1的531页)。
12.10 存在223-1(8 388 607)个独立的以太网 IP多播地址。记住保留的 IP组224.0.0.0。
12.11 这个假设正确,因为 i n _ a d d m u l t i拒绝所有新增请求,如果接口没有调用 ioctl函
数,说明如果if_ioctl为空,则永不会调用 in_delmulti。
12.12 m b u f 永远不会被释放,说明 i p _ g e t m o p t i o n s 包含了一个存储器泄露。
ip_getmoptions由ip_ctloutput调用,调用语句如下:
ip_getmoptions(IP_ADD_MEMBERSHIP, 0, mp)

会引发ip_getmoptions中的一个差错。

第13章

13.1 要求环回接口响应 I C M P请求是没有必要的,因为本地主机是环回网络中唯一的系


统,它已经知道自己的成员状态。
13.2 m a x _ l i n k h d r + s i z e o
(fs t r u c t i)p+IGMP_MINLEN=16+20+8=44<100
13.3 报告成员状态时出现随机延迟的主要原因是为了最大限度地减少出现在多播网络中
的报告数(理想情况下应等于 1)。一个点到点网络只包括两个接口,因此,无需延迟

Linux公社 www.linuxidc.com
bbs.theithome.com
860 计计TCP/IP详解 卷 2:实现

以减少响应的数量。一个接口 (假定是一个多播路由器 )发出请求,另一个接口响应。


另一个原因是避免过多的成员状态报告淹没接口的输出队列。大量 I G M P成员状态报文可
能会超出输出队列关于数据报和字节的限制。例如,在 S L I P驱动程序中,如果输出队列
已满或设备过忙,就会丢弃队列中所有等待的数据报 (图5-16)。

第14章

14.1 5个,分别对应网络 A~E。


14.2 g r p l s t _ m e m b e r 只被 i p _ m f o r w a r d 调用,但在协议处理过程中,
i p _ m f o r w a r d又将被 i p i n t r或者i p _ o u t p u t调用,i p _ o u t p u t可以由插口层
间接调用。缓存是一个共享数据区,在更新时必须加以保护。 add_lgrp和
del_lgrp在更新成员列表时,通过 splx保护此共享数据结构。
14.3 S I O C D E L M U L T I命令只影响以太网接口的多播列表,不改变 I P多播组列表,因此,
接口仍然保留为组中成员。只要依旧是接口 I P组列表中的一员,接口将继续接收属
于该组的多播数据报。
14.4 只有虚接口才能成为多播树的父接口。如果分组在隧道上接收,那么对应的物理接
口不可能成为父接口, ip_mforward丢弃分组。

第15章

15.1 插口可以在分支上共享,或通过 UNIX域插口传给应用进程 ([Stevens])。


15.2 a c c e p t返回后,结构的 s a _ l e n成员大于缓存大小。对固定长度的 I n t e r n e t地址而
言,这不是问题,但它有可能用于可变长度的地址,例如 OSI协议支持的地址格式。
15.4 只有s o _ q l e n不等于0时,才会调用 s o q r e m q u e。如果s o q r e m q u e返回一个空指
针,说明插口队列代码必然出现了内核无法处理的问题。
15.5 复制的目的在于结构锁定时仍可调用 b z e r o 清零,并可在 s p l x 后接着调用
d o m _ d i s p o s e和s b r e a l s e,从而最大程度地减少了 C P U停留在s p l i m p的时间,
即网络中断被阻塞的时间。
15.6 宏s b s p a c e返回0,从而s b a p p e n d a d d r和s b a p p e n d c o n t r o l函数(由UDP调用)
将拒绝向队列添加新报文段。 T C P调用s b a p p e n d,后者假定调用者已事先检查过
可用空间。即使 s b s p a c e返回0,T C P也会调用 s b a p p e n d,但放入接收队列中的
数据还不能提交给应用进程,因为 S S _ C A N T R C V M O R E标志阻止r e a d系统调用返回
任何数据。

第16章

16.1 如果给 u i o结构中的 u i o _ r e s i d赋值,它将成为一个大负数。 s o s e n d拒绝带有


EINVAL的报文段。
Net/2不检查负值,sosend起始处的注释说明了这个问题 (图16-23)。
16.2 不。向簇中填充的字节数少于 M C L B Y T E S只可能出现在报文段尾部,此时剩余的字
节数小于M C L B Y T E S。此时,r e s i d等于0,循环在 394行b r e a k语句处终止,还未

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 861
到达测试条件spce>0。
16.5 应用进程阻塞,直到缓存解锁。本例中,只有在另一个进程检查缓存或向协议层传
送数据时,缓存才会被锁定;而在应用进程等待缓存可用空间时不会加锁,后者有
可能等待无限长的时间。
16.6 如果发送缓存包括许多 m b u f,每个都包括若干字节的数据,那么当 m b u f分配大块
存储器时, s b _ c c很可能大大低于 s b _ h i w a t规定的限制。如果内核不限制每个缓
存可拥有的 mbuf的数量,应用进程就能轻易地造成存储器枯竭。
16.7 recvit分别由r e c v f r o m和r e c v m s g调用。只有 r e c v m s g处理控制信息。它把完
整的 m s g h d r 结构,包括控制信息长度,复制给应用进程。至于地址信息,
r e c v m s g把n a m e l e n p参数设为空,因为它可从 m s g _ n a m e l e n中得到所需长度。
当r e c v f r o m调用r e c v i t时,n a m e l e n p非空,因为函数需要从 * n a m e l e n p中得
到所需长度。
16.8 M S G _ E O R由s o r e c e i v e 清除,因此,它不可能在 M _ E O R m b u f被处理前,被
soreceive返回。
16.9 s e l e c t检查描述符时,实际上存在一种竞争。如果某个选定事件发生在 s e l s c a n
查看描述符之后,但在 s e l e c t调用t s l e e p之前,该事件不会被发现,应用进程
将保持睡眠状态,直到下一个选定事件发生。

第17章

17.1 简化在内核和应用进程间复制数据的代码。copyin和copyout可用于单个的mbuf,
但需要uiomove处理多个mbuf。
17.2 代码工作正确,因为 linger结构的第一个成员是所要求的整数标志。

第18章

18.1 做一个 8行的表格,每行对应一种查找键、路由表键和路由表掩码中比特的组合方


式:

行 1 2 3 1&3 2 = = 4? 1^2 6&3


查找键 路由表键 路由表掩码

1 0 0 0 0 是 0 0=是
2 0 0 1 0 是 0 0=是
3 0 1 0 0 否 1 0=是
4 0 1 1 0 否 1 1=否
5 1 0 0 0 是 1 0=是
6 1 0 1 1 否 1 1=否
7 1 1 0 0 否 0 0=是
8 1 1 1 1 是 0 0=是

图 A-5

标注为“2 == 4?”和标注为“ 6 & 3”的两栏,值应相等。第一眼看上去,似乎并


不完全相同,但我们可以略过第 3行和第 7行,因为这两行中路由表比特等于 1,而

Linux公社 www.linuxidc.com
bbs.theithome.com
862 计计TCP/IP详解 卷 2:实现

在路由表掩码中的对应比特也等于 1。构建路由表时,键值与掩码逻辑与,保证掩
码中的等于 0每一比特位,键值中的对应比特位也等于 0。
可以从另一个角度理解图 1 8 - 4 0中的异或和逻辑与操作,异或结果等于 1的条件是查
找键比特不同于路由表键值中对应的比特位。之后的逻辑与操作忽略所有与掩码中
等于0的比特相对应的比特。如果结果依然非零,则查找键与路由表键值不匹配。
18.2 r t e n t r y结构的大小等于 1 2 0字节,其中包括两个 r a d i x _ n o d e结构。每条记录还
要求两个 sockaddr_in结构(图18-28),有152字节。总数约为 3兆字节。
18.3 因为 r n _ b是一个短整数,假定短整数占 16 bit ,因此,每个键值最多有 3 2 7 6 7
bit(4095字节)。

第19章

19.1 图19-15中,如果重定向报文创建了新的路由,将置位 R T F _ D Y N A M I C标志;如果重


定向报文修改了现有路由的网关字段,则置位 R T F _ M O D I F I E D标志。如果重定向
报文新建了一条路由,之后另一个重定向报文又修改了它,则两个标志都会置位。
19.2 在每个可通过默认路由到达的主机上创建一条主机路由。 T C P能够对每个主机维护
并更新路由矩阵 (图27-3)。
19.3 每个r t _ m s g h d r结构需要 7 6字节。主机路由中包括还两个 s o c k a d d r _ i n结构(目的地
和网关 ),因此,报文段大小为 1 0 8字节。每条 A R P记录的报文段为 11 2字节:一个
sockaddr_in和一个sockaddr_dl。总长度等于(15×112+20×108)即3840字节。
一条网络路由 (非主机路由)还需要另外的 8个字节存放网络掩码 (数据大小等于 116字
节,而非108字节),因此,如果20条路由全部为网络路由,总长度等于 4000字节。

第20章

20.1 返回值放入报文段的 r t m _ e r r n o成员变量中 (图2 0 - 1 4 ),同时也做为 w r i t e的返回


值(图2 0 - 2 2 )。后者更可靠,因为前者可能会因为 m b u f短缺,而丢弃响应报文段 (图
20-17)。
20.2 对S O C K _ R A W型的插口, p f f i n d p r o t o函数(图7 - 2 0 )将返回协议值等于 0 (通配)的
记录,如果没有找到可匹配的记录。

第21章

21.1 它基于假定 ifnet结构位于arpcom的开头,事实也是如此 (图3-20)。


21.2 发送I C M P的回显请求不需要 A R P,因为目的地址是广播地址。但 I C M P的回显响应
一般都是点对点的,因此,发送者必须通过 A R P确定目的以太网地址。本地主机收
到ARP请求时,in_arpinput应答并为另一主机创建一条记录。
21.3 如果创建了一条新的 A R P 记录,图 1 9 - 8 中的 r t r e q u e s t 从源记录中复制
r t _ g a t e w a y值,本例中为 s o c k a d d r _ d l结构。图 2 1 - 1中,我们看到该记录的
sdl_alen值等于0。
21.4 N e t / 3中,如果 a r p r e s o l v e的调用者提供了指向路由表表项的指针,则不会再调
用a r p l o o k u p,通过 r t _ g a t e w a y指针可得到所需的以太网地址 (假定它还未超

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 863
时)。这样可以避免通常意义上的任何类型的查询。第 2 2章中,我们将看到 T C P和
U D P在自己的协议控制块中保存指向路由表的指针, T C P不再需要搜索路由表 (连
接的目的IP地址不会变化),在目的地址不变时 UDP也不需这样做。
21.5 如果A R P记录不完整,则它在记录创建后 0 ~ 5分钟超时。 a r p r e s o l v e发送A R P请
求时,令 r t _ e x p i r e等于当前时间。下一次执行 a r p r e s o l v e时,如果记录还没
有解析,则删除它。
21.6 e t h e r _ o u t p u t返回E H O S T U N R E A C H,而非 E H O S T D O W N,从而 i p _ f o r w a r d将
发送ICMP主机不可达差错报文。
21.7 图21-28中,为140.252.13.35创建记录时,值等于当前时间。它不会改变。
1 4 0 . 2 5 2 . 1 3 . 3 3和1 4 0 . 2 5 2 . 1 3 . 3 4记录的值复制自 1 4 0 . 2 5 2 . 1 3 . 3 2,因为 r t r e q u e s t根
据140.252.13.32复制前两条记录。之后, a r p r e s o l v e发送ARP请求时,把这两条
记录的值更新为当前时间,最后由 i n _ a r p i n p u t将其更新为收到 A R P响应的时间
加上20分钟。
21.8 修改图21-19开始处的arplookup,第二个参数永远等于 1(创建标志)。
21.9 在下一秒的后半秒发送第一个数据报。因此,第一个和第二个数据报都会导致发送
A R P请求,间隔约为 500 ms,因为内核的 t i m e . t v _ s e c变量在这两个数据报发送
时的值不同。
21.10 每个待发送的数据报都是一个 m b u f链, m _ n e x t p k t指针指向每个链的第一个
mbuf,用于构成等待传输的 mbuf链表。

第22章

22.1 无限循环等待某个端口变为可用,假定允许应用进程打开足够多的描述符,绑定所
有临时端口。
22.2 极少有服务器支持此选项。 [ C h e s w i c k和Bellovin 1994]提到为什么它可用于实现防
火墙系统。
22.4 u d b结构初始化为 0,因此, u d b . i n p _ l p o r t从0开始。第一次调用 i p _ p c b b i n
时,它增加为1,因为小于 1024,所以被设定为 1024。
22.5 一般情况下,调用者把地址族 (s a _ f a m i l y)设为A F _ I N E T,但我们在图 2 2 - 2 0的注
释中看到,最好不进行关于地址族的测试。调用者设定长度变量 (s a _ l e n),但我
们在图 1 5 - 2 0 中看到,函数 s o c k a r g s 将其做为 b i n d 的第 3 个参数,对于
sockaddr_in结构,应等于16,通常都使用C的sizeof操作符。
本地 I P 地址 (s i n _ a d d r )可以指明为通配地址或某个本地 I P 地址。本地端口号
(s i n _ p o r t ),可以等于 0 (告诉内核选择一个临时端口 )或非 0,如果应用进程希
望指明端口号。通常情况下, T C P或U D P服务器指明一个通配 I P地址,端口号等
于0。
22.6 应用进程可以 b i n d一个本地广播地址,因为 i f a _ i f w i t h a d d r(图22-22)的调用成
功。它被用做在该插口上发送的 I P数据报的源地址。 C . 2节中指出, RFC 11 2 2不允
许这种做法。但试图绑定 2 5 5 . 2 5 5 . 2 5 5 . 2 5 5时会失败,因为 i f a _ i f w i t h a d d r不接
受该地址。

Linux公社 www.linuxidc.com
bbs.theithome.com
864 计计TCP/IP详解 卷 2:实现

第23章

23.1 s o s e n d把用户数据放入单个的 m b u f中,如果其长度小于等于 1 0 0字节;放入两个


mbuf中,如果长度小于等于 207字节;否则,放入多个 mbuf中,每个都带有一个簇。
此外,如果长度小于 1 0 0字节, s o s e n d调用M H _ A L I G N,希望能在 m b u f起始处为
协议首部保留空间。因为udp_output调用M_PREPEND,下述5种情况都是可能的:
( 1 )如果用户数据长度小于等于 7 2字节,一个 m b u f就可以存放 I P首部、 U D P首部和
数据;(2)如果长度位于 73字节和100字节之间, s o s e n d为用户数据分配一个 mbuf,
M _ P E R P E N D为I P和T C P首部再分配一个 m b u f;( 3 )如果长度位于 1 0 1字节和2 0 7字节
之间,s o s e n d为用户数据分配两个 mbuf,M _ P R E P E N D为IP和TCP首部再分配一个
m b u f;( 4 )如果长度位于 2 0 8字节和 M C L B Y T E S之间, s o s e n d为用户数据分配一个
带簇的m b u f,M _ P E R P E N D为I P和T C P首部再分配一个 m b u f;( 5 )如果长度超出,则
s o s e n d分配足够多的 m b u f和簇,以存放数据 (最大数据长度 6 5 5 0 7字节,需分配 6 4
个带1024字节簇的mbuf),M_PERPEND为IP和TCP首部再分配一个 mbuf。
23.2 IP选项提交给 i p _ o u t p u t,后者调用 i p _ i n s e r t o p t i o n s在输出IP数据报中插入
IP选项。它接着分配一个新的 mbuf,存放带有IP选项的IP首部,如果第一个 mbuf指
向一个簇 ( U D P输出不可能出现这种情况 ),或者第一个 m b u f中没有足够的剩余空间
存放新增选项。上个习题中给出的第一种情况中,选项大小将决定
ip_insertoptions是否分配另一个mbuf:如果用户数据长度小于 100-28-optlen
(IP选项占用的字节数 ),说明mbuf足够存放IP首部、IP选项、UDP首部和数据。
第2、3、4和5种情况中, 第一个mbuf都由M_PREPEND分配,只存放IP和UDP首部。
M _ P R E P E N D调用M _ P R E P E N D,接着调用 M H _ A L I G N,把2 8字节的首部移到 m b u f
尾部,因此,第一个 mbuf中必定有空间存放最大为 40字节的IP选项。
23.3 不。函数 i n _ p c b c o n n e c t只有在应用程序调用 c o n n e c t,或者在一个未连接的
U D P插口上发送第一个数据报时,才会被调用。因为本地地址是通配地址,本地端
口号等于 0,所以 i n _ p c b c o n n e c t 给本地端口号赋一个临时端口 (通过调用
in_pcbbind),并根据到达目的地的路由设定本地地址。
23.4 处理器优选级仍为 splnet不变,没有还原为初始值,这是代码的一个差错。
23.5 不。 i n _ p c b c o n n e c t不允许与等于 0的端口建立连接。即使应用进程没有直接调
用connect,也会间接地执行 connect,因此,in_pcbconnect总会被调用。
23.6 应用程序必须调用 ioctl,命令为SIOCGIFCONF,返回所有已配置的 IP接口信息。
之后,在 i o c t l返回的所有 I P地址和广播地址中寻找接收数据报中的目的地址 (也
可不用 i o c t l,1 9 . 1 4节中介绍的 s y s c t l系统调用也能够返回所有配置接口的信
息)。
23.7 recvit释放带有控制信息的 mbuf。
23.8 为了断开一个已建立连接的 U D P插口,调用 c o n n e c t,传递一个无效的地址参数,
如 0 . 0 . 0 . 0 ,端口号等于 0 。因为插口已经建立了连接, s o c o n n e c t 调用
s o d i s c o n n e c t,后者调用u d p _ u s r r e q,发送P R U _ D I S C O N N E C T请求,令远端
地址等于 0.0.0.0,远端端口号等于 0。这样,接下来调用 s e n d t o时可以指明目的地

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 865
址。由于指明地址无效,sodisconnect发送的PRU_CONNECT请求失败。实际上,
我们不希望 c o n n e c t 成功,只是要执行 P R U _ D I S C O N N E C T 请求,而且通过
c o n n e c t 来执行这一请求的做法是唯一可行的方案,因为插口 A P I 没有提供
disconnect函数。
手册中关于c o n n e c t(2)的描述通常包括下述说明:“可通过把数据报插口连接到一
个无效地址,如空地址,来断开其当前连接。”但没有明确指出调用 c o n n e c t时,
如果传送的地址无效,会返回一个差错。“空地址”的含义也易造成混淆,它指 I P
地址0.0.0.0,而非bind的第二个参数的空指针。
23.9 因为i n _ p c b b i n d能够建立 UDP插口与远端IP地址间的临时连接,情况与应用进程
调用connec t类似:如果某接口的目的 IP地址与该接口的广播地址对应,则从该接
口发送数据报。
23.10 服务器必须设定 I P _ R E C V D S T A D D R插口选项,并调用 r e c v m s g从客户请求中获
取目的 I P地址。为了成为响应报文段中的源地址,必须将其绑定在插口上。由于
一个插口只能bind一次,服务器每次响应时都必须创建新的插口。
23.11 注意,i p _ o u t p u t(图8 - 2 2 )中,I P不修改调用者传递的 D F比特。需要定义新的插
口选项,促使udp_output在把数据报传递给 IP之前,设定 DF比特。
23.12 不。它只被 udp_input使用,且应为该函数的局部变量。

第24章

24.1 状态为ESTABLISHED的连接总数为126 820。除以发送和接收的总字节数,得到每


个方向上的平均字节数,约为 30 000字节。
24.2 t c p _ o u t p u t 中,保存 I P 和 T C P 首部的 m b u f 还有空间容纳链路层首部
(m a x _ l i n k h d r)。试图通过b c o p y把I P和T C P首部原型复制到 m b u f中是行不通的,
因为有可能会把 40字节的首部分散在两个 mbuf中。尽管40字节的首部必须放入单个
mbuf中,但链路层首部不存在这样的限制。不过这样做会降低性能,因为后续处理
不得不为链路层首部再次分配 mbuf。
24.3 在作者的 b s d i 系统中,计数器等于 1 6,其中 1 5个是标准系统守护程序 ( Te l n e t、
Rlogin、FTP,等等)。而v a n g o g h . c s . b e r k e l e y . e d u系统,一个约有 20个用户
的中等规模的多用户系统,计数器约为 6 0。对于大型的带有 1 5 0个用户的多用户系
统(world.std.com),则有417个TCP端点和809个UDP端点。

第25章

25.1 图24-5中,2 592 000秒 (30天)中出现了531 285次延迟ACK,平均每5秒钟有一次延


迟A C K,或者说每 2 5次调用 t c p _ f a s t t i m o,才会有一次延迟 A C K。这说明在代
码检查所有 T C P控制块,判定延迟 A C K标志是否置位时, 9 6 % ( 2 5次中有 2 4次)的时
间都未置位。对于习题 2 4 . 3中给出的大型的多用户系统,意味着需查看超过 4 0 0个
的TCP控制块,每秒钟查询 5次。
另一种解决方案是,在需要延迟 A C K时,设定全局标志。只有当全局标志置位时,
才检查控制块列表。或者为需要延迟 ACK的控制块单独建立并维护一个列表。例如,

Linux公社 www.linuxidc.com
bbs.theithome.com
866 计计TCP/IP详解 卷 2:实现

图13-14中的变量igmp_timers_are_running。
25.2 这 样 使 得 变 量 t c p _ k e e p i n t v l 绑 定 在 运 行 中 的 内 核 上 , 下 次 调 用
tcp_slowtimo时,内核可以改变 tcp_maxidle的值。
25.3 t _ i d l e中保存的实际上是从最后一次接收或发送报文段后算起的时间。因为 T C P
的输出必须被对端确认,与收到数据报文段相同,收到 ACK也将清零t_idle。
25.4 图A-6给出代码的一种可能的重写方式。

图 A-6

25.5 如果收到了重复的 A C K,t _ i d l e等于1 5 0,但被复位为 0。FIN_WAIT_2时钟超时


后,t _ i d l e将等于1048 (11 9 8-1 5 0 ),因此,定时器被设定为 1 5 0个滴答。定时器
再次超时,t _ i d l e应等于11 9 8 + 1 5 0,导致连接被关闭。重复 A C K令时间延长,直
到连接被关闭。
25.6 第一次连接探测报文段将在 1小时后发送。应用进程设定该选项时,实际只置位了
s o c k e t结构中的 S O _ K E E P A L I V E选项。由于设定了该选项,定时器将于 1小时后
超时,图25-15中的代码将发送第一次连接探测报文段。
25.7 t c p _ r t t d f l t用于为每条 T C P连接初始化 RT T估计器的值。如果需要,主机可通
过更改全局变量,改变默认设置。如果通过 #define将其定义为常量,则只有通过重
新编译内核文件才能改变默认值。

第26章

26.1 事实上, T C P并没有刻意计算从连接上最后一次发送报文段算起的时间,因为连接


上的计时器 t_idle一直在起作用。
26.2 图25-26中,snd_nxt被设定为snd_una,len等于0。
26.3 如果运行 Net/3系统,但对端主机却无法处理某个新选项 (例如,对端拒绝建立连接,
即使被要求忽略无法辨识的选项 )。遇到这种情况时,通过在内核中改变这个全局
变量的值,可以禁止某一个或两个选项。
26.4 时间戳选项能够在每次收到对新数据的 A C K时,更新 RT T估计器值。因此,使用时
间戳选项后, RT T估计器值将被更新 1 6次,是不使用该选项时更新次数的两倍。注
意,在时刻 2 1 7 . 9 4 4时,收到了对 6 1 4 5的A C K,RT T估计器值被更新,但这个新的
计算值并不准确—或者是在时刻 3 . 7 4 0发送的携带5 6 3 3 ~ 6 1 4 4字节的数据段,或者
是收到的对 6145的ACK,在网络中延迟了 200秒。
26.5 这种存储器引用时,无法确保 2字节的MSS值能够正确地对齐。

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 867
26.6 (该解决方案来自于 Dave Borman) 一个数据段能够携带的 T C P数据量的最大值为
6 5 4 9 5字节,即 6 5 5 3 5减去I P和T C P首部的最小值 ( 4 0 )。因此,紧急数据偏移量可取
值范围中有 3 9个值是无意义的: 6 5 4 9 6 ~ 6 5 5 3 5,包括 6 5 5 3 5。无论何时,只要发送
方得到一个超过 6 5 4 9 5的紧急数据偏移量,则将其替换为 6 5 5 3 5,并置位 U R G标志。
从而迫使接收方进入紧急模式,并告知接收方紧急数据偏移量所指向的数据尚未被
发送。发送方将持续发送紧急数据偏移量等于 6 5 5 3 5、且U R G标志置位的数据报文
段,直到紧急数据偏移量小于等于 65495,说明真正的紧急数据偏移量的开始。
26.7 我们提到,数据段的传输是可靠的 (重传机制 ),而A C K则有可能丢失。 R S T报文段
的传输同样也是不可靠的。如果连接上收到了一个假报文段 (例如,不属于本连接
的报文段,或者一个不属于任何连接的报文段 ),则传送RST报文段。如果 RST报文
段被i p _ o u t p u t丢弃,当对端重传导致发送 R S T报文段的数据报文段时,将再次
生成RST报文段。
26.8 应用程序执行了 8次写入1 0 2 4字节的操作。头 4次调用s o s e n d时,t c p _ o u t p u t被
调用,报文段被发送。因为这 4个报文段都包含了发送缓存中最后一个字节的数据,
每个报文段的 P S H标志都置位(图2 6 - 2 5 )。第二个缓存装满后,应用进程进行下一次
写操作,调用 s o s e n d时被挂起。收到对端通告窗口大小等于 0的A C K后,丢弃发
送缓存中已被确认的 4096字节的数据,应用进程被唤醒,又连续执行了 4次写操作,
发送缓存再次被填满。但只有当接收方通告窗口大小不等于 0时,才能继续发送数
据。条件满足时,接下来的 4个报文段被发送,但只有最后一个报文段的 P S H标志
置位,因为前3个报文段并未清空发送缓存。
26.9 如果正在发送的报文段不属于任何连接,传给 t c p _ r e s p o n d的t p参数可以是空指
针。代码只有在指针为空时,才会查看 tp,并代之以默认值。
26.10 t c p _ o u t p u t通常调用 M G E T H D R,分配一个仅能容纳 I P和T C P首部的m b u f,参见
图 2 6 - 2 5 和 图 2 6 - 2 6 。 在 新 的 m b u f 的 前 部 , 代 码 只 预 留 了 链 路 层首 部
(m a x _ l i n k h d r )大小的空间。如果使用了 I P 选项,而且选项的大小超过了
m a x _ l i n k h d r,i p _ i n s e r t o p t i o n s会自动分配另一个 mbuf。但如果 IP选项的
大小小于等于 max_linkhdr,则ip_insertoptions也会占用mbuf首部的空间,
从而导致ether_output仅为链路层首部分配另一个 mbuf(假定以太网输出)。
为了避免多余的 mbuf,图26-25和图26-26中的代码,可以在报文段中携带 IP选项时
调用MH_ALIGN。
26.11 约有80行代码,假定采用了 RFC 1323中的时间戳选项,且报文段被计时。
宏M G E T H D R调用了宏 M A L L O C,后者可能调用函数 m a l l o c。函数m _ c o p y也会
被调用,但一个完整大小的报文段可能需要一个簇,因此,不复制 m b u f,而是保
存一个对簇的引用。 m _ c o p y中调用 M G E T,可能会导致对 m a l l o c的调用。函数
bcopy复制模板,而in_cksum计算TCP的检验和。
26.12 调用w r i t e v没有区别,因为处理逻辑由 s o s e n d实现。因为数据大小等于 1 5 0字
节,小于 M I N C L S I Z E ( 2 0 8 ),所以为头 1 0 0个字节分配了一个 m b u f。并且因为协
议支持数据的分段, P R U _ S E N D请求被发送。接着为剩余的 5 0字节再分配一个
mbuf,并发送相应的 P R U _ S E N D请求(对于P R _ A T O M I C协议,如UDP,w r i t e v只

Linux公社 www.linuxidc.com
bbs.theithome.com
868 计计TCP/IP详解 卷 2:实现

生成一条“记录”,即只发送一个 PRU_SEND请求。)
如果两个缓存的长度分别等于 200和300,总长度超过了 M I N C L S I Z E,则分配一个
mbuf簇,且只发送一次 PRU_SEND请求。TCP只生成一个 500字节的报文段。

第27章

27.1 表中前 6行记录的差错,都是由于接收报文段或者定时器超时引起的异步差错。通


过在s o _ e r r o r中保存非零的差错代码,应用进程能够在下一次读 /写操作中收到差
错信息。但如果调用来自 t c p _ d i s c o n n e c t,说明应用进程调用了 c l o s e,或者
应用进程终止时系统自动关闭其所拥有的描述符。无论是哪一种情况,描述符被关
闭,应用进程不可能再通过读 /写操作来获取差错代码。此外,因为应用进程必须
明确设定插口选项,强迫 R S T置位,此时返回一个差错代码并不能向应用进程提供
有用的信息。
27.2 假定它是32 bit的u_long,最大值小于4298秒(1.2小时)。
27.3 路由表中的统计数据由 tcp_close更新,但只有当连接进入 CLOSED状态时,它才
会被调用。因为 F T P客户终止向对端发送数据 (执行主动关闭 ),本地连接端点进入
TIME_WAIT状态。必须经过 2MSL后,路由表统计值才会被更新。

第28章

28.1 0、1、2和3。
28.2 34.9Mb/s。对于更高的速率,连接两端需要更大的缓存。
28.3 通常, t c p _ d o o p t i o n不知道两个时间戳值是否按 32 bit边界对齐。图 2 8 - 4中的代
码,在指定情况下,能够确认时间戳值按 32 bit边界对齐,从而避免调用 bcopy。
28.4 图2 8 - 4中实现“选项预测”代码,只能处理系统推荐的格式。如果连接对端未采用
系统推荐的格式,会导致为每个接收到的报文段调用 t c p _ d o o p t i o n s,降低了处
理速度。
28.5 如果在每次创建插口时,而非每次连接建立时,调用 t c p _ t e m p l a t e,则系统中
的每个监听服务器都会拥有一个 tcp_template,而该结构可能永远不会被使用。
28.6 时间戳时钟频率应该在1 b/ms 和1 b/s之间(Net/3采用了2 b/s)。如果采用最高的时钟频
率1 b/ms,32 bit的时间戳将在231 / (24×60×60×1000)天,即24.8天后发生符号位回绕。
28.7 如果频率为每500 ms 1 bit,32 bit的时间戳将在231 / (24×60×60×2)天,即12 427
天,约34年后才会出现符号位回绕。
28.8 对R S T报文段的处理应优先于时间戳,而且, R S T报文段中最好不携带时间戳选项
(图26-24中的tcp_input代码确保了这一点 )。
28.9 因为客户端状态为 E S TA B L I S H E D,处理将在图 2 8 - 2 4的代码处结束。 t o d r o p等于
1,因为r c v _ n x t在收到第一个SYN时已递增过。SYN标志被清除 (因为这是一个重
复报文段),t i _ s e q递增,t o d r o p减为0。因为t o d r o p和t i _ l e n都等于0,执行
图2 8 - 2 5起始处的 i f语句,并跳过下一个 i f语句,直接调用 m _ a d j。但下一章中介绍
tcp_input后续代码时,将谈到在某些情况下不会调用 tcp_output,本题即是一例。
因此,客户端会不响应重复的 S Y N / A C K。服务器端超时后,再次发送 S Y N / A C K

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 869
(图2 8 - 1 7中介绍了某个被动打开的插口收到 S Y N时,定时器的设置 ),这个重发的
SYN/ACK报文段同样被忽略。我们现在讨论的其实是图 28-25代码中的另一个差错,
图28-30中给出的代码同样也纠正了这一差错。
28.10 客户发出的 S Y N到达服务器,并被交给处于 T I M E _ WA I T状态的插口。图 2 8 - 2 4中
的代码关闭 S Y N标志,图 2 8 - 2 5中的代码跳转至 d r o p a f t e r a c k,丢弃该报文段,
但生成一个 A C K ,确认字段等于 r c v _ n x t ( 图 2 6 - 2 7 ) 。它被称作“再同步
(resynchronization) ACK”报文段,因为其目的是告诉对端本地希望接收的下一序
号。客户端收到此 A C K后(客户处于 S Y N _ S E N T状态),发现它的确认字段所携带
的序号不等于自己期待得到的序号后 (图2 8 - 1 8 ),向服务器发送 R S T报文段。 R S T
报文段的 A C K标志被清除,且序号等于再同步 A C K报文段中确认字段携带的序号
(图2 9 - 2 8 )。服务器收到此 R S T报文段后,其 TIME_WAIT状态提前终止,相应插口
被关闭(图28-36)。客户端6秒钟后超时,重传 SYN报文段。假定监听服务器进程在
服务器主机上运转正常,新的连接将建立。由于 TIME_WAIT状态的这种防护作用,
新连接建立时,下一个 S Y N报文段携带的序号既可以高于前一连接上最后收到的
序号(图28-28中的测试),也可以低于该序号。

第29章

29.1 假定 RT T等于2秒钟。服务器被动打开,客户端在时刻 0主动打开。服务器在时刻 1


收到客户发出的 SYN,并作出响应,发送自己的 SYN和对客户SYN的ACK。客户端
在时刻 2收到报务器的响应报文段,图 2 8 - 2 0中的代码调用 s o i s c o n n e c t e d(唤醒
客户进程 ),完成主动打开过程,并向服务器发送 A C K响应。服务器在时刻 3收到客
户的ACK,图29-2中的代码完成服务器端的被动打开过程,控制返回给服务器进程。
一般情况下,客户进程比服务器进程提早 1/2 RTT时间得到控制。
29.2 假定SYN的序号等于 1000,50字节数据的序号等于 1001~1050。t c p _ i n p u t处理此
SYN报文段时,首先执行图28-15中的起始case语句,令rcv_nxt等于1001,接着跳到
step6。图29-22中的代码调用cp_reass,把数据放入插口的重组队列中。但数据还不
能放入插口的接收缓存 ( 图2 7 - 2 3 ) ,因此, r c v _ n x t 还是等于 1 0 0 1 。在调用
tcp_output生成ACK响应时,rcv_nxt (1001)被放入ACK报文段的确认字段。也
说是说,SYN被确认,但与之同时到达的 50字节的数据没有被确认。因此,客户端不
得不重发50字节的数据,所以,在完成主动打开的SYN报文段中携带数据是没有意义
的。
29.3 客户端的 A C K / F I N报文段到达时,服务器处于 S Y N _ R C V D状态,因此,图 2 9 - 2中
的 t c p _ i n p u t 代码将结束对 A C K 的处理。连接转移到 E S TA B L I S H E D状态,
t c p _ r e a s s把已在重组队列中的数据放入接收缓存, r c v _ n x t 递增为 1 0 5 1。
t c p _ i n p u t继续执行,图29-24中的代码负责处理 FIN标志,此时 T F _ A C K N O W标志
置位, r c v _ n x t等于1 0 5 2。s o c a n t r c v m o v e设定插口的状态,使服务器在读取
50字节的数据之后,得到“文件结束”指示。服务器的插口也转移到
C L O S E _ WA I T状态。调用 t c p _ o u t p u t,确认客户端的 F I N (因为 r c v _ n x t等于
1 0 5 2 )。假定服务器进程在收到“文件结束”指示后,关闭其插口,服务器也将向

Linux公社 www.linuxidc.com
bbs.theithome.com
870 计计TCP/IP详解 卷 2:实现

客户发送FIN,并等待回应。
在这个例子中,这了从客户端向服务器传送50字节的数据,双方需3个来回,共发送6
个报文段。为了减少所需的报文段数,应采用“用于交易的TCP扩展[Braden 1994]”。
29.4 收到服务器响应时,客户插口处于 S Y N _ S E N T状态。图 2 8 - 2 0中的代码处理该报文
段,连接转移到 ESTABLISHED状态,控制跳转到 s t e p 6,由图29-22中的代码继续
处理数据。 T C P _ R E A S S把数据添加到插口的接收缓存,并递增 r c v _ n x t。之后,
图2 9 - 2 4中的代码开始处理 F I N,再次递增 r c v _ n x t,连接转移到 CLOSE_WAIT状
态。在调用 t c p _ o u t p u t时,r c v _ n x t同时确认了SYN、50字节的数据和 FIN。随
后的客户进程首先读取 5 0字节的数据,接着是“文件结束”指示,并可能关闭其插
口。客户端连接进入 L A S T _ A C K状态,向服务器发送 F I N报文段,并等待其响应报
文段。
29.5 问题出在图 24-16中的tcp_outflags[TCPS_CLOSING]。它设定了TH_FIN标志,
而状态变迁图 (图2 4 - 1 5 )并未规定 F I N应被重传。解决问题的方法是,从该状态的
t c p _ o u t f l a g s中除去T H _ F I N标志。这个问题没有什么危害—只不过多交换两
个报文段—而且同时关闭或者在关闭后紧接着自连接的情况是非常罕见的。
29.6 没有。系统调用 w r i t e返回O K,只说明数据已复制到插口的缓存中。在数据得到
对端确认时, N e t / 3不再通知应用进程。如果需要得到此类信息,应设计并实现应
用级的确认机制。
29.7 RFC 1323的时间戳选项,造成“首部压缩”失效。因为只要时间戳变化,即 TCP选
项发生了改变,报文段发送时就不会被压缩。窗口大小选项无效,因为 T C P首部中
值的长度仍为16 bit。
29.8 IP 中I D字段的取值来自一个全局变量,只要发送一个 I P数据报,该变量递增一次。
这种方式导致在同一 T C P连接上两个连续 T C P报文段间的 I D差值大于 1的可能性大
大增加。一旦ID差值大于 1,图29-34中的∆ipid字段将被发送,增大了压缩首部的大
小。一个更好的解决方案是, TCP自己维护一个计数器,用于 ID的赋值。

第30章

30.2 是的,仍会发送 RST报文段。应用进程终止的处理中包括关闭它打开的所有描述符。


同一个函数 (s o c l o s e)最终会被调用,无论是应用进程明确地关闭了插口描述符,
还是隐含地进行了关闭 (首先被终止 )。
30.3 不。这个常量只有在监听插口设定 S O _ L I N G E R选项,且延迟时间等于 0时,才会被
用到。正常情况下,插口选项的这种设定方式会导致在连接关闭时发送 R S T报文段
(图30-12),但图30-2中对于接收连接请求的监听插口,把该值从 0改为120(滴答)。
30.4 如果这是第一次使用默认路由,则为两次;否则为一次。当创建插口时,
in_pcballoc将Internet PCB置为0,从而将PCB结构中的route结构设为0。发送
第一个报文段 ( S Y N )时, t c p _ o u t p u t调用i p _ o u t p u t。因为 r o _ r t指针为空,
因此向 r o _ d s t填充I P数据报的目的地址,并调用 r t a l l o c。在该连接的 P C B中,
route结构的ro_rt变量中保存默认路由。当ip_output调用ether_output时,
后者检查路由表中的 r t _ g w r o u t e变量是否为空。如果是,则调用 r t a l l o c 1。假

Linux公社 www.linuxidc.com
bbs.theithome.com
附录 A 部分习题的解答计计 871
定路由没有改变,该连接每次调用 t c p _ o u t p u t时,都会使用保存的 r o _ r t指针,
以避免多余的路由表查询。

第31章

31.1 因为在bpf_wakeup调用唤醒任何沉睡进程之前, catchpacket肯定会结束。


31.2 打开BPF设备的应用进程可能调用fork,导致多个应用进程都有权访问同一个BPF设备。
31.3 只有支持 B P F的设备才会出现在 B P F接口表 (b p f _ i f l i s t)中,因此,如果无法找
到指定接口,bpf_setif将返回ENXIO。

第32章

32.1 在第一个例子中等于 0,第二个例子中等于 255。这些值都是 RFC 1700 [Reynolds和


Postel 1994]中的保留值,不应出现在数据报中。也就是说,如果某个插口创建时的
协议号设定为 I P P R O T O _ R A W,则必须设定其 I P _ H D R I N C L插口选项,且写入到该
插口的数据报必须拥有一个有效的协议值。
32.2 因为 I P协议值 2 5 5是保留值,不会出现在网络中传送的数据报中。但这又是一个非
零的协议值, r i p _ i n p u t的3项测试中的第一项测试将忽略所有协议值不等于 2 5 5
的数据报。因此,应用进程无法在该插口上收到任何数据报。
32.3 即使该协议值是一个保留值,不会出现在网络中传送的数据报中,但 r i p _ i n p u t
的3项测试中的第一项测试保证此类型的插口能够接收任何协议类型的数据报。如
果应用进程调用了 connect或者bind,或者两者都调用,对于此种原始插口而言,
对输入的唯一限制是 IP报的源地址和目的地址。
32.4 因为i p _ p r o t o x数组(图7-22)保存了有关内核所能支持的协议类型的信息,只有在
该协议既没有相关的原始监听插口,而且指针 i n e t s w [ i p _p r o t o x [ i p -
>ip_p]].pr_input等于rip_input时,才会生成ICMP差错报告。
32.5 两种情况下,应用进程都必须自己构造 IP首部,以及其后的内容 (UDP报文段,TCP
报文段或任何其他的报文段 )。对于原始 I P插口,输出时同样调用 s e n d t o,通过
I n t e r n e t插口地址结构指明目的 I P地址。调用 i p _ o u t p u t,并依据给定的目的 I P地
址执行正常的IP选路。
B P F要求应用进程提供完整的数据链路层首部,例如以太网首部。输出时,需调用
w r i t e ,因为无法指明目的地址。数据分组被直接交给接口输出函数,跳过
i p _ o u t p u t函数(图3 1 - 2 0 )。应用进程通过 B I O C S E T I F i o c t l(图3 1 - 1 6 )选择外
出接口。因为未执行 I P选路,数据帧只能发给是直接相连的网络上的另一个主机
(除非应用进程重复 I P选路函数,并将数据帧发给直接相联网络上的某个路由器,
由路由器根据目的 IP地址完成转发)。
32.6 原始I P插口只能接收具有内核不处理的协议类型的数据报,例如,应用进程无法在
原始插口上接收 TCP报文段或UDP报文段。
B P F 能 够接 收到 达 指定 接口 的 所有 数据 帧, 无 论它 们是 否是 I P 数据 报 。
B I O C P R O M I S Ci o c t l使接口处于一种混杂状态,甚至能够接收不是发给本主机
的数据报。

Linux公社 www.linuxidc.com
bbs.theithome.com
附录B 源代码的获取
URL:统一资源定位符

本附录列出源代码所在的网址和下载方式。例如,常见的“匿名 FTP”地址表示如下:
ftp://ftp.cdrom.com/pub/bsd-sources/4.4BSD-Lite.tar.gz

即主机为 f t p . c d r o m . c o m。通过匿名 F T P客户登录后,从目录 p u b / b s d - s o u r c e s下载文


件4 . 4 B S D - L i t e . t a r . g z。后缀. t a r说明文件以标准的 t a r( 1 )格式存储, . g z说明文件由
G N U g z i p(1)程序压缩。

4.4BSD-Lite

有多种方式可得到 4 . 4 B S D - L i t e的正式版代码。完整的 4 . 4 B S D - L i t e正式版代码可通过


Walnut Creek CD-ROM公司得到,网址为
ftp://ftp.cdrom.com/pub/bsd-sources/4.4BSD-Lite.tar.gz

或者直接得到其光碟版。联系电话为 18007869907或+1510 674 0783。


O’Reilly & Associates出版的C D - R O M,包括全套的 4 . 4 B S D手册和 4 . 4 B S D - L i t e正式版代
码。联系电话为 1 800 889 8989或者+1 707 829 0515。

运行4.4BSD-Lite 网络软件的操作系统

4 . 4 B S D - L i t e正式版不是一个完整的操作系统。为了测试本书中介绍的网络软件,需要内
置4.4BSD-Lite正式版的操作系统,或者支持 4.4BSD-Lite的操作系统。
作者使用的操作系统是 Berkeley Software Design Inc.生产的商用系统,联系电话为 1 800
ITS BSD8,+1 719 260 8114,或者info@bsdi.com。
还有些免费的操作系统,已内置了 4 . 4 B S D - L i t e,如N e t B S D、3 8 6 B S D和F r e e BSd。详
情请见Walnut Creek CD-ROM(ftp.cdrom.com)或者comp.os.386bsd Usenet新闻组。

RFC

所有RFC都是免费的,通过电子邮件或匿名 FTP服务器可从英特网上得到所需文档。向下
述地址发送电子邮件:
To: rfc-info@ISI.EDU.
Subject: getting rfcs
help: ways_to_get_rfcs

回复邮件中会列出通过电子邮件或匿名 FTP服务器获取 RFC不同方法的详细说明。


记住,首先应先下载最新的 RFC 索引,从中查找所需的 R F C,确认所需的 R F C没有被新
的RFC更新或取代。

Linux公社 www.linuxidc.com
bbs.theithome.com
附录B 源代码的获取计计873
GNU软件

利用GNU Indent程序对本书出现的所有源代码进行格式调整,并利用 GNU Gzip程序对文


件做了压缩。这些程序可在下列站点找到:
ftp://prep.ai.mit.edu/pub/gnu/indent-1.9.1.tar.gz
ftp://prep.ai.mit.edu/pub/gnu/gzip-1.2.2.tar

文件名中的数字随版本的不同而不同。此外还有用于其他操作系统的 Gzip程序,如MS-DOS。
英特网上还有许多其他站点也提供 G N U资源,p r e p . a i . m i t . e d u主机的问候词中列出
了这些站点的名称。

PPP软件

有些PPP实现是免费的。 c o m p . p r o t o c o l s . p p p FAQ的第5部分提供了很多有价值的信
息。
http://cs.uni-bonn.de/ppp/part5.html

mrouted软件

mrouted软件的最新版本和其他多播应用程序可从 Xerox Palo Alto研究中心的站点得到:


ftp://parcftp.xerox.com/pub/net-research/

ISODE软件

ISODE软件包中的 SNMP代理实现与 Net/3兼容。详细信息参见 ISODE论坛的网站:


http://www.isode.com/

Linux公社 www.linuxidc.com
bbs.theithome.com
欢迎点击这里的链接进入精彩的Linux公社 网站

Linux公社(www.Linuxidc.com)于2006年9月25日注册并开通网
站,Linux现在已经成为一种广受关注和支持的一种操作系统,
IDC是互联网数据中心,LinuxIDC就是关于Linux的数据中心。

Linux公社是专业的Linux系统门户网站,实时发布最新Linux资
讯,包括Linux、Ubuntu、Fedora、RedHat、红旗Linux、Linux教
程、Linux认证、SUSE Linux、Android、Oracle、Hadoop、
CentOS、MySQL、Apache、Nginx、Tomcat、Python、Java、C语
言、OpenStack、集群等技术。

Linux公社(LinuxIDC.com)设置了有一定影响力的Linux专题栏
目。

Linux公社 主站网址:www.linuxidc.com 旗下网站:


www.linuxidc.net
包括:Ubuntu 专题 Fedora 专题 Android 专题 Oracle 专题
Hadoop 专题 RedHat 专题 SUSE 专题 红旗 Linux 专题
CentOS 专题

Linux 公社微信公众号:linuxidc_com

You might also like