Linux Kernel 核心中文手册(10)--网络

Networks (网络)
 
    Linux 和网络几乎是同义词。实际上 Linux 是 Internet 或 WWW 的产物。它的开
发者和用户使用 web 交换信息、想法、代码而 Linux 自身也常用于支持一些组织的联
网需求。本章描述了 Linux 如何支持统称为 TCP/IP 的网络协议。
    TCP/IP 协议设计用来支持连接在 ARPANET 上的计算机之间的通讯。 ARPANET 是美
国政府投资的一个美国的研究网络。 ARPANET 是一些网络概念的先驱,例如报文交换和
协议分层,让一种协议利用其它协议提供的服务。 ARPANET 于 1988 年退出,但是它的
后继者( NSF NET 和 Internet )发展的甚至更大。现在所知的 World Wide Web 是在
 ARPANET 中发展的,它本身也是由 TCP/IP 协议支持的。 Unix 在 ARPANET 上大量使
用,第一个发布的网络版的 Unix 是 4.3BSD 。 Linux 的网络实现是基于 4.3BSD 的模
型,它支持 BSD socket (和一些扩展)和全系列的 TCP/IP 网络功能。选择这种编程
接口是因为它的流行程度,而且可以帮助程序在 Linux 和其它 Unix 平台之间移植。


10.1 An Overview of TCP/IP Networking ( TCP/IP 网络概览)
    本节为 TCP/IP 网络的主要原理给出了一个概览。这并不是一个详尽的描述。要更
详细的描述,阅读第 10 本参考书(附录)。
    在一个 IP 网络中,每一个机器都分配一个 IP 地址,这是一个 32 位的数字,唯
一标识这一台机器。 WWW 是一个非常巨大、不断增长的 IP 网络,每一个连接在上面的
机器都分配了一个独一无二的 IP 地址。 IP 地址用点分隔的四个数字表示,例如, 1
6.42.0.9 。 IP 地址实际上分为两个部分:网络地址和主机地址。这些地址的大小(尺
寸)可能不同(有几类 IP 地址),以 16.42.0.9 为例,网络地址是 16.42 ,主机地
址是 0.9 。主机地址可以进一步划分成为子网( subnetwork )和主机地址。再次以
16.42.0.9 为例,子网地址可以是 16.42.0 ,主机地址为 16.42.0.9 。对于 IP 地址
进行进一步划分允许各个组织划分它们自己的网络。例如,假设 16.42 是 ACME 计算机
公司的网络地址, 16.42.0 可以是子网 0 , 16.42.1 可以是子网 1 。这些子网可以
在分离的大楼里,也许通过电话专线或者甚至通过微波连接。 IP 地址由网络管理员分
配,使用 IP 子网是分散网络管理任务的一个好办法。 IP 子网的管理员可以自由地分
配他们自己子网内的 IP 地址。
    但是,通常 IP 地址难于记忆,而名字更容易记忆。 Linux.acme.com 比 16.42.0
.9 更好记。必须使用一种机制把网络名字转换为 IP 地址。这些名字可以静态地存在
/etc/hosts 文件中或者让 Linux 询问一个分布式命名服务器( Distributed Name Se
rver DNS )来解析名字。这种情况下,本地主机必须知道一个或多个 DNS 服务器的 I
P 地址,在 /etc/resolv.conf 中指定。
    不管什么时候你连接另外一台机器的时候,比如读取一个 web page ,都要使用它
的 IP 地址和那台机器交换数据。这种数据包括在 IP 报文( packet )中,每一个报


文都有一个 IP 头(包括源和目标机器的 IP 地址,一个校验和和其它有用的信息。这
个校验和是从 IP 报文的数据中得到的,可以让 IP 报文的接收者判断传输过程中 IP
报文是否损坏(可能是一个噪音很大的电话线)。应用程序传输的数据可能被分解成容
易处理的更小的报文。 IP 数据报文的大小依赖于连接的介质而变化:以太网报文通常
大于 PPP 报文。目标主机必须重新装配这些数据报文,然后才能交给接收程序。如果你
通过一个相当慢的串行连接访问一个包括大量图形图像的 web 页,你就可以用图形的方
式看出数据的分解和重组。
    连接在同一个 IP 子网的主机可以互相直接发送 IP 报文,而其它的 IP 报文必须
通过一个特殊的主机(网关)发送。网关(或路由器)连接在多于一个子网上,它们会
把一个子网上接收的 IP 报文重新发送到另一个子网。例如,如果子网 16.42.1.0 和
16.42.0.0 通过一个网关连接,那么所有从子网 0 发送到子网 1 的报文必须先发送到
网关,这样才能转发。本地的主机建立一个路由表,让它可以把要转发的 IP 报文发送
到正确的机器。对于每一个 IP 目标,在路由表中都有一个条目,告诉 Linux 要到达目
标需要先把 IP 报文发送到那一台主机。这些路由表是动态的,而且当应用程序使用网
络和网络拓扑变化的时候不断改变。
    IP 协议是传输层协议,被其他协议使用,携带它们的数据。传输控制协议( TCP
)是一个可靠的端到端的协议,使用 IP 传送和接收它的报文。象 IP 报文有自己的头
一样, TCP 也有自己的头。 TCP 是一个面向连接的协议,两个网络应用程序通过一个
虚拟的连接连接在一起,甚至它们中间可能会有许多子网、网关和路由器。 TCP 在两个
应用程序之间可靠地传送和接收数据,并且保证不会有丢失和重复的数据。当 TCP 使用
 IP 传送它的报文的时候,在 IP 报文中包含的数据就是 TCP 报文自身。每一个通讯的
主机的 IP 层负责传送和接收 IP 报文。用户数据报协议( UDP )也使用 IP 层传送它


的报文,但是不象 TCP , UDP 不是一个可靠的协议,它只提供数据报服务。其它协议
也可以使用 IP 意味着当接收到 IP 报文,接收的 IP 层必须知道把这个 IP 报文中包
含的数据交给哪一个上层协议。为此,每一个 IP 报文的头都有一个字节,包含一个协
议标识符。当 TCP 请求 IP 层传输一个 IP 报文的时候 IP 报文的头就说明它包含一个
 TCP 报文。接收的 IP 层,使用这个协议标识符来决定把接收到的数据向上传递给哪一
个协议,在这种情况下,是 TCP 层。当应用程序通过 TCP/IP 通讯的时候,它们不但必
须指定目标的 IP 地址,也要指定目标应用程序的端口( port )地址。一个端口地址
唯一标识一个应用程序,标准的网络应用程序使用标准的端口地址:例如 web 服务器使
用端口 80 。这些已经注册的端口地址可以在 /etc/services 中查到。
    协议分层不仅仅停留在 TCP 、 UDP 和 IP 。 IP 协议本身使用许多不同的物理介
质和其它 IP 主机传输 IP 报文。这些介质自己也可能增加它们自己的协议头。这样的
例子有以太网层、 PPP 和 SLIP 。一个以太网允许许多主机同时连接在一个物理电缆上
。每一个传送的以太帧可以被所有连接的主机看到,所以每一个以太网设备都有一个独
一无二的地址。每一个传送到那个地址的以太网帧会被那个地址的主机接收,而被连接
到这个网络的其它主机忽略掉。这个独一无二的地址当每一个以太网设备制造的时候内
建在设备里边,通常保存在以太网卡的 SROM 中。以太地址由 6 个字节长,例如,可能
是 08-00-2b-00-49-4A 。一些以太网地址保留用于多点广播,用这种目标地址发送的以
太网帧会被网络上的所有的主机接收。因为以太网帧中可能运载许多不同的协议(作为
数据),和 IP 报文一样,它们的头中都包含一个协议标识符。这样以太网层可以正确
地接收 IP 报文并把数据传输到 IP 层。
    为了通过多种连接协议,例如通过以太网来传输 IP 报文, IP 层必须找出这个 I
P 主机的以太网地址。这是因为 IP 地址只是一个寻址的概念,以太网设备自己有自己


的物理地址。 IP 地址可以由网络管理员根据需要分配和再分配,而网络硬件则只响应
具有它自己物理地址的以太网帧,或者特殊的多点广播地址(所有的机器都必须接收)
。 Linux 使用地址解析协议( ARP )让机器把 IP 地址转换成真实的硬件地址例如以
太网地址。为了得到一个 IP 地址所联系的硬件地址,一个主机会发送一个 ARP 请求包
,包含它希望转换的 IP 地址,发送到一个多点广播地址,让网络上所有的点都可以收
到。具有这个 IP 地址的目标主机用一个 ARP 回应来应答,这中间包括了它的物理硬件
地址。 APR 不仅仅限制在以太网设备,它也可以解析其它物理介质的 IP 地址,例如
FDDI 。不能进行 ARP 的设备会有标记,这样 Linux 就不需要试图对它们进行 ARP 。
也有一个相反的功能,反向 ARP ,或 RARP ,把物理地址转换到 IP 地址。这用于网关
,回应对于代表远端网络的 IP 地址的 ARP 请求。
10.2 The Linux TCP/IP Networking Layers ( Linux TCP/IP 网络分层)
    象网络协议一样,图 10.2 显示了 Linux 对于 internet 协议地址族的实现就好像
一系列连接的软件层。 BSD socket 由只和 BSD socket 相关的通用的 socket 管理软
件来支持。支持这些的是 INET socket 层,它管理以 IP 为基础的协议 TCP 和 UDP 的
通讯端点。 UDP 是一个无连接的协议,而 TCP 是一个可靠的端到端的协议。当传送 U
DP 报文的时候, Linux 不知道也不关心它们是否安全到达目的地。 TCP 报文进行了编
号, TCP 连接的每一端都要确保传送的数据正确地接收到。 IP 层包括了网际协议(
Internet Protocol )的代码实现。这种代码在传送的数据前增加 IP 头,而且知道如
何把进来的 IP 报文转送到 TCP 或者 UDP 层。在 IP 层之下,支持 Linux 联网的是网
络设备,例如 PPP 和以太网。网络设备并非总是表现为物理设备:其中一些比如 loop
back 设备只是纯粹的软件设备。不象标准的 Linux 设备用 mknod 命令创建,网络设备
只有在底层的软件找到并且初始化它们之后才出现。你只有在建立俄一个包含恰当的以


太望设备驱动程序的核心之后你才能看到设备文件 /dev/eth0 。 ARP 协议位于 IP 层
和支持 ARP 的协议之间。
10.3 The BSD Socket Interface ( BSD socket 接口)
    这是一个通用的接口,不仅仅支持多种形式的联网,也是一种进程间通讯机制。一
个 socket 描述了通讯连接的一端,两个通讯进程每一个都会有一个 socket ,描述它
们之间通讯连接的自己部分。 Socket 可以想象成一种特殊形式的管道,但是和管道不
同, socket 对于可以容纳的数据量没有限制。 Linux 支持几种类型的 socket ,这些
类叫做 address families (地址族)。这是因为每一类都有自己通讯寻址方式。 Lin
ux 支持以下 socket address families 或 domain :
UNIX Unix domain sockets,
INET The Internet address family supports communications via
TCP/IP protocols
AX25 Amateur radio X25
IPX Novell IPX
APPLETALK Appletalk DDP
X25 X25
    有几种 socket 类型,每一种都代表了连接上支持的服务的类型。并非所有的 add
ress families 都支持所有类型的服务。 Linux BSD socket 支持以下 socket 类型。
 
    Stream 这种 socket 提供了可靠的、双向顺序的数据流,保证传输过程中数据不会
丢失、损坏或重复。 Stream socket 在 INET address family 中由 TCP 协议支持
    Datagram 这种 socket 也提供了双向的数据传输,但是和 stream socket 不同,


它不保证消息会到达。甚至它到达了也不保证它们会顺序到达或没有重复或损坏。这种
类型的 socket 在 Internet address family 中由 UDP 协议支持。
    RAW 这允许进程直接(所以叫“ raw ”)访问底层的协议。例如,可以向一个以太
网设备打开一个 raw socket ,观察 raw IP 数据流。
    Reliable Delivered Messages 这很象数据报但是数据保证可以到达
    Sequenced Packets 象 stream socket 但是数据报文大小是固定的
    Packet 这不是标准的 BSD socket 类型,它是 Linux 特定的扩展,允许进程直接
在设备层访问报文
    使用 socket 通讯的进程用一个客户服务器的模型。服务器提供服务,而客户使用
这种服务。一个这样的例子是一个 Web 服务器,提供 web page 和一个 web 客户(或
浏览器),读取这些页。使用 socket 的服务器,首先创建一个 socket ,然后为它 b
ind 一个名字。这个名字的格式和 socket 的 address family 有关,它是服务器的本
地地址。 Socket 的名字或地址用 sockaddr 数据结构指定。一个 INET socket 会绑定
一个 IP 端口地址。注册的端口编号可以在 /etc/services 中看到:例如, web 服务
器的端口是 80 。在 socket 上绑定一个地址后,服务器就 listen 进来的对于绑定的
地址的连接请求。请求的发起者,客户,创建一个 socket ,并在上面执行一个连接请
求,指定服务器的目标地址。对于一个 INET socket ,服务器的地址是它的 IP 地址和
它的端口地址。这些进来的请求必须通过大量的协议层,找到它的路径,然后就在服务
器的监听端口等待。一旦服务器接收到了进来的请求,它可以接受( accept )或者拒
绝它。如果要接受进来的请求,服务器必须创建一个新的 socket 来接受它。一旦一个
 socket 已经用于监听进来的连接请求,它就不能再用于支持一个连接。连接建立之后
,两端都可以自由地发送和接收数据。最后,当一个连接不再需要的时候,它可以被关


闭。必须小心,保证正确地处理正在传送的数据报文。
    一个 BSD socket 上的操作的确切意义依赖于它底层的地址族。建立一个 TCP/IP
连接和建立一个业余无线电 X.25 连接有很大的不同。象虚拟文件系统一样, Linux 在
和独立的地址族相关的软件所支持的 BSD socket 层抽象了 BSD socket 和应用程序之
间的 socket 接口。当核心初始化的时候,建立在核心的地址族就向 BSD socket 接口
登记自己。稍后,当应用程序创建和使用 BSD socket 的时候,在 BSD socket 和它的
支撑地址族之间建立一个联系。这种联系是通过交叉的数据结构和地址族支持例程表实
现的。例如,当应用程序创建一个新的 socket 的时候, BSD socket 接口就使用地址
族相关的 socket 创建例程。
    当配置核心的时候,一组地址族和协议都建立到了 protocols 向量表中。每一个都
用它的名称(例如“ INET ”)和它的初始化例程的地址来代表。当启动的时候, soc
ket 接口初始化,每一个协议的初始化代码都要被调用。对于 socket 地址族,它们里
边会登记一系列协议操作。这都是一些例程,每一个都执行一个和地址族相关的特殊操
作。登记的协议操作保存在 pops 向量表中,这个向量表保存指向 proto_ops 数据结构
的指针。 Proto_ops 数据结构包括协议族类型和一批和特定地址族相关的 socket 操作
例程的指针。 Pops 向量表用地址族的标识符作为索引,例如 Internet address fami
ly 的标识符( AF_INET 是 2 )。
参见 include/linux/net.h
10.4 The INET Socket Layer
    INET socket 层支持包含 TCP/IP 协议的 internet address family 。象上面讨论
的,这些协议是分层的,每一个协议都使用其它协议的服务。 Linux 的 TCP/IP 代码和
数据结构反映了这种分层。它和 BSD socket 层的接口是通过网络初始化的时候它向 B


SD socket 层登记的 internet address family socket 操作进行的。这些和其它登记
的地址族一起放在 pops 向量表中。 BSD socket 层通过调用在登记的 proto_ops 数据
结构中的 INET 层的 socket 支持例程完成它的工作。例如,一个地址族是 INET 的 B
SD socket 创建请求会使用底层的 INET socket 创建函数。每一次操作 BSD socket 层
都把代表 BSD socket 的 socket 数据结构传递给 INET 层。 INET socket 层使用它自
己的数据结构 socket ,连接到 BSD socket 数据结构,而不是用 TCP/IP 相关的信息
把 BSD socket 搞乱。这种连接参见图 10.3 。它使用 BSD socket 中的 data 指针把
 sock 数据结构和 BSD socket 数据结构连接起来。这意味着后续的 INET socket 调用
可以很容易地获取这个 sock 数据结构。在创建的时候 sock 数据结构的协议操作指针
也被建立,这些指针依赖于请求的协议。如果请求 TCP ,则 sock 数据结构的协议操作
指针会指向 TCP 连接所需要的一系列 TCP 协议的操作。
参见 include/net/sock.h
10.4.1 Creating a BSD Socket (创建一个 BSD Socket )
    创建一个新的 socket 的系统调用需要传递它的地址族的标识符、 socket 的类型
和协议。首先,用请求的地址族在 pops 向量表中查找一个匹配的地址族。它可能是一
个使用核心模块实现的特殊的地址族,如果这样, kerneld 核心进程必须加载这个模块
,我们才能继续。然后分配一个新的 socket 数据结构来表示这个 BSD socket 。实际
上这个 socket 数据结构物理上是 VFS inode 数据结构的一部分,分配一个 socket 实
际上就是分配一个 VFS inode 。这看起来比较奇怪,除非你考虑让 socket 可以用和普
通文件一样的方式进行操作。象所有文件都用 VFS inode 数据结构表示一样,为了支持
文件操作, BSD socket 也必须用一个 VFS inode 数据结构表示。
    这个新创建的 BSD socket 数据结构包括一个指针指向和地址族相关的 socket 例


程,这个指针被设置到从 pops 向量表中取出的 proto_ops 数据结构。它的类型被设置
成请求的 socket 类型: SOCK_STREAM 、 SOCK_DGRAM 等等其中之一,然后用 proto_
ops 数据结构中保存的地址调用和地址族相关的创建例程。
    然后从当前进程的 fd 向量表中分配一个空闲的文件描述符,它所指向的 file 数
据结构也被初始化。这包括设置文件操作指针,指向 BSD socket 接口支持的 BSD soc
ket 文件操作例程。所有将来的操作会被定向到 socket 接口,依次通过调用支撑的地
址族的操作例程传递到相应的地址族。
10.4.2 Binding an Address to an INET BSD Socket (为一个 INET BSD socket 绑定
一个地址)
    为了监听进来的网际连接请求,每一个服务器必须创建一个 INET BSD socket 并把
自己的地址绑定到它上面。 Bind 的操作大部分由 INET socket 层处理,另一些需要底
层的 TCP 和 UDP 协议层的支持。已经绑定了一个地址的 socket 不能用于其它通讯。
这意味着这个 socket 的状态必须是 TCP_CLOSE 。传递给 bind 操作的 sockaddr 包括
要绑定的 IP 地址和一个端口号(可选)。通常,绑定的地址会是分配给支持 INET 地
址族的网络设备的地址之中的一个,而且接口必须是开启的并能够使用。你可以用 ifc
onfig 命令看系统中哪一个网络接口当前是激活的。 IP 地址也可以是 IP 广播地址(
全是 1 或 0 )。这是意味着“发送给每一个人”的特殊地址。如果这个机器作为一个
透明的 proxy 或者防火墙,这个 IP 地址也可以设置成任何 IP 地址。不过只有具有超
级用户特权的进程可以绑定任意 IP 地址。这个绑定的 IP 地址被存在 sock 数据结构
的 recv_addr 和 saddr 域中。它们分别用于 hash 查找和发送 IP 地址。端口号是可
选的,如果没有设置,会向支撑的网络请求一个空闲的。按照惯例,小于 1024 的端口
号不能被没有超级用户特权的进程使用。如果底层的网络分配端口号,它总是分配一个


大于 1024 的端口。
    当底层的网络设备接收报文的时候,这些报文必须被转到正确的 INET 和 BSD soc
ket 才能被处理。为此, UDP 和 TCP 维护 hash table ,用于查找进来的 IP 信息的
地址,把它们转到正确的 socket/sock 对。 TCP 是一个面向连接的协议,所以处理 T
CP 报文比处理 UDP 报文所包括的信息要多。
    UDP 维护一个已经分配的 UDP 端口的 hash table , udp_table 。这包括 sock
数据结构的指针,用一个根据端口号的 hash 函数作为索引。因为 UDP hash table 比
允许的端口号要小的多( udp_hash 只有 128 , UDP_HTABLE_SIZE )表中的一些条目
指向一个 sock 数据结构的链表,用每一个 sock 的 next 指针连接在一起。
    TCP 更加复杂,因为它维护几个 hast table 。但是,在绑定操作中, TCP 实际上
并不把绑定的 sock 数据结构加到它的 hash table 中,它只是检查请求的端口当前没
有被使用。在 listen 操作中 sock 数据结构才加到 TCP 的 hash table 中。
10.4.3 Making a Connection to an INET BSD Socket
    一旦创建了一个 socket ,如果没有用于监听进来的连接请求,它就可以用于建立
向外的连接请求。对于无连接的协议,比如 UDP ,这个 socket 操作不需要做许多,但
是对于面向连接的协议如 TCP ,它涉及在两个应用程序之间建立一个虚拟电路。
    一个向外的连接只能在一个正确状态的 INET BSD socket 上进行:就是说还没有建
立连接,而且没有用于监听进来的连接。这意味着这个 BSD socket 数据结构必须在 S
S_UNCONNECTED 状态。 UDP 协议不在两个应用程序之间建立虚拟连接,所有发送的消息
都是数据报,发出的消息可能到到也可能没有到达它的目的地。但是,它也支持 BSD s
ocket 的 connect 操作。在一个 UDP INET BSD socket 上的一个连接操作只是建立远
程应用程序的地址:它的 IP 地址和它的 IP 端口号。另外,它也要建立一个路由表条


目的缓存区,这样,在这个 BSD socket 上发送的 UDP 数据报不需要在检查路由表数据
库(除非这个路由变成无效)。这个缓存的路由信息被 INET sock 数据结构中的 ip_r
oute_cache 指针指向。如果没有给出地址信息,这个 BSD socket 发送的消息就自动使
用这个缓存的路由和 IP 地址信息。 UDP 把 sock 的状态改变成为 TCP_ESTABLISHED

    对于在一个 TCP BSD socket 上进行的连接操作, TCP 必须建立一个包括连接信息
的 TCP 消息,并发送到给定的 IP 目标。这个 TCP 消息包括连接的信息:一个独一无
二的起始消息顺序编号、发起主机可以管理的消息的最大尺寸、发送和接收的窗口大小
等等。在 TCP 中,所有的消息都编了号,初始顺序编号用作第一个消息编号。 Linux
选择一个合理的随机数以避免恶意的协议攻击。每一个从 TCP 连接的一端发送,被另一
端成功接收的消息被确认,告诉它成功地到达,而且没有损坏。没有确认的消息会被重
发。发送和接收窗口大小是确认前允许的消息的数目。如果接收端的网络设备支持的最
大消息尺寸比较小,则这个连接会使用两个中间最小的一个。执行向外的 TCP 连接请求
的应用程序现在必须等待目标应用程序的响应,是接受还是拒绝这个连接请求。对于期
望进来的消息的 TCP sock ,它被加到了 tcp_listening_hash ,这样进来的 TCP 消息
可以定向到这个 sock 数据结构。 TCP 也启动计时器,这样如果目标应用程序对于请求
不响应,向外的连接请求会超时。
10.4.4 Listening on an INET BSD Socket
    一旦一个 socket 拥有了一个绑定的地址,它就可以监听指定这个绑定地址的进来
的连接请求。一个网络应用程序可以不绑定地址直接在一个 socket 上监听,这种情况
下, INET socket 层找到一个未用的端口号(对于这种协议而言),自动把它绑定到这
个 socket 上。这个 socket 的 listen 函数把 socket 变成 TCP_LISTEN 的状态,并


且执行所需的和网络相关的工作,一边允许进来的连接。
    对于 UDP socket ,改变 socket 的状态已经足够,但是 TCP 已经激活它现在要把
 socket 的 sock 数据结构加到它的两个 hash table 中。这是 tcp_bound_hash 和 t
cp_listening_hash 表。这两个表都通过一个基于 IP 端口号的 hash 函数进行索引。
 
    不论何时接收到一个对于激活的监听 socket 的进来的 TCP 连接请求, TCP 都要
建立一个新的 sock 数据结构表示它。这个 sock 数据结构在它最终被接受之前成为这
个 TCP 连接的 buttom half 。它也克隆包含连接请求的进来的 sk_buff 并把它排在监
听的 sock 数据结构的 receive_queue 队列中。这个克隆的 sk_buff 包括一个指针,
指向这个新创建的 sock 数据结构。
10.4.5 Accepting Connection Requests
    UDP 不支持连接的概念,接受 INET socket 的连接请求只应用于 TCP 协议,在一
个监听的 sock 上进行接受( accept )操作会从原来的监听的 socket 克隆出一个新
的 socket 数据结构。然后这个 accept 操作传递给支撑的协议层,在这种情况下,是
 INET 去接受任何进来的连接请求。如果底层的协议,比如 UDP 不支持连接, INET 协
议层的 accept 操作会失败。否则,连接的请求会传递到真正的协议,在这里,是 TCP
 。这个 accept 操作可能是阻塞,也可能是非阻塞的。在非阻塞的情况下,如果没有需
要 accept 的进来的连接,这个 accept 操作会失败,而新创建的 socket 数据结构会
被废弃。在阻塞的情况下,执行 accept 操作的网络应用程序会被加到一个等待队列,
然后挂起,直到接收到一个 TCP 的连接请求。一旦接收到一个连接请求,包含这个请求
的 sk_buff 会被废弃,这个 sock 数据结构被返回到 INET socket 层,在这里它被连
接到先前创建的新的 socket 数据结构。这个新的 socket 的文件描述符( fd )被返


回给网络应用程序,应用程序就可以用这个文件描述符对这个新创建的 INET BSD sock
et 进行 socket 操作。
10.5 The IP Layer ( IP 层)
10.5.1 Socket Buffers
    使用分成许多层,每一层使用其它层的服务,这样的网络协议的一个问题是,每一
个协议都需要在传送的时候在数据上增加协议头和尾,而在处理接收的数据的时候需要
删除。这让协议之间传送数据缓冲区相当困难,因为每一层都需要找出它的特定的协议
头和尾在哪里。一个解决方法是在每一层都拷贝缓冲区,但是这样会没有效率。替代的
, Linux 使用 socket 缓冲区或者说 sock_buffs 在协议层和网络设备驱动程序之间传
输数据。 Sk_buffs 包括指针和长度域,允许每一协议层使用标准的函数或方法操纵应
用程序数据。
 
 
图 10.4 显示了 sk_buff 数据结构:每一个 sk_buff 都有它关联的一块数据。 Sk_bu
ff 有四个数据指针,用于操纵和管理 socket 缓冲区的数据
参见 include/linux/skbuff.h
head 指向内存中的数据区域的起始。在 sk_buff 和它相关的数据块被分配的时候确定
的。
Data 指向协议数据的当前起始为止。这个指针随着当前拥有这个 sk_buff 的协议层不
同而变化。
Tail 指向协议数据的当前结尾。同样,这个指针也随拥有的协议层不同而变化。
End 指向内存中数据区域的结尾。这是在这个 sk_buff 分配的时候确定的。


    另有两个长度字段 len 和 truesize ,分别描述当前协议报文的长度和数据缓冲区
的总长度。 Sk_buff 处理代码提供了标准的机制用于在应用程序数据上增加和删除协议
头和尾。这种代码安全地操纵了 sk_buff 中的 data 、 tail 和 len 字段。
Push 这把 data 指针向数据区域的起始移动,并增加 len 字段。用于在传送的数据前
面增加数据或协议头
参见 include/linux/skbuff.h skb_push()
Pull 把 data 指针从数据区域起始向结尾移动,并减少 len 字段。用于从接收的数据
中删除数据或协议头。
参见 include/linux/skbuff.h skb_pull()
Put 把 tail 指针向数据区域的结尾移动并增加 len 字段,用于在传输的数据尾部增加
数据或协议信息
参见 include/linux/skbuff.h skb_put()
trim 把 tail 指针向数据区域的开始移动并减少 len 字段。用于从接收的数据中删除
数据或协议尾
参见 include/linux/skbuff.h skb_trim()
sk_buff 数据结构也包括一些指针,使用这些指针,在处理过程中这个数据结构可以存
储在 sk_buff 的双向环形链表中。有通用的 sk_buff 例程,在这些列表的头和尾中增
加 sk_buffs 和删除其中的 sk_buff 。
10.5.2 Receiving IP Packets
    第 8 章描述了 Linux 的网络设备驱动程序如何建立到核心以及被初始化。这产生
了一系列 device 数据结构,在 dev_base 列表中链接在一起。每一个 device 数据结
构描述了它的设备并提供了一组回调例程,当需要网络驱动程序工作的时候网络协议层


可以调用。这些函数大多数和传输数据以及网络设备的地址有关。当一个网络设备从它
的网络上接收到数据报文的时候,它必须把接收到的数据转换到 sk_buff 数据结构。这
些接收的 sk_buff 在接收的时候被网络驱动程序增加到 backlog 队列。如果 backlog
 队列增长的太大,那么接收的 sk_buff 就被废弃。如果有工作要执行,这个网络的 b
utton half 标记成准备运行。
参见 net/core/dev.c netif_rx()
    当网络的 bottom half 处理程序被调度程序调用的时候,它首先处理任何等待传送
的网络报文,然后才处理 sk_buff 的 backlog backlo 队列,确定接收到的报文需要
传送到那个协议层。当 Linux 网络层初始化的时候,每一个协议都登记自己,在 ptyp
e_all 列表或者 ptype_base hash table 中增加一个 packet_type 的数据结构。这个
 packet_type 数据结构包括协议类型,一个网络驱动设备的指针,一个协议的数据接收
处理例程的指针和一个指针,指向这个列表或者 hash table 下一个 packet_type 数据
类型。 Ptype_all 链表用于探测( snoop )从任意网络设备上接收到的所有的数据报
文,通常不使用。 Ptype_base hash table 使用协议标识符 hash ,用于确定哪一种协
议应该接收进来的网络报文。网络的 bottom half 把进来的 sk_buff 的协议类型和任
一表中的一个或多个 packet_type 条目进行匹配。协议可能会匹配一个或多个条目,例
如当窥测所有的网络通信的时候,这时,这个 sk_buff 会被克隆。这个 sk_buff 被传
递到匹配的协议的处理例程。
参见 net/core/dev.c net_bh()
参见 net/ipv4/ip_input.c ip_recv()
10.5.3 Sending IP Packets
    报文在应用程序交换数据的过程中传送,或者也可能是为了支持已经建立的连接或


为了建立连接而由网络协议产生产生。不管数据用什么方式产生,都建立一个包含数据
的 sk_buff ,并当它通过协议层的时候增加许多头。
    这个 sk_buff 需要传递到进行传输的网络设备。但是首先,协议,例如 IP ,需要
决定使用哪一个网络设备。这依赖于这个报文的最佳路由。对于通过 modem 连接到一个
网络的计算机,比如通过 PPP 协议,这种路由选择比较容易。报文应该要么通过 loop
back 设备传送给本地主机,要么传送到 PPP modem 连接的另一端的网关。对于连接到
以太网的计算机而言,这种选择比较困难,因为网络上连接了许多计算机。
    对于传送的每一个 IP 报文, IP 使用路由表解析目标 IP 地址的路由。对于每一
个 IP 目标在路由表中进行的查找,成功就会返回一个描述要使用的路由的 rtable 数
据结构。包括使用的源 IP 地址,网络 device 数据结构的地址,有时候还会有一个预
先建立的硬件头。这个硬件头和网络设备相关,包含源和目的物理地址和其它同介质相
关的信息。如果网络设备是以太网设备,硬件头会在图 10.1 中显示,其中的源和目的
地址会是物理的以太网地址。硬件头和路由缓存在一起,因为在这个路由传送的每一个
 IP 报文都需要追加这个头,而建立这个头需要时间。硬件头可能包含必须使用 ARP 协
议才能解析的物理地址。这时,发出的报文会暂停,直到地址解析成功。一旦硬件地址
被解析,并建立了硬件头,这个硬件头就被缓存,这样以后使用这个接口的 IP 报文就
不需要进行 ARP 。
参见 include/net/route.h
10.5.4 Data Fragmentation
    每一个网络设备都有一个最大的报文尺寸,它无法传送或接收更大的数据报文。 I
P 协议允许这种数据,会把数据分割成网络设备可以处理的报文大小的更小的单元。 I
P 协议头包含一个分割字段,包含一个标记和分割的偏移量。


    当要传输一个 IP 报文的时候, IP 查找用来发送 IP 报文的网络设备。通过 IP
路由表来查找这个设备。每一个设备都有一个字段描述它的最大传输单元(字节),这
是 mtu 字段。如果设备的 mtu 比等待传送的 IP 报文的报文尺寸小,那么这个 IP 报
文必须被分割到更小的碎片( mtu 大小)。每一个碎片用一个 sk_buff 代表:它的 I
P 头标记了它被分割,以及这个 IP 报文在数据中的偏移量。最后一个报文被标记为最
后一个 IP 碎片。如果在分割成碎片的过程中, IP 无法分配一个 sk_buff ,这次传送
就失败。
    接收 IP 碎片比发送更难,因为 IP 碎片可能以任意顺序被接收,而且它们必须在
重组之前全部接收到。每一次一个 IP 报文被接收的时候,都检查它是否是一个 IP 碎
片。收到一个消息的第一个碎片, IP 就建立一个新的 ipq 数据结构,并连接到等待组
装的 IP 碎片的 ipqueue 列表中。当更多的 IP 碎片接收到的时候,就查到正确的 ip
q 数据结构并建立一个新的 ipfrag 数据结构来描述这个碎片。每一个 ipq 数据结构都
唯一描述了一个成为碎片的 IP 接收帧,包括它的源和目标 IP 地址,上层协议标识符
和这个 IP 帧的标识符。当接收到所有的碎片的时候,它们被组装在一起成为一个单一
的 sk_buff ,并传递到下一个协议层去处理。每一个 ipq 包括一个计时器,每一次接
收到一个有效的碎片的时候就重新启动。如果这个计时器过期,这个 ipq 数据结构和它
的 ipfrag 就被去除,并假设这个消息在传输过程中丢失了。然后由高层的协议负责重
新传输这个消息。
参见 net/ipv4/ip_input.c ip_rcv()
10.6 The Address Resolution Protocol (ARP)
    地址解析协议的任务是提供 IP 地址到物理硬件地址的转换,例如以太网地址。 I
P 在它把数据(用一个 sk_buff 的形式)传送到设备驱动程序进行传送的时候才需要这


种转换。它进行一些检查,看这个设备是否需要一个硬件头,如果是,这个报文的硬件
头是否需要重建。 Linux 缓存硬件头以免频繁地重建。如果硬件头需要重建,它就调用
和设备相关的硬件头重建例程。所有的一台设备使用相同的通用的头重建例程,然后使
用 ARP 服务把目标的 IP 地址转换到物理地址。
参见 net/ipv4/ip_output.c ip_build_xmit()
参见 net/ethernet/eth.c rebuild_header()
    ARP 协议本身非常简单,包含两种消息类型: ARP 请求和 ARP 应答。 ARP 请求包
括需要转换的 IP 地址,应答(希望)包括转换的 IP 地址和硬件地址。 ARP 请求被广
播到连接到网络的所有的主机,所以,对于一个以太网所有连在以太网上的机器都可以
看到这个 ARP 请求。拥有这个请求中包括的 IP 地址的机器会回应这个 ARP 请求,用
包含它自己物理地址的 ARP 应答。
    Linux 中的 ARP 协议层围绕着一个 arp_table 数据结构的表而建立。每一个描述
一个 IP 和物理地址的对应。这些条目在 IP 地址需要转换的时候创建,随着时间推移
变得陈旧的时候被删除。每一个 arp_table 数据结构包含以下域:
Last used 这个 ARP 条目上一次使用的时间
Last update 这个 ARP 条目上一次更新的时间
Flags 描述这个条目的状态:它是否完成等等
IP address 这个条目描述的 IP 地址
Hardware address 转换(翻译)的硬件地址
Hardware header 指向一个缓存的硬件头的指针
Timer 这是一个 timer_list 的条目,用于让没有回应的 ARP 请求超时
Retries 这个 ARP 请求重试的次数


Sk_buff queue 等待解析这个 IP 地址的 sk_buff 条目的列表
    ARP 表包含一个指针( arp_tables 向量表)的表,把 arp_table 的条目链接在一
起。这些条目被缓存,以加速对它们的访问。每一个条目用它的 IP 地址的最后两个字
节做表的索引进行查找,然后跟踪这个条目链,直到找到正确的条目。 Linux 也缓存从
 arp_table 条目预先建立的硬件头,用 hh_cache 数据结构的形式进行缓存。
    当请求一个 IP 地址转换的时候,没有对应的 arp_table 条目, ARP 必须发送一
个 ARP 请求消息。它在表中创建一个新的 arp_table 条目,并把需要地址转换的包括
了网络报文的 sk_buff 放到这个新的条目的 sk_buff 队列。它发出一个 ARP 请求并让
 ARP 过时计时器运行。如果没有回应, ARP 会重试几次。如果仍旧没有回应, ARP 会
删除这个 arp_table 条目。任何排队等待这个 IP 地址进行转换的 sk_buff 数据结构
会被通知,由传输它们的上层协议负责处理这种失败。 UDP 不关心丢失的报文,但是
TCP 会在一个建立的 TCP 连接上试图重新发送。如果这个 IP 地址的属主用它的硬件地
址应答,这个 arp_table 条目标记为完成,任何排队的 sk_buff 会被从对队列中删除
,继续传送。硬件地址被写到每一个 sk_buff 的硬件头中。
ARP 协议层也必须回应指明它的 IP 地址的 ARP 请求。它登记它的协议类型( ETH_P_
ARP ),产生一个 packet_type 数据结构。这意味着网络设备接收到的所有的 ARP 报
文都会传给它。象 ARP 应答一样,这也包括 ARP 请求。它使用接收设备的 device 数
据结构中的硬件地址产生 ARP 应答。
    网络拓扑结构不断变化, IP 地址可能被重新分配到不同的硬件地址。例如,一些
拨号服务为它建立的每一个连接分配一个 IP 地址。为了让 ARP 表中包括最新的条目,
 ARP 运行一个定期的计时器,检查所有的 arp_table 条目,看哪一个超时了。它非常
小心,不删除包含包含一个或多个缓存的硬件头的条目。删除这些条目比较危险,因为


其它数据结构依赖它们。一些 arp_table 条目是永久的,并被标记,所以它们不会被释
放。 ARP 表不能增长的太大:每一个 arp_table 条目都要消耗一些核心内存。每当需
要分配一个新的条目而 ARP 表到达了它的最大尺寸的时候,就查找最旧的条目并删除它
们,从而修整这个表。
10.7 IP Routing
    IP 路由功能确定发向一个特定的 IP 地址的 IP 报文应该向哪里发送。当传送 IP
 报文的时候,会有许多选择。目的地是否可以到达?如果可以,应该使用哪一个网络设
备来发送?是不是有不止一个网络设备可以用来到达目的地,哪一个最好? IP 路由数
据库维护的信息可以回答这些问题。有两个数据库,最重要的是转发信息数据库( For
warding Information Database )。这个数据库是已知 IP 目标和它们最佳路由的详尽
的列表。另一个小一些,更快的数据库,路由缓存( route cache )用于快速查找 IP
 目标的路由。象所有缓存一样,它必须只包括最常访问的路由,它的内容是从转发信息
数据库中得来的。
    路由通过 BSD socket 接口的 IOCTL 请求增加和删除。这些请求被传递到具体的协
议去处理。 INET 协议层只允许具有超级用户权限的进程增加和删除 IP 路由。这些路
由可以是固定的,或者是动态的,不断变化的。多数系统使用固定路由,除非它们本身
是路由器。路由器运行路由协议,不断地检查所有已知 IP 目标的可用的路由。不是路
由器的系统叫做末端系统( end system )。路由协议用守护进程的形式来实现,例如
 GATED ,它们也使用 BSD socket 接口的 IOCTL 来增加和删除路由。
10.7.1 The Route Cache
    不论何时查找一个 IP 路由的时候,都首先在路由缓存中检查匹配的路由。如果在
路由缓存中没有匹配的路由,才查找转发信息数据库。如果这里也找不到路由, IP 报


文发送会失败,并通知应用程序。如果路由在转发信息数据库而不在路由缓存中,就为
这个路由产生一个新的条目并增加到路由缓存中。路由缓存是一个表( ip_rt_hash_ta
ble ),包括指向 rtable 数据结构链的指针。路由表的索引是基于 IP 地址最小两字
节的 hash 函数。这两个字节通常在目标中有很大不同,让 hash value 可以最好地分
散。每一个 rtable 条目包括路由的信息:目标 IP 地址,到达这个 IP 地址要使用的
网络设备( device 结构),可以使用的最大的信息尺寸等等。它也有一个引用计数器
( refrence count ),一个使用计数器( usage count )和上次使用的时间戳(在
jiffies 中)。每一次使用这个路由的时候这个引用计数器就增加,显示利用这个路由
的网络连接数目,当应用程序停止使用这个路由的时候就减少。使用计数器每一次查找
路由的时候就增加,用来让这个 hash 条目链的 rtable 条目变老。路由缓存中所有条
目的最后使用的时间戳用于定期检查这个 rtable 是否太老。如果这个路由最近没有使
用,它就从路由表中废弃。如果路由保存在路由缓存中,它们就被排序,让最常用的条
目在 hash 链的前面。这意味着当查找路由的时候找到这些路由会更快。
参见 net/ipv4/route.c check_expire()
 
10.7.2 The Forwarding Information Database
 
    转发信息数据库(图 10.5 显示)包含了当时从 IP 的观点看待系统可用的路由。
它是非常复杂的数据结构,虽然它已经进行了合理有效的安排,但是它对于参考而言并
不是一个快速的数据库。特别是如果每一个传输的 IP 报文都在这个数据库中查找目标
会非常慢。这也是为什么要有路由缓存:加速已经知道最佳路由的 IP 报文的传送。路
由缓存从这个转发信息数据库得到,表示了它最常用的条目。


表指向。 Hash 索引取自 IP 子网掩码。所有通向同一子网的路由都用排在每一个 fib
_zone 数据结构的 fz_list 队列中得的成对的 fib_node 和 fib_info 数据结构来描述
。如果这个子网的路由数目变得太大,就生成一个 hash table ,让 fib_node 数据结
构的查找更容易。
    对于同一个 IP 子网,可能存在多个路由,这些路由可能穿过多个网关之一。 IP
路由层不允许使用相同的一个网关对于一个子网有多于一个路由。换句话说,如果对于
一个子网有多个路由,那么要保证每一个路由都是用不同的网关。和每一个路由关联的
是它的量度( metric ),这是用来衡量这个路由的益处。一个路由的量度,基本上,
是它在到达目标子网之前必须跳过的子网数目。这个量度越高,路由越差。

你可能感兴趣的:(linux驱动开发篇)