- 论坛徽章:
- 0
|
Linux网络
Linux和网络几乎是同义词。实际上Linux是Internet或WWW的产物。它的开发者和用户使用web交换信息、想法、代码,而Linux自身也常用于支持一些组织的联网需求。众所周知,TCP/IP 协议是 Internet 的标准协议,同时也是事实上的工业标准。Linux 的网络实现支持 BSD 套接字,支持完整的 TCP/IP 协议。本章描述了Linux如何支持统称为TCP/IP的网络协议。
10.1 TCP/IP网络概览(An Overview of TCP/IP Networking)
本节简要描述 TCP/IP 协议,这并不是一个详尽的描述,有关该协议的详细内容。请阅读第10本参考书(附录)。
TCP/IP 协议是 Internet 网的基本协议,它实际由许多协议组成,并以协议组的形式存在,其中的主要协议有:传输控制协议(TCP)、用户数据报协议(UDP)、网际协议(IP)、网际信报控制协议(ICMP)和地址解析协议(ARP)等。
设计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平台之间移植。
在一个IP网络中,每一台主机都必须具有唯一的地址,这一地址称为“IP 地址”。IP地址是一个32位的数字,它唯一地标识一台机器。例如,WWW是一个非常巨大、不断增长的IP网络,每一个连接在上面的机器都分配了一个独一无二的IP地址。IP地址用由点分隔的四个数字表示,每个数字的取值范围一般在 0 ~ 255 之间,例如,16.42.0.9。在 Internet 上,主机的 IP 地址分为五类,分别是 A、B、C、D和E五类。主机的唯一 IP 地址一般从 A、B或C类地址中派生;D类地址用来将计算机组织成一个功能组;而E类地址是试验性的,当前不可用。另外,一些特殊的 IP 地址被保留用于特殊目的,例如,127.0.0.1 就是用来特指本地主机的回环地址。
IP地址实际上分为两个部分:网络地址和主机地址。这些地址的大小(尺寸)可能不同,用来分割这两部分的是机器的子网掩码。以16.42.0.9为例,如果其子网掩码是255.255.0.0,则该IP地址表示的网络地址是16.42,主机地址是0.9。TCP/IP 软件利用子网掩码判断数据传输的目标主机是否和源主机处于同一子网中。例如,某主机的 IP 地址为 192.1.1.1,而子网掩码为 255.255.255.0,如果目标主机的 IP 地址为 192.1.1.4,则说明目标主机和源主机处于同一子网中,而如果目标主机的 IP 地址为 192.1.2.1,则说明不在同一子网中。上述判断通过利用子网掩码计算两台主机所在的子网地址而实现。主机 IP 地址和子网掩码的二进制与运算的结果称为“子网地址”,例如,主机 IP 地址 192.1.1.1 和子网掩码 255.255.255.0 的二进制与运算的结果为 192.1.1.0,即该主机所在子网的地址为 192.1.1.0,对地址为 192.1.1.4 的主机来说,可计算该主机所在子网地址为 192.1.1.0,于是说明目标主机和源主机处于同一子网中,而192.1.2.1 却不在同一子网中。
主机地址可以再进一步划分成为子网(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地址。
子网掩码也同时定义了子网中主机的最大数目。如果子网掩码为 255.255.255.0,在上面的例子中,IP 地址从 192.1.1.1 到 192.1.1.254 的主机均可出现在同一子网中(IP 地址 192.1.1.0 和 192.1.1.255 分别作为子网地址和子网中的广播地址),因此,该子网中的主机数目最多为 254 台。
因为数字式的 IP 地址非常难于记忆,因而通常利用主机域名标识主机,例如,bbs.tsinghua.edu.cn 是清华大学 BBS 服务器的域名,其 IP 地址为 202.112.58.200。但在利用域名标识主机的同时,也需要能够将域名转换为 IP 地址的机制,这种机制称为“域名解析”。在一般的 TCP/IP 主机中,这一名称可通过静态的 hosts 文件指定,也可通过 DNS (分布式名称服务器)服务器动态获得,这种情况下,本地主机必须知道一个或多个DNS服务器的IP地址。在 Linux 中,/etc/hosts 文件指定静态的主机名称,而 etc/resolv.conf 文件指定 DNS 服务器的 IP 地址。
不管何时,每当连接另外一台机器时,比如读取一个web page,都要使用它的IP地址(和那台机器交换数据)。这种数据包括在IP报文(packet)中,每一个报文都有一个IP头(包括源和目标机器的IP地址、一个校验和和其它有用的信息)。这个校验和是从IP报文的数据中通过计算得来的,IP报文的接收者可以用它来判断IP报文在传输过程中是否被损坏(可能是一个噪音很大的电话线)。应用程序传输的数据可能被分解成容易处理的更小的报文。IP数据报文的大小依赖于连接的介质而变化:以太网报文通常大于PPP报文。目标主机必须重新装配这些数据报文,然后才能交给接收程序。如果通过一个相当慢的串行连接访问一个包括大量图形图像的web页,就可以用图形的方式看出数据的分段和重组。
一般而言,逻辑上处于同一子网的两台主机处于同一局域网中,如果目标主机和源主机处于同一子网,就可通过某种机制获得目标主机的网卡物理地址,从而利用局域网技术实现数据传输。从主机的 IP 地址获得物理地址的机制称为“地址解析”,在 TCP/IP 协议中,地址解析可由专门的协议(地址解析协议)完成。如果目标主机和源主机不在同一子网中,这时的数据传输就要通过其他计算机完成,如果目标主机和源主机跨越大的地理距离,则可能要通过许多计算机的参与才能实现数据的传输。跨越不同子网的数据传输通过网关或路由器实现。网关(或路由器)连接在多于一个的子网上,它们会把一个子网上接收的IP报文重新发送到另一个子网。当 TCP/IP 软件发现数据传输的目标主机处于其他子网时,它首先将数据发送到网关,然后由网关选择适当的路径传输,直到数据到达目标主机为止。例如,如果子网16.42.1.0和16.42.0.0通过一个网关连接,那么所有从子网0发送到子网1的报文必须先发送到网关,以便网关转发它们。Linux 维护一个路由表,每个目标 IP 地址均对应一个路由表项。利用路由表项,Linux 可将每个跨子网的 IP 数据包发送到一个适当的主机(路由器)。系统中的路由表实际是动态的,并随着应用程序的网络使用情况和网络拓扑结构的变化而变化。
图10.1 TCP/IP协议层次结构
如前所述,TCP/IP 实际是以协议组的形式存在的,图 10-1 是 TCP/IP 协议层次结构。从图中可看出,TCP/IP映射为四层的结构化模型。这一模型也称为网际协议组(Internet Protocol Suit),可划分为网络接口、网际、传输和应用四层。
网络接口层(Network Interface Layer)负责和网络的直接通讯。它必须理解正在使用的网络结构,诸如令牌环和以太网等,并且还要提供允许网际层与之通讯的接口。网际层负责和网络接口层之间的直接通讯。
网际层(Internet Layer)主要完成利用网际协议(IP)的路由和数据包传递。传输层上的所有协议均要使用IP发送数据。网际协议定义如下规则:如何寻址和定向数据包;如何处理数据包的分段和重新组装;如何提供安全性信息;以及如何识别正在使用的服务类型等。
但是,由于IP不是基于连接的协议,因此它不能保证在线路中传输的数据不会丢失、破坏、重复或颠倒次序。这由网络模型中的高层,即传输层或应用层负责。网际层中还有一些其他的协议:网际信报控制协议(ICMP),网际组管理协议(IGMP)以及地址解析协议(ARP)等。
传输层(Transport Layer)负责提供应用程序之间的通讯。这种通讯可以是基于连接的,也可以是非基于连接的。这两种连接类型的主要差别在于是否跟踪数据以及是否确保数据发送到目标等。传输控制协议(Transmission Control Protocol, TCP)是基于连接的协议,能提供可靠的数据传输;而用户数据报协议(User Datagram Protocol, UDP)是非基于连接的协议,不能确保数据的正确传输。
Internet协议组的应用层(Application Layer)作为应用程序和网络组件之间的接口而存在,其中存在大量的协议,包括简单网络管理协议(Simple Network Management Protocol,SNMP)、文件传输协议(File Transfer Protocol,FTP)、简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)等。
图10.2 TCP数据包的传输
图 14-2 给出了 TCP 数据包的在网际协议组中的传输情况。TCP 利用 IP 数据包传输它自己的数据包,这时,IP 数据包中的数据是 TCP 数据包本身。UDP 也利用 IP 数据包进行数据的传输,在这种情况下,接收方的 IP 层必须能够知道接收到的 IP 数据包要发送给传输层中的哪个协议。为此,每个 IP 数据包头中包含一个字节,专门用作协议标识符。接收方的 IP 层利用这一标识符决定将数据包发送给传输层的哪一个协议处理。和上面的情况类似,同一台主机上利用同一协议进行通讯的应用程序可能有许多,因此,也需要一种机制来标识应由哪一个应用程序处理同一种数据包。为此,应用程序利用 TCP/IP 协议进行通讯时,不仅要指定目标 IP 地址,还要指定应用程序的“端口”地址。端口可唯一标识应用程序,标准的网络应用程序使用标准的端口地址,例如 Web 服务器的标准端口为 80。在网络接口中,IP 地址和端口地址合称为“套接字”。
IP 协议层可利用许多不同的物理介质传输 IP 数据包,图 14-2 中,IP 数据包进一步包装在以太网数据帧中传输。除以太网外,IP 数据包还可以在令牌环网等其他物理介质上传输。以太网数据帧头中包含了数据帧的目标以太网地址,以太网地址实际就是以太网卡的硬件地址或物理地址,一般由 6 位整数组成,如 00-A0-0C-13-CC-78。以太网卡的物理地址是唯一的,一些特殊的物理地址保留用于广播等目的。因为以太网数据帧和 IP 数据包一样,可以传输不同的协议数据,因此,数据帧中也包含一个标识协议的整数。
为了通过多种连接协议(例如通过以太网)来传输IP报文,IP层必须找出目标IP主机的以太网地址。这是因为IP地址只是一个寻址的概念,以太网设备自己有自己的物理地址。IP地址可以由网络管理员根据需要分配和再分配,而网络硬件则只响应具有它自己物理地址的以太网帧,或者特殊的多点广播地址(所有的机器都必须接收)。在以太网中,数据的传输是通过物理地址或硬件地址实现的,而 IP 地址实际只是一种概念性的逻辑地址,因此,在类似以太网这样的网络中,必须采用地址解析协议(ARP)将 IP 地址翻译为实际的硬件地址。ARP 负责为 IP 所请求的任意一个本地 IP 地址找出其本地物理地址。为了得到一个IP地址所关联的硬件地址,主机会发送一个ARP请求包,其中包含它希望转换的IP地址,该包被作为一个多点广播包发送,网络上所有的点都可以收到它。具有这个IP地址的目标主机用一个ARP回应来应答,这中间包括了它的物理硬件地址。ARP 在内存的高速缓冲区中维护最近所映射的物理地址,以备后用。如果目标 IP 地址是本地地址,ARP 可发送一个本地广播请求获取目标 IP 主机的物理地址,并将物理地址返回给 IP。如果 IP 发现目标 IP 地址处于远程子网中,则数据包必须发送到路由器,这时,ARP 可替 IP 找到路由器的物理地址。APR不仅仅限制在以太网设备,它也可以解析其它物理介质的IP地址,例如FDDI。不能进行ARP的设备会有标记,这样Linux就不需要试图对它们进行ARP。
还有一个相反的功能,反向ARP,或RARP,用来把物理地址转换到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层。
10.2 Linux TCP/IP网络分层
图10.3 Linux的网络分层
如图10.3所示,象网络协议本身一样, Linux对于internet 协议地址族的实现就象一系列连接的软件层。
? BSD socket由只和它相关的通用的socket管理软件来支持。
? 支持BSD socket的是INET socket层,它管理以IP为基础的协议(TCP和UDP)的通讯端点。UDP是一个无连接的协议,而TCP是一个可靠的端到端的协议。
? 当传送UDP报文的时候,Linux不知道也不关心它们是否安全到达了目的地。相反地,TCP对其报文进行了编号,TCP连接的每一端都要确保传送的数据被正确地接收到。
? IP层包括了网际协议(Internet Protocol)的代码实现。这种代码在传送的数据前增加IP头,而且知道如何把进来的IP报文转送到TCP或者UDP层。
? 在IP层之下,支持Linux联网的是网络设备,例如PPP和以太网。网络设备并非总是物理设备,其中一些(比如loopback设备)就是纯粹的软件设备。不象标准的用mknod命令创建的Linux设备,网络设备只有在底层的软件找到并且初始化它们之后才出现。只有当你把适当的以太网设备驱动程序建到内核中以后,才能看到设备文件/dev/eth0。
? ARP协议位于IP层和支持ARP的协议之间。
上面的层次模型可以再进一步抽象为三个层次,即:套接字层、网络协议层和网络设备层。在具体实现中,每个层次被抽象为一个对象,它们是:
? 套接字(socket)。一个套接字就是网络中的一个连接,它向用户提供了基于文件I/O(read、write等)的网络数据传输。Socket通过网络协议实现自身,它与网络协议密切相关,体现了网络和文件系统、进程管理之间的关系,它是网络传输的入口。
? 网络协议(protocol)。网络协议是一种网络语言,它规定了通信双方交换信息的一种规范,是网络传输的基础。
? 设备接口(device and interface)。网络设备接口控制着网络数据由软件到硬件和由硬件到软件的过程,体现了网络和设备的关系,是网络传输的桥梁。
? 套接字缓冲区(network buffer)。套接字缓冲区是一块保存网络数据的内存区域,是网络各层之间交换数据的地方。要在网络上发送的数据保存在套接字缓冲区中,协议的各层对其处理并增加自己的包装;从网络上接收的数据也保存在套接字缓冲区中,协议的各层对其处理并去掉自己的包装。套接字缓冲区与网络的各层都有密切关系,是网络传输的灵魂。
10.3 BSD socket 接口
套接字(Socket)既可看成是支持多种网络操作形式的接口,也可看成是一种进程间通讯接口。在一条通讯连接中,每个参与通讯的进程有一个套接字(Socket)描述。一个socket描述了通讯连接的一端,两个通讯进程每一个都会有一个socket,描述它们之间通讯连接中自己端的情况。可以将套接字(Socket)看成是某种特殊类型的管道,但和管道不同的是,套接字并不限制其中可以包含的数据数量。Linux 支持多种套接字种类,不同的套接字种类称为“地址族” (address families)或协议族(Protocol families)或域(domain),这是因为每种套接字种类拥有自己的通讯寻址方法。Linux 所支持的套接字地址族见下表10-1。
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
#define AF_AX25 3 /* Amateur Radio AX.25 */
#define AF_IPX 4 /* Novell IPX */
#define AF_APPLETALK 5 /* AppleTalk DDP */
#define AF_NETROM 6 /* Amateur Radio NET/ROM */
#define AF_BRIDGE 7 /* Multiprotocol bridge */
#define AF_ATMPVC 8 /* ATM PVCs */
#define AF_X25 9 /* Reserved for X.25 project */
#define AF_INET6 10 /* IP version 6 */
#define AF_ROSE 11 /* Amateur Radio X.25 PLP */
#define AF_DECnet 12 /* Reserved for DECnet project */
#define AF_NETBEUI 13 /* Reserved for 802.2LLC project */
#define AF_SECURITY 14 /* Security callback pseudo AF */
#define pseudo_AF_KEY 15 /* PF_KEY key management API */
#define AF_NETLINK 16
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
#define AF_PACKET 17 /* Packet family */
#define AF_ASH 18 /* Ash */
#define AF_ECONET 19 /* Acorn Econet */
#define AF_ATMSVC 20 /* ATM SVCs */
#define AF_SNA 22 /* Linux SNA Project (nutters!) */
#define AF_IRDA 23 /* IRDA sockets */
#define AF_MAX 32 /* For now.. */
和虚拟文件系统类似,Linux将上述套接字地址族抽象为统一的 BSD 套接字接口,应用程序关心的只是 BSD 套接字接口,而 BSD 套接字由各地址族专有的软件支持(实现)。一般而言,BSD 套接字可支持多种套接字类型,不同的套接字类型提供的服务不同,Linux 所支持的 BSD 套接字类型见表 10-2,表 10-1 中的套接字地址族并不一定全部支持这些套接字类型。但同一协议族可以提供多种服务类型,如TCP/IP协议族提供虚电路和数据报两种服务。
Linux BSD socket支持以下socket类型:
#define SOCK_STREAM 1 /* stream (connection) socket */
#define SOCK_DGRAM 2 /* datagram (conn.less) socket */
#define SOCK_RAW 3 /* raw socket */
#define SOCK_RDM 4 /* reliably-delivered message */
#define SOCK_SEQPACKET 5 /* sequential packet socket */
#define SOCK_PACKET 10 /* linux specific way of */
/* getting packets at the dev */
/* level. For writing rarp and */
/* other similar things on the */
/* user level. */
其中:
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 (RDM)
可靠的消息递交,它很象数据报但是可以保证数据到达。
Sequenced Packets
定序分组socket,象stream socket但是数据报文大小是固定的。
Packet
这不是标准的BSD socket类型,它是Linux特定的扩展,允许进程直接在设备层访问报文。
使用socket通讯的进程采用客户服务器模型。服务器提供服务,而客户使用这种服务。Web 服务器是这样的一个例子,web服务器提供web page, web 客户(或浏览器)读取这些页。使用socket的服务器,首先创建一个socket,然后为它bind一个名字。这个名字的格式和socket的address family有关,对INET地址族,名字是服务器的本地地址(IP地址+端口号)。Socket的名字或地址用sockaddr数据结构指定。
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
一个INET socket会绑定一个IP端口地址。注册的端口编号可以在/etc/services中看到:例如,web服务器的端口号是80。在socket上绑定一个地址后,服务器就listen进来的对于绑定地址的连接请求。请求的发起者(客户),创建一个socket,并在上面执行一个连接请求,在请求中指定服务器的目标地址。对于一个INET socket,服务器的地址是它的IP地址和它的端口号。这些进来的请求必须通过大量的协议层,找到它的路径,然后在服务器的监听端口等待。一旦服务器接收到了进来的请求,它可以接受(accept)或者拒绝(reject)它。如果要接受进来的请求,服务器必须创建一个新的socket来接受它。因为,一个已经用于监听进来的连接请求的socket,不能再用于支持一个连接。连接建立之后,两端都可以自由地发送和接收数据。最后,当一个连接不再需要的时候,它可以被关闭。双方都必须小心,以保证正确地处理正在传送的数据报文。
在一个BSD socket上,操作的确切意义依赖于它底层的地址族。建立一个TCP/IP连接和建立一个业余无线电X.25连接有很大的不同。象虚拟文件系统一样,Linux 用BSD socket层作为应用程序所关心的一个抽象的BSD socket接口,该层由独立的与地址族相关的软件(如INET Socket 层)提供支持。当内核初始化的时候,建立在内核的地址族就向BSD socket层登记自己。稍后,当应用程序创建和使用BSD socket的时候,在BSD socket和它的支撑地址族之间就会建立一个联系。这种联系是通过交叉的数据结构和地址族支持例程表实现的。例如,当应用程序创建一个新的socket的时候,BSD socket层就使用地址族相关的socket创建例程。
当配置内核的时候,它所支持的地址族和协议都建立到了protocols向量表中。该向量表的每一项都由地址族的名称(例如“INET”)和它的初始化例程的地址组成。
struct net_proto {
const char *name; /* Protocol name */
void (*init_func)(struct net_proto *); /* Bootstrap */
};
struct net_proto protocols[] = {
#ifdef CONFIG_PACKET
{ "PACKET", packet_proto_init },
#endif
#ifdef CONFIG_UNIX
{ "UNIX", unix_proto_init }, /* Unix domain socket family */
#endif
#ifdef NEED_802
{ "802.2", p8022_proto_init }, /* 802.2 demultiplexor */
{ "SNAP", snap_proto_init }, /* SNAP demultiplexor */
#endif
#ifdef NEED_LLC
{ "802.2LLC", llc_init }, /* 802.2 LLC */
#endif
#ifdef CONFIG_INET
{ "INET", inet_proto_init }, /* TCP/IP */
#ifdef CONFIG_IPV6
{ "INET6", inet6_proto_init}, /* IPv6 */
#endif
#endif
…………
}
当系统启动的时候,socket接口初始化,每一个协议的初始化代码都要被调用。对于socket地址族,这会导致一系列协议操作的登记。
Linux支持的所有的协议都记录在数组net_families[]中。该数组的定义如下:
#define NPROTO 32 /* should be enough for now.. */
struct net_proto_family *net_families[NPROTO];
这是一个指针数组,每个指针都指向一个net_proto_family数据结构。数据结构net_proto_family的定义为:
struct net_proto_family
{
int family;
int (*create)(struct socket *sock, int protocol);
/* These are counters for the number of different methods of each
we support */
short authentication;
short encryption;
short encrypt_net;
};
这里主要有两项内容:
family是协议地址族编码,定义在表1.1中。该编码是协议在数组net_families[]中的下标。
Create是协议创建函数,在创建相应协议的Socket时调用。函数create根据协议类型type,为所建立的Socket指定一个与之对应的操作集struct proto_ops。该操作集定义了协议对基本的Socket操作的实现。定义如下:
此后,对Socket的所有操作都会转化为和地址族相关的特殊操作,并经过这些操作完成Socket操作。
10.4 TCP/IP协议初始化
在执行系统初始化的sock_init()时,要调用定义在数组protocols[]中的每个协议的初始化函数,实现该协议的初始化。协议初始化函数完成的工作大致相同,下面以TCP/IP协议为例,介绍该初始化过程。
TCP/IP协议的初始化函数是inet_proto_init,它定义在net/ipv4/af_inet.c中。
该函数的定义为:
void inet_proto_init(struct net_proto *pro)
其中pro是一个net_proto数据结构,即protocols[]数组中的当前元素。该数据结构定义了两个元素:协议名和协议初始化函数。
函数inet_proto_init所做的工作如下:
一、 调用函数sock_register(&inet_family_ops)向BSD Socket注册。
函数sock_register十分简单,它把自己的协议族注册到数组net_families[]中。
int sock_register(struct net_proto_family *ops)
{
if (ops->family >= NPROTO) {
printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n",
ops->family, NPROTO);
return -ENOBUFS;
}
net_families[ops->family]=ops;
return 0;
}
这里ops是具体协议的协议族。INET协议的协议族定义为:
struct net_proto_family inet_family_ops = {
PF_INET,
inet_create
};
其中:PF_INET是INET协议族的编号,其值为2。
inet_create是INET协议Socket的创建函数。
net_proto_family结构的其余三个短整数(short)此处未定义。
函数调用sock_register(&inet_family_ops)的意思是给数组net_families[2]赋值inet_family_ops。
每个协议的初始化函数都要调用函数sock_register,将描述自己协议族的数据结构net_proto_family放入数组net_families[]的相应位置。
二、 处理inet协议
一个inet协议由一个inet_protocol数据结构描述。该结构的定义如下:
struct inet_protocol
{
int (*handler)(struct sk_buff *skb, unsigned short len);
void (*err_handler)(struct sk_buff *skb, unsigned char *dp, int len);
struct inet_protocol *next;
unsigned char protocol;
unsigned char copy:1; /* 同协议编号的协议是否已经存在 */
void *data;
const char *name;
};
初始化开始时,描述INET的几个协议的inet_protocol结构连成了一个链表,其表头指针为inet_protocol_base。
为了以后对协议查找的方便,将INET所支持协议的inet_protocol数据结构放到一个Hash表中。该Hash表为:
#define MAX_INET_PROTOS 32
struct inet_protocol *inet_protos[MAX_INET_PROTOS] =
{
NULL
};
将链表inet_protocol_base中的各元素按其协议编号(protocol)插入到Hash表中。
下面是已定义的各INET协议的编号:
/* Standard well-defined IP protocols. */
enum {
IPPROTO_IP = 0, /* Dummy protocol for TCP */
IPPROTO_ICMP = 1, /* Internet Control Message Protocol */
IPPROTO_IGMP = 2, /* Internet Group Management Protocol */
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */
IPPROTO_TCP = 6, /* Transmission Control Protocol */
IPPROTO_EGP = 8, /* Exterior Gateway Protocol */
IPPROTO_PUP = 12, /* PUP protocol */
IPPROTO_UDP = 17, /* User Datagram Protocol */
IPPROTO_IDP = 22, /* XNS IDP protocol */
IPPROTO_RSVP = 46, /* RSVP protocol */
IPPROTO_GRE = 47, /* Cisco GRE tunnels (rfc 1701,1702) */
IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */
IPPROTO_PIM = 103, /* Protocol Independent Multicast */
IPPROTO_RAW = 255, /* Raw IP packets */
IPPROTO_MAX
};
三、 ARP协议初始化:arp_init()
在linux/net/ipv4/arp.c。
1、当Linux网络层初始化的时候,每一个协议都登记自己:在ptype_all链表或者ptype_base hash table中增加一个packet_type的数据结构。当网络设备接收到一个数据包时,底层软件分析该包,确定其类型,而后查找ptype_all链表和ptype_base hash table,在其中寻找与接收包协议匹配的packet_type数据结构,并据此确定处理该包的协议和相应的处理例程。
packet_type的定义如下:
struct packet_type
{
unsigned short type; /* This is really htons(ether_type). */
struct device *dev; /* NULL is wildcarded here */
int (*func) (struct sk_buff *, struct device *,
struct packet_type *);
void *data; /* Private to the packet type */
struct packet_type *next;
};
这个packet_type数据结构包括协议类型(定义在include/linux/if_ether.h中)、一个指向网络驱动设备的指针、一个指向协议的数据接收处理例程的指针和一个指向这个列表或者hash table中下一个packet_type数据类型的指针。
ptype_all是一个packet_type结构的链表,用于探听(snoop)从任意网络设备上接收到的所有的数据报文(其type为ETH_P_ALL,即处理所有类型的包),通常不使用。
ptype_base是一个hash table ,其定义如下:
struct packet_type *ptype_base[16];
ptype_base Hash table使用协议标识符(type域)作hash值,用于确定进来的网络报文应该由哪一种协议接收。网络的bottom half把进来的sk_buff中的协议类型和上述任一表中的一个或多个packet_type条目进行匹配。协议可能会匹配一个或多个条目,例如当探听(snoop)所有的网络通信的时候,这时,这个sk_buff会被克隆。最后,这个sk_buff被传递到匹配的协议的处理例程。
当然,ARP协议要注册其协议类型和协议处理例程。其packet_type数据结构是:
static struct packet_type arp_packet_type =
{
__constant_htons(ETH_P_ARP),
NULL, /* All devices */
arp_rcv,
NULL,
NULL
};
从中可见,ARP协议的包处理例程是arp_rcv。
2、注册ARP协议的/PROC文件系统操作集。
static struct proc_dir_entry proc_net_arp = {
PROC_NET_ARP, 3, "arp",
S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_net_inode_operations,
arp_get_info
};
3、创建一个neigh_sysctl_table数据结构
neigh_sysctl_register(NULL, &arp_tbl.parms, NET_IPV4, NET_IPV4_NEIGH, "ipv4");
四、 IP协议初始化:ip_init()
在linux/net/ipv4/ip_output.c。
1、与ARP协议相似,IP协议也要注册其接收的包的类型和处理函数。其注册的packet_type数据结构定义为:
static struct packet_type ip_packet_type =
{
__constant_htons(ETH_P_IP),
NULL, /* All devices */
ip_rcv,
NULL,
NULL,
};
可见,IP包的处理函数是ip_rcv。
2、IP路由初始化:ip_rt_init()
在linux/net/ipv4/router.c
3、注册IP协议的/PROC文件系统操作集。
五、 TCP V4初始化:tcp_v4_init(&inet_family_ops)
在linux/net/ipv4/tcp_ipv4.c
1、 初始化一个inode:tcp_inode;
2、 初始化inode tcp_inode中的socket部分。
六、 TCP初始化:tcp_init()
在linux/net/ipv4/tcp.c
1、为数据结构open_request建立一个slab cache。
struct open_request {
struct open_request *dl_next; /* Must be first member! */
__u32 rcv_isn;
__u32 snt_isn;
__u16 rmt_port;
__u16 mss;
__u8 retrans;
__u8 __pad;
unsigned snd_wscale : 4,
rcv_wscale : 4,
tstamp_ok : 1,
sack_ok : 1,
wscale_ok : 1;
/* The following two fields can be easily recomputed I think -AK */
__u32 window_clamp;/* window clamp at creation time */
__u32 rcv_wnd; /* rcv_wnd offered first time */
__u32 ts_recent;
unsigned long expires;
struct or_calltable *class;
struct sock *sk;
union {
struct tcp_v4_open_req v4_req;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct tcp_v6_open_req v6_req;
#endif
} af;
#ifdef CONFIG_IP_TRANSPARENT_PROXY
__u16 lcl_port; /* LVE */
#endif
};
2、为数据结构tcp_bind_bucket建立一个slab cache。
struct tcp_bind_bucket {
unsigned short port;
unsigned short flags;
#define TCPB_FLAG_LOCKED 0x0001
#define TCPB_FLAG_FASTREUSE 0x0002
#define TCPB_FLAG_GOODSOCKNUM 0x0004
struct tcp_bind_bucket *next;
struct sock *owners;
struct tcp_bind_bucket **pprev;
};
3、 为数据结构tcp_tw_bucket建立一个slab cache。
struct tcp_tw_bucket {
/* These _must_ match the beginning of struct sock precisely.
* XXX Yes I know this is gross, but I'd have to edit every single
* XXX networking file if I created a "struct sock_header". -DaveM
*/
struct sock *sklist_next;
struct sock *sklist_prev;
struct sock *bind_next;
struct sock **bind_pprev;
__u32 daddr;
__u32 rcv_saddr;
__u16 dport;
unsigned short num;
int bound_dev_if;
struct sock *next;
struct sock **pprev;
unsigned char state,
zapped;
__u16 sport;
unsigned short family;
unsigned char reuse,
nonagle;
/* And these are ours. */
__u32 rcv_nxt;
struct tcp_func *af_specific;
struct tcp_bind_bucket *tb;
struct tcp_tw_bucket *next_death;
struct tcp_tw_bucket **pprev_death;
int death_slot;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct in6_addr v6_daddr;
struct in6_addr v6_rcv_saddr;
#endif
};
七、 ICMP初始化:icmp_init(&inet_family_ops)
在linux/net/ipv4/icmp.c
1、 初始化一个inode:icmp_inode;
2、 初始化inode icmp_inode中的socket部分。
八、 如果定义了IP隧道,则初始化IP隧道:ipip_init()
在linux/net/ipv4/ipip.c
IP隧道是一个设备,需要建立device数据结构并注册。这部分可以作为模块。
1、 注册设备。
2、 在inet_protos[] hash 表中增加表示IPIP协议的inet_protocol数据结构。
九、 如果定义了GRE over IPv4 tunneling driver,则初始化它:ipgre_init()
在linux/net/ipv4/ip_gre.c
GRE是一个设备,需要建立device数据结构并注册。这部分可以作为模块。
1、 注册设备。
2、 在inet_protos[] hash 表中增加表示GRE协议的inet_protocol数据结构。
十、 如果定义了IP防火墙,则初始化防火墙:ip_fw_init()
在linux/net/ipv4/ip_fw.c
1、 初始化几个chain :ip_init_chain()。
2、 注册该防火墙操作。
struct firewall_ops
{
struct firewall_ops *next;
int (*fw_forward)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_input)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_output)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
/* Data falling in the second 486 cache line isn't used directly
during a firewall call and scan, only by insert/delete and other
unusual cases
*/
int fw_pf; /* Protocol family */
int fw_priority; /* Priority of chosen firewalls */
};
注册的防火墙操作为:
struct firewall_ops ipfw_ops=
{
NULL,
ipfw_forward_check,
ipfw_input_check,
ipfw_output_check,
PF_INET,
0 /* We don't even allow a fall through so we are last */
};
3、 注册防火墙对应的/PROC文件系统操作。
十一、 如果定义了IP伪装(ip masquerading),则初始化它:ip_masq_init()
定义在linux/net/ipv4/ip_masq.c
十二、 如果定义了多点传送路由,则初始化它:ip_mr_init()
在linux/net/ipv4/ipmr.c
十三、 注册有关的/PROC操作
10.5 The INET Socket Layer
整个Socket系统只提供了一个系统调用接口:sys_socketcall,在sys_call_table中只占一项。该函数的定义如下:
asmlinkage int sys_socketcall(int call, unsigned long *args)
这里:call是Socket系统调用号,args是给该调用的参数。
Linux向用户提供的系统调用包括:
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
所以,Socket接口大致包含17个操作。对这些操作的调用有一个大致的顺序,如下图所示。
函数sys_socketcall的定义非常简单,它根据参数call的值,分别调用相应的处理函数。具体的工作在处理函数中实现。
参见include/linux/net.h,linux/net/socket.c
描述BSD Socket接口的是一个数据结构,其定义如下:
struct socket
{
socket_state state;
unsigned long flags;
struct proto_ops *ops;
struct inode *inode;
struct fasync_struct *fasync_list; /* Asynchronous wake up list */
struct file *file; /* File back pointer for gc */
struct sock *sk;
struct wait_queue *wait;
short type;
unsigned char passcred;
unsigned char tli;
};
其中:
struct proto_ops {
int family;
int (*dup)(struct socket *newsock, struct socket *oldsock);
int (*release)(struct socket *sock, struct socket *peer);
int (*bind)(struct socket *sock, struct sockaddr *umyaddr,
int sockaddr_len);
int (*connect)(struct socket *sock, struct sockaddr *uservaddr,
int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1, struct socket *sock2);
int (*accept)(struct socket *sock, struct socket *newsock,
int flags);
int (*getname)(struct socket *sock, struct sockaddr *uaddr,
int *usockaddr_len, int peer);
unsigned int (*poll)(struct file *file, struct socket *sock,
struct poll_table_struct *wait);
int (*ioctl) (struct socket *sock, unsigned int cmd,
unsigned long arg);
int (*listen)(struct socket *sock, int len);
int (*shutdown)(struct socket *sock, int flags);
int (*setsockopt)(struct socket *sock, int level, int optname,
char *optval, int optlen);
int (*getsockopt)(struct socket *sock, int level, int optname,
char *optval, int *optlen);
int (*fcntl) (struct socket *sock, unsigned int cmd, unsigned long arg);
int (*sendmsg)(struct socket *sock, struct msghdr *m, int total_len,
struct scm_cookie *scm);
int (*recvmsg)(struct socket *sock, struct msghdr *m, int total_len,
int flags, struct scm_cookie *scm);
};
但该结构并不独立存在,实际上,它是inode结构的一部分(inode结构中描述具体文件系统信息的联合中的一个成员)。因此分配socket结构实际是分配一个inode。也就是说,BSD socket实际是VFS的一个具体实现,虽然BSD socket本身也是虚拟的,也需要具体的协议实现(如INET协议)对它的支持。所以对socket的操作与文件系统操作是一致的。
INET socket层支持包含TCP/IP协议的internet address family。
在socket初始化时,BSD socket的各个支持协议都向BSD socket层登记其address family,即在数组net_families[]登录其net_proto_family数据结构。该数据结构中包含有协议族编号和一个socket创建函数。当要创建一个具体的socket时,系统创建一个socket数据结构(实际是inode数据结构),但该结构中不包含具体协议的细节。一个具体协议的细节信息保存在协议专门的数据结构中,如INET socket层使用它自己的数据结构sock。为了访问的方便,两个数据结构中都有指针互相指向对方。
Sock数据结构是一个很大的结构,其定义如下:
struct sock {
/* This must be first. */
struct sock *sklist_next;
struct sock *sklist_prev;
/* Local port binding hash linkage. */
struct sock *bind_next;
struct sock **bind_pprev;
/* Socket demultiplex comparisons on incoming packets. */
__u32 daddr; /* Foreign IPv4 addr */
__u32 rcv_saddr; /* Bound local IPv4 addr */
__u16 dport; /* Destination port */
unsigned short num; /* Local port */
int bound_dev_if; /* Bound device index if != 0 */
/* Main hash linkage for various protocol lookup tables. */
struct sock *next;
struct sock **pprev;
volatile unsigned char state, /* Connection state */
zapped; /* In ax25 & ipx means not linked */
__u16 sport; /* Source port */
unsigned short family; /* Address family */
unsigned char reuse, /* SO_REUSEADDR setting */
nonagle; /* Disable Nagle algorithm? */
atomic_t sock_readers; /* User count */
int rcvbuf; /* Size of receive buffer in bytes*/
struct wait_queue **sleep; /* Sock wait queue */
struct dst_entry *dst_cache; /* Destination cache */
atomic_t rmem_alloc; /* Receive queue bytes committed */
struct sk_buff_head receive_queue; /* Incoming packets */
atomic_t wmem_alloc; /* Transmit queue bytes committed*/
struct sk_buff_head write_queue; /* Packet sending queue */
atomic_t omem_alloc; /* "o" is "option" or "other" */
__u32 saddr; /* Sending source */
unsigned int allocation; /* Allocation mode */
int sndbuf; /* Size of send buffer in bytes*/
struct sock *prev;
/* Not all are volatile, but some are, so we might as well say they
* all are. XXX Make this a flag word -DaveM
*/
volatile char dead,
done,
urginline,
keepopen,
linger,
destroy,
no_check,
broadcast,
bsdism;
unsigned char debug;
int proc;
unsigned long lingertime;
int hashent;
struct sock *pair;
/* Error and backlog packet queues, rarely used. */
struct sk_buff_head back_log, error_queue;
struct proto *prot;
unsigned short shutdown;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
union {
struct ipv6_pinfo af_inet6;
} net_pinfo;
#endif
union {
struct tcp_opt af_tcp;
#if defined(CONFIG_INET) || defined (CONFIG_INET_MODULE)
struct raw_opt tp_raw4;
#endif
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct raw6_opt tp_raw;
#endif /* CONFIG_IPV6 */
#if defined(CONFIG_SPX) || defined (CONFIG_SPX_MODULE)
struct spx_opt af_spx;
#endif /* CONFIG_SPX */
} tp_pinfo;
int err, err_soft; /* Soft holds errors that don't
* cause failure but are the cause
* of a persistent failure not just
* 'timed out' */
unsigned short ack_backlog;
unsigned short max_ack_backlog;
__u32 priority;
unsigned short type;
unsigned char localroute; /* Route locally only */
unsigned char protocol;
struct ucred peercred;
#ifdef CONFIG_FILTER
/* Socket Filtering Instructions */
struct sk_filter *filter;
#endif /* CONFIG_FILTER */
/* This is where all the private (optional) areas that don't
* overlap will eventually live.
*/
union {
void *destruct_hook;
struct unix_opt af_unix;
#if defined(CONFIG_ATALK) || defined(CONFIG_ATALK_MODULE)
struct atalk_sock af_at;
#endif
#if defined(CONFIG_IPX) || defined(CONFIG_IPX_MODULE)
struct ipx_opt af_ipx;
#endif
#if defined (CONFIG_DECNET) || defined(CONFIG_DECNET_MODULE)
struct dn_scp dn;
#endif
#if defined (CONFIG_PACKET) || defined(CONFIG_PACKET_MODULE)
struct packet_opt *af_packet;
#endif
#if defined(CONFIG_X25) || defined(CONFIG_X25_MODULE)
x25_cb *x25;
#endif
#if defined(CONFIG_AX25) || defined(CONFIG_AX25_MODULE)
ax25_cb *ax25;
#endif
#if defined(CONFIG_NETROM) || defined(CONFIG_NETROM_MODULE)
nr_cb *nr;
#endif
#if defined(CONFIG_ROSE) || defined(CONFIG_ROSE_MODULE)
rose_cb *rose;
#endif
#ifdef CONFIG_NETLINK
struct netlink_opt af_netlink;
#endif
#if defined(CONFIG_ECONET) || defined(CONFIG_ECONET_MODULE)
struct econet_opt *af_econet;
#endif
#if defined(CONFIG_IRDA) || defined(CONFIG_IRDA_MODULE)
struct irda_sock *irda;
#endif
} protinfo;
/* IP 'private area' or will be eventually. */
int ip_ttl; /* TTL setting */
int ip_tos; /* TOS */
unsigned ip_cmsg_flags;
struct ip_options *opt;
unsigned char ip_hdrincl; /* Include headers ? */
__u8 ip_mc_ttl; /* Multicasting TTL */
__u8 ip_mc_loop; /* Loopback */
__u8 ip_recverr;
__u8 ip_pmtudisc;
int ip_mc_index; /* Multicast device index */
__u32 ip_mc_addr;
struct ip_mc_socklist *ip_mc_list; /* Group array */
/* This part is used for the timeout functions (timer.c). */
int timeout; /* What are we waiting for? */
struct timer_list timer; /* This is the sock cleanup timer. */
struct timeval stamp;
/* Identd */
struct socket *socket;
/* RPC layer private data */
void *user_data;
/* Callbacks */
void (*state_change)(struct sock *sk);
void (*data_ready)(struct sock *sk,int bytes);
void (*write_space)(struct sock *sk);
void (*error_report)(struct sock *sk);
int (*backlog_rcv)(struct sock *sk, struct sk_buff *skb);
void (*destruct)(struct sock *sk);
};
struct proto {
/* These must be first. */
struct sock *sklist_next;
struct sock *sklist_prev;
void (*close)(struct sock *sk, long timeout);
int (*connect)(struct sock *sk,struct sockaddr *uaddr,
int addr_len);
struct sock * (*accept) (struct sock *sk, int flags);
void (*retransmit)(struct sock *sk, int all);
void (*write_wakeup)(struct sock *sk);
void (*read_wakeup)(struct sock *sk);
unsigned int (*poll)(struct file * file, struct socket *sock,
struct poll_table_struct *wait);
int (*ioctl)(struct sock *sk, int cmd, unsigned long arg);
int (*init)(struct sock *sk);
int (*destroy)(struct sock *sk);
void (*shutdown)(struct sock *sk, int how);
int (*setsockopt)(struct sock *sk, int level,
int optname, char *optval, int optlen);
int (*getsockopt)(struct sock *sk, int level,
int optname, char *optval, int *option);
int (*sendmsg)(struct sock *sk, struct msghdr *msg,int len);
int (*recvmsg)(struct sock *sk, struct msghdr *msg,
int len, int noblock, int flags, int *addr_len);
int (*bind)(struct sock *sk, struct sockaddr *uaddr,
int addr_len);
int (*backlog_rcv) (struct sock *sk, struct sk_buff *skb);
/* Keeping track of sk's, looking them up, and port selection methods.
*/
void (*hash)(struct sock *sk);
void (*unhash)(struct sock *sk);
void (*rehash)(struct sock *sk);
unsigned short (*good_socknum)(void);
int (*verify_bind)(struct sock *sk, unsigned short snum);
unsigned short max_header;
unsigned long retransmits;
char name[32];
int inuse, highestinuse;
};
数据结构的关系如下图所示:
在上述结构中有三个主要的操作集:
1、 在file结构中的文件操作集,这是一个file_operations数据结构。BSD Socket对该操作集的实现是socket_file_ops。这组文件操作集由定义在socket上的proto_ops操作集实现。
2、 在socket结构中的协议操作集,这是一个proto_ops数据结构。INET协议对它有两种实现:inet_stream_ops和inet_dgram_ops。这两个操作集中的函数又由sock结构上的操作集proto实现。
3、 在sock结构中的协议操作集,这是一个proto数据结构。传输层对它的实现有tcp_prot、udp_prot、raw_prot。
4、 传输层和网络层(即TCP/IP)与网络设备驱动程序之间的接口是device数据结构。其中定义了网络设备驱动程序需要实现的函数集。各种网络设备对device中的函数集有不同的实现。
5、 而每个底层协议都定义一个struct inet_protocol数据结构,用于接收并处理来自底层的该协议的数据包。
在建立一个socket时,至少要确定上述三个操作集。下面分别讨论socket的系统调用。
10.5.1 创建一个BSD Socket(Creating a BSD Socket)
创建新socket的系统调用是sys_socket,其定义如下:
int sys_socket(int family, int type, int protocol)
该函数需要三个参数:地址族标识符、socket类型和协议。
该函数所做工作如下:
1、 检查协议族是否在1到NPROTO范围内,不是则报错返回。
2、 如果协议族没有注册(net_families[family]==NULL)并且内核支持模块,则请求模块装入进程装入指定的协议族,并初始化它。
request_module(module_name),其中module_name的格式为net-pf-%d,%d的内容是协议族的标识符。
如果装入进程执行后,协议族仍然没有注册,则出错返回。
3、 检查类型type的值是否正确,即其值是否为Linux支持的BSD Socket类型。这些类型包括:SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW、SOCK_RDM、SOCK_PACKET、SOCK_WEB。支持SOCK_PACKET的是专门的一个地址族:AF_PACKET(Packet family),该地址族与AF_INET并列。
4、 申请一个socket数据结构并填入相应的值。
由于socket是inode数据结构的一部分,所以申请一个socket实际上就是申请一个空白inode。如果申请inode成功,其socket结构为:
inode->u.socket_i
设置inode的相应域:i_mode域为socket,存取权限为完全许可(0777);i_sock域为1;i_uid和i_gid为当前进程的fsuid和fsgid。
设置socket的相应域:
sock->inode = inode;
init_waitqueue(&sock->wait);
sock->fasync_list = NULL;
sock->state = SS_UNCONNECTED; /* 当前状态为未连接 */
sock->flags = 0;
sock->ops = NULL;
sock->sk = NULL;
sock->file = NULL;
sock->type = type;
5、 调用协议族相关的socket创建函数。
协议族相关的socket创建函数在描述该协议族的net_proto_family数据结构中,而该net_proto_family数据结构已经注册到了net_families[]数组中。以family为下标,查找数组net_families[]即可找到指定协议族的socket创建函数。执行该函数完成与具体协议相关的初始化工作。
显然,不同的协议族其socket创建函数是不同的。INET协议族的socket创建函数是inet_create(在linux/net/ipv4/af_inet.c中),该函数的定义如下:
static int inet_create(struct socket *sock, int protocol)
其中:sock是前面已分配的socket数据结构;protocol是协议编号。
该函数所做工作如下:
1) 如果socket的类型是SOCK_PACKET,而且该协议族还没有注册,则申请插入该协议族对应的模块。而后调用协议族SOCK_PACKET的socket创建函数。
2) 将socket的状态改为SS_UNCONNECTED。
3) 申请sock数据结构。
在初始化时,已经为sock数据结构建立的slab cache,从中申请一个sock数据结构。将申请到的sock结构清0。
sk->family = family;
4) 根据socket的类型分别处理:
? SOCK_STREAM:支持该类型的协议必须是TCP。
protocol = IPPROTO_TCP;
prot = &tcp_prot;
sock->ops = &inet_stream_ops;
? SOCK_SEQPACKET:
未实现。
? SOCK_DGRAM:
protocol = IPPROTO_UDP;
prot=&udp_prot;
sock->ops = &inet_dgram_ops;
? SOCK_RAW:
prot = &raw_prot;
sock->ops = &inet_dgram_ops;
5) 初始化socket和sock数据结构的域。由函数sock_init_data(在net/core/sock.c)完成。包括:
sk->state = TCP_CLOSE;
以及sock数据结构中的几个处理函数,如state_change、data_ready、write_space、error_report、destruct等。
6) 初始化sock的其余各域。如:
sk->family = PF_INET;
sk->protocol = protocol;
sk->prot = prot;
…………………………
6)如果在sk->prot中定义了初始化函数init,则执行该函数。对TCP协议来说,其初始化函数是tcp_prot-> tcp_v4_init_sock。
6、 在当前进程的文件描述符表current->files->fd[]中找一个空闲的文件描述符,作上相应的标记,表示它已被分配。
7、 找一个空闲的file数据结构,初始化其中的各域,将其填入当前进程的文件描述符表中。
file->f_op = &socket_file_ops;
file->f_mode = 3;
file->f_flags = O_RDWR;
file->f_pos = 0;
socket是对VFS的一种实现,因此socket也要实现文件操作集file_operations,这个实现就是socket_file_ops。对所有的socket,该文件操作集都是一样的。有了这组对文件操作集的实现,就可以将对socket的操作看成普通的文件操作了。
8、 sock->file = fcheck(retval); 该指针指向该打开socket对应的file数据结构。
9、 返回文件描述符,以后即可利用该文件描述符和普通的文件操作来处理socket通信了。
10.5.2 为一个INET BSD socket绑定一个地址(Binding an Address to an INET BSD Socket)
为了监听进来的网际连接请求,每一个服务器都必须创建一个INET BSD socket并把自己的地址绑定到它上面。绑定IP地址的系统调用是sys_bind,定义在(net/socket.c中)。其定义为:
int sys_bind(int fd, struct sockaddr *umyaddr, int addrlen)
这里:fd是打开的socket的文件描述符;umyaddr是一个socket地址,其定义如下:
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
addrlen是该地址的长度。
该函数所做的工作如下:
1、 根据socket的文件描述符fd,找到它对应的file、inode、socket数据结构。
2、 将socket地址从用户空间拷贝到内核。
3、 执行sock->ops->bind (sock, (struct sockaddr *)address, addrlen);
因此,bind的主要工作在INET socket 层完成,并需要底层的TCP和UDP协议层提供一些支持。一个已经绑定了地址的socket不能再用于其它通讯。通常,绑定的地址是分配给支持INET地址族的网络设备的地址,而且接口必须是开启的并能够使用。
INET socket 层完成bind工作的函数是inet_bind(定义在net/ipv4/af_inet.c中),该函数的定义如下:
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
该函数所做工作如下:
1、 将socket地址转化为Internet地址。Uaddr是一个由16个字节组成的socket地址,它不带格式,适用于所有的地址族。在使用时,要根据地址族将这个地址转化成相应格式的地址,如此处将该地址转化为Internet地址。一个Internet地址是一个数据结构,其定义如下:
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
2、 如果sock目前的状态不是TCP_CLOSE则错误返回。
3、 如果addr_len过短或该sock已经指定了端口号(num!=0)则错误返回。
4、 调用函数inet_addr_type(定义在net/ipv4/fib_frontend.c)确定地址类型。
? 前8位为0的IP地址是广播地址RTN_BROADCAST;
? 前4位为F的IP地址是广播地址RTN_BROADCAST;
? 前4位为E的IP地址是多目地址RTN_MULTICAST;
? 如果local_table不空,则调用它的tb_lookup函数确定IP地址的类型。
5、 检查地址及其类型的合法性。要么地址为0,要么地址类型为RTN_LOCAL、RTN_MULTICAST、RTN_BROADCAST、RTN_UNICAST,否则错误返回。
6、 将绑定的IP地址保存在sock数据结构中。在sock中保存两个地址:
rcv_saddr用于hash lookups,此处的地址就是要绑定的地址。
saddr用于transmit,此处的地址一般情况下与rcv_saddr相同,但对多目地址和广播地址,saddr为0。
7、 取出绑定地址的端口号。如果该端口号为0,则调用sock操作集中的函数good_socknum找一个空闲的端口号。对TCP协议,该函数是tcp_good_socknum。如果找到的端口号prot->rehash(sk);
add_to_prot_sklist(sk);
dst_release(sk->dst_cache);
sk->dst_cache=NULL;
当底层的网络设备接收到报文时,这些报文必须被转到正确的INET和BSD socket处处理。为此,UDP和TCP都维护一个hash table,用于查找进来的IP信息的地址,从而把它们转到正确的socket/sock对。TCP是一个面向连接的协议,所以正处理的TCP报文中要比正处理的UDP报文中包含更多的信息。
UDP维护一个已分配的UDP端口的hash table:udp_table。该表用一个基于端口号的hash函数作索引,包含一个指向sock数据结构的指针。因为UDP hash table比允许的端口号要小得多(udp_hash只有128,或UDP_HTABLE_SIZE),所以表中的一些条目指向的是一个sock数据结构链表,该链表用每一个sock的next 指针连接在一起。
TCP更加复杂,因为它维护了几个hash table 。但是,在绑定操作中,TCP实际上并不把绑定的sock数据结构加到它的hash table中,它只是检查请求的端口当前没有被使用。在listen操作中,sock数据结构才加到TCP的hash table中。
10.5.3 Listening on an INET BSD Socket
一旦一个socket拥有了一个绑定的地址,它就可以监听进来的连接请求。如果进来的连接请求指定了这个绑定地址,该socket就会响应。一个网络应用程序也可以不绑定地址而直接在一个socket上监听,这种情况下,INET socket层会找到一个未用的端口号(对于这种协议而言),并自动把它绑定到这个socket上。
socket的listen函数把socket变成TCP_LISTEN的状态,并且执行所需的和网络相关的工作,以便允许进来的连接。
对于UDP socket,改变socket的状态已经足够,但是TCP要把socket的sock数据结构加到它的两个hash table中,原因是该socket已经激活。这两个hash表分别是tcp_bound_hash和tcp_listening_hash 表。这两个表都通过一个基于IP端口号的hash函数进行索引。
完成listen功能的是函数sys_listen,其定义如下:
int sys_listen(int fd, int backlog)
其中backlog是请求队列的长度,listen以此参数限制排队请求的个数,Linux允许的最大个数是128(SOMAXCONN)。
该函数所做工作如下:
1、 根据socket对应文件描述符fd,找到该socket对应的file、inode、socket数据结构。
2、 调用socket协议操作集上的listen函数完成真正的listen工作。
3、 返回。
所以真正的listen在协议操作集中完成。INET提供的listen函数是inet_listen。该函数的定义如下:
int inet_listen(struct socket *sock, int backlog)
他所完成的工作如下:
1、 找到与该socket关联的sock数据结构。
2、 如果socket的状态不是SS_UNCONNECTED或socket的类型不是SOCK_STREAM,则出错返回。
3、 如果sock结构上没有绑定端口(sock->num==0),则调用sock操作集中的函数good_socknum为其找一个空闲的端口号。如果还不能成功,则错误返回。
4、 设置sock的max_ack_backlog域的值为backlog(参数)。
5、 如果sock的状态不是TCP_LISTEN,则:
sk->ack_backlog = 0;
sk->state = TCP_LISTEN;
dst_release(xchg(&sk->dst_cache, NULL));
sk->prot->rehash(sk);
add_to_prot_sklist(sk);
6、 socket->flags |= SO_ACCEPTCON;
7、 返回。
10.5.4 Making a Connection to an INET BSD Socket
一旦创建了一个socket,如果它没有被用于监听进来的连接请求,它就可以用于建立向外的连接请求。对于无连接的协议,比如UDP,这个socket操作不需要做许多,但是对于面向连接的协议,如TCP,则需要在两个应用程序之间建立一个虚拟电路。
一个向外的连接只能在一个正确状态的INET BSD socket上进行:就是说,在该socket上还没有建立起连接,而且该socket也没有用于监听进来的连接。这意味着这个BSD socket数据结构必须在SS_UNCONNECTED状态。
完成连接的系统调用是函数sys_connect,其定义为:
int sys_connect(int fd, struct sockaddr *uservaddr, int addrlen)
该函数完成的工作如下:
1、 根据socket的文件描述符fd,找到它对应的file、inode、socket数据结构。
2、 将地址uservaddr由用户空间拷贝到内核空间。
3、 调用socket操作集上的connect函数完成真正的连接工作。
不同的协议族对connect函数的实现是不同的,甚至同一个协议族对它的实现也不尽相同。如INET协议族对它的实现就有两种:inet_stream_connect和inet_dgram_connect。
inet_stream_connect的定义如下:
int inet_stream_connect(struct socket *sock, struct sockaddr * uaddr,
int addr_len, int flags)
它所完成的工作如下:
1、 找出socket对应的sock数据结构。
2、 如果socket的状态不是SS_UNCONNECTED而且不是SS_CONNECTING则错误返回。
3、 如果socket的状态是SS_CONNECTING 。
如果sock的状态是TCP_ESTABLISHED、TCP_CLOSE_WAIT、TCP_FIN_WAIT1、TCP_FIN_WAIT2、TCP_SYN_RECV中之一,则将socket的状态改为SS_CONNECTED,成功返回。
if (sk->zapped || sk->err)
goto sock_error;
if (flags & O_NONBLOCK)
return -EALREADY;
4、 如果socket的状态不是SS_CONNECTING,即如果socket的状态是SS_UNCONNECTED。
如果该socket上还没有绑定端口(num==0),则为其找一个空闲端口。如果还不能成功,则错误返回。
调用sock操作集中的connect函数。如果成功,则将socket的状态改为SS_CONNECTING;否则,错误返回。
5、 如果sock的状态是TCP_TIME_WAIT、TCP_CLOSE、TCP_CLOSE_WAIT、TCP_LAST_ACK、TCP_LISTEN、TCP_CLOSING且socket的状态是SS_CONNECTING,则错误返回。因为,当socket的状态是SS_CONNECTING时,sock不应该出现上述状态。
6、 如果sock的状态不是TCP_ESTABLISHED,而且此次连接建立不许阻塞(flags & O_NONBLOCK不等于0),则返回(-EINPROGRESS )。
7、 如果sock的状态是TCP_SYN_SENT或TCP_SYN_RECV,则调用函数inet_wait_for_connect(sk),将当前进程挂起(将当前进程的状态改为TASK_INTERRUPTIBLE,重新调度)。
8、 将socket的状态改为SS_CONNECTED。成功返回。
9、 如果因为各种原因导致此次连接不能成功,则将sock的状态改为TCP_CLOSE,将socket的状态改为SS_UNCONNECTED,并释放必要的资源,而后返回错误代码。
显然,主要的连接工作是在sock操作集的connect函数函数中完成的。TCP完成该任务的函数是tcp_v4_connect,UDP完成该任务的函数是udp_connect。
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.5.5 Accepting Connection Requests
UDP不支持连接的概念,接受对INET socket的连接请求只适用于TCP协议。在一个监听的socket上进行接受(accept)操作会从原来的监听的socket克隆出一个新的socket数据结构。然后,这个accept操作被传递给支撑的协议层,在这种情况下,是INET去接受任何进来的连接请求。如果底层的协议,比如UDP,不支持连接,INET协议层的accept操作会失败。否则,accept操作会通过真正的协议(在这里是TCP)传递。
accept操作可能是阻塞,也可能是非阻塞的。在非阻塞的情况下,如果没有可接受(accept)的进来的连接,这个accept操作会失败,而新创建的socket数据结构将会被废弃。在阻塞的情况下,执行accept操作的网络应用程序会被加到等待队列,然后挂起,直到接收到一个TCP的连接请求。
不论何时,当接收到一个进来的、对于激活的、正在监听的socket的TCP连接请求时,TCP都要建立一个新的sock数据结构来表示它。当连接最终被接受时,该sock数据结构将成为这个TCP连接的buttom half。它也克隆包含连接请求的、进来的sk_buff,并把该sk_buff排在监听的、sock数据结构的receive_queue队列中(receive_queue是sock数据结构的一个域)。这个克隆的sk_buff包括一个指针,指向这个新创建的sock数据结构。
一旦接收到一个连接请求,包含这个请求的sk_buff会被废弃,这个sock数据结构被返回到INET socket层,在这里它被连接到先前创建的新的socket数据结构。这个新的socket的文件描述符(fd)被返回给网络应用程序,应用程序就可以用这个文件描述符对这个新创建的INET BSD socket进行socket操作。
实现accept操作的是系统调用sys_accept,其定义如下:
int sys_accept(int fd, struct sockaddr *upeer_sockaddr, int *upeer_addrlen)
它所做的工作如下:
1、 根据socket的文件描述符fd,找到它对应的file、inode、socket数据结构。
2、 申请一个新的socket(inode)。
3、 新socket的类型(type)等于老socket的类型。
4、 调用socket操作集中的dup函数,复制一个socket。INET中完成该操作的函数是sock_no_dup,实际是调用协议集的create函数重新创建一个socket。
5、 调用新socket操作集中的accept操作。
6、 创建新的file数据结构,填写其内容(如文件操作集是socket_file_ops),将其插入到当前进程的files数组中。
7、 如果参数upeer_sockaddr不空,则:
调用新socket操作集中的getname函数,获得本次连接的目的地址和端口号(远程主机的地址和端口号)。
? 如果函数getname执行不成功(连接还没有建立),则关掉新建立的socket,转2,重新开始。
? 如果函数getname执行成功(连接已经建立),则将获得的地址拷贝到用户地址空间,即拷贝到upeer_sockaddr中。返回新socket的文件描述符fd。
显然,accept的主要工作在第5步完成。INET对该函数有两个实现:inet_accept和sock_no_accept,前者用于stream类型,后者用于dgram类型。
inet_accept的定义如下:
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
其中:sock是老socket,newsock是新socket,flags是老socket对应的file数据结构中的f_flags标志。
它完成以下工作:
1、 根据新、老socket数据结构,分别找到它们对应的sock数据结构。新的sock数据结构也是刚刚创建的。
2、 如果老socket的状态不是SS_UNCONNECTED,或老socket结构的flags 标志中没有定义SO_ACCEPTCON,则错误返回。listen操作设置flags的SO_ACCEPTCON标志位,因此,如果一个socket没有设置此标志,表示它没有执行过listen操作,所以不能在其上accept。
3、 如果老sock的pair域(一个执行sock结构的指针)不空,暂存该域的值到变量sk2中,将老sock的pair域清空。
否则,即如果老sock的pair域为空,则执行老sock的操作集中的accept操作,完成accept动作。TCP中实现的accept函数是tcp_accept,UDP中没有实现accept函数。accept函数返回一个sock数据结构,将该结构暂存到变量sk2中。
4、 新socket的sock结构就是sk2所指的sock数据结构,在新socket和新sock数据结构之间建立连接。
5、 如果参数flags中指明O_NONBLOCK,则释放原来为新socket创建的sock数据结构。成功返回。
6、 如果新sock的状态是TCP_ESTABLISHED,则释放原来为新socket创建的sock数据结构,将新socket的状态改为SS_CONNECTED。成功返回。
7、 如果其间出现了错误,或sk2所指的sock的状态为TCP_CLOSE,则释放sk2所指的sock数据结构,错误返回。
真正的accept动作在第3步中由sock操作集中的accept函数完成。
TCP中完成accept动作的函数是tcp_accept(定义在net/ipv4/tcp.c中)。该函数的定义为struct sock *tcp_accept(struct sock *sk, int flags),它所完成的工作如下:
1、 根据sock *sk找到它对应的tcp_opt数据结构:sk->tp_pinfo.af_tcp。
2、 如果sock的状态不是TCP_LISTEN,则出错返回。
3、 在tcp_opt的syn_wait_queue队列上排列的是所有到来的TCP请求,查找该队列,看是否有连接请求(sock状态为TCP_SYN_SENT或TCP_SYN_RECV的请求)。每个请求都用一个数据结构open_request表示,其中有个指针指向sock数据结构。
4、 如果没有找到请求,而且参数flags表示不能阻塞(O_NONBLOCK),则返回。
5、 如果没有找到请求,而且参数flags表示可以阻塞,则调用函数wait_for_connect,将当前进程挂起,等待连接请求的到来。
6、 接到连接请求,从中取出sock数据结构,将该请求从其队列中摘下,释放它的数据结构open_request。
7、 返回从连接请求中摘下的sock数据结构。
10.5.6 Shutdown a socket
当socket不再使用时,应该将其关掉,完成该项工作的系统调用是sys_shutdown,其定义如下:
int sys_shutdown(int fd, int how)
其中:how是关闭的方式,共有两种:RCV_SHUTDOWN和SEND_SHUTDOWN。
该函数完成如下工作:
1、 根据参数fd,找到要关掉的socket的file、inode、socket数据结构。
2、 调用socket操作集中的shutdown函数。
INET注册的shutdown函数是inet_shutdown,该函数的定义如下:
int inet_shutdown(struct socket *sock, int how)
该函数完成如下工作:
1、 根据socket参数找到与之关联的sock数据结构。
2、 检查参数how的合法性。
3、 如果socket的状态是SS_CONNECTING并且sock的状态是TCP_ESTABLISHED,则将socket的状态改为SS_CONNECTED。
4、 如果sock为空或sock的状态表示还没有在其上建立连接,则出错返回。
5、 执行sock操作集中的shutdown函数。
6、 执行sock结构上的state_change函数。
7、 成功返回。
TCP注册的用于完成shutdown的函数是tcp_shutdown,该函数的定义如下:
void tcp_shutdown(struct sock *sk, int how)
它完成如下工作:
1、 如果参数how中没有指明SEND_SHUTDOW,则说明不需要向对方发送信息,因此,简单返回。
2、 如果sock的状态是TCP_ESTABLISHED、TCP_SYN_SENT、TCP_SYN_RECV、TCP_CLOSE_WAIT,则向对方发送一个FIN包。
3、 释放sock数据结构。
10.5.7 Read data from a socket
两个socket建立起来连接以后,就可以在其上发送和接收数据包。BSD Socket对数据包的发送和接收采用的方法与操作普通文件相同,即对socket描述符fd的读写操作。在socket建立时,要生成一个file数据结构,在该结构中注册的文件操作集是socket_file_ops,其定义为:
struct file_operations socket_file_ops = {
sock_lseek,
sock_read,
sock_write,
NULL, /* readdir */
sock_poll,
sock_ioctl,
NULL, /* mmap */
sock_no_open, /* special open code to disallow open via /proc */
NULL, /* flush */
sock_close,
NULL, /* no fsync */
sock_fasync
};
以后在socket描述符上所使用的文件操作就都按上述操作集,定位到了socket函数上。Socket操作集中最重要的是它的读和写函数,这两个函数完成socket数据包的发送和接收。
完成socket读操作的是函数sock_read,其定义如下:
ssize_t sock_read(struct file *file, char *ubuf, size_t size, loff_t *ppos)
它所做的工作如下:
1、 检查参数的合法性(包括size和ppos)。
2、 根据参数file找到它对应的socket数据结构。
3、 填写一个msghdr数据结构,用于信息的接收。Msghdr数据结构的定义如下:
struct msghdr {
void * msg_name; /* Socket name */
int msg_namelen; /* Length of name */
struct iovec * msg_iov; /* Data blocks */
__kernel_size_t msg_iovlen; /* Number of blocks */
void * msg_control; /* Per protocol magic (eg BSD file
* descriptor passing) */
__kernel_size_t msg_controllen; /* Length of cmsg list */
unsigned msg_flags;
};
struct iovec
{
void *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
对消息头msghdr的各个域赋如下初值:
msg.msg_name=NULL;
msg.msg_namelen=0;
msg.msg_iov=&iov;
msg.msg_iovlen=1;
msg.msg_control=NULL;
msg.msg_controllen=0;
iov.iov_base=ubuf;
iov.iov_len=size;
4、 调用函数sock_recvmsg,从底层接收一个消息,其返回值是实际接收的消息长度。
显然,完成socket读操作的主要是函数sock_recvmsg。该函数定义在net/socket.c中,其定义如下:
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int size, int flags)
该函数完成如下工作:
1、定义一个scm_cookie数据结构(Socket level control messages),该数据结构的定义为(在include/net/scm.h):
struct scm_cookie
{
struct ucred creds; /* Skb credentials */
struct scm_fp_list *fp; /* Passed files */
unsigned long seq; /* Connection seqno */
};
其中:
struct scm_fp_list
{
int count;
struct file *fp[SCM_MAX_FD];
};
struct ucred {
__u32 pid;
__u32 uid;
__u32 gid;
};
将该scm_cookie结构清空。
2、调用socket操作集上的recvmsg函数,从底层接收消息。其返回值是实际接收到的消息的长度。
3、 调用函数scm_recv对接收到的消息做进一步的处理。
因此,主要的接收工作在第二步中由socket操作集上的recvmsg函数完成。INET实现的recvmsg函数是inet_recvmsg。该函数的定义如下:
int inet_recvmsg(struct socket *sock, struct msghdr *msg, int size,
int flags, struct scm_cookie *scm)
1、
10.5 IP层(The IP Layer)
10.5.1 Socket Buffers
Linux将网络协议分成许多层,每一层都使用其它层提供的服务。但这样的网络协议会有一个问题:每一个协议都要在传送的时候在数据上增加协议头和尾,而在处理接收数据的时候删除协议头和尾。这使得在协议之间传送数据缓冲区相当困难,因为每一层都需要找出它的特定的协议头和尾在哪里。一个解决方法是在每一层都拷贝缓冲区,但是这样会非常低效。替代的,Linux使用socket 缓冲区或者说sk_buffs在协议层之间,或协议层与网络设备驱动程序之间传输数据。sk_buffs包括指针和长度域,允许每一协议层使用标准的函数或方法操纵应用程序数据。
struct sk_buff {
struct sk_buff * next; /* Next buffer in list */
struct sk_buff * prev; /* Previous buffer in list */
struct sk_buff_head * list; /* List we are on */
struct sock *sk; /* Socket we are owned by */
struct timeval stamp; /* Time we arrived */
struct device *dev; /* Device we arrived on/are leaving by */
/* Transport layer header */
union
{
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct spxhdr *spxh;
unsigned char *raw;
} h;
/* Network layer header */
union
{
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
struct ipxhdr *ipxh;
unsigned char *raw;
} nh;
/* Link layer header */
union
{
struct ethhdr *ethernet;
unsigned char *raw;
} mac;
struct dst_entry *dst;
char cb[48];
unsigned int len; /* Length of actual data */
unsigned int csum; /* Checksum */
volatile char used; /* Data moved to user and not MSG_PEEK */
unsigned char is_clone, /* We are a clone */
cloned, /* head may be cloned (check refcnt to be sure). */
pkt_type, /* Packet class */
pkt_bridged, /* Tracker for bridging */
ip_summed; /* Driver fed us an IP checksum */
__u32 priority; /* Packet queueing priority */
atomic_t users; /* User count - see datagram.c,tcp.c */
unsigned short protocol; /* Packet protocol from driver. */
unsigned short security; /* Security level of packet */
unsigned int truesize; /* Buffer size */
unsigned char *head; /* Head of buffer */
unsigned char *data; /* Data head pointer */
unsigned char *tail; /* Tail pointer */
unsigned char *end; /* End pointer */
void (*destructor)(struct sk_buff *); /* Destruct function */
#ifdef CONFIG_IP_FIREWALL
__u32 fwmark; /* Label made by fwchains, used by pktsched */
#endif
#if defined(CONFIG_SHAPER) || defined(CONFIG_SHAPER_MODULE)
__u32 shapelatency; /* Latency on frame */
__u32 shapeclock; /* Time it should go out */
__u32 shapelen; /* Frame length in clocks */
__u32 shapestamp; /* Stamp for shaper */
__u16 shapepend; /* Pending */
#endif
#if defined(CONFIG_HIPPI)
union{
__u32 ifield;
} private;
#endif
};
图10.4显示了sk_buff数据结构:每一个sk_buff都有和它关联的一块数据。sk_buff有四个数据指针,用于操纵和管理socket缓冲区的数据:
参见include/linux/skbuff.h
head 指向内存中的数据区的起始位置。它在sk_buff以及和它相关的数据块被分配的时候确定。
data 指向协议数据的当前起始位置。这个指针随着当前拥有这个sk_buff 的协议层的不同而变化。
tail 指向协议数据的当前结尾位置。同样,这个指针也随拥有这个sk_buff的协议层不同而变化。
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_buff和删除其中的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队列,确定接收到的报文需要传送到那个协议层。
当Linux网络层初始化的时候,每一个协议都登记自己:在ptype_all列表或者ptype_base hash table中增加一个packet_type的数据结构。packet_type的定义如下:
struct packet_type {
unsigned short type; /* This is really htons(ether_type). */
struct device * dev;
int (*func) (struct sk_buff *, struct device *,
struct packet_type *);
void *data;
struct packet_type *next;
};
这个packet_type数据结构包括协议类型、一个指向网络驱动设备的指针、一个指向协议的数据接收处理例程的指针和一个指向这个列表或者hash table中下一个packet_type数据类型的指针。ptype_all链表用于探听(snoop)从任意网络设备上接收到的所有的数据报文,通常不使用。ptype_base hash table 的定义如下:
struct packet_type *ptype_base[16];
ptype_base hash table使用协议标识符作hash值,用于确定进来的网络报文应该由哪一种协议接收。网络的bottom half把进来的sk_buff中的协议类型和上述任一表中的一个或多个packet_type条目进行匹配。协议可能会匹配一个或多个条目,例如当探听(snoop)所有的网络通信的时候,这时,这个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数据结构。
Rtable数据结构的定义如下:
struct rtable
{
struct rtable *rt_next;
__u32 rt_dst;
__u32 rt_src;
__u32 rt_gateway;
atomic_t rt_refcnt;
atomic_t rt_use;
unsigned long rt_window;
atomic_t rt_lastuse;
struct hh_cache *rt_hh;
struct device *rt_dev;
unsigned short rt_flags;
unsigned short rt_mtu;
unsigned short rt_irtt;
unsigned char rt_tos;
};
其中包括使用的源IP地址、网络device数据结构的地址,有时候还会有一个预先建立的硬件头。这个硬件头和网络设备相关,包含源和目的物理地址和其它同介质相关的信息。如果网络设备是以太网设备,硬件头如图10.1中所示,其中的源和目的地址会是物理的以太网地址。硬件头和路由缓存在一起,因为在这个路由传送的每一个IP报文都需要追加这个头,而建立这个头需要时间。硬件头可能包含必须使用ARP协议才能解析的物理地址。这时,发出的报文会暂停,直到地址解析成功。一旦硬件地址被解析,并建立了硬件头,这个硬件头就被缓存,这样以后使用这个接口的IP报文就不需要再进行ARP。
参见include/net/route.h
10.5.4 Data Fragmentation
每一个网络设备都有一个最大的报文尺寸,它无法传送或接收比最大的报文尺寸更大的数据报文。IP协议允许使用大报文,但为了适应网络设备的处理能力,IP协议会把大数据报分割成网络设备能处理的小数据报。IP协议头中有一个分片域,其中包含一个标记和一个分片偏移量。
当要传输一个IP报文的时候,IP查找用来发送IP报文的网络设备。通过IP路由表来查找这个设备。每一个设备(device数据结构)都有一个字段描述它的最大传输单元(字节)的大小,这个字段是mtu。如果设备的mtu比等待传送的IP报文的报文尺寸小,那么这个IP报文就必须被分割到更小的片段(mtu大小)。每一个片段用一个sk_buff代表:它的IP头标记了它是一个片段,以及这个IP报文包含的片段在整个数据中的偏移量。最后一个报文被标记为最后一个IP片段。如果在分割成片段的过程中,IP无法分配sk_buff,这次传送就会失败。
接收IP片段比发送更难,因为IP片段可能以任意顺序被接收,而且它们必须在重组之前全部接收到。每一次一个IP报文被接收的时候,都要检查看它是不是一个IP片段。收到一个消息的第一个片段时,IP就建立一个新的ipq数据结构,并将其连接到等待组装的IP片段的ipqueue列表中。当更多的IP片段到来时,找到正确的ipq数据结构并建立一个新的ipfrag数据结构%C
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/20375/showart_124496.html |
|