当前位置:Linux教程 - Linux - Networks(网络)

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地址用点分隔的四个数字表示,例如,16.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 Server DNS)来解析名字。这种情况下,本地主机必须知道一个或多个DNS服务器的IP地址,在/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层必须找出这个IP主机的以太网地址。这是因为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是一个可靠的端到端的协议。当传送UDP报文的时候,Linux不知道也不关心它们是否安全到达目的地。TCP报文进行了编号,TCP连接的每一端都要确保传送的数据正确地接收到。IP层包括了网际协议(Internet Protocol)的代码实现。这种代码在传送的数据前增加IP头,而且知道如何把进来的IP报文转送到TCP或者UDP层。在IP层之下,支持Linux联网的是网络设备,例如PPP和以太网。网络设备并非总是表现为物理设备:其中一些比如loopback设备只是纯粹的软件设备。不象标准的Linux设备用mknod命令创建,网络设备只有在底层的软件找到并且初始化它们之后才出现。你只有在建立俄一个包含恰当的以太望设备驱动程序的核心之后你才能看到设备文件/dev/eth0。ARP协议位于IP层和支持ARP的协议之间。

     

     

     



    10.3 The BSD Socket Interface(BSD socket 接口)

     

    这是一个通用的接口,不仅仅支持多种形式的联网,也是一种进程间通讯机制。一个socket描述了通讯连接的一端,两个通讯进程每一个都会有一个socket,描述它们之间通讯连接的自己部分。Socket可以想象成一种特殊形式的管道,但是和管道不同,socket对于可以容纳的数据量没有限制。Linux支持几种类型的socket,这些类叫做address families(地址族)。这是因为每一类都有自己通讯寻址方式。Linux支持以下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类型,每一种都代表了连接上支持的服务的类型。并非所有的address 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,然后为它bind一个名字。这个名字的格式和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”)和它的初始化例程的地址来代表。当启动的时候,socket接口初始化,每一个协议的初始化代码都要被调用。对于socket地址族,它们里边会登记一系列协议操作。这都是一些例程,每一个都执行一个和地址族相关的特殊操作。登记的协议操作保存在pops向量表中,这个向量表保存指向proto_ops数据结构的指针。Proto_ops数据结构包括协议族类型和一批和特定地址族相关的socket操作例程的指针。Pops向量表用地址族的标识符作为索引,例如Internet address family的标识符(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层的接口是通过网络初始化的时候它向BSD socket层登记的internet address family socket操作进行的。这些和其它登记的地址族一起放在pops向量表中。BSD socket层通过调用在登记的proto_ops数据结构中的INET层的socket支持例程完成它的工作。例如,一个地址族是INET的BSD 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 socket文件操作例程。所有将来的操作会被定向到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地址族的网络设备的地址之中的一个,而且接口必须是开启的并能够使用。你可以用ifconfig命令看系统中哪一个网络接口当前是激活的。IP地址也可以是IP广播地址(全是1或0)。这是意味着“发送给每一个人”的特殊地址。如果这个机器作为一个透明的proxy或者防火墙,这个IP地址也可以设置成任何IP地址。不过只有具有超级用户特权的进程可以绑定任意IP地址。这个绑定的IP地址被存在sock数据结构的recv_addr和saddr域中。它们分别用于hash查找和发送IP地址。端口号是可选的,如果没有设置,会向支撑的网络请求一个空闲的。按照惯例,小于1024的端口号不能被没有超级用户特权的进程使用。如果底层的网络分配端口号,它总是分配一个大于1024的端口。

     

    当底层的网络设备接收报文的时候,这些报文必须被转到正确的INET和BSD socket才能被处理。为此,UDP和TCP维护hash table,用于查找进来的IP信息的地址,把它们转到正确的socket/sock对。TCP是一个面向连接的协议,所以处理TCP报文比处理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数据结构必须在SS_UNCONNECTED状态。UDP协议不在两个应用程序之间建立虚拟连接,所有发送的消息都是数据报,发出的消息可能到到也可能没有到达它的目的地。但是,它也支持BSD socket的connect操作。在一个UDP INET BSD socket上的一个连接操作只是建立远程应用程序的地址:它的IP地址和它的IP端口号。另外,它也要建立一个路由表条目的缓存区,这样,在这个BSD socket上发送的UDP数据报不需要在检查路由表数据库(除非这个路由变成无效)。这个缓存的路由信息被INET sock数据结构中的ip_route_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和tcp_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 socket进行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_buff有四个数据指针,用于操纵和管理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就被废弃。如果有工作要执行,这个网络的button half标记成准备运行。

    参见net/core/dev.c netif_rx()

     

    当网络的bottom half处理程序被调度程序调用的时候,它首先处理任何等待传送的网络报文,然后才处理sk_buff的backlog backlo队列,确定接收到的报文需要传送到那个协议层。当Linux网络层初始化的时候,每一个协议都登记自己,在ptype_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协议,这种路由选择比较容易。报文应该要么通过loopback设备传送给本地主机,要么传送到PPP modem连接的另一端的网关。对于连接到以太网的计算机而言,这种选择比较困难,因为网络上连接了许多计算机。

     

    对于传送的每一个IP报文,IP使用路由表解析目标IP地址的路由。对于每一个IP目标在路由表中进行的查找,成功就会返回一个描述要使用的路由的rtable数据结构。包括使用的源IP地址,网络device数据结构的地址,有时候还会有一个预先建立的硬件头。这个硬件头和网络设备相关,包含源和目的物理地址和其它同介质相关的信息。如果网络设备是以太网设备,硬件头会在图10.1中显示,其中的源和目的地址会是物理的以太网地址。硬件头和路由缓存在一起,因为在这个路由传送的每一个IP报文都需要追加这个头,而建立这个头需要时间。硬件头可能包含必须使用ARP协议才能解析的物理地址。这时,发出的报文会暂停,直到地址解析成功。一旦硬件地址被解析,并建立了硬件头,这个硬件头就被缓存,这样以后使用这个接口的IP报文就不需要进行ARP。

    参见include/net/route.h

     

    10.5.4 Data Fragmentation

     

    每一个网络设备都有一个最大的报文尺寸,它无法传送或接收更大的数据报文。IP协议允许这种数据,会把数据分割成网络设备可以处理的报文大小的更小的单元。IP协议头包含一个分割字段,包含一个标记和分割的偏移量。

     

    当要传输一个IP报文的时候,IP查找用来发送IP报文的网络设备。通过IP路由表来查找这个设备。每一个设备都有一个字段描述它的最大传输单元(字节),这是mtu字段。如果设备的mtu比等待传送的IP报文的报文尺寸小,那么这个IP报文必须被分割到更小的碎片(mtu大小)。每一个碎片用一个sk_buff代表:它的IP头标记了它被分割,以及这个IP报文在数据中的偏移量。最后一个报文被标记为最后一个IP碎片。如果在分割成碎片的过程中,IP无法分配一个sk_buff,这次传送就失败。

     

    接收IP碎片比发送更难,因为IP碎片可能以任意顺序被接收,而且它们必须在重组之前全部接收到。每一次一个IP报文被接收的时候,都检查它是否是一个IP碎片。收到一个消息的第一个碎片,IP就建立一个新的ipq数据结构,并连接到等待组装的IP碎片的ipqueue列表中。当更多的IP碎片接收到的时候,就查到正确的ipq数据结构并建立一个新的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地址到物理硬件地址的转换,例如以太网地址。IP在它把数据(用一个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路由数据库维护的信息可以回答这些问题。有两个数据库,最重要的是转发信息数据库(Forwarding 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_table),包括指向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报文的传送。路由缓存从这个转发信息数据库得到,表示了它最常用的条目。

     

    每一个IP子网用一个fib_zone数据结构表示。所有这些都被fib_zones hash表指向。Hash索引取自IP子网掩码。所有通向同一子网的路由都用排在每一个fib_zone数据结构的fz_list队列中得的成对的fib_node和fib_info数据结构来描述。如果这个子网的路由数目变得太大,就生成一个hash table,让fib_node数据结构的查找更容易。

    对于同一个IP子网,可能存在多个路由,这些路由可能穿过多个网关之一。IP路由层不允许使用相同的一个网关对于一个子网有多于一个路由。换句话说,如果对于一个子网有多个路由,那么要保证每一个路由都是用不同的网关。和每一个路由关联的是它的量度(metric),这是用来衡量这个路由的益处。一个路由的量度,基本上,是它在到达目标子网之前必须跳过的子网数目。这个量度越高,路由越差。


    发布人:netbull 来自:非常Linux