一个实现和修改Linux网络协议栈的指南
Glenn Herrin
TR 00-04
May 31, 2000
摘要
这篇文档是一个用于理解Linux内核(针对2.2.14版本)怎样实现网络协议的指导,主要关于互连网协议(IP)的内容。通过总结、描述运行过程、源代码解释和举例给实践人员一个完整的参考。第一部分是有关网络代码、数据结构和相关功能的深层分析。这是关于网络初始化,连接并建立套接字,接收、传输和转发数据包的章节。第二部分是修改内核代码和安装新模块的详细指导。这些章节有关于内核安装,模块使用方法介绍,与网络进程有关的proc文件系统和一个完整的例子。
目录(略)
第 1 章
1. 介绍
这篇文档的版本是1.0,2000年5月31日发表,参考Linux 2.2.14版本的内核。
1.1.背景
Linux成为一个替代操作系统正越来越流行起来。因为它是作为开源运动的一部分,对于每个人都是免费使用的,成千上万的程序员在坚持不懈地维护这个代码以实现新功能、改进已有代码、解决代码中的错误和无效率的代码。从源代码本身(可以从网络上下载)到订阅“HOW-TOs”和有许多不同主题的论坛,那儿有非常多的资源以供学习有关Linux知识。
该文档致力于把许多有关修改Linux内核中网络源代码的参考和指导资料集成在一起。从四个层次来展开:总体概述,更加详细地分析网络活动,详述函数运行过程,参考实际代码和数据结构。这对于读者的要求也许是非常详细也许是不详细的。这是特定针对Linux 2.2.14版本内核的(已有更高的版本如2.2.15),其中许多的例子来源于Red Hat 6.1发布的资料;希望这里提供的信息是全面足够的,在新发布的版本和新内核中仍然适用。该文档专注于TCP/UDP,IP,和以太网(Ethernet),这些是最普通的,但并不意味着Linux平台只有网络协议才有用。
作为内核编程者的参考,该文档包含了如修改和重编译内核、自行设计和安装模块、使用“/proc”文件系统的信息和方法。也有个程序例子能从选定的主机中截取数据包并分析结果。在描述和例子之间,应该可以回答许多关于Linux如何执行网络操作和如何修改代码以达到你的目的的问题。
这次创作始于新汉普(英)大学计算机科学系网络实验室在Linux内核上实验不同路由算法,实验过程中很快就明白盲目地修改内核不是个好的办法,所以写成这个文档以记录所做的研究和给未来程序员作为参考。这个文档最后变得很大(希望足够有用),于是我们决定推广它,完善它并向公众发布。
最后提示一下,Linux是个不断变化的系统并且要真正掌握它,如果要把当前的系统参考资料放在这个文档中要过很久的时间才能得到。如果你发现文档中有任何错误的描述、遗漏、严重错误,甚至是打印错误,可以向当前维护这个文档的人联系。这个文档的目的是建立一个自由可用和对程序员有用的参考。
1.2.文档约定
该文档假定读者懂得C语言和具有普通网络协议知识。要看懂它并不需要更多的知识,但该文档中的详细资料是针对有经验的程序员的,对于Linux一般用户也许是难以理解的。
里面几乎所有的代码需要使用超级用户的访问权限来实现。其中一些例子会产生以前不存在的安全漏洞;编程者在对内核代码实验后应当小心地把系统恢复到普通状态。
文件参考和程序名用斜体字来表示,代码、命令行和组织名称用一般形式来表示,变量(如输出文件名)和注释用斜体字来表示。
1.3.网络示范
文档中有许多例子来帮助阐明有关网络的内容。为了使得这些例子具有一致性和能够被熟悉,它们都参考图1.1中的网络示例。
这个网络表示一个虚构的某大学的计算机系统。网络中有一个路由器与因特网连接(上图中的chrysler)。路由器通过名为“jeep”的主机连接校园网,网址为“u.edu”,该校园网由名为拥有汽车公司的Chrysler的计算机组成(如dodge, eagle等)。那儿也有一个归属计算机科学系的位于“Dodge”主机后面的局域网子网,网址为“cs.u.edu”,该子网中有如名为“stealth”、“neon”等的主机,它们都通过“dodge/viper”计算机连接校园网。“u.edu”和“cs.u.edu”网络都是使用以太网硬件和协议。
这显然不是一个真正的网络。所有的IP地址都是源于为B类私有网络保留使用的范围(那样不能确保地址是全球唯一的)。大部分B类网络拥有更多的计算机,只有八台计算机组成的网络也许不可能形成一个子网。通过“chrysler”连接因特网要通过一条T1或T3线,而且路由器可能是一个“真正”的路由器(如一个思科系统的硬件路由器)胜于用一台带有两块网卡的计算机来作路由器。然而,这个示例实际上足以满足我们的目的:分析Linux网络的实现和在主机、子网、因特网之间的交互。
1.4.版权,许可,免责声明
该文档版权属于Glenn Herrin所有(于2000年登记)。该文档可以被自由全部或部分复制,但必须有象下面的版权属于原作者的声明:
从Linux IP Networking复制,网址:
http://original.source/location.
(在显著位置声明版本在每个被复制的文档中必须要有!)商业发布是被允许和鼓励的。所有对该文档的修改,包括翻译、摘选、引用,必须满足如下要求:
1. 修改的版本号必须被一样地标识;
2. 修改人的名字必须被注明;
3. 原作者的致谢部分必须被保留;
4. 文档中没有被修改的位置要注明;
5. 修改后的文档在没有经过原作者的允许下不可以把原作者的名字用于声称或暗示对文档的认可。
要注意对文档包括删除的任何修改。
如下有关于Linux文档工程(LDP)的许可(会有变化)的变更情况:
· http://www.linuxdoc.org/COPYRIGHT.html
这个文档现在不是LDP的一部分,但将来可能会被提交上去。
希望这个文档将会有用处,于是发布了这个文档,但并不确保能适合任何使用目的。要结合自己的目的来使用这个文档。
1.5.致谢
作为我的为新汉普大学计算机科学系做的硕士项目的一部分我写了这个文档。我要感谢Pilar de la Torre教授设立了这个项目,Radim Bartos教授既是我的这个项目的发起人,也是我的指导老师,给了我很多指导和鼓励,并向我提供一组电脑来做实验。我也要把我的成功归功于美国陆军,我在那里呆了11年,它为我支付了上新汉普大学的学费。
Glenn Herrin
Major, United States Army
Primary Documenter and Researcher, Version 1.0
<gherrin@cs.unh.edu>
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dznlong/archive/2007/03/27/1542915.aspx
2.信息传输概览
这一章是对整个Linux信息传输系统作一个总体描述。里面有对配置方法的讨论,介绍相关数据结构,描述IP路由的基础知识。
2.1.网络传输路径
因特网协议(IP)是Linux信息传输系统的心脏。Linux或多或少严格根据分层的概念—它可能使用不同的协议(如ATM)--IP几乎总是数据包流的核心。IP通过打包数据来实现网络层中路由和转发功能。图2.1是描述网络数据包通过Linux内核的一个简化图。
当一个应用程序要发送数据,便通过套接字(sockets)把数据包传给传输层(TCP或UDP),然后再传到网络层(IP)。在IP传输层,内核在路由缓存中或根据转发信息表(Forwarding Information Base—FIB)寻找把数据包传给目标主机的路由。如果数据包是传给另一个计算机,内核找到相应主机地址并把数据包发给链路层输出接口(典型如以太网设备),链路层输出接口再通过物理层把数据包发出。
当数据包通过传输介质到达目的地时,输入接口收到数据包并检查数据包是否真的是发给该主机的,如果是,则把数据传给IP层,在IP层查找到达数据包目的地的路由。如果数据包要被转发给另一台电脑,在IP层又把数据包发回到输出接口。如果数据包是一个应用,则把数据包向上发给传输层,当数据准备好时,针对这个应用的套接口(sockets)就会去读它。
顺着这条传输路径,每一个套接口和协议执行不同的检查和格式化功能,在后面的章节中会详细描述。这整个处理过程是通过查询和跳转表以使每个协议相互独立来实现的,这些是在计算机启动期间被设置好的,详细信息可以看第三章的关于初始化过程。
2.2.协议栈
网络设备位于协议栈的最底层,使用和其它设备进行通信的链路层协议(通常是以太网)来进行发送和接收数据。输入接口从传输媒介取了数据包,并进行查错,然后把数据转传给网络层。输出接口从网络层接收到数据,并进行查错,然后通过传输媒介把数据发出去。
IP是标准的网络层协议。它检查传进来的数据包是否是要发给本地主机还是要进行转发。如果需要还要进行数据包组合,接着把数据提交给传输协议进行处理。IP协议维护一个用于把数据包发出去的路由数据库,再把数据包发给下层的链路层之前要进行寻址,如果需要还要对数据包进行分组。
TCP和UDP是最常用的传输层协议。UDP提供一个在计算机端口简单地收发数据包的框架,TCP则是基于连接的更复杂的操作,包括因丢失数据包的重发机制和传输管理的实现。二者都会在用户和内核之间复制要传输的数据。然而,TCP和UDP都只是应用程序和网络之间的一个过渡层。
IP中的网络(INET)类型套接字是数据基础和一般套接字的执行方式。它们用队列和编码的方法来执行诸如读数据、写数据和建立连接等的套接字操作。它们承担应用程序套接口和传输层协议之间的中间角色。
一般的BSD套接字包含了网络(INET)类型后是个更抽象的结构。应用程序从BSD套接字读出或写入数据,BSD套接字把这些操作转换成网络套接字的操作。更多套接字内容可以看第四章部分。
应用程序运行在用户空间,组成协议栈的最上层,简单地看是双向交流的连接,复杂地来说可以认为是路由信息协议(RIP—看第9章)。
2.3.数据包结构
要保持严格的协议分层结构并不会因复制参数和传输数据而浪费时间的关键在于数据包的数据结构(套接字缓存,即sk_buff—图2.2)。在所有有使用传输数据以实现该协议的函数中只复制了两次传输数据,一次是从用户层把数据复制到内核层,一次于从内核层把数据复制到输出媒介(形成一个向外传送的数据包)。
这个结构包含了指向所有有关数据包信息的指针,如套接字、设备、路由、数据地址等等。当设备驱动程序收到数据生成数据包后,传输协议从输出缓存中建立这些数据包结构,于是每层协议便会在这个数据结构中填入处理数据包所需要的信息。所有的协议,如传输协议(TCP/UDP)、网络协议(IP)、链路层协议(以太网),都是使用相同的套接字缓存。
2.4.网络路由
IP协议层处理计算机之间的路由。IP协议层保持存在两个数据结构:一个是存储每个已知路由的所有详细路径的转发信息表(FIB),一个是当前正在使用的传向目的地的更快能查找到的路由缓存。(存在第三个结构—邻表—保存某些计算机到某个主机的固定连接路径)。
FIB是个主要的路由参考,它由32个区(其中一个存储IP地址的每一位)和每一个已知可以到达目标的路由信息组成。每个区包含网络和主机的路径,且网络和主机可以通过某个比特值来唯一被标识----一个带有“255.0.0.0”子网掩码的网络有个重要的8个比特数并且是在第8个区中,对于带有“255.255.255.0”子网掩码的网络则有24个重要的比特数并且是在第24个区中。当传输IP数据需要一个路由时,则从特殊的区开始寻找整个表直到发现一个适合的路径(应当最少总有一个默认的路径。在“/proc/net/route”目录下的文件有关于FIB的内容。
路由缓存是一个哈希散列表,用于存储数据包实际发送的路线。由256个表示当前路由的链表组成,每个路由信息的存储位置由哈希算法来确定。当一个主机要发送数据包时,IP网络层在路由缓存中寻找路由,如果没有,则在FIB中寻找一个合适的路由并在路由缓存中插入该新的路由。(这个路由信息是各种协议通用的,而不是FIB中的路由信息。)路由缓存中的路由信息只要有被用则一直保存着,如果对某个目标不存在信息传输时,则等到超时后就被删除了。目录“/proc/net/rt_cache”下的文件有关于路由缓存的内容。
这些表在一个普通系统上执行了所有的路由功能。甚至其它协议(诸如RIP协议)也使用同样的结构,它们只是在内核中通过“ioctl()”函数修改这些已经存在的表。有关路由的详细信息可以看第8章。
第三章
3.网络初始化
本章描述在操作系统启动时的网络初始过程,总结在Linux操作系统启动时有关网络的操作,描述Linux内核和配置、路由支持程序如何建立网络连接,描述有关配置的几个例子之间的区别,总结内核代码的执行过程和网络程序的实现。
3.1.概览
如果计算机被配置成要连入网络时,Linux在启动时会初始化路由表。(几乎所有装有Linux的计算机都会要实现与网络互连,甚至单独存在的计算机也一样,比如只是用作网络回送(loopback)的设备。)当Linux内核完成内核加载后,便开始运行常规的内核程序和读取配置文件,其中有些是设定该电脑的网络性能。这些过程目的是确定本机地址,初始化网络设备接口(诸如以太网卡),加入关键的已知静态路由(诸如是一个连接到因特网的路由)。如果该计算机本身是一个路由器,可能执行一个允许动态更新路由表的程序(但不是所有的主机都是这样的)。
整个配置过程可以是静态的也可以是动态的。如果主机地址或名称一直(或不是经常的)没有变化,系统管理员必须在建立网络系统时定义好网络参数和环境变量。在一个更为灵活的环境中,主机使用诸如动态硬件配置协议(DHCP)来寻找地址、路由和在主机启动时就被配置好的域名解析(DNS)信息。(实际上,对这两情况管理员几乎总是使用图形界面(GUI)(如Red Hat的控制面板)来自动地把配置信息写入文件。)
一个要注意的重要一点是虽然大部分计算机采用相同的方式启动Linux操作系统,但不是按所谓的标准化来设计程序和定制操作系统的,这可以广泛地依赖于不同的版本发布、安全考虑,或者是来自于系统管理员的奇思异想。本章尽可能作一般性的描述,但假定在Red Hat Linux 6.1操作系统中并且是在一般的静态网络环境中的。
3.2.启动
当Linux作为操作系统启动时,先是把映象文件从磁盘载入到内存中并解压文件,然后通过安装文件系统、内存管理和其它关键系统建立起一个操作系统。内核最后一个任务是执行“init”初始化程序,这个程序读取一个用于引导执行启动脚本(Red Hat版本是在目录 /etc/rc.d)的配置文件(/etc/inittab目录下),这时就会转入更多的脚本操作,最终会执行到网络脚本(/etc/rc.d/init.d/network3.3节中有关脚本和文件交互的例子)。目录下)。(看 中
3.2.1.网络初始化脚本
网络初始化脚本设置环境变量以确认主机电脑并建立该电脑是否要使用网络。依靠给定的设置,网络脚本运行或关闭IP数据包转发功能和IP数据包分组功能,它还建立默认的网络传输路由以使该设备能进行传输数据。最后挂起使用“ifconfig”和路由程序的所有网络设备。(在动态环境中是通过查询DHCP服务器来取得网络信息而不用去读取自己的文件。)
这些脚本包含了建立能够非常简便的网络互连,一个大的脚本只是执行一系列命令来完全用于设立一种机器是完全有可能的。然而,大部分Linux版本带有大数量的一般脚本来适应广泛不同机器的设置,这些脚本里面带有大量间接的和有条件的执行方法,但这样使得设置任何一台机器变得更容易。例如在Red Hat版本中,“/etc/rc.d/init.d/network ”会脚本运行几个其它的脚本,象“interfaces_boot”程序一样跟踪“/etc/sysconfig/network-scripts/ifup”脚本的运行来设置环境变量。手动跟踪这些过程是非常复杂的,但可以通过只是简单地修改两个配置文件(在“/etc/sysconfig/network”和“/etc/sysconfig/network-scripts/ifcfg-eth0”文件中输入适当名称和IP地址)就可以以设置整个系统(图形用户界面使得这些操作更加简单)。
当网络脚本执行完后,转子发信息表包含了已知主机和网络的路由信息,这时路由缓存和邻表是空的。当开始进行通信时,内核便更新邻表和路由缓存,这是网络操作的一部分。(如果主机是动态配置或要获取网络时钟,网络通信可能在初始化阶段就开始了。)
3.2.2. ifconfig
“ifconfig”程序用于配置所使用的接口设备。(这个程序虽然被广泛使用,但不是内核的一部分。)这个程序会生成每个设备的IP地址、子网掩码和广播地址。设备将按序运行自己的初始化函数(以设置所有静态变量)、注册设备中断方法和被内核调用的驱动程序。在网络脚本中“ifconfig”命令格式是这样:
ifconfig ${DEVICE} ${IPADDR} netmask ${NMASK} broadcast ${BCAST} (上面的变量既可以在脚本中用直接式表达也可以在其它脚本中定义)。
“ifconfig”程序也可以提供有关当前已配置的网络设备信息(不带参数直接调用“ifconfig”可以显示所有活动接口;调用时带“-a”选项则是显示所有接口,包括活动和非活动的):
ifconfig 该命令提供每个正在使用的接口的所有有用信息:地址,状态,数据包统计,操作系统定义。通常至少有两个接口----一个网卡和网络回送设备。每个接口的信息就象这样(这是Viper主机的接口):
eth0 Link encap:Ethernet HWaddr 00:C1:4E:7D:9E:25 inet addr:172.16.1.1 Bcast:172.16.1.255 Mask:255.255.255.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:389016 errors:16534 dropped:0 overruns:0 frame:24522 TX packets:400845 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:100 Interrupt:11 Base address:0xcc00超级用户可以使用“ifconfig”通过命令行来修改接口设置,这里是这种操作的语法:
· ifconfig interface [aftype] options | address ...
…下面一些用法有些更有用:
ifconfig eth0 down - shut down eth0 ifconfig eth1 up - activate eth1 ifconfig eth0 arp - enable ARP on eth0 ifconfig eth0 -arp - disable ARP on eth0 ifconfig eth0 netmask 255.255.255.0 - set the eth0 netmask ifconfig lo mtu 2000 - set the loopback maximum transfer unit ifconfig eth1 172.16.0.7 - set the eth1 IP address 注意修改接口配置可以直接导致路由表的变化。例如,改变子网掩码可能使某些路由失效(包括默认的路由甚至通向主机本身的路由),于是内核会把这些没用的路由删掉。
3.2.3. route
“route”程序只是简单地添加为接口设备预先定义好的路由到转发信息表(FIB)中。这也不是属于内核程序,它是个用户程序,在脚本中的命令格式如下:
route add -net ${NETWORK} netmask ${NMASK} dev ${DEVICE} -or- route add -host ${IPADDR} ${DEVICE}(这里的变量也可以直接表示或在其它脚本中定义)。
如果带“del”选项也可以删除路由,如果不带有任何选项则可获取当前的路由信息,如:
route 例如下面显示的是(“stealth”计算机)内核中表示的IP路由表(即FIB),但不是路由缓存:
Kernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Iface172.16.1.4 * 255.255.255.255 UH 0 0 0 eth0172.16.1.0 * 255.255.255.0 U 0 0 0 eth0127.0.0.0 * 255.0.0.0 U 0 0 0 lodefault viper.u.edu 0.0.0.0 UG 0 0 0 eth0超级用户可以通过“route”命令来添加或删除IP路由,这里是基本语法:
route add [-net|-host] target [option arg] route del [-net|-host] target [option arg] …下面是些有用的例子:
route add -host 127.16.1.0 eth1 - adds a route to a host route add -net 172.16.1.0 netmask 255.255.255.0 eth0 - adds a network route add default gw jeep - sets the default route through jeep (Note that a route to jeep must already be set up) route del -host 172.16.1.16 - deletes entry for host 172.16.1.16 3.2.4.动态路由程序
如果某计算机是个路由器,网络脚本会运行一个路由程序,如routed或gated程序。既然大部分计算机总是在相同的有线连接的网络中,并且使用相同的一组地址和既定的路由参数,所以大部分计算机没有运行这些路由程序。(如果以太网的网线被切断了,则没有数据包要被传送,于是则不必要去变更路由或调整路由表。)第9章中有更多的关于路由选择的信息。
3.3.例子
下面是关于配置文件的例子,用于通过三种不同方法来进行系统设置,也是对上面列举的程序使用方法的解释。典型地,每个计算机都会运行一个需要读配置文件的网络脚本,除非配置文件告诉计算机不要实现网络互连。
3.3.1.本地计算机
这些是在一个不会永久连接到网络的计算机上的文件,但这个计算机有一个调制解调器(modem)来实现点对点(PPP)网络接入。(这一节不参考常规网络中的计算机。)
下面是网络脚本要读取的第一个文件,它的功能是设置几个环境变量。前面两个变量是设置让计算机运行网络程序(虽然它不在网络上)但不转发数据(因为没有其它电脑存在)。最后两个变量是网络的一般入口。
/etc/sysconfig/network
NETWORKING=yes FORWARD_IPV4=false HOSTNAME=localhost.localdomain GATEWAY= 设置完这些变量后,网络脚本将决定配置至少一个网络设备以使该电脑成为网络的一部分。下面的第二个文件(在所有Linux计算机中几乎都一样设置)是用于设置网络回送设备的环境变量。这个文件设置了该设备的名称,赋予标准的IP地址、子网掩码、广播地址,这些设置和其它网络设备一样。(“ONBOOT”变量是用于让脚本程序告诉系统在启动时是否要配置该设备的标记。)大部分计算机,甚至那些从没有连入因特网的计算机为了进行内部进程的通信也会安装网络回送设备。
/etc/sysconfig/network-scripts/ifcfg-lo
DEVICE=lo IPADDR=127.0.0.1 NMASK=255.0.0.0 NETWORK=127.0.0.0 BCAST=127.255.255.255 ONBOOT=yes NAME=loopback BOOTPROTO=none 完成这些设置后,脚本将运行“ifconfig”程序直到全部完成为止。然而,当点对点(PPP)程序连接到因特网服务提供商(ISP)时,系统将建立PPP设备并根据ISP分配的动态值来寻址和路由选择。DNS和其它的连接信息设置在“ifcfg-ppp”文件中。
3.3.2.局域网中的主机
这些配置文件用于与局域网连接的电脑上,该电脑有一个以太网卡,只要当电脑启动时网卡都会相应启动起来。从下面一般性的例子中可以看出这些文件反映了“stealth”电脑的接入局域网方法。
下面这个文件是网络脚本要读取的第一个文件,前面两个变量也是只是决定计算机要做网络互连但不转发数据包。后面四个变量标识了该电脑名称和要连入因特网时的其它设备名称(不是每个设备都会在局域网上有存在)。
/etc/sysconfig/network
NETWORKING=yes FORWARD_IPV4=false HOSTNAME=stealth.cs.u.edu DOMAINNAME=cs.u.edu GATEWAY=172.16.1.1 GATEWAYDEV=eth0设置完这些变量后,网络脚本将配置网络设备。下面这个文件用于设置以太网卡的环境变量,里面定义了设备名,该电脑的IP地址,子网掩码,广播地址,与其它设备一样具有这些变量。这类计算机也有一个网络回送配置文件,与没有网络连接的计算机完全一样。
/etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0 IPADDR=172.16.1.1 NMASK=255.255.255.0 NETWORK=172.16.1.0 BCAST=172.16.1.255 ONBOOT=yes BOOTPROTO=static /etc/sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1 IPADDR=172.16.1.4 NMASK=255.255.255.0 NETWORK=172.16.1.0 BCAST=172.16.1.255 ONBOOT=yes BOOTPROTO=none设置完这些变量,网络脚本便开始运行“ifconfig”程序来启动网络设备。最后,网络脚本运行路由程序以添加默认路由(到网关的路由)和其它已定义好的路由(如果有,则会在“/etc/sysconfig/static-routes” 文件中找到)。在这个例子中只有一个默认的路由被设定,因为所有的通信是在局域网中(计算机可以通过ARP协议来发现其它主机的地址)或要经过路由器与外面网络相连。
3.3.3.网络路由计算机
这里的文件用于在两个网络之间作为路由器功能的计算机上,该计算机有两个以太网卡,一个网卡针对一个网络。当有其它电脑是在一个子网(LAN)上时,则有一个网卡用于从大型网络(WAN)连接到因特网(这里还要经过另一个路由器)。局域网中的计算机与因特网中的计算机进行通信则要经过这台的计算机(反之亦然)。从下面一般性的例子中可以看出这些文件反映了“dodge/viper”电脑的接入方法。
下面这个文件是网络脚本要读取的第一个文件,用于设置几个环境变量。前面两个变量设定该电脑要做网络互连(因为它是在网络上)和要转发数据包(一个网络转发到另一个其它网络)。IP数据包转发功能在大部分内核中都有,但没有被激活,除非在文件“/proc/net/ipv4/ip_forward”中设置为“1”。(如果变量“FORWARD_IPV4”为“true”则其中一个网络脚本会执行这样的命令:echo 1 > /proc/net/ipv4/ip_forward)最后四个变量用于设定计算机名和连入因特网时的其它设备名称(不是每个设备都会在局域网上有存在)。
/etc/sysconfig/network
NETWORKING=yes FORWARD_IPV4=true HOSTNAME=dodge.u.edu DOMAINNAME=u.edu GATEWAY=172.16.0.1 GATEWAYDEV=eth1设置完这些变量后,网络脚本将配置网络设备。这些文件用于设置两个以太网卡的环境变量,里面定义了设备名,该电脑的IP地址,子网掩码,广播地址。(注意“BOOTPROTO”变量是针对第二块网卡的定义。)这类计算机也有一个标准的网络回送配置文件。
/etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0 IPADDR=172.16.0.7 NMASK=255.255.0.0 NETWORK=172.16.0.0 BCAST=172.16.255.255 ONBOOT=yes /etc/sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1 IPADDR=172.16.0.7 NMASK=255.255.0.0 NETWORK=172.16.0.0 BCAST=172.16.255.255 ONBOOT=yes 设置完这些变量后,网络脚本运行“ifconfig”程序来启动每个网络设备。最后,脚本运行路由程序来添加默认路由(GATEWAY)和其它已定义好的路由(如果有,则会在“/etc/sysconfig/static-routes” 文件中找到)。在这个例子中也是只有一个默认的路由被设定,因为所有的通信可以通过网络掩码来确定是否是在局域网中或要经过默认路由器与外面因特网相连。
3.4.内核与网络程序的函数
下面按字母次序列出与初始化有关的最重要的Linux内核和网络程序的函数列表,列表中注明了源代码位置及实现方法,“SOURCES”目录下的网络文件包含有相应的源代码。可执行文件在任何Linux版本中都会存在,但源代码可能会没有。
这些源代码是从内核源代码中独立出来的一个安装包(Red Hat Linux使用“rpm”命令来管理这些安装包)。 下面的代码是安装“net-tools-1.53-1root)可以通过下面的命令来安装(在安装包所在的目录下执行这个命令):”源代码包(1999-08-29)的方法,这个包可以从www.redhat.com/apps/download网站上获得。下载后,超级用户 (root)可以通过下面的命令来安装(在安装包所在的目录下执行这个命令):
rpm -i net-tools-1.53-1.src.rpm cd /usr/src/redhat/SOURCES tar xzf net-tools-1.53.tar.gz 上面的命令会建立一个“/usr/src/redhat/SOURCES/net-tools-1.53Linux版本应该是相似的(但并不完全一样)。”目录并把有关“ifconfig”和“route”程序(其中也有其它的程序)的源代码文件放到该目录下。这个过程跟其它的
3.4.1. ifconfig
devinet_ioctl() - net/ipv4/devinet.c (398) 建立一个请求信息结构并把数据从用户层复制到内核空间 如果它是网络请求或行为则这个函数被退出 如果它是一个设备请求或行为,则调用设备函数把请求信息复制回到用户内存空间。 成功则返回0 >>> ifconfig main() - SOURCES/ifconfig.c (478) 打开一个套接口(只有当使用“ioctl”函数时) 寻找命令行中设置参数 如果没有参数或参数是一个接口名称则调用“if_print()”函数 循环检查其它参数,设置或清除标记,或调用“ioctl()”来设置接口变量if_fetch() - SOURCES/lib/interface.c (338) 通过多次调用“ioctl()”取得flags, hardware address, metric, MTU, map, 和 address信息,把这些信息赋值给接口数据结构。 if_print() - SOURCES/ifconfig.c (121) 为特定(或所有)接口调用“ife_print()”函数 (如果需要则调用“if_readlist()”函数来生成结构表,然后显示每个接口信息)if_readlist() - SOURCES/lib/interface.c (261) 打开“/proc/net/dev”目录并解析其中数据把数据转换成接口数据结构 调用“add_interface()”函数把每个设备数据结构加入到设备表中inet_ioctl() - net/ipv4/af_inet.c (855) 根据传入的命令执行不同的函数[对于命令ifconfig则调用devinet_ioctl()函数]ioctl() - 跳到一个适当的子程序去执行[= inet_ioctl()]3.4.2. route
INET_rinput() - SOURCES/lib/inet_sr.c (305) 检查错误(不能影响表中数据或修改路由缓存) 调用“INET_setroute()”函数 INET_rprint() - SOURCES/lib/inet_gr.c (442) 如果设置了FIB标志则调用“rprint_fib()”函数 (读取,解析和显示“/proc/net/route”中的内容) 如果设置了CACHE标志,则调用“rprint_cache()”函数 读取,解析和显示“/proc/net/rt_cache”中的内容) INET_setroute() - SOURCE/lib/inet_sr.c (57) 确定是否是到达一个网络或一个主机的路由 检查地址是否合法 通过循环读取参数填写rtentry数据结构 检查子网掩码是否出现冲突 建立一个临时套接口 能过参数rtentry调用“ioctl()”来添加或删除路由 关闭套接口并返回0 ioctl() - 跳到一适当的子程序[= ip_rt_ioctl()]去执行ip_rt_ioctl() - net/ipv4/fib_frontend.c (246) 把传入的参数转换成路由表结构(rtentry结构) 如果要删除一个路由:调用“fib_get_table()”函数取得一个含有该路由信息的表调用“table->tb_delete()”函数把表中相应的路由信息删除掉 如果要添加一个路由:调用“fib_net_table()”找到一个路由表指针 调用“table->tb_insert()”函数插入路由信息到该表中 如果成功则返回0 >>> route main() - SOURCES/route.c (106) 调用初始化函数来设置打印和编辑功能 读取并解析命令行中参数(通过设置标志符或显示信息来直接执行命令行中的参数) 检查参数(如果出现错误则打印相应的使用方法信息) 如果没有参数则调用“route_info()”函数 如果命令行中的参数是添加、删除或修改路由信息则调用“route_edit()”函数并传入相应的参数 如果参数是错误的,则打印相应的使用方法信息 返回函数处理结果 route_edit() - SOURCES/lib/setroute.c (69) 调用“get_aftype()”函数把地址格式从文本转换成一个指针 检查错误(不支持的或不存在的格式) 调用地址格式输入函数“rinput()” [= INET_rinput()] route_info() - SOURCES/lib/getroute.c (72) 调用“get_aftype()”函数把地址格式从文本转换成一个指针 检查错误(不支持的或不存在的格式) 调用地址格式打印函数“rinput()” [= INET_ rprint ()]
第四章
4.连接
这一章描述网络连接过程,有对连接过程的总结、对套接口数据结构的描述,也有对路由系统的介绍、内核实现代码的总结。
4.1.概述
最简单的网络互连形式是在两个主机之间进行连接:在每个终端上,一个应用程序取得一个套接口,建立传输层上的连接,然后发送和接收数据包。在Linux操作系统,一个套接口实际上由两个套接口数据结构组成(其中的一个包含着另外一个)。当一个应用程序建立一个套接口时,套接口被初始化为空。当一个套接口建立连接时(不管是不是和另一个终端进行通信),在IP层会决定到达远程主机的路由并把路由信息存储到套接口中。由此,所有通信传输都是使用路由来建立连接----发送的数据包将通过正确的设备和准确的路由到达远程主机,接收的数据包将在套接口队列中出现。
4.2.套接口数据结构
在Linux中有两种主要的套接口结构:一般的BSD套接口和IP定义的“INET”类型的套接口。这两个是紧密相关的:BSD套接口把INET套接口作为它的成员数据,INET套接口有一个指向BSD套接口的指针。
BSD套接口数据结构定义在文件“include/linux/socket.hstruct socket”。BSD套接口变量通常被命名为“sock” 或对这个名称做一点变化。这个结构只有一些成员,下面描述了其中最重要的结构成员。”文件中,名为“
· struct proto_ops *ops -这个结构中有指向实现普通套接口操作的IP协议函数指针,如ops- > sendmsg指向inet_sendmsg()函数。
· struct inode *inode - 这个结构指针指向与该套接口相联系的文件结点。
· struct sock *sk -指向与该套接口相联系的INET套接口。
INET套接口是“struct socksk或与这个相似的名称。这个结构中有许多与广泛不同用途相关可配置的数据域,依赖这些数据域可以有许多用法和配置。下面描述的是最重要的数据成员:”数据结构类型,定义在“include/net/sock.h”中。INET套接口变量通常被命名为
· struct sock *next, *pprev -所有套接口连接着不同的协议,这些指针允许协议层应用去循环遍历它们。
· struct dst_entry *dst_cache -这是一个指向到达套接口另一边(发送数据包要到达的目的地)的路由的指针。
· struct sk_buff_head receive_queue -这是个接收队列的头位置。
· struct sk_buff_head write_queue -这是个发送队列的头位置。
· __u32 saddr -该套接口的源地址(IP地址)。
· struct sk_buff_head back_log,error_queue -待发送的数据包的后备队列(不要和待发送主队列相混淆),该套接口中错误的数据包队列。
· struct proto *prot -这个结构指针指向传输层协议中定义的函数,如“prot- > recvmsg” 可能指向“tcp_v4_recvmsg()”函数。
· union struct tcp_op af_tcp; tp_pinfo -该套接口的有关TCP设置。
· struct socket *sock - 上层的BSD套接口。
· 注意在这个结构中有其它更多的结构成员,其它的结构成员不是非常重要或可以根据其它名字来知道其意思(例如,“ip_ttl”是指IP数据包存在网络中的计数器)。
4.3.套接口和路由寻找
套接口为每个通信目标只执行一次路由寻找过程(在建立连接时)。因为Linux套接口与IP协议紧密相关,所以套接口包含有到另一个终端的路由(路由信息保存在“sock- > sk- > dst_cache”变量中)。在连接过程中传输协议调用“ip_route_connect()”函数来确定从一个主机到另一个主机的路由,函数调用后,这个路由是假定不会发生变化的(虽然“dst_cache”中的路由信息可能真的被改变了)。套接口没有必要为每个发送包或接收包持续地从表中寻找建立健路由信息,只有当发生意外情况时(如相邻的一台电脑不存了)才再建立路由信息。这就是使用连接进行通信的好处。
4.4.连接过程
4.4.1.建立连接
应用程序通过一系列的系统调用,如寻找远程主机地址,建立一个套接口,然后连接到另一个终端的机器,这样便建立起了套接口通信。
/* look up host */ server = gethostbyname(SERVER_NAME); /* get socket */ sockfd = socket(AF_INET, SOCK_STREAM, 0); /* set up address */ address.sin_family = AF_INET; address.sin_port = htons(PORT_NUM); memcpy(&address.sin_addr,server->h_addr,server->h_length); /* connect to server */ connect(sockfd, &address, sizeof(address)); “gethostbyname()”函数只是寻找主机(如“viper.cs.u.edu”)的IP地址并返回包含有IP地址的结构数据。这个函数涉及的路由操作很少(只是因为该电脑要通过网络来查询IP地址),并且只是把人类可读的形式(文本)转换成与计算机兼容的形式(一个无符号类型的整数)。
“socket()”调用更有意思。它建立一个套接口对象,并赋给一个适当的数据类型(如表示是INET类型的套接口)和初始化套接口数据结构,套接口对象包含了索引结点的信息和各种与网络协议有关的函数指针。它也建立了一些默认队列(如接收数据包队列、发送数据包队列、发生发送错误的数据包队列、待发送的数据包队列)、有关TCP套接口的空的头信息和各种状态信息。
最后,“connect()”内调用IP协议连接子函数(如“tcp_v4_connect()”或“udp_connect()”)。UDP只是建立一个到目的地的路由(因为不用建立虚连接)。TCP则建立路由并开始做TCP连接,发送带有连接请求和窗口标记设置的数据包到另一个目标终端。
4.4.2.套接口函数内运行流程
· 检查调用是否有错误
· 建立(为套接口对象分配内存)套接口对象
· 把套接口对象移入INODE链表中
· 建立网络协议函数指针(INET类型)
· 保存套接口类型值和协议族信息
· 设置套接口为关闭状态
· 初始化数据包队列
4.4.3.连接函数内运行流程
· 检查错误
· 确定到目标的路由:
o 检查路由表看相应路中是否存在(如果存在则返回路由信息)
o 在FIB中寻找路由信息
o 建立新路由信息
o 把新路由信息存入路由表中并退出
· 把指向路由信息的指针存入套接口数据结构中
· 调用网络协议连接函数(如发送一个TCP连接请求数据包)
· 把套接口设为已建立状态
4.4.4.关闭连接
关闭套接口相当直接。一个应用程序在套接口中调用“close()”函数,该函数会调用“sock_close()”函数。这样会改变套接口状态为正在断开连接状态并调用释放套接口(INET类型套接口)成员数据的函数。INET套接口接下来便清除队列和调用传输协议的关闭函数,如“tcp_v4_close()”或“udp_close()”。这些函数会执行任何需要的操作(TCP函数则会发送一个结束连接的请求数据包到目标主机)并清空任何留下的数据结构。注意这里路由信息没有被改变:这个被关闭连接的套接口(现在是空的)不再跟原来的目标有联系,该路由信息保留在路由缓存中直到不能用为止。
4.4.5.关闭函数内运行流程
· 检查错误(如套接口是否存在?)
· 改变套接口状态为正在断开连接以阻止该套接口再被使用
· 运行所有网络协议关闭操作(如发送一个FIN位被设为1的数据包)
· 释放套接口数据结构占用的内存(包括TCP/UDP和INET类型的套接口)
· 从INODE链表删除套接口
4.5.内核函数
下面按字母次序列出对于与连接有关的最重要的Linux内核函数列表,列表中注明了源代码位置及实现方法,下面有关建立一个套接口的函数的调用是从“sock_create()”函数引发的,有关关闭一个套接口的函数的调用则是从“sock_close()”函数引发的。
destroy_sock - net/ipv4/af_inet.c (195) 删除所有定时器 调用所有网络协议中的销毁函数 释放套接口队列 释放套接口本身结构 fib_lookup() - include/net/ip_fib.h (153) 调用“tb_lookup()”函数[= fn_hash_lookup()]在本地表和主要表中查询 返回路由信息或不能到达的错误 fn_hash_lookup() - net/ipv4/fib_hash.c (261) 针对某地址进行查找并返回路由信息 inet_create() - net/ipv4/af_inet.c (326) 调用“sk_alloc()”为套接口申请一块内存 初始化套接口数据结构: 设置“proto”值为TCP或UDP 调用“sock_init_data()”函数 设置family,protocol等变量 调用协议初始化函数(如果是TCP或UDP类型的套接口) inet_release() - net/ipv4/af_inet.c (463) 改变套接口状态为正在断开状态 函数离开多点传输组(如果需要的话) 设置本身套接口的数据成员为空 调用“sk->prot->close()”函数[=TCP/UDP_close()] ip_route_connect() - include/net/route.h (140) 调用“ip_route_output()”函数取得目标地址 如果能取到地址或产生了错误则退出 否则清除指向路由信息的指针并再试着去取目标地址 ip_route_output() - net/ipv4/route.c (1664) 计算地址哈希值 遍历路由表(从哈希值开始)寻找能和地址和TOS匹配的路由信息 如果找到匹配地址,则更新状态并返回路由信息,否则调用函数 ip_route_output_slow() ip_route_output_slow() - net/ipv4/route.c (1421) 如果源地址是已知的,则寻找输出设备 如果目标地址是未知的,则启动网络回送设备 调用“fib_lookup()”函数在FIB表中查找路由 为新的路由项分配新的内存 初始化新路由表中的源地址、目标地址、TOS、输出设备、标志 调用“rt_set_nexthop()”函数寻找下一个目标地址 返回在路由表中安装路由信息的函数“rt_intern_hash()”调用结果 rt_intern_hash() - net/ipv4/route.c (526) 循环搜索“rt_hash_table”表(从哈希值所确定的位置开始) 如果关键字段匹配则把路由表中相应表项移到桶数据结构的前面 否则把路由表表项哈希表中的哈希位置 >>> sock_close() - net/socket.c (476) 检查套接口是否存在(套接口可能空) 调用“sock_fasync()”函数把套接口从异步列表中删除 调用“sock_release()”函数 >>> sock_create() - net/socket.c (571) 检查参数是否合法 调用“sock_alloc()”函数为套接口取得一个可用的索引结点并初始化它 设置“socket->type”值(为SOCK_STREAM, SOCK_DGRAM...) 调用函数“net_family->create()[= inet_create()]”建立套接口结构。 返回已建立的套接口 sock_init_data() - net/core/sock.c (1018) 初始化所有套接口所有结构成员值 sock_release() - net/socket.c (309) 改变状态为断开连接状态 调用函数“sock->ops->release() [= inet_release()]” 调用函数“iput()”把套接口从索引结点删除 sys_socket() - net/socket.c (639) 调用“sock_create()”取得并初始化套接口 调用“get_fd()”分配一个fd给该套接口 设置函数指针“socket->file”指向函数“fcheck()”(指向文件的指针) 如果出现任何失败则调用函数“sock_release()” tcp_close() - net/ipv4/tcp.c (1502) 检查错误 所有数据包从接收队列出栈并丢弃 发送消息到目标主机以关闭连接(如果需要的话) tcp_connect() - net/ipv4/tcp_output.c (910) 生成带有适当比特值和窗口长度设置的连接数据包 把数据包移入套接口发送队列中 调用函数“tcp_transmit_skb()”以发送数据包,初始化TCP连接 tcp_v4_connect() - net/ipv4/tcp_ipv4.c (571) 检查错误 调用函数“ip_route_connect()”寻找到目的地的路由 建立连接数据包 调用函数“tcp_connect()”发送数据包 udp_close() - net/ipv4/udp.c (954) 调用函数udp_v4_unhash()把套接字从套接字队列中删除掉 调用函数destroy_sock() udp_connect() - net/ipv4/udp.c (900) 调用函数“ip_route_connect()”寻找到达目的地的路由 更新套接口中的源、目标地址和端口 改变套接口状态为已建立状态 保存到目的主机的路由到变量“sock->dst_cache”中
5.发送消息
这章是有关发送消息的传输过程。本章总体描述整个过程,研究数据包如何经过协议层,详细描述每一层的动作,总结内核代码的实现。
5.1. 概述
一个往外发送的消息于开始于应用系统调用函数写入到套接口,套接口检查自己的连接类型并调用适当的发送函数(特指INET类型),发送函数检验套接口状态,检查协议类型,发送数据给传输层函数(如TCP或UDP),网络协议为发送数据包建立一个新的缓存(一个套接口缓存,或一个类型为“sk_buff”的变量skb),在把数据包传给从应用程序的缓存中复制发送数据并在把数据包传给网络层(IP)前加入数据包头信息(如端口号、设置和校验和),IP网络层发送函数加入更多的有关协议的包头信息(如IP地址,设置和校验和)。如果需要它也可以对数据包进行分包。接下来IP网络层把数据包传给链路层函数,链路层函数把数据包移入发送设备上的xmit队列并通知设备已经有数据包要被发送。最后,设备(如网卡)告诉总线要发送数据。
5.2.发送流程
5.2.1.把数据包写入到套接口
· 写数据到一个套接口(应用程序的操作)
· 在数据包中加入消息头(套接口的操作)
· 检查基本的错误:套接口是否绑定了端口号?套接口可以发数据吗?套接口在否存某些错误?
· 把消息头传给相应的传输协议栈(INET套接口的操作)
5.2.2.建立一个UDP数据包
· 检查错误:数据是否太长?是不是UDP连接?
· 确定是否有到达目的地的路由(如果路由没有建立则调用IP路由子程序;如果路由不存在则失败退出)
· 建立该数据包的UDP头
· 调用IP协议建立和发送数据包函数
5.2.3.建立TCP数据包
· 检查连接:连接是否建立?套接口是否打开?套接口是否可用?
· 如果可能则检查并组合部分数据包
· 建立数据包缓存
· 从用户空间把发送数据复制到上面建立的缓存中
· 添加数据包到发送队列中
· 在数据包中建立当前TCP数据包头(如ACKs,SYN,等)
· 调用IP协议传输函数
5.2.4.在IP网络层封装数据包
· 建立数据包缓存(如果是UDP通信)
· 寻找到达目的地的路由(如果是TCP通信)
· 把IP协议包头信息加入到数据包中
· 从用户空间复制传输层协议包头信息和发送数据
· 发送数据包到目标路由设备的输出函数中
5.2.5.传输一个数据包
· 把数据包输入到设备输出队列中
· 唤醒该设备
· 等待调度程序运行设备驱动程序
· 检查通信媒介(通信设备)
· 发送连接请求
· 告诉总线要通过该媒介传输数据包
5.3.内核函数
下面按字母次序列出对于与传输消息有关的最重要的Linux内核函数列表,列表中注明了源代码位置及实现方法,下面函数的调用是从“sock_write()”函数引发的。
dev_queue_xmit() - net/core/dev.c (579)调用函数“start_bh_atomic()” 如果设备有一个队列 调用函数“enqueue()”添加数据包到队列中 调用函数“qdisc_wakeup() [= qdisc_restart()]”唤醒设备 否则调用函数“hard_start_xmit()” 函数“end_bh_atomic()” DEVICE->hard_start_xmit()(由具体设备决定), drivers/net/DEVICE.c 检查传输媒介是否打开 发送数据包头 告诉总线要发送数据包 更新状态 inet_sendmsg() - net/ipv4/af_inet.c (786) 提取出套接口中套接字指针 检查套接口是否能用 检查协议函数指针 返回函数“sk->prot[tcp/udp]->sendmsg()”的返回值 ip_build_xmit - net/ipv4/ip_output.c (604) 调用“sock_alloc_send_skb()”函数为skb申请内存 设置skb头 调用函数“getfrag() [= udp_getfrag()]”以从用户空间复制缓存 返回函数“rt->u.dst.output() [= dev_queue_xmit()]”的返回值 ip_queue_xmit() - net/ipv4/ip_output.c (234) 寻找路由 建立IP数据包头 如果需要则进行分组 添加IP校验和 调用函数“skb->dst->output() [= dev_queue_xmit()]” qdisc_restart() - net/sched/sch_generic.c (50) 把数据包移出队列 调用函数“dev->hard_start_xmit()” 更新状态 如果出现错误则数据包再次进入发送队列 sock_sendmsg() - net/socket.c (325) 调用函数“scm_sendmsg() [socket control message]” 调用函数“sock->ops[inet]->sendmsg()”并释放csm变量 >>> sock_write() - net/socket.c (399) 调用函数“socki_lookup()”,该函数跟带“fd/file”索引结点的套接口有关系 建立带有数据长度和地址的消息头 返回函数“sock_sendmsg()”返回值 tcp_do_sendmsg() - net/ipv4/tcp.c (755) 如果需要则等待连接成功 调用函数“skb_tailroom()”,如果可能则添加数据到等待发送的数据包 检查窗口状态 调用函数“sock_wmalloc()”为skb取得一块内存 调用函数“csum_and_copy_from_user()”来复制数据包并设置校验和 调用函数“tcp_send_skb()” tcp_send_skb() - net/ipv4/tcp_output.c (160) 调用函数“__skb_queue_tail()”把数据包添加到队列中 如果可能则调用函数“tcp_transmit_skb()” tcp_transmit_skb() - net/ipv4/tcp_output.c (77) 建立TCP数据包头并添加校验和 调用函数“tcp_build_and_update_options()” 检查ACKs,SYN内容 调用函数“tp->af_specific[ip]->queue_xmit()” tcp_v4_sendmsg() - net/ipv4/tcp_ipv4.c (668) 检查IP地址类型、连接与否、端口号、地址 返回函数“tcp_do_sendmsg()”返回值 udp_getfrag() - net/ipv4/udp.c (516) 从用户空间复制一块缓存并添加校验和 udp_sendmsg() - net/ipv4/udp.c (559) 检查长度、标记、协议类型 设置UDP数据包头和地址信息 检查是否进行多点传送 数据包中加入路由信息 填写剩下的头信息 调用函数“ip_build_xmit()” 更新UDP状态 返回错误值err
第六章
6.接收数据包
这一章描述通信过程中接收方接收数据的过程。对处理过程进行总体描述,研究网络层中数据经过过程,详细描述网络每一层的操作,总结内核实现代码。
6.1.概述
Figure 6.1: Receiving messages.
当系统告知通信设备有一个消息到达时,便产生一个中断开始接收消息。通信设备分配存储空间并让总线把消息存入该空间中。然后把数据包传给链接层,链接层再把数据包放入后备队列并设置网络标记让后面的bottom-half程序能够运行。
(注: 对于不同的中断服务程序,由于它们可能不再是内核中的任务,未必能享有进程调度程序的优待。为了实现同步,将它们分为两部分:top-half和bottom-half。top-half在运行时,不能被其他任何中断再次中断,也不能被其他进程中断,它通过对CPU内的中断屏蔽置位实现,而bottom-half则只对top-half开中断。这样,系统就可以根据中断服务程序的访问特点,安排那些访问临界区的服务程序为top-half,其他中断服务程序为bottom-half。)
bottom-half是Linux系统为在一个中断过程中使得系统处理的工作量最小化的方式。在一个中断中处理很多的过程精确地讲是不好的,因为那样会中断一个正在运行的进程,可替代的方法是把一个中断分为“top-half”和“bottom-half”两部分。当有一个中断产生时,“top-half”部分先运行并小心处理对临界区的操作,如把数据从设备队列中转移到内核内存中,然后做个标志告诉内核已经完成了,当处理器有时间时则再回到当前进程进行处理。进程调度器下次运行时看到这个标志于是开始做剩下的操作,只有这样才能使得任何普通进程能被调度运行。
当进程调度器发现有网络任务要运行时则运行网络bottom-half程序。网络bottom-half程序把数据包从后备队列中取出,对数据包中的通信协议进行匹配(这里特指IP协议),然后把数据包传给协议接收函数。IP网络协议检查数据包是否有误并为数据包选择路由,这个数据包将进入发送队列(如果是发给另一个主机)或者向上传给传输层(如TCP或UDP协议层)。在传输层再次检查错误,寻找与数据包中设定的端口号有联系的套接口,然后把数据包移入到套接口队列的末端。
一旦数据包进入到套接口队列中,套接口将唤醒拥有该套接口的应用程序(如果需要)。应用程序进程便开始被调用或调用读取函数把数据包从队列中复制到应用程序缓存中并返回调用结果。(如果该进程不是在等待数据包则进程不做任何操作,当需要数据包时才从队列中取得数据包。)
6.2.数据包接收流程
6.2.1.从一个套接口读数据(第一部分)
· 尝试从一个套接口读数据(应用程序操作)
· 在本地缓存中写入消息头数据(套接口操作)
· 检查基本错误如:套接口的端口号是否匹配?套接口能否接受消息?套接口是否存在错误?
· 传递消息头信息到适当传输层协议(INET套接口操作)
· 等待直到从套接口读到足够数据为止(TCP/UDP传输层操作)
6.2.2.接收一个数据包
· 唤醒正在准备接收数据的设备(中断调用)
· 检查传输媒介(通信设备操作)
· 接收到连接请求头信息
· 为数据包分配存储空间
· 告诉总线要把数据包存入内存中
· 把数据包存入后备队列
· 设置标志如果可能则运行网络bottom-half程序
· 把CPU控制权还给当前进程
6.2.3.运行网络“bottom-half”程序
· 运行网络bottom-half程序(进程调度器调度)
· 把所有数据包传给正在等待数据的传输层协议并关闭中断(网络bottom-half程序操作)
· 遍历后备队列并把数据包向上传给因特网接收协议----IP
· 再次清空发送队列
· 退出网络bottom-half程序结构
6.2.4.在IP网络层解析数据包
· 检查数据包是否有误: 是否太短?是否太长?版本是否有误?校验和是否有错?
· 如果需要组合数据包
· 为这个数据包取得路由(可能是发给本地主机或可能需要转发)
· 把数据包发给后面的处理程序(TCP或UDP传输层接收处理程序,或可能转发给另个主机)
6.2.5.在UDP传输层接收一个数据包
· 检查UDP数据包头是否有错误
· 匹配目标套接口
· 如果找不到匹配套接口则发回一个带错误信息的消息
· 把数据包移入相应套接口的接收队列
· 唤醒从该套接口等待数据的任何进程
6.2.6.在TCP传输层接收一个数据包
· 检查数据包序号和标志,如果可能则把数据包存储在正确的位置
· 如果该数据包已经接收了则立即返回ACK消息并抛弃该数据包
· 确定数据包是属于哪个套接口
· 把数据包移入对应套接口接收队列中
· 唤醒从该套接口等待数据的任何进程
6.2.7.从套接口读取数据(第二部分)
· 如果数据已经全部接收好了则唤醒读取操作进程(套接口操作)
· 调用传输层接收函数
· 把数据从接收队列复制到用户缓存中(TCP/UDP层操作)
· 返回数据并把CPU控制权还给应用程序(套接口操作)
6.3.内核函数
下面按字母次序列出对于与接收消息有关的最重要的Linux内核函数列表,列表中注明了源代码位置及实现方法,下面从网络底层向上调用的函数开始于函数“DEVICE_rx()”,从应用程序向下调用的函数开始于函数” sock_read()”。
>>> DEVICE_rx() - device dependent, drivers/net/DEVICE.c (利用中断方法取得CPU控制权) 检查状态以确定是否应该接收数据 调用函数“dev_alloc_skb()”为数据包分配存储空间 从系统总线取得数据包 调用函数“eth_type_trans()”以确定协议类型 调用函数“netif_rx()” 更新网卡状态 (退出中断处理并返回) inet_recvmsg() - net/ipv4/af_inet.c (764) 从套接口中提取出套接字指针 检查套接口以确定是否正在等待接收数据包 检查协议函数指针 返回函数“sk->prot[tcp/udp]->recvmsg()”运行返回值 ip_rcv() - net/ipv4/ip_input.c (395) 检查数据是否有错误: 长度错误(太短或太长) 不正确的版本号(不等于4) 检验和不正确 调用函数“__skb_trim()”去除数据包中额外信息 如果需要则组合数据包 调用函数“ip_route_input()”确定数据包路由 检查并处理IP设置 返回函数“skb->dst->input() [= tcp_rcv,udp_rcv()]”调用返回值 net_bh() - net/core/dev.c (835) 被进程调度器调度) 如果有数据包要等着往外发送则调用函数“qdisc_run_queues()” (看发送数据包章节) 当后备队列不为空 让其它bottom-half程序运行 调用“skb_dequeue()”取得下一个数据包 如果数据包要发给其它主机(如FASTROUTED)则把数据包移入发送队列中 遍历协议列表(分支和主干)以匹配协议类型 调用函数“pt_prev->func() [= ip_rcv()]”以把数据包传给相应的网络协议 调用函数“qdisc_run_queues()”并清空输出队列(如果需要) netif_rx() - net/core/dev.c (757) 把时间赋值给“skb->stamp”变量 如果后备队列太满则抛弃该数据包 其它 调用函数“skb_queue_tail()”把数据包移入后备队列中 标记后面要执行的bottom-half程序 sock_def_readable() - net/core/sock.c (989) 调用函数“wake_up_interruptible()”把等待进程移入运行队列 调用函数“sock_wake_async()”发送“SIGIO”信号给套接口进程 sock_queue_rcv_skb() - include/net/sock.h (857) 调用函数“skb_queue_tail()”把数据包移入套接口接收队列中 调用函数“sk->data_ready() [= sock_def_readable()]” >>> sock_read() - net/socket.c (366) 建立消息头信息 返回函数“sock_recvmsg()”的读取结果 sock_recvmsg() - net/socket.c (338) 读套接口管理包(scm)或通过调用函数“sock->ops[inet]->recvmsg()”打包数据 tcp_data() - net/ipv4/tcp_input.c (1507) 如果需要则缩短接收队列 调用函数“tcp_data_queue()”对数据包进行排队 调用函数“sk->data_ready()”唤醒套接口进程 tcp_data_queue() - net/ipv4/tcp_input.c (1394) 如果数据包序号超出范围: 如果数据包是接收过的则立即抛弃它 否则则计算数据包存储位置 调用“__skb_queue_tail()”函数把数据包保存在套接口接收队列中 更新连接状态 tcp_rcv_established() - net/ipv4/tcp_input.c (1795)如果是快速路径 检查所有数据包标志和头信息 发送ACK消息 调用“_skb_queue_tail()”函数把数据包存入套接口接收队列中 否则(慢路径) 如果数据包序号超出范围则发送ACK消息并丢弃该数据包 检查FIN,SYN,RST,ACK标志 调用函数“tcp_data()”对数据进行排列 发送ACK消息 tcp_recvmsg() - net/ipv4/tcp.c (1149) 检查错误 等待直到至少有一个数据包到达 如果连接被关闭则清除套接口 调用函数“memcpy_toiovec()”把数据包从套接口缓存复制到用户空间 调用函数“cleanup_rbuf()”释放内存如果需要则发送ACK消息 调用函数“remove_wait_queue()”唤醒进程(如果需要) udp_queue_rcv_skb() - net/ipv4/udp.c (963) 调用函数“sock_queue_rcv_skb()” 更新UDP状态(如果队列排列失败则释放skb) udp_rcv() - net/ipv4/udp.c (1062) 取得UDP数据包头,去除下层协议加入的数据包信息,检查校验和(如果需要) 检查是否是多路传输 调用函数“udp_v4_lookup()”来寻找与数据包匹配的套接口 如果找不到相应的套接口,发回ICMP消息,释放skb,并终止接收数据 调用函数“udp_deliver() [= udp_queue_rcv_skb()]” udp_recvmsg() - net/ipv4/udp.c (794) 调用函数“skb_recv_datagram()”从接收队列中取得数据包 调用函数“skb_copy_datagram_iovec()”把数据包从套接口缓存中复制到用户空间 更新套接口时间戳 在消息头中填写源信息 释放数据包占用的内存