Linux 内核网络栈实现源代码分析


LINUX-1.2.13 内核网络栈实现 源代码分析 中国科学技术大学 (安徽,合肥) 近代物理系 快电子实验室 曹桂平(著) 声明:本文档供 Linux 内核爱好者之间学习交流之用,未经作者本人同意,任何单位和个人 不得进行出版发行,违者必究! This page is intentionally left blank. 序言 使用老的 LINUX 版本进行内核代码分析在很多人看来是一种“避实就虚”的“卑鄙”手段。 因为老的 LINUX 版本代码较为简单,分析起来基本不费“吹灰之力”,所以为很多“高手” 所不齿。而对于很多新手而言,学习老代码似乎觉得有点让人看不起,或者自己有点看不起 自己,最“酷”的莫过于捧着最新版本的内核源代码去“啃”;再者,有些新手对于学习老 代码的作用有些怀疑,毕竟这些代码已成为历史,现在运行的系统代码较之那些之前的老代 码简直是“面目全非”,甚至基本找不到老代码的影子,此时会对学习这些老代码的必要性 产生怀疑,本人之前也是抱此怀疑。于是本人就去找最新的版本去看,去研究,比如国内比 较有名的关于 LINUX 内核源代码分析的书《LINUX 内核源代码情景分析》上下册。这本书 分析的是 LINUX2.4 内核早期版本代码,相比较现在的 2.6 版本而言,这也算是老版本了。 读过此书的人恐怕都深有体会,这个体会不是说读完了(真是精神可嘉)这本书感觉受益匪 浅,而是感觉相当的痛苦,坦白的说,如果没有对操作系统有深刻的理解并且之前接触过操 作系统编程,恐怕只能获得一个非常泛泛的理解。因为其传授的知识太多,一方面让我们不 知所措,另一方面也感觉自己“内功”过于薄弱,无法承受这种“上乘武功“。那么之后(最 近几年)出版的由同济大学赵炯博士所写的《LINUX-0.11 完全注释》可以说对于很多 LINUX 内核爱好者而言帮助很大。LINUX-0.11 内核版本在如今真可算得上是”古董“了。不过我 想研读过这本书的读者肯定收获颇丰。而且之后当我们试图去分析较新版本的内核时,或者 再去研读其它较新版本的有关内核分析的文章时,有种似曾相识之感,而且理解上也变得容 易。原因很简单,中国有句古语,”麻雀虽小,五脏俱全“,LINUX-0.11 内核版本虽然很小, 但是是一个可以运行的操作系统版本,我们研读这本书了解了组成一个可运行操作系统需要 完成的工作,通过代码实例可以更深刻的理解操作系统的各种概念。对于很多读者而言包括 本人在内,之前对于操作系统”心存敬畏“,可是现在在很多人面前我都敢说,”哦,操作 系统实现并不复杂,只是比较麻烦…“。早期代码的分析可以让我们把握问题的实质,从本 文分析的主题网络部分代码来看,这点尤为突出。从 LINUX-0.96C 版本开始就包含网络代 码,本人从 0.96C 版本开始,对网络部分代码进行过系统分析,一直到 1.3.0 版本,可以将 这之前的版本都归为早期版本,这些版本有一个共同特点,即网络部分所有代码都在一个文 件夹中,每种协议的实现都只是一个文件与之对应(接近 1.3.0 版本的代码,某些协议开始 有多个文件对应),而此之后的版本(从 1.3.0 开始,包括 1.3.0)开始进行细分,这一方面 代码更加完善所以也就更加庞大,另一方面随着代码的逐渐成熟,层次关系更加明确。如现 在对于内核数据包的封装结构 sk_buff,对于早期代码而言并没有专门的处理文件,而现在 对于此结构就有一个专门的操作函数集,集中放置在 sk_buff.c 文件中。 研究早期代码一个最大的优势就是可以直接接触问题的本质。因为早期代码更注重实际功能 的实现,而非辅助功能的实现,辅助功能的代码很少或者基本没有,例如早期代码并没有专 门的函数集去处理 sk_buff 结构的操作问题,一般在需要分配或操作该结构时直接编码操作, 而现在这些”直接的编码“被分离出来,成为专门的操作函数集。从内核的演变来看,这是 必要的,但从代码的分析来看,变得越来越复杂,如果对内核代码组织不熟悉(很多新手即 如此),甚至不知道一个被调用函数在哪儿,而使用查找工具来回翻看,不一会就会产生疲 劳,而且深感内核的庞大复杂,从而削减学习的积极性。而分析早期代码一方面可以更直接 的抓住问题实质,另一方面可以将我们的注意力集中于对问题本质的理解上。 本文选择 LINUX-1.2.13 内核所包含的网络部分代码分析(注意网络部分代码与内核代码的 演变是分离的,如 LINUX1.2.8 网络代码与 1.2.13 是一样的,而内核显然是有差异的)。 LINUX-1.2.13 网络部分所有实现代码仍然是集中在一个文件夹中(net 文件夹),在此之后 (1.3.0 内核版本)的网络部分代码在 net 文件夹下进行了细分,除此之外,对于某些协议(如 TCP,IP)协议的实现也从之前的一个单一文件分立为几个文件以加强层次和功能关系。为 了消除结构上不必要的复杂性并保持功能实现上的完整性,选择 LINUX1.2.13 内核版本网 络代码进行分析是比较适宜的。这个版本代码坦白的说在某些细节的处理上还存在很多问 题,但基本功能及网络栈的层次性已经十分清晰,而且与其之后版本结构承接性较好,即后 续版本基本完全保存了此版本代码的结构组织,主要是对功能实现上的补充和完善,当然在 这过程中某些结构会增加或删除某些字段,但基本实现的功能没有发生改变,所以可以作为 其后版本分析的一个较好的出发点。会有极少数结构名称发生改变如到 2.4 版本将 device 结 构名改为 net_device。不过对于此类更改不会对后续版本分析造成实质性影响。 选择早期版本分析一方面减小分析的难度,从而同时减小了读者理解上的难度。既然是一个 早期的版本,也就是说此处的工作只是为读者建立好一个起跑的起点,如果读者只是为方便 理解某些用户接口函数的功能,那么此处的工作已是足够帮助你更好的进行理解了,如果读 者有志成为网络黑客,那么有必要在此基础上,向更新版本更复杂的代码发出挑战。在这过 程中你会发觉对早期版本代码深刻理解的重要性,你会发现这个挑战的过程会比你之前想象 的更为顺利,因为从早期代码的学习中你已经掌握了问题的实质。 各种版本Linux源码(包括本书分析所用的Linux-1.2.13 内核版本)可到如下网站: http://www.kernel.org/ 进行下载。 鉴于作者能力有限,不可能对源代码中每一个问题都理解到位,如果发现理解有误之处,还 请广大读者指正,可通过邮件通知我,你我一起进行探讨:ingdxdy@gmail.com。 曹桂平 2009.02.20 目 录 序言 ............................................................................................................................................4 引言 ............................................................................................................................................8 第一章 网络协议头文件分析 ................................................................................................9 1.1 include/linux/etherdevice.h 头文件 .....................................................................................10 1.2 include/linux/icmp.h 头文件 ................................................................................................11 1.3 include/linux/if.h 头文件 .....................................................................................................18 1.4 include/linux/if_arp.h 头文件 ..............................................................................................22 1.5 include/linux/if_ether.h 头文件 ............................................................................................25 1.6 include/linux/ if_plip.h 头文件 .............................................................................................27 1.7 include/linux/if_slip.h 头文件 ..............................................................................................28 1.8 include/linux/igmp.h 头文件 ................................................................................................29 1.9 include/linux/in.h 头文件 .....................................................................................................33 1.10 include/linux/inet.h 头文件 ................................................................................................37 1.11 include/linux/interrupt.h 头文件 .........................................................................................38 1.12 include/linux/ip.h 头文件 ....................................................................................................41 1.13 include/linux/ip_fw.h 头文件 ..............................................................................................50 1.14 include/linux/ipx.h 头文件 ..................................................................................................55 1.15 include/linux/net.h 头文件 ..................................................................................................61 1.16 include/linux/netdevice.h 头文件 ........................................................................................68 1.17 include/linux/notifier.h 头文件 ............................................................................................82 1.18 include/linux/ppp.h 头文件 ..................................................................................................87 1.19 include/linux/route.h 头文件 ...............................................................................................110 1.20 include/linux/skbuff.h 头文件 .............................................................................................114 1.21 include/linux/socket.h 头文件 .............................................................................................124 1.22 include/linux/sockios.h 头文件 ...........................................................................................127 1.23 include/linux/tcp.h 头文件 ..................................................................................................130 1.24 include/linux/timer.h 头文件 ...............................................................................................139 1.25 include/linux/udp.h 头文件 .................................................................................................142 1.26 include/linux/un.h 头文件 ...................................................................................................144 1.27 本章小结 ............................................................................................................................144 第二章 网络协议实现文件分析 .............................................................................................145 2.1 net/protocols.c 文件 ..............................................................................................................146 2.2 net/socket.c 文件 ...................................................................................................................148 2.3 net/inet/af_inet.c 文件 ...........................................................................................................187 2.4 net/inet/tcp.c 文件 .................................................................................................................243 2.5 net/inet/tcp.h 头文件 .............................................................................................................421 2.6 net/inet/udp.c 文件 ................................................................................................................426 2.7 net/inet/udp.h 头文件 ............................................................................................................449 2.8 net/inet/sock.h 头文件 ...........................................................................................................450 2.9 net/inet/sock.c 文件 ...............................................................................................................460 2.10 net/inet/datagram.c 文件 ......................................................................................................477 2.11 net/inet/icmp.c 文件 .........................................................................................................484 2.12 net/inet/icmp.h 头文件 .....................................................................................................514 2.13 net/inet/igmp.c 文件 .........................................................................................................516 2.14 net/inet/dev_mcast.c 文件 ................................................................................................534 2.15 net/inet/snmp.h 头文件 .....................................................................................................540 2.16 net/inet/protocol.h 头文件 ................................................................................................543 2.17 net/inet/protocol.c 头文件 ................................................................................................545 2.18 net/inet/proc.c 文件 ..........................................................................................................551 2.19 net/inet/route.h 头文件 .....................................................................................................558 2.20 net/inet/route.c 文件 .........................................................................................................560 2.21 net/inet/ip.c 文件 ..............................................................................................................582 2.22 net/inet/ip_fw.c 文件 ........................................................................................................677 2.23 net/inet/raw.c 文件 ............................................................................................................715 2.24 net/inet/raw.h 文件 ...........................................................................................................726 2.25 net/inet/packet.c 文件 .......................................................................................................728 2.26 net/inet/p8022.h 头文件 ...................................................................................................743 2.27 net/inet/p8022call.h 头文件 .............................................................................................743 2.28 net/inet/datalink.h 头文件 ................................................................................................746 2.29 net/inet/p8022.c 文件 .......................................................................................................746 2.30 net/inet/psnap.h 头文件 ....................................................................................................750 2.31 net/inet/psnapcall.h 头文件 ..............................................................................................751 2.32 net/inet/psnap.c 文件 ........................................................................................................751 2.33 net/inet/eth.c 文件 ............................................................................................................756 2.34 net/inet/eth.h 头文件 ........................................................................................................763 2.35 net/inet/p8023.c 文件 .......................................................................................................764 2.36 net/inet/arp.c 文件 ............................................................................................................768 2.37 net/inet/arp.h 头文件 ........................................................................................................812 2.38 net/inet/devinit.c 文件 ......................................................................................................812 2.39 net/inet/dev.c 文件 ............................................................................................................819 2.40 本章小结 .........................................................................................................................862 第三章 网络设备驱动程序分析 .........................................................................................863 3.1 关键变量,函数定义及网络设备驱动初始化 ..............................................................863 3.2 网络设备驱动程序结构小结 ..........................................................................................871 3.3 本章小结 ..........................................................................................................................871 第四章 系统网络栈初始化 .................................................................................................872 4.1 网络栈初始化流程 ..........................................................................................................872 4.2 数据包传送通道解析 ......................................................................................................873 4.3 本章小结 ..........................................................................................................................875 附录 A - TCP 协议可靠性数据传输实现原理分析 .............................................................876 参考文献 .................................................................................................................................881 引言 如果你主观上感觉去做某件事情会很复杂,很费力,大多数时候是因为你对这件事了解的不 够深刻。当你去不断的搜集材料,不断对它进行分析,有一天你会豁然开朗,原来事情确实 如此简单。大多数应用程序程序员对操作系统内核编程心存敬畏,认为内核编程是一件非常 复杂的事,但是对于大多数分析过 LINUX“古董“级内核版本的 LINUX 爱好者而言,即便 其不懂什么 VC++,JAVA,但其对操作系统内核编程却信心十足。LINUX 的开源性使得越 来越多的程序员可以觊觎编写内核的那种满足感和成就感。那么我们将范围缩小,编一个网 卡驱动程序怎么样,很难吗,我想很多程序员会回答是,因为这是属于内核编程的一部分, 无论其作为内核的固定组成部分,还是以一个内核模块的方式加载。而且网卡驱动程序相对 而言应该是 LINUX 驱动程序编写中比较复杂的一类。但是如果此时有人跳出来说,编写网 卡驱动程序就那么回事,很简单,你是不是觉得他有点造作?但是如果这话由你自己说出, 你是不是也是这样认为。对 LINUX 内核网络栈代码进行完全的分析后,回过头来再看网卡 驱动程序的编写,简直是不值一提。在你脑海中,你会“画出”一条网络数据包的”旅行“路 线,路线上的各个”关卡“以及每一个”关卡“对数据包进行何种处理你一清二楚。而此时 网卡驱动程序代码只不过是这些”关卡“中较不起眼的一个。因为它“基本没做什么工作, 就写几个寄存器而已”。 对于 LINUX-1.2.13 网络栈代码实现本文采用两种分析方式,第一种是按部就班的逐文件逐 函数进行分析,此种分析方式较少涉及各种协议之间的关联性;第二种是从结构上和层次上 进行分析,着重阐述数据包接收和发送通道,分析数据包的传输路线上各处理函数,此时将 不针对某个函数作具体分析,只是简单交待功能后,继续关注其上下的传输。在第一种分析 的基础上辅以第二种分析使问题的本质可以更好被理解和掌握。分析网络栈实现不可不涉及 网络协议,如果泛泛而谈,不但浪费篇幅,也不起作用,但是在分析某种协议的代码之前, 又不可不对该协议有所了解,本文尽量介绍这些协议的主要方面,但无法对这些协议进行完 全介绍,只是在分析某段代码所实现功能时,为帮助理解,在此之前会给出协议的相关方面 介绍,如在分析 TCP 协议的拥塞处理代码时,在这之前会对 TCP 的拥塞处理进行介绍。 LINUX-1.2.13 内核网络栈实现代码分布于四个文件夹中,以 LINUX-1.2.13 作为内核源代码 根目录,则此四个目录的位置如下所示: 本书共分为四章,第一章着重介绍网络协议涉及的头文件,即对 include/linux 子目录下相关 文件进行分析;第二章分析网络协议具体实现代码,这是本书的重点,这部分代码都集中在 net 子目录下;第三章介绍网络设备驱动层相关内容,包括网络设备的初始化过程以及驱动 程序结构,这部分代码分布在 drivers 子目录下;第四章介绍系统网络栈的初始化流程以及 理清网络数据包的上下传送通道,从而再次从整体实现上进行把握。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 第一章 网络协议头文件分析 本章内容集中分析网络协议头文件,这些头文件集中在 include/linux 子目录下。表 1.1 以首 字母序列出了该子目录下涉及网络协议的所有头文件名称及其功能。(注意 include/linux 子 目录下还包括内核其它头文件,此处抽出了所有涉及网络部分的头文件,另外在分析网络实 现代码时可能需要一些其它头文件中内容,这在相关处会给出相关代码。) 表 1.1 网络相关头文件列表(以首字母为序) 头文件名 最后修改日期 说明 etherdevice.h 1994-04-18 16:38 以太网协议相关函数声明 icmp.h 1993-12-01 20:44 ICMP 协议结构定议 if.h 1994-12-01 03:53 接口相关结构定议 if_arp.h 1995-02-05 20:27 ARP 协议结构定议 if_ether.h 1995-02-02 02:03 以太网首部及标志位定义 if_plip.h 1995-02-13 03:11 并行线网络协议 if_slip.h 1994-05-07 19:12 串行线协议 igmp.h 1995-01-24 21:32 IGMP 协议结构定义 in.h 1995-02-23 19:32 协议号定义 inet.h 1995-01-27 18:17 INET 域部分函数声明 interrupt.h 1995-01-26 13:38 下半部分结构定义 ip.h 1995-04-17 18:47 IP 协议结构定义 ip_fw.h 1995-03-10 02:33 防火墙相关结构定义 ipx.h 1995-01-31 15:36 IPX 包交换协议结构定义 net.h 1995-02-23 19:26 INET 层关键结构定义 netdevice.h 1995-02-05 19:48 设备相关结构定义 notifier.h 1995-01-07 18:57 事件响应相关结构定义 ppp.h 1994-08-11 00:26 点对点协议结构定义 route.h 1994-08-11 00:26 路由结构定义 skbuff.h 1995-01-24 21:27 数据包封装结构定义 socket.h 1995-02-02 02:03 常数选项定义 sockios.h 1995-01-24 21:27 选项定义 tcp.h 1995-04-17 18:47 TCP 协议结构定义 timer.h 1995-01-23 03:30 定时器相关结构定义 udp.h 1993-12-01 20:44 UDP 协议结构定义 un.h 1994-06-17 12:53 UNIX 域地址结构定义 下文中原始头文件代码显示在两行包含该文件路径的星号之间。如下所示。 /* include/linux/etherdevice.h *****************************************************/ //etherdevice.h 头文件内容 /* include/linux/etherdevice.h *****************************************************/ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 路由器与网关的区别:路由器和网关都是网络中连接不同子网的主机。二者都可对到达该主 机的数据包进行转发。但二者具有本质区别。路由器相对网关而言较为简单。路由器工作在 OSI 模型的物理层,链路层和网络层。路由器在多个互联网之间中继包。它们将来自某个网 络的包路由到互联网上任何可能的目的网络中。路由器区别于网关的最大之处在于路由器本 身只能在使用相同协议的网络中转发数据包。而网关是一个协议转换器,其可以接收一种协 议的数据包如 AppleTalk 格式的包,然后在转发之前将其转换成另一种协议形式的包如 TCP/IP 格式继而发送出去。另外网关可能工作在 OSI 模型的所有七层之中。另外需要澄清 的一点多协议路由器仅仅表示该路由器可转发多种协议格式的包,如一个路由器既可转发 IP 格式的包,亦可转发 IPX(Novell 网的网络层协议)格式的包,如此工作模式的路由器对 于每种协议都有一张路由表。注意多协议路由器与单协议路由器本质相同,且区别于网关, 多协议路由器仍然不可对数据包进行协议上的格式转换,而仅仅在于其内部集成了多个协议 的路由器,使得其可以转发多种协议格式的数据包,而网关可更改数据包的格式。 1.1 etherdevice.h 头文件 该头文件声明了以太网用于建立 MAC 首部的函数,其中 eth_header 函数用于首次进行 MAC 首部建立,而 eth_rebuild_header 则用于首次建立 MAC 首部失败后之后的重新建立。当暂时 无法确知 IP 地址所对应的硬件地址时,系统会在之后调用 eth_rebuild_header 函数重新建立 MAC 首部。eth_trans_type 被处理接收数据包的函数调用用以得知以太网首部中的 TYPE 字 段值,系统将根据这个值调用相应的上一层处理函数(如对应 IP 数据包为 ip_rcv)。 /* include/linux/etherdevice.h *****************************************************/ /* INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. NET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions for the Ethernet handlers. * * Version: @(#)eth.h 1.0.4 05/13/93 * * Authors: Ross Biro, * Fred N. van Kempen, * * Relocated to include/linux where it belongs by Alan Cox * * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. * * WARNING: This move may well be temporary. This file will get merged with others RSN. * */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #ifndef _LINUX_ETHERDEVICE_H #define _LINUX_ETHERDEVICE_H #include #ifdef __KERNEL__ extern int eth_header(unsigned char *buff, struct device *dev, unsigned short type, void *daddr, void *saddr, unsigned len, struct sk_buff *skb); extern int eth_rebuild_header(void *buff, struct device *dev, unsigned long raddr, struct sk_buff *skb); extern unsigned short eth_type_trans(struct sk_buff *skb, struct device *dev); #endif #endif /* _LINUX_ETHERDEVICE_H */ /* include/linux/etherdevice.h *****************************************************/ 1.2 icmp.h 头文件 该头文件定义 ICMP 首部以及 ICMP 错误类型。一个 ICMP 错误包括两个部分:类型(type) 和代码(code),分别对应 ICMP 首部中的 type 和 code 字段。 ICMP 本身是网络层协议,但其报文段并不是直接传送给链路层。实际上,ICMP 报文首先 要封装成 IP 数据报。图 1.1 显示了相关关系。 图 1.1 ICMP 的封装 根据 RFC-792,有如下类型 ICMP 错误。 1. 目的端不可达:Type=3 该 ICMP 错误对应的首部格式如图 1.2 所示。 图 1.2 目的端不可达 ICMP 首部格式 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 其中 Code 取值表示具体的错误代码,可取如下值: 0— 网络不可达 1— 主机不可达 2— (网络层)协议不可达(目的端不认识该协议,无相应处理函数) 3— 端口不可达 4— 需要分片但 IP 首部中 DF 位置 1 5— 源路由失败 其它 Code 值如该文件中定义。 该文件中第一簇 define 语句定义了 ICMP 错误类型,之后的定义为某类型对应的具体代码值 如第二簇 define 语句定义的是类型 Type=3 对应的所有的可能代码值,类型值表示错误的分 类,而代码值表示的是某个分类中的具体的错误类型。 检验和字段包括 ICMP 报文段的所有数据,包括 ICMP 首部。 首部中“原 IP 首部+8 字节 IP 数据”指的是引起该 ICMP 错误报文的数据包中 IP 首部及其 8 字节 IP 数据。如果从本地的角度看,由于之前发送给远端的数据包存在某种错误,由于 这种错误现在本地接收到一个 ICMP 错误通知报文,该报文中包含了之前这个存在错误的数 据包的 IP 首部及其 8 字节的 IP 数据。 返回原 IP 首部及其 8 字节 IP 数据可以使得本地得知哪个应用程序(从端口号得知,而端口 号包含在 8 字节的 IP 数据中)发送了这个有问题的数据包,从而通知该应用程序或者采取 其它相关措施如错误不可恢复时则简单关闭发送该问题数据包的套接字。 /* include/linux/icmp.h *********************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions for the ICMP protocol. * * Version: @(#)icmp.h 1.0.3 04/28/93 * * Author: Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_ICMP_H #define _LINUX_ICMP_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define ICMP_ECHOREPLY 0 /* Echo Reply */ #define ICMP_DEST_UNREACH 3 /* Destination Unreachable */ #define ICMP_SOURCE_QUENCH 4 /* Source Quench */ #define ICMP_REDIRECT 5 /* Redirect (change route) */ #define ICMP_ECHO 8 /* Echo Request */ #define ICMP_TIME_EXCEEDED 11 /* Time Exceeded */ #define ICMP_PARAMETERPROB 12 /* Parameter Problem */ #define ICMP_TIMESTAMP 13 /* Timestamp Request */ #define ICMP_TIMESTAMPREPLY 14 /* Timestamp Reply */ #define ICMP_INFO_REQUEST 15 /* Information Request */ #define ICMP_INFO_REPLY 16 /* Information Reply */ #define ICMP_ADDRESS 17 /* Address Mask Request */ #define ICMP_ADDRESSREPLY 18 /* Address Mask Reply */ /* Codes for UNREACH. */ #define ICMP_NET_UNREACH 0 /* Network Unreachable */ #define ICMP_HOST_UNREACH 1 /* Host Unreachable */ #define ICMP_PROT_UNREACH 2 /* Protocol Unreachable */ #define ICMP_PORT_UNREACH 3 /* Port Unreachable */ #define ICMP_FRAG_NEEDED 4 /* Fragmentation Needed/DF set */ #define ICMP_SR_FAILED 5 /* Source Route failed */ #define ICMP_NET_UNKNOWN 6 #define ICMP_HOST_UNKNOWN 7 #define ICMP_HOST_ISOLATED 8 #define ICMP_NET_ANO 9 #define ICMP_HOST_ANO 10 #define ICMP_NET_UNR_TOS 11 #define ICMP_HOST_UNR_TOS 12 /* Codes for REDIRECT. */ #define ICMP_REDIR_NET 0 /* Redirect Net */ #define ICMP_REDIR_HOST 1 /* Redirect Host */ #define ICMP_REDIR_NETTOS 2 /* Redirect Net for TOS */ #define ICMP_REDIR_HOSTTOS 3 /* Redirect Host for TOS */ /* Codes for TIME_EXCEEDED. */ #define ICMP_EXC_TTL 0 /* TTL count exceeded */ #define ICMP_EXC_FRAGTIME 1 /* Fragment Reass time exceeded */ struct icmphdr { unsigned char type; unsigned char code; unsigned short checksum; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 union { struct { unsigned short id; unsigned short sequence; } echo; unsigned long gateway; } un; }; struct icmp_err { int errno; unsigned fatal:1; }; #endif /* _LINUX_ICMP_H */ /* include/linux/icmp.h *********************************************************/ 2.超时错误:Type=11 该错误类型对应的 ICMP 首部格式如图 1.3 所示。 图 1.3 ICMP 超时错误首部格式 其中 Code 字段可取如下值: 0— 传输中 TTL(生存时间)超时 1— 分片组合时间超时(某个数据包的所有分片在规定时间内未完全到达) 注意对于 Code=1,如果第一个分片尚未到达而此时发生超时,则不发送 ICMP 错误报文。 其它字段与前文中所述意义相同。 3.参数错误:Type=12 图 1.4 显示了该错误类型对应的 ICMP 首部格式。 图 1.4 ICMP 参数错误类型首部格式 此处首部中多出一个指针字段。如果 Code=0,则该字段表示错误参数所在的字节偏移量。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 为使指针字段有效,一般代码(Code)字段取值为 0。其它字段意义同前文介绍。 4.源端节制(Source Quench)报文:Type=4 图 1.5 显示了该类型 ICMP 报文首部格式。 图 1.5 ICMP 源端节制首部格式 此种类型的 ICMP 报文并不是由本地发送的一个问题报文引起的。而是远端为了缓解接收压 力发送给本地的通知其减缓发送报文的速度,本地接收到该报文后所采取的策略应是延缓发 送本地报文(即进行源端节制),但实现上(如本版本 LINUX 网络代码)只是更新统计信 息,并不采取具体的节制措施。注意此处所指的远端不一定是数据包的最终目的端,大多指 的是中间的某个路由器或网关。当这些路由器或网关没有足够内存缓存需要转发的数据包 时,就会发送一个这样的 ICMP 报文给这些数据包的发送端(即源端)。 其中 Code 字段取值应为 0。其它字段意义同前文。 5.重定向(Redirect)报文:Type=5 图 1.6 显示了该 ICMP 报文类型对应的首部格式。 图 1.6 ICMP 重定向报文首部格式 其中 Code 可取如下值: 0—基于网络的重定向报文 1—基于主机的重定向报文 2—基于服务类型和网络的重定向报文 3—基于服务类型和主机的重定向报文 新网关 IP 地址表示数据包应该发送到的网关 IP 地址。 一个网关在如下情况下会发送一个重定向 ICMP 报文: 网关 G1 从其连接的局域网某个主机接收到一个数据包,G1 查询路由表项得到下一站网关 G2 的 IP 地址,如果发现 G2 的 IP 地址与所转发数据包中内含的源端 IP 地址同属于一个网 络,即表示 G2 与源端主机在同一个局域网中,此时发送一个 ICMP 重定向报文给源端,通 知其今后直接发送报文给 G2,不需要经过 G1 转发,此时 ICMP 首部中新网关 IP 地址即是 G2 的 IP 地址。另外一种判断方式是:该数据包进站接口与转发该数据包时的出站接口是同 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 一个接口。 6.回复(Echo)和回复应答(Echo Reply)报文 Type=8 回复报文,Type=0 回复应答报文 图 1.7 显示了这两种类型报文的 ICMP 首部格式。 图 1.7 ICMP 回复和回复应答报文首部格式 其中 Code 字段值在两种情况下均取 0。 标志码和序列码由发送回复报文端填写,回复应答报文发送端必须原样返回这两个字段的值 进行匹配,使得源端确定此应答报文是否是对本地回复报文的应答。 数据部分由回复报文发送端填写,回复应答发送端也必须原样返回这些数据。 其它字段意义同前文。 7.时间戳(Timestamp)和时间戳应答(Timestamp Reply)报文 Type=13 时间戳报文; Type=14 时间戳应答报文 图 1.8 显示了这两种报文类型的 ICMP 首部格式。 图 1.8 ICMP 时间戳和时间戳应答报文首部格式 其中 Code 取值在两种情况下均为 0。 起始时间戳字段值由本地发送端填写,表示该起始 ICMP 时间戳报文由本地发送的时间。接 收时间戳字段表示远端接收到该报文时的时间值,发送时间戳表示远端发送应答报文时的时 间值。实现上,远端发送时间戳应答报文时,复制所接收时间戳报文中的标志码,序列码, 起始时间戳。之后填写其它首部字段如类型值变为 14。理论上,接收时间戳和发送时间戳 是两个不同的值,但实现上,这两个字段都被赋值为相同的值即发送该应答报文时的时间值。 8.信息查询(Information Request)和信息应答(Information Reply)报文 Type=15 信息查询报文; Type=16 信息应答报文 图 1.9 显示了这两种报文的 ICMP 首部格式。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1.9 ICMP 信息查询和信息应答首部格式 其中 Code 字段取值均为 0。标志码与序列码用于匹配。 信息查询报文中 IP 首部中源端地址初始化为本地 IP 地址,而目的地址被初始化为 0,表示 本地网络。相应的信息应答报文 IP 首部中源端和目的端 IP 地址则按正常方式填写。 信息查询和信息应答报文用于主机查询该主机连接的网络数。 综合以上,有如下 ICMP 类型(以 Type 值为依据): 0—回复应答(Echo Reply)报文 3—目的端不可达(Destination Unreachable)报文 4—源端节制(Source Quench)报文 5—重定向(Redirect)报文 8—回复(Echo)报文 11—超时(Time Exceeded)报文 12—参数错误(Parameter Problem)报文 13—时间戳(Timestamp)报文 14—时间戳应答(Timestamp Reply)报文 15—信息查询(Information Request)报文 16—信息应答(Information Reply) 报文 此时对应查看 icmp.h 文件中各种常数定义应不难理解。 在该文件尾部定义有 icmp_err 结构,如下所示: struct icmp_err { int errno; unsigned fatal:1; }; 结构中 errno 字段表示具体的错误,而 fatal 字段值表示这种错误是否是可恢复的:1-不可恢 复,0-可恢复。如果不可恢复,则关闭套接字连接。 该结构使用在 icmp.c 文件中,以下代码片断摘取自 net/icmp.c 文件, /* An array of errno for error messages from dest unreach. */ struct icmp_err icmp_err_convert[] = { { ENETUNREACH, 0 }, /* ICMP_NET_UNREACH */ { EHOSTUNREACH, 0 }, /* ICMP_HOST_UNREACH */ { ENOPROTOOPT, 1 }, /* ICMP_PROT_UNREACH */ { ECONNREFUSED, 1 }, /* ICMP_PORT_UNREACH */ { EOPNOTSUPP, 0 }, /* ICMP_FRAG_NEEDED */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 { EOPNOTSUPP, 0 }, /* ICMP_SR_FAILED */ { ENETUNREACH, 1 }, /* ICMP_NET_UNKNOWN */ { EHOSTDOWN, 1 }, /* ICMP_HOST_UNKNOWN */ { ENONET, 1 }, /* ICMP_HOST_ISOLATED */ { ENETUNREACH, 1 }, /* ICMP_NET_ANO */ { EHOSTUNREACH, 1 }, /* ICMP_HOST_ANO */ { EOPNOTSUPP, 0 }, /* ICMP_NET_UNR_TOS */ { EOPNOTSUPP, 0 } /* ICMP_HOST_UNR_TOS */ }; ICMP 协议处理函数在接收到一个 ICMP 错误报告时,根据接收到的具体错误查询该表,根 据 fatal 字段值确定随后的操作。如果该错误不可恢复(fatal=1),则进行套接字关闭操作, 否则忽略之。 1.3 if.h 头文件 该文件定义有接口标志位,ifaddr 结构,ifreq 结构,ifmap 结构,ifconf 结构。 接口标志位标志接口的状态。 ifmap,ifreq,ifconf 结构均使用在选项设置和获取函数中。 ifaddr 目前暂未使用。ifmap 结构用于设置设备的基础信息,这一点可以从 ifmap 结构中字 段与 device 结构(netdevice.h)中字段间匹配关系看出。ifmap 结构目前作为 set_config, ether_config 函数的参数而存在。ifreq,ifconf 也是主要用于设备接口信息的获取和设置,使 用在 dev_ifconf, dev_ifsioc 函数中(二者均定义在 dev.c 文件中)。 有关各个结构中字段的 说明在文件中有简单的注释。 /* include/linux/if.h ************************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Global definitions for the INET interface module. * * Version: @(#)if.h 1.0.2 04/18/93 * * Authors: Original taken from Berkeley UNIX 4.3, (c) UCB 1982-1988 * Ross Biro, * Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_IF_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define _LINUX_IF_H #include /* for "caddr_t" et al */ #include /* for "struct sockaddr" et al */ /* Standard interface flags. */ #define IFF_UP 0x1 /* interface is up */ #define IFF_BROADCAST 0x2 /* broadcast address valid */ #define IFF_DEBUG 0x4 /* turn on debugging */ #define IFF_LOOPBACK 0x8 /* is a loopback net */ #define IFF_POINTOPOINT 0x10 /* interface is has p-p link */ #define IFF_NOTRAILERS 0x20 /* avoid use of trailers */ #define IFF_RUNNING 0x40 /* resources allocated */ #define IFF_NOARP 0x80 /* no ARP protocol */ #define IFF_PROMISC 0x100 /* receive all packets */ /* Not supported */ #define IFF_ALLMULTI 0x200 /* receive all multicast packets*/ #define IFF_MASTER 0x400 /* master of a load balancer */ #define IFF_SLAVE 0x800 /* slave of a load balancer */ #define IFF_MULTICAST 0x1000 /* Supports multicast */ /* * The ifaddr structure contains information about one address * of an interface. They are maintained by the different address * families, are allocated and attached when an address is set, * and are linked together so all addresses for an interface can * be located. */ struct ifaddr { struct sockaddr ifa_addr; /* address of interface 接口地址*/ union { struct sockaddr ifu_broadaddr; //接口广播地址 struct sockaddr ifu_dstaddr; //接口目的端地址,只使用在点对点连接中 } ifa_ifu; struct iface *ifa_ifp; /* back-pointer to interface */ struct ifaddr *ifa_next; /* next address for interface */ }; #define ifa_broadaddr ifa_ifu.ifu_broadaddr /* broadcast address */ #define ifa_dstaddr ifa_ifu.ifu_dstaddr /* other end of link */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* * Device mapping structure. I'd just gone off and designed a * beautiful scheme using only loadable modules with arguments * for driver options and along come the PCMCIA people 8) * * Ah well. The get() side of this is good for WDSETUP, and it'll * be handy for debugging things. The set side is fine for now and * being very small might be worth keeping for clean configuration. */ struct ifmap { unsigned long mem_start; //硬件读写缓冲区首地址 unsigned long mem_end; //硬件读写缓冲区尾地址 unsigned short base_addr; //本设备所使用 I/O 端口地址 unsigned char irq; //本设备所使用的中断号 unsigned char dma; //本设备所使用的 dma 通道号 unsigned char port; 对于 device 结构中的 if_port 字段,该字段使用在某些特殊情况下 /* 3 bytes spare */ }; /* * Interface request structure used for socket * ioctl's. All interface ioctl's must have parameter * definitions which begin with ifr_name. The * remainder may be interface specific. */ //每个 ifreq 表示一个设备接口的信息。 struct ifreq { #define IFHWADDRLEN 6 #define IFNAMSIZ 16 union { char ifrn_name[IFNAMSIZ]; /* if name, e.g. "en0" 设备接口名*/ char ifrn_hwaddr[IFHWADDRLEN]; /* Obsolete */ } ifr_ifrn; union { struct sockaddr ifru_addr; //设备 IP 地址 struct sockaddr ifru_dstaddr; //点对点连接中对端地址 struct sockaddr ifru_broadaddr; //广播 IP 地址 struct sockaddr ifru_netmask; //地址掩码 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 struct sockaddr ifru_hwaddr; //设备对应的硬件地址 short ifru_flags; //设备对应的标志字段值 int ifru_metric; //代价值 int ifru_mtu; //设备对应的最大传输单元 struct ifmap ifru_map; //该结构用于设置/获取设备的基本信息 char ifru_slave[IFNAMSIZ]; /* Just fits the size */ caddr_t ifru_data; /*caddr_t 定义在 ctype.h 文件中:typedef char * caddr_t *该字段的作用相当于 raw,即只表示存储单元或存储数据。*/ } ifr_ifru; }; #define ifr_name ifr_ifrn.ifrn_name /* interface name */ #define old_ifr_hwaddr ifr_ifrn.ifrn_hwaddr /* interface hardware */ #define ifr_hwaddr ifr_ifru.ifru_hwaddr /* MAC address */ #define ifr_addr ifr_ifru.ifru_addr /* address */ #define ifr_dstaddr ifr_ifru.ifru_dstaddr /* other end of p-p lnk */ #define ifr_broadaddr ifr_ifru.ifru_broadaddr /* broadcast address */ #define ifr_netmask ifr_ifru.ifru_netmask /* interface net mask */ #define ifr_flags ifr_ifru.ifru_flags /* flags */ #define ifr_metricifr_ifru.ifru_metric /* metric */ #define ifr_mtu ifr_ifru.ifru_mtu /* mtu */ #define ifr_map ifr_ifru.ifru_map /* device map */ #define ifr_slave ifr_ifru.ifru_slave /* slave device */ #define ifr_data ifr_ifru.ifru_data /* for use by interface */ /* * Structure used in SIOCGIFCONF request. * Used to retrieve interface configuration * for machine (useful for programs which * must know all networks accessible). */ //该结构用于信息获取,ifc_len 表示缓冲区长度,而 ifc_ifcu 字段则存储具体信息。 //信息是以 ifreq 结构为单元,ifcu_buf 指向的缓冲区中存储的信息是一个 ifreq 结构数组。 //每个 ifreq 结构代表一个设备接口的信息。系统将遍历系统中所有设备接口,向 ifcu_buf 指 //指向的缓冲区中填写设备信息,直到缓冲区满或者设备接口遍历完。 struct ifconf { int ifc_len; /* size of buffer */ union { caddr_t ifcu_buf; struct ifreq *ifcu_req; } ifc_ifcu; }; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define ifc_buf ifc_ifcu.ifcu_buf /* buffer address */ #define ifc_req ifc_ifcu.ifcu_req /* array of structures */ /* BSD UNIX expects to find these here, so here we go: */ #include #include #endif /* _NET_IF_H */ /* include/linux/if.h ************************************************************/ 1.4 if_arp.h 头文件 该文件定义 ARP 首部以及与之相关的常数值。 ARP 协议全称为地址解析协议(Address Resolution Protocol)。ARP 为 IP 地址到对应的硬件 地址之间提供动态映射。ARP 数据包的封装形式如图 1.10 所示。 图 1.10 用于以太网的 ARP 请求或应答分组格式 ARP 请求使用广播形式,而 ARP 应答则使用单播形式。一个 ARP 请求数据包是用于询问 协议地址(一般为 IP 地址)所对应的硬件地址(即以太网首部中使用的地址,亦称以太网 地址)。 以太网头部中的目的硬件地址对于 ARP 请求而言设置为全 1,表示向网络广播 ARP 请求。 源端以太网地址表示该 ARP 数据包的本地发送端硬件地址。以太网类型字段对于 ARP 请求 和应答而言均为 0x0806. 硬件类型字段表示硬件地址的类型。该字段值为 1 表示以太网地址。 协议类型字段表示要映射的协议地址类型。该字段值为 0x0800 即表示 IP 地址。 接下来两个 1-byte 的字段,即硬件地址长度和协议地址长度字分别指出硬件地址和协议地 址的长度,以字节为单位。对于以太网上 IP 地址的请求与应答而言,它们的值分别为 6 和 4。 图 3 中 op 字段为两个字节,该字段具体表示 ARP 协议的类型: ARP 请求,对应值为 1 ARP 应答,对应值为 2 RARP 请求,值为 3 RARP 应答,值为 4 该字段是必须的。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 接下来的四个字段是发送端硬件地址(以太网地址),发送端的协议地址(IP 地址),目的 端硬件地址和目的端协议地址。此处有一个重复之地:在以太网的数据帧以太网首部中和 ARP 协议首部中都有发送端的硬件地址。 对于一个 ARP 请求而言,除目的端硬件地址字段外,其他字段都有填充值。当系统接收到 一份目的端协议地址为本机的协议地址的 ARP 请求报文后,它就将其硬件地址填充到相应 字段,然后在修改 ARP 首部一些字段值之后(如修改操作字段值为 2,修改发送端和目的 端硬件地址和协议地址),就将数据包发送出去(注意此时是单播发送)。 /* include/linux/if_arp.h ********************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Global definitions for the ARP (RFC 826) protocol. * * Version: @(#)if_arp.h 1.0.1 04/16/93 * * Authors: Original taken from Berkeley UNIX 4.3, (c) UCB 1986-1988 * Portions taken from the KA9Q/NOS (v2.00m PA0GRI) source. * Ross Biro, * Fred N. van Kempen, * Florian La Roche. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_IF_ARP_H #define _LINUX_IF_ARP_H //arp 首部中硬件地址类型值 /* ARP protocol HARDWARE identifiers. */ #define ARPHRD_NETROM 0 /* from KA9Q: NET/ROM pseudo*/ #define ARPHRD_ETHER 1 /* Ethernet 10Mbps */ #define ARPHRD_EETHER 2 /* Experimental Ethernet */ #define ARPHRD_AX25 3 /* AX.25 Level 2 */ #define ARPHRD_PRONET 4 /* PROnet token ring */ #define ARPHRD_CHAOS 5 /* Chaosnet */ #define ARPHRD_IEEE802 6 /* IEEE 802.2 Ethernet- huh? */ #define ARPHRD_ARCNET 7 /* ARCnet */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define ARPHRD_APPLETLK 8 /* APPLEtalk */ /* Dummy types for non ARP hardware */ #define ARPHRD_SLIP 256 #define ARPHRD_CSLIP 257 #define ARPHRD_SLIP6 258 #define ARPHRD_CSLIP6 259 #define ARPHRD_RSRVD 260 /* Notional KISS type */ #define ARPHRD_ADAPT 264 #define ARPHRD_PPP 512 #define ARPHRD_TUNNEL 768 /* IPIP tunnel */ //arp 首部中操作码字段值 /* ARP protocol opcodes. */ #define ARPOP_REQUEST 1 /* ARP request */ #define ARPOP_REPLY 2 /* ARP reply */ #define ARPOP_RREQUEST 3 /* RARP request */ #define ARPOP_RREPLY 4 /* RARP reply */ //该结构用于设置/获取 arp 表项信息,在 arp_req_set, arp_req_get 函数中(arp.c)使用。 /* ARP ioctl request. */ struct arpreq { struct sockaddr arp_pa; /* protocol address */ struct sockaddr arp_ha; /* hardware address */ int arp_flags;/* flags */ struct sockaddr arp_netmask; /* netmask (only for proxy arps) */ }; //如下定义 ARP 表项的状态值。 /* ARP Flag values. */ #define ATF_COM 0x02 /* completed entry (ha valid) */ #define ATF_PERM 0x04 /* permanent entry */ #define ATF_PUBL 0x08 /* publish entry */ #define ATF_USETRAILERS 0x10 /* has requested trailers */ #define ATF_NETMASK 0x20 /* want to use a netmask (only for proxy entries) */ /* * This structure defines an ethernet arp header. */ //ARP 首部,结合图 1.10 理解该首部定义。 struct arphdr { unsigned short ar_hrd; /* format of hardware address */ unsigned short ar_pro; /* format of protocol address */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 unsigned char ar_hln; /* length of hardware address */ unsigned char ar_pln; /* length of protocol address */ unsigned short ar_op; /* ARP opcode (command) */ #if 0 /* * Ethernet looks like this : This bit is variable sized however... */ unsigned char ar_sha[ETH_ALEN]; /* sender hardware address */ unsigned char ar_sip[4]; /* sender IP address */ unsigned char ar_tha[ETH_ALEN]; /* target hardware address */ unsigned char ar_tip[4]; /* target IP address */ #endif }; #endif /* _LINUX_IF_ARP_H */ /* include/linux/if_arp.h ********************************************************/ 1.5 if_ether.h 头文件 以太网首部 ethhdr 定义和一些常量定义。文件尾部 enet_statistics 结构用于统计信息记录。 常量主要是以太网首部中类型字段可取值。 /* include/linux/if_ether.h *******************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Global definitions for the Ethernet IEEE 802.3 interface. * * Version: @(#)if_ether.h 1.0.1a 02/08/94 * * Author: Fred N. van Kempen, * Donald Becker, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_IF_ETHER_H #define _LINUX_IF_ETHER_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* IEEE 802.3 Ethernet magic constants. The frame sizes omit the preamble and FCS/CRC (frame check sequence). */ //一些长度常量定义,如以太网硬件地址长度为 6,以太网首部长度为 14,等。 #define ETH_ALEN 6 /* Octets in one ethernet addr */ #define ETH_HLEN 14 /* Total octets in header. */ #define ETH_ZLEN 60 /* Min. octets in frame sans FCS */ //IP 首部及其数据部分长度,通常称之为 MTU。 #define ETH_DATA_LEN 1500 /* Max. octets in payload */ //帧长度,1514=1500+14,有时加上帧尾部的 FCS 字段长度为 1518。 #define ETH_FRAME_LEN 1514 /* Max. octets in frame sans FCS */ //以太网首部中类型字段可取值。 /* These are the defined Ethernet Protocol ID's. */ #define ETH_P_LOOP 0x0060 /* Ethernet Loopback packet */ #define ETH_P_ECHO 0x0200 /* Ethernet Echo packet */ #define ETH_P_PUP 0x0400 /* Xerox PUP packet */ #define ETH_P_IP 0x0800 /* Internet Protocol packet */ #define ETH_P_ARP 0x0806 /* Address Resolution packet */ #define ETH_P_RARP 0x8035 /* Reverse Addr Res packet */ #define ETH_P_X25 0x0805 /* CCITT X.25 */ #define ETH_P_ATALK 0x809B /* Appletalk DDP */ #define ETH_P_IPX 0x8137 /* IPX over DIX */ #define ETH_P_802_3 0x0001 /* Dummy type for 802.3 frames */ #define ETH_P_AX25 0x0002 /* Dummy protocol id for AX.25 */ #define ETH_P_ALL 0x0003 /* Every packet (be careful!!!) */ #define ETH_P_802_2 0x0004 /* 802.2 frames */ #define ETH_P_SNAP 0x0005 /* Internal only */ //以太网首部定义。 /* This is an Ethernet frame header. */ struct ethhdr { unsigned char h_dest[ETH_ALEN]; /* destination eth addr */ unsigned char h_source[ETH_ALEN]; /* source ether addr */ unsigned short h_proto; /* packet type ID field */ }; //进出站数据包统计信息结构 /* Ethernet statistics collection data. */ struct enet_statistics{ int rx_packets; /* total packets received */ int tx_packets; /* total packets transmitted */ int rx_errors; /* bad packets received */ int tx_errors; /* packet transmit problems */ int rx_dropped; /* no space in linux buffers */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 int tx_dropped; /* no space available in linux */ int multicast; /* multicast packets received */ int collisions; /* detailed rx_errors: */ int rx_length_errors; int rx_over_errors; /* receiver ring buff overflow */ int rx_crc_errors; /* recved pkt with crc error */ int rx_frame_errors; /* recv'd frame alignment error */ int rx_fifo_errors; /* recv'r fifo overrun */ int rx_missed_errors; /* receiver missed packet */ /* detailed tx_errors */ int tx_aborted_errors; int tx_carrier_errors; int tx_fifo_errors; int tx_heartbeat_errors; int tx_window_errors; }; #endif /* _LINUX_IF_ETHER_H */ /* include/linux/if_ether.h *******************************************************/ 1.6 if_plip.h 头文件 PLIP 是 Parallel Line Internet Procotol 的简称,意为并行线网络协议。PLIP 是一种使用特制 电缆通过计算机并口进行数据传送的协议。其类似于使用串口进行数据传输的 SLIP 协议, 不过 PLIP 每次可以同时传输 4 个比特。这种特制电缆称为 LabLink 或 null-printer 电缆,允 许两台计算机通过各自的并口直接建立通信通道,无需网卡或者调制解调器的帮助。这种 LabLink 电缆将两台计算机并口的五个信号管脚直接相连。由于缺少内部时钟同步信号,同 步信号需要软件明确完成。于是在五个信号管脚中特意分出一个管脚用于信号同步。故每次 可以传送 4 个比特的数据。这些信号管脚上的逻辑值可以通过 I/O 端口读写命令读取。PLIP 协议现在使用很少,已逐渐被基于网络的以太网协议或其他点对点连接协议所取代。现在只 在一些老式的计算机上才有并口,新式的计算机上已不附带并口。 /* include/linux/if_plip.h ********************************************************/ /* * NET3 PLIP tuning facilities for the new Niibe PLIP. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. * 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 */ #ifndef _LINUX_IF_PLIP_H #define _LINUX_IF_PLIP_H #include #define SIOCDEVPLIP SIOCDEVPRIVATE //PLIP 配置命令结构 struct plipconf { unsigned short pcmd; //设置(PLIP_SET_TIMEOUT)或获取(PLIP_GET_TIMEOUT) unsigned long nibble; //数据传输超时时间 unsigned long trigger; //同步信号传输超时时间 }; #define PLIP_GET_TIMEOUT 0x1 #define PLIP_SET_TIMEOUT 0x2 #endif /* include/linux/if_plip.h *********************************************************/ 1.7 if_slip.h 头文件 S L I P的全称是Serial Line IP。它是一种在串行线路上对IP数据报进行封装的简单形式,在 RFC 1055中有详细描述。SLIP适用于家庭中每台计算机几乎都有的RS-232串行端口和高速 调制解调器接入网络。 下面的规则描述了SLIP协议定义的帧格式: 1) IP数据报以一个称作END(0xc0)的特殊字符结束。同时,为了防止数据报到来之前 的线路噪声被当成数据报内容,大多数实现在数据报的开始处也传一个END字符(如果有 线路噪声,那么END字符将结束这份错误的报文。这样当前的报文得以正确地传输,而前 一个错误报文交给上层后,会发现其内容毫无意义而被丢弃)。 2) 如果IP报文中某个字符为END,那么就要连续传输两个字节0xdb和0xdc来取代它。 0xdb这个特殊字符被称作SLIP的ESC字符,但是它的值与ASCII码的ESC字符 (0x1b)不同。 3) 如果IP报文中某个字符为SLIP的ESC字符,那么就要连续传输两个字节0xdb和 0xdd来取代它。 由于串行线路的速率通常较低(19200 b/s或更低),而且通信经常是交互式的(如Telnet 和Rlogin,二者都使用TCP),因此在S L I P线路上有许多小的TCP分组进行交换。为了传 送1个字节的数据需要20个字节的IP首部和20个字节的TCP首部,总数超过40个字节。 SLIP的变体有CSLIP即压缩版的SLIP,CSLIP对SLIP报文中各个首部进行必要的压缩以增加 数据传输率。CSLIP一般能把上面的40个字节压缩到3或5个字节。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* include/linux/if_slip.h *********************************************************/ /* * Swansea University Computer Society NET3 * * This file declares the constants of special use with the SLIP/CSLIP/ * KISS TNC driver. */ #ifndef __LINUX_SLIP_H #define __LINUX_SLIP_H #define SL_MODE_SLIP 0 #define SL_MODE_CSLIP 1 #define SL_MODE_KISS 4 #define SL_OPT_SIXBIT 2 #define SL_OPT_ADAPTIVE 8 #endif /* include/linux/if_slip.h ********************************************************/ 该文件主要是对一些标志位的声明。 1.8 igmp.h 头文件 IGMP 是 Internet Group Management Protocol 的简称,意为网络组管理协议。IGMP 已有几个 版本,以下分析以版本 2 为依据(不过注意本版本网络实现代码只支持 IGMP 版本 1,第二 章中对 net/inet/igmp.c 文件分析时,给出版本 1 格式)。图 1-11 显示了 IGMP 的报文类型。 图 1-11 IGMP 报文类型 图 1-12 给出了 IGMPv2 报文首部格式。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-12 IGMP 报文首部格式 类型字段:如表 1-2 所示。 最大响应时间:这个 8 位字段定义了查询必须在多长时间内回答,其单位为 0.1 秒。该字段 只在查询报文中有效,在其它类型报文中该字段值为 0。 检验和:计算 IGMP 首部及其数据的检验和。 组地址:在一般查询报文中该字段值为 0。这个字段在特殊查询报文,成员关系报文以及退 出报文中为组多播地址(groupip)。 表 1-2 IGMP 类型字段值 类型 值 一般或特殊 0x11 成员关系报告 0x16 退出报告 0x17 类似于 ICMP,虽然 IGMP 协议属于网络层协议,与 IP 协议同属于一个层次,但 IGMP 报 文段仍然需要首先被封装在 IP 数据报中,才能传送给链路层。 图 1-13 显示了报文之间的关系。 图 1-13 IGMP 分组封装格式 有关 IGMPv2 的详细介绍请参考 RFC-2236。 /* include/linux/igmp.h *********************************************************/ /* * Linux NET3: Internet Gateway Management Protocol [IGMP] * * Authors: * Alan Cox * * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_IGMP_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define _LINUX_IGMP_H /* * IGMP protocol structures */ //IGMP 版本 1 首部定义,结合上文介绍不难理解 struct igmphdr { unsigned char type; unsigned char unused; //对应版本 2 中的最大响应时间 unsigned short csum; unsigned long group; }; #define IGMP_HOST_MEMBERSHIP_QUERY 0x11 /* From RFC1112 */ //成员关系报告,这是 IGMPv1 版本定义的值,IGMPv2 版本已改为 16。 #define IGMP_HOST_MEMBERSHIP_REPORT 0x12 /* Ditto */ #define IGMP_HOST_LEAVE_MESSAGE 0x17 /* An extra BSD seems to send */ //对于任何一个主机而言,其网络接口在初始化时默认加入 224.0.0.1 多播组。 /* 224.0.0.1 */ #define IGMP_ALL_HOSTS htonl(0xE0000001L) /* * struct for keeping the multicast list in */ #ifdef __KERNEL__ //多播地址及对应的发送接收接口 struct ip_mc_socklist { unsigned long multiaddr[IP_MAX_MEMBERSHIPS]; /* This is a speed trade off */ struct device *multidev[IP_MAX_MEMBERSHIPS]; }; ip_mc_socklist 结构被 sock 结构使用,在本书的分析中,我们可以知道每个套接字都唯一的 对应一个 sock 结构,一般而言,一个套接字是与一个进程相绑定的,当进程加入到一个多 播组中时,对应的 sock 结构中必须维护加入的多播组的信息,从而底层网络实现可以根据 该信息判断是否将接收到的一个多播数据报复制一份送给该 sock 结构对应的进程。所以 ip_mc_socklist 结构从进程的角度上表示了加入的多播组,支持的最大多播组数为 IP_MAX_MEMBERSHIPS. //多播链表 struct ip_mc_list { 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 struct device *interface; //接收或发送对应多播数据包的接口 unsigned long multiaddr; //对应的多播地址 struct ip_mc_list *next; struct timer_list timer; //定时器,用于多播查询中,每个主机对于多播查询报文并不立即 //应答,而是延迟一段随机时间后,如果在此时间内没有其它具有 //相同多播组地址报文被其它主机发送,则方才发送应答报文。 int tm_running; int users; }; ip_mc_list 结构被 device 结构使用,每个 device 结构都唯一对应主机上一个网络设备(此处 不考虑虚拟设备)。ip_mc_list 结构即被 device 结构使用,维护多播 IP 地址信息。进程对加 入的多播组由一个二元组构成:对应的网络设备以及多播组 IP 地址。换句话说,一个网络 接口可以对应本机上多个进程。所以需要有一个结构维护对这些 IP 地址的信息。ip_mc_list 即用于此目的。在第二章中分析 igmp.c 文件时,我们将这种分析该结构的作用。 //ip_mc_head 定义在 igmp.c 文件中,作为本地多播地址链表。 extern struct ip_mc_list *ip_mc_head; //函数声明,这些函数定义在 igmp.c 文件中 extern int igmp_rcv(struct sk_buff *, struct device *, struct options *, unsigned long, unsigned short, unsigned long, int , struct inet_protocol *); extern void ip_mc_drop_device(struct device *dev); extern int ip_mc_join_group(struct sock *sk, struct device *dev, unsigned long addr); extern int ip_mc_leave_group(struct sock *sk, struct device *dev,unsigned long addr); extern void ip_mc_drop_socket(struct sock *sk); #endif #endif /*include/linux/igmp.h ***********************************************************/ 1.9 in.h 头文件 该头文件主要是一些常量定义。具体情况见文件中注释。 以下简单介绍几种地址分类。 起初使用 IP 地址时,IP 地址使用分类的概念。这种体系结构被称为分类编址。20 世纪 90 年代中期,一种叫做无分类编址的新体系结构出现,这种体系结构将最终替代原来的体系。 但目前绝大多数的因特网地址还是使用分类编址方式。本文也只介绍分类编址。 在分类编址中,地址空间被分为 5 类:A,B,C,D,E。每一类占据地址空间的一部分。 1. A 类地址(如下 n 表示网络部分(network),h 代表主机部分(host)) 地址组成:0nnnnnnn hhhhhhhh hhhhhhhh hhhhhhhh 1> 第一个比特(MSL 位)为 0,随后 7 比特为网络位,其它 24 比特为主机位。 2> 第一个字节范围:0-127 3> 具有 126 个网络地址:0 和 127 均被保留(如 127 被用于本地回环地址) 4> 每个网络地址对应有 16777214 个主机(2^24) 5> 该类地址网络掩码为:255.0.0.0 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 2. B 类地址 地址组成:10nnnnnn nnnnnnnn hhhhhhhh hhhhhhhh 1> 前两个比特固定为 10;随后 14 比特为网络部分比特位,其余 16 比特为主机位。 2> 第一个字节范围:128-191 3> 具有 16384 个网络地址 4> 每个网络地址对应有 65532 个主机地址 5> 该类地址网络掩码为:255.255.0.0 3. C 类地址 地址组成:110nnnnn nnnnnnnn nnnnnnnn hhhhhhhh 1> 前三个比特固定为 110;随后 21 个比特为网络地址部分,最后 8 个比特为主机位。 2> 第一字节范围:192-223 3> 具有 2097152 个网络地址 4> 每个网络地址对应有 254 个主机地址 5> 该类地址网络掩码为:255.255.255.0 4. D 类地址(m 代表多播地址位 multicast bit) 地址组成:1110mmmm mmmmmmmm mmmmmmmm mmmmmmmm 1> 前四个比特固定为 1110,其余 28 比特均为多播地址位。 2> 第一字节范围:224-247 3> D 类地址均为多播地址 5. E 类地址(r 代表保留地址位 reserved bit) 地址组成:1111rrrr rrrrrrrr rrrrrrrr rrrrrrrr 1> 前四个比特固定为 1111;其余 28 比特保留。 2> 第一个字节范围:248-255 3> 该类地址为保留地址,或者用于测试目的。 表 1-3 较为清晰的比较了各种地址之间的关系。 表 1-3 各种地址类型比较 地址类别 最左端比特位取值 开始地址 截止地址 A 0xxx 0.0.0.0 127.255.255.255 B 10xx 128.0.0.0 191.255.255.255 C 110x 192.0.0.0 223.255.255.255 D 1110 224.0.0.0 239.255.255.255 E 1111 224.0.0.0 255.255.255.255 结合以上内容不难理解 in.h 文件中地址类别定义。 /* include/linux/in.h *************************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions of the Internet Protocol. * 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * Version: @(#)in.h 1.0.1 04/21/93 * * Authors: Original taken from the GNU Project file. * Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_IN_H #define _LINUX_IN_H //IP 首部中上层协议字段取值 /* Standard well-defined IP protocols. */ enum { IPPROTO_IP = 0, /* Dummy protocol for TCP */ IPPROTO_ICMP = 1, /* Internet Control Message Protocol */ IPPROTO_IGMP = 2, /* Internet Gateway 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_RAW = 255, /* Raw IP packets */ IPPROTO_MAX }; //in_addr 与下面的 sockaddr_in 结构是标准的网络接口地址结构定义。 //熟悉网络编程的读者对此应该不陌生。 /* Internet address. */ struct in_addr { unsigned long int s_addr; }; //对多播地址或其对应的网络接口进行设置或获取 //该结构作为一个“中间人”而存在 /* Request struct for multicast socket ops */ struct ip_mreq { struct in_addr imr_multiaddr; /* IP multicast address of group */ struct in_addr imr_interface; /* local IP address of interface */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 }; /* Structure describing an Internet (IP) socket address. */ #define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */ struct sockaddr_in { short int 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)]; }; #define sin_zero __pad /* for BSD UNIX comp. -FvK */ //以下是对几种地址类型的定义 /* * Definitions of the bits in an Internet address integer. * On subnets, host and network parts are found according * to the subnet mask, not these masks. */ #define IN_CLASSA(a) ((((long int) (a)) & 0x80000000) == 0) #define IN_CLASSA_NET 0xff000000 #define IN_CLASSA_NSHIFT 24 #define IN_CLASSA_HOST (0xffffffff & ~IN_CLASSA_NET) #define IN_CLASSA_MAX 128 #define IN_CLASSB(a) ((((long int) (a)) & 0xc0000000) == 0x80000000) #define IN_CLASSB_NET 0xffff0000 #define IN_CLASSB_NSHIFT 16 #define IN_CLASSB_HOST (0xffffffff & ~IN_CLASSB_NET) #define IN_CLASSB_MAX 65536 #define IN_CLASSC(a) ((((long int) (a)) & 0xe0000000) == 0xc0000000) #define IN_CLASSC_NET 0xffffff00 #define IN_CLASSC_NSHIFT 8 #define IN_CLASSC_HOST (0xffffffff & ~IN_CLASSC_NET) #define IN_CLASSD(a) ((((long int) (a)) & 0xf0000000) == 0xe0000000) #define IN_MULTICAST(a) IN_CLASSD(a) #define IN_MULTICAST_NET 0xF0000000 #define IN_EXPERIMENTAL(a) ((((long int) (a)) & 0xe0000000) == 0xe0000000) 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define IN_BADCLASS(a) ((((long int) (a)) & 0xf0000000) == 0xf0000000) /* Address to accept any incoming messages. */ #define INADDR_ANY ((unsigned long int) 0x00000000) /* Address to send to all hosts. */ #define INADDR_BROADCAST ((unsigned long int) 0xffffffff) /* Address indicating an error return. */ #define INADDR_NONE 0xffffffff /* Network number for local host loopback. */ #define IN_LOOPBACKNET 127 /* Address to loopback in software to local host. */ #define INADDR_LOOPBACK 0x7f000001 /* 127.0.0.1 */ /* Defines for Multicast INADDR */ #define INADDR_UNSPEC_GROUP 0xe0000000 /* 224.0.0.0 */ #define INADDR_ALLHOSTS_GROUP 0xe0000001 /* 224.0.0.1 */ #define INADDR_MAX_LOCAL_GROUP 0xe00000ff /* 224.0.0.255 */ /* contains the htonl type stuff.. */ #include #endif /* _LINUX_IN_H */ /* include/linux/in.h *************************************************************/ 1.10 inet.h 头文件 该文件只是几个函数的简单声明。 /* include/linux/inet.h ************************************************************/ /* * Swansea University Computer Society NET3 * * This work is derived from NET2Debugged, which is in turn derived * from NET2D which was written by: * Fred N. van Kempen, * * This work was derived from Ross Biro's inspirational work * for the LINUX operating system. His version numbers were: * * $Id: Space.c,v 0.8.4.5 1992/12/12 19:25:04 bir7 Exp $ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * $Id: arp.c,v 0.8.4.6 1993/01/28 22:30:00 bir7 Exp $ * $Id: arp.h,v 0.8.4.6 1993/01/28 22:30:00 bir7 Exp $ * $Id: dev.c,v 0.8.4.13 1993/01/23 18:00:11 bir7 Exp $ * $Id: dev.h,v 0.8.4.7 1993/01/23 18:00:11 bir7 Exp $ * $Id: eth.c,v 0.8.4.4 1993/01/22 23:21:38 bir7 Exp $ * $Id: eth.h,v 0.8.4.1 1992/11/10 00:17:18 bir7 Exp $ * $Id: icmp.c,v 0.8.4.9 1993/01/23 18:00:11 bir7 Exp $ * $Id: icmp.h,v 0.8.4.2 1992/11/15 14:55:30 bir7 Exp $ * $Id: ip.c,v 0.8.4.8 1992/12/12 19:25:04 bir7 Exp $ * $Id: ip.h,v 0.8.4.2 1993/01/23 18:00:11 bir7 Exp $ * $Id: loopback.c,v 0.8.4.8 1993/01/23 18:00:11 bir7 Exp $ * $Id: packet.c,v 0.8.4.7 1993/01/26 22:04:00 bir7 Exp $ * $Id: protocols.c,v 0.8.4.3 1992/11/15 14:55:30 bir7 Exp $ * $Id: raw.c,v 0.8.4.12 1993/01/26 22:04:00 bir7 Exp $ * $Id: sock.c,v 0.8.4.6 1993/01/28 22:30:00 bir7 Exp $ * $Id: sock.h,v 0.8.4.7 1993/01/26 22:04:00 bir7 Exp $ * $Id: tcp.c,v 0.8.4.16 1993/01/26 22:04:00 bir7 Exp $ * $Id: tcp.h,v 0.8.4.7 1993/01/22 22:58:08 bir7 Exp $ * $Id: timer.c,v 0.8.4.8 1993/01/23 18:00:11 bir7 Exp $ * $Id: timer.h,v 0.8.4.2 1993/01/23 18:00:11 bir7 Exp $ * $Id: udp.c,v 0.8.4.12 1993/01/26 22:04:00 bir7 Exp $ * $Id: udp.h,v 0.8.4.1 1992/11/10 00:17:18 bir7 Exp $ * $Id: we.c,v 0.8.4.10 1993/01/23 18:00:11 bir7 Exp $ * $Id: wereg.h,v 0.8.4.1 1992/11/10 00:17:18 bir7 Exp $ * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_INET_H #define _LINUX_INET_H #ifdef __KERNEL__ extern void inet_proto_init(struct net_proto *pro); extern char *in_ntoa(unsigned long in); extern unsigned long in_aton(char *str); #endif #endif /* _LINUX_INET_H */ /* include/linux/inet.h **********************************************************/ 其中 inet_proto_init 是 INET 域(目前是相对 UNIX 域而言)的初始化函数,定义在 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 net/inet/af_inet.c 文件中。in_ntoa, in_aton 这两个函数定义在 net/inet/utils.c 文件中,完成的功 能是实现点分十进制 ASCII 码表示到 32 位整型数字之间的转换。 1.11 interrupt.h 头文件 将该头文件也列在此处,主要是因为网络代码部分在数据报接收时需要用到此文件中定义的 “下半部分(Bottom Half)“结构及相关函数。“下半部分”是一种软件处理手段。为减少中 断处理程序的执行时间,一般将中断处理程序人为的分为两个部分:“上半部分”和“下半 部分”。“上半部分”主要完成一些比较紧急的工作,在完成这些工作后,其安排“下半部分” 完成剩下一般性工作,虽然这些工作也同样重要,但并不要求实时,故可稍后完成。这样就 大大减少了实际中断处理程序的执行时间,中断处理程序在处理完“上半部分”后即可返回。 从而从整体上提高了系统性能。 “下半部分”在软件实现上是由一个 bh_struct 结构表示。该结构定义如文件中所示,有两 个字段,一个是“下半部分”的执行函数,另一个是该函数的参数。系统目前定义有 32 个 “下半部分”插槽。即系统中最多可同时存在 32 个下半部分。bh_active 用于表示正处于活 跃状态的“下半部分”,当一个“下半部分”处于活跃状态时,即表示该“下半部分”需要 执行。函数 mark_bh 用于改变某个“下半部分”的活跃状态。bh_mask 是掩码,即是我们通 常所说的使能位,这是 32 位比特的整型数值,每位对应一个“下半部分”。当某位为 0 时, 表示屏蔽该“下半部分”,此时即使该“下半部分”处于活跃状态,也不会被系统执行。 enable_bh, disable_bh 这两个函数用于改变“下半部分”的使能位。bh_base 是 bh_struct 结构 类型的数组,共有 32 个元素,每个元素对应一个系统“下半部分”。系统目前使用了前 8 个元素,如文件中 enum 定义。其中用于网络代码部分的“下半部分”是第五个位置所指向 的元素即 bh_base[INET_BH]。而系统中(包括网络部分代码)所使用的大量定时器则是作 为第一个“下半部分”元素执行的。 由此可见,虽然“下半部分”并不要求实时性,但一个活跃的“下半部分”必须得到执行, 无论其延迟多长时间,因为其完成的功能是很重要的。系统中所有下半部分的执行均是在 do_bottom_half 函数中完成的。该函数定义在 kernel/softirq.c 文件中。由于该函数较短,下 面给出该函数的实现。 /* kernel/softriq.c - do_bottom_half 函数*****************************************/ asmlinkage void do_bottom_half(void) { unsigned long active; unsigned long mask, left; struct bh_struct *bh; bh = bh_base; active = bh_active & bh_mask; for (mask = 1, left = ~0 ; left & active ; bh++,mask += mask,left += left) { if (mask & active) { void (*fn)(void *); bh_active &= ~mask; fn = bh->routine; if (!fn) 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 goto bad_bh; fn(bh->data); } } return; bad_bh: printk ("irq.c:bad bottom half entry %08lx\n", mask); } /* kernel/softriq.c 代码摘录******************************************************/ 该函数本身的实现比较简单,但完成的功能却相当重要。其遍历 bh_base 这个“下半部分” 数组,如果某个“下半部分”处于活跃状态并且相应使能位为 1,则执行该“下半部分”, 只要其中之一不满足,则跳过该“下半部分”,暂时不执行。 /* include/linux/interrupt.h *******************************************************/ /* interrupt.h */ #ifndef _LINUX_INTERRUPT_H #define _LINUX_INTERRUPT_H #include #include struct bh_struct { void (*routine)(void *); void *data; }; extern unsigned long bh_active; extern unsigned long bh_mask; extern struct bh_struct bh_base[32]; asmlinkage void do_bottom_half(void); /* Who gets which entry in bh_base. Things which will occur most often should come first - in which case NET should be up the top with SERIAL/TQUEUE! */ enum { TIMER_BH = 0, CONSOLE_BH, TQUEUE_BH, SERIAL_BH, NET_BH, IMMEDIATE_BH, KEYBOARD_BH, 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 CYCLADES_BH }; extern inline void mark_bh(int nr) { set_bit(nr, &bh_active); } extern inline void disable_bh(int nr) { clear_bit(nr, &bh_mask); } extern inline void enable_bh(int nr) { set_bit(nr, &bh_mask); } /* * Autoprobing for irqs: * * probe_irq_on() and probe_irq_off() provide robust primitives * for accurate IRQ probing during kernel initialization. They are * reasonably simple to use, are not "fooled" by spurious interrupts, * and, unlike other attempts at IRQ probing, they do not get hung on * stuck interrupts (such as unused PS2 mouse interfaces on ASUS boards). * * For reasonably foolproof probing, use them as follows: * * 1. clear and/or mask the device's internal interrupt. * 2. sti(); * 3. irqs = probe_irq_on(); // "take over" all unassigned idle IRQs * 4. enable the device and cause it to trigger an interrupt. * 5. wait for the device to interrupt, using non-intrusive polling or a delay. * 6. irq = probe_irq_off(irqs); // get IRQ number, 0=none, negative=multiple * 7. service the device to clear its pending interrupt. * 8. loop again if paranoia is required. * * probe_irq_on() returns a mask of snarfed irq's. * * probe_irq_off() takes the mask as a parameter, * and returns the irq number which occurred, * or zero if none occurred, or a negative irq number * if more than one irq occurred. 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 */ extern unsigned int probe_irq_on(void); /* returns 0 on failure */ extern int probe_irq_off(unsigned int); /* returns 0 or negative on failure */ #endif /* include/linux/interrupt.h ********************************************************/ 该文件除了对“下半部分”结构及相关重要变量和辅助函数进行了声明和定义之外,还另外 声明了用于探测中断号的两个函数:probe_irq_on, probe_irq_off. 这两个函数的用法是: 1.清除设备的先前中断标志 2.调用 sti 函数使能系统中断 3.调用 probe_irq_on 函数,保存其返回的所有空闲中断号 4.设置设备使其产生一个中断 5.等待一段合理的时间,等待系统探测到该中断 6.调用 probe_irq_off 函数,并将之前由 probe_irq_on 返回的空闲中断号作为参数传替给 probe_irq_off 函数。 7.检查 probe_irq_off 函数的返回值: 1>0—没有探测该设备的中断号 对于此种情况可以在执行一次这个流程,并适当延长第 5 步中等待时间。 2>负数—检测到多个中断号:根据具体情况分析原因 3>正数—该中断号可用 1.12 ip.h 头文件 IP 是 Internet Protocol 的简称,意为网际协议。该协议在 TCP/IP 协议簇中占据重要地位。IP 是一种不可靠的无连接的数据报协议,其尽最大努力传输数据报。IP 本身不提供跟踪或差 错检查。当可靠性很重要时,IP 必须与可靠的上层协议(如 TCP)配合使用。图 1-14 显示 了 IP 首部格式。 图 1-14 IP 首部格式 版本号:4 比特 这个字段定义了 IP 协议的版本,目前使用较多的版本是 4,但在将来版本 6 会取代版本 4。 本文只讨论版本 4。 首部长度:4 比特 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 该字段以 4 字节为单位。故首部长度最大可达 15*4=60 字节。 其中首部固定(或者说是最小)长度是 20 字节,其它 40 字节用于各种 IP 选项,IP 选项是 可选的。 服务类型:8 比特 下图显示了该字段的位分配。 图 1-15 服务类型 优先(precedence)是 3 位字段,它的值从 0(000B)到(111B)。 111—Network Control 110—Internetwork Control 101—CRITIC/ECP 100—Flash Override 011—Flash 010—Immediate 001—Priority 000—Routine 优先字段定义了在出现一些问题时(如拥塞)数据报的优先级。当路由器出现拥塞而必须丢 弃一些数据报时,具有低优先级的数据报将首先被丢弃。 总长度:16 比特 该字段长度值包括 IP 首部和 IP 数据,该字段定义了包括首部在内的数据报总长度。 即:IP 数据长度=总长度-首部长度 标志码:16 比特 该字段用于数据报分片,当数据报长度大于 MTU 时,该数据报在源端或中间路由器上会被 分割成小的分片(注意仍然是数据报),这些分片陆续到达目的端后,需要被重新组合成原 来的一个大的数据报。标志位字段用于将这些分片归类,具有相同标志位字段的分片同属于 一个数据报,即只有具有相同标志位字段的分片才会组合在一起。 标志(控制)位:3 比特 下图给出了各控制位的具体意义。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-16 控制位 Bit 0:保留,必须为 0 Bit 1:(DF) 0—如果数据报长度很大超过 MTU,可对其进行分片 1—不可进行分片 Bit 2:(MF) 0—这是最后一个分片 1—还有后续分片(这是第一个分片或者是中间分片) 分片偏移:13 比特 该字段表示该分片在原先整个大数据报中的位置。该字段以 8 字节为单位。第一个分片该字 段值为 0。最后一个分片该字段值加上其数据长度为原先整个大数据报的长度值。 生存时间:8 比特 该字段在最初设计时是打算保持一个时间戳,由经过的每一个路由器将这个字段的值减 1。 当该字段值变为 0 时,就丢弃这个数据报。但使用时间戳要求所有机器必须是同步的,而且 需要知道数据报从前一个机器传送到本机的时间,操作性很差。故后来该字段就简单表示所 经过的路由器数目。每一个路由器在处理一个需要转发的数据报时,首先将该字段值减 1, 如果该字段值变为 0,则丢弃该数据报,并给该数据报的起始发送端发送一个 ICMP 错误报 文。 上层协议字段:8 比特 该字段表示所使用上一层协议号,该字段可取值定义在 in.h 文件中。表 1-4 给出了常用值。 表 1-4 IP 首部上层协议字段取值 值 协议 1 ICMP 2 IGMP 4 IPIP 6 TCP 17 UDP 255 RAW 网络栈将根据该字段值将数据报传送给上层相应的处理函数。如对于 TCP 协议,处理函数 为 tcp_rcv, UDP 对应的处理函数为 udp_rcv,等等。 校验和:16 比特 该字段涵盖了范围仅仅为 IP 首部部分(包括 IP 选项,如有),不包括 IP 数据。 源端 IP 地址:32 比特 目的端 IP 地址:32 比特 这两个 32 位地址字段表示了该数据报的发送端 IP 地址和最终目的端 IP 地址。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 IP 选贤:长度可变 IP 选项是可选的,也就是说一个 IP 数据报中可以包含也可以不包含 IP 选项。当包含 IP 选 项时,可以有一个或多个选项同时存在。正因如此,该字段的长度是不固定的。 IP 选项可以分为两大类: 1>单个字节选项,这种 IP 选项只有一个类型字节。 2>多字节选项,这种选项的结构如下:一个类型字节;一个长度字节;多个数据字节。 类型字节具有内部结构: 图 1-17 给出了其内部结构。 类型字节被分为三个子域: 1>1 比特标志域:是否复制标志位 如该位为 0,则表示对应的选项在数据报进行分片时不用复制到每个分片中,只需复制到第 一个分片即可。 如该位为 1,则表示对应的选项必须复制到所有的分片中。 2>2 比特类域: 该子域表示选项所属的类。可取如下值: 0— 控制类 1— 保留将来使用 2— 调试,测试之用 3— 保留将来使用 3>5 比特 option number 子域: 该子域类子域进行细分,与类子域组成具体的选项类型。 具体选项类型: 1. 结束选项(End of Option List) 该选项表示所有选项的结束。系统在解析 IP 选项时遇到该选项后则停止解析,因为其 后已经不存在其它选项。故该选项使用地点应是在其它所有选项之后。注意不同选项类 型之间的填充不可使用该选项,而是使用无操作(No Operation)选项。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 2. 无操作选项(No Operation) 该选项主要用于其它选项之间的填充。这种填充是为了使得某些选项位于 32-bit 边界上。 3. 安全级别选项(Security) 该选项使用较少,其中 S 子域,C 子域,H 子域分别为 16 比特,TCC 子域为 24 比特。 这些子域所代表的含义为: S—安全级别(Security) C—信息划分(Compartment) H—限制性处理(Handling Restrictions) TCC—传输控制码(Transmission Control Code) 4. 松源地址和记录路由(Loose Source and Record Route) 该选项提供了一种机制让数据报发送源端提供路由信息,这些信息可以被网关使用以转 发数据报,同时记录下该网关的路由信息。 选项起始于一个类型字节,值为 131。第二个字节为长度,该长度包括类型字节,其本 身,指针字节以及其后的路由数据部分。第三个字节为指针字节,该指针指向路由数据 第一个字节位置,位置的计算是从类型字节开始的,且起始计算从 1 开始,即如果路由 数据在最初没有被使用时,指针字节字段的值应为 4,这也是指针字段最小的合理值。 路由数据字段包括一系列 IP 地址,如果指针字段的值大于长度字段值,则表示无路由 数据。例如长度字段值为 3 时,指针字段值取最小值即 4,此时即表示无路由数据。 如果数据报到达 IP 首部中目的端地址所表示的路由器(主机)后,且此时指针字段值 小于长度字段值,表示存在有效的路由数据,则将指针指向的 IP 地址填入 IP 首部中目 的端地址字段,并且将该路由器的出站设备的 IP 地址覆盖指针指向的值(这个值已被 使用)。之后指针值前移 4 字节指向下一个地址。图 1-18 显示了地址之间的交换关系。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-18 松源地址和记录路由工作方式 之所以称为松源地址和记录路由是相对于紧源地址和记录路由选项而言。对于松源地址 和记录路由选项而言,在选项中路由数据指定的地址所对应的网关之间可以允许经过多 个其它网关,而对于紧源地址和记录路由选项而言,则经过的网关则必须是选项中指定 的地址所对应的网关,中间不可经过其它网关。 5.紧源地址和记录路由(Strict Source and Record Route) 紧源地址和记录路由选项格式与松源地址和记录路由选项相同,二者之间工作方式上存 在差别。这种差别前文中业已交待。 6. 记录路由选项(Record Route) 该选项前面部分格式同于松源地址和记录路由选项,路由数据部分格式不同。对于该选 项而言,路由数据部分起初并无数据,而是预留的空间供中间路由器填写其转发数据报 的出站设备的 IP 地址。这样就可以由此得知该数据报的传输路径。 7. 流标志符(Stream Identifier) 对于不支持流工作方式的网络,该选项提供了一种机制通过传送 16 比特流标志符来模 拟流工作方式。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 8. 时间戳选项 oflw 字段表示由于缺少空间而无法记录时间戳的主机数目,注意如果经过的主机过多, 该字段也会溢出,因为其只有 4 比特,最多可表示 15 个主机。 flg 字段决定了该选项的具体工作方式。该字段占据 4 比特,可取如下值: 0— 只记录时间戳,每个时间戳按 4 字节连续排列,最多可有 37/4=9 个时间戳。 1— 同时记录 IP 地址和时间戳,即当一个转发该数据报时,在当前指针位置所指处填入 数据报出站设备的 IP 地址,然后指针前移 4 个字节,紧接着填入转发的时间。这样 每经过一个网关,就使用掉 8 个字节。由于 IP 选项最多可有 40 字节,除去类型字 段,长度字段,指针字段共 3 个字节,可用字节数为 37 个字节,故最多可记录 37/8=4 个网关信息。 3—与 1 类型类似,但是 IP 地址是数据报原始发送端预先填入的。但转发数据报的该网 关 IP 地址与指针所指向的源端预先填入的 IP 地址相同时,则在其后填入转发该数 据报的时间戳。由此可见,原始发送端在预先填写 IP 地址时,在每个 IP 地址后都 预留有 4 个字节的时间戳空间,以便被指定 IP 地址的网关填入,即 IP 地址是以 4 个字节进行间隔的。此种方式下最多可记录 4 个网关转发时间信息。 9. 填充字节 填充字节是为保证 IP 首部(包括选项部分)在 32 比特边界上结束。填充字节一般取值 为 0。 /* include/linux/ip.h *********************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions for the IP protocol. * * Version: @(#)ip.h 1.0.2 04/28/93 * * Authors: Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_IP_H #define _LINUX_IP_H #include //定义具体选项类型字段值,参考前文中对各选项的介绍。 #define IPOPT_END 0 #define IPOPT_NOOP 1 #define IPOPT_SEC 130 //Security #define IPOPT_LSRR 131 #define IPOPT_SSRR 137 #define IPOPT_RR 7 #define IPOPT_SID 136 //stream identifier #define IPOPT_TIMESTAMP 68 #define MAXTTL 255 //时间戳选项 struct timestamp { __u8 len; //选项长度 __u8 ptr; //指针 union { #if defined(LITTLE_ENDIAN_BITFIELD) __u8 flags:4, //标志字段,该字段指定了的具体的时间戳类型,参考前文所述。 overflow:4; //溢出字段 #elif defined(BIG_ENDIAN_BITFIELD) __u8 overflow:4, flags:4; #else #error "Please fix " #endif __u8 full_char; } x; __u32 data[9]; //在只记录时间戳的情况下,最多可有 9 个时间戳缓存空间。 }; #define MAX_ROUTE 16 struct route { char route_size; //选项长度字段 char pointer; //指针字段 unsigned long route[MAX_ROUTE]; //路由数据,虽然 MAX_ROUTE 定义为 16,但实 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 //际可用的路由数据空间仅为 37 个字节。 }; //当接收一个具有IP选项的数据报时,内核将根据数据报中具体的IP选项初始化该结构, //以便内核其它代码的处理。 struct options { struct route record_route; struct route loose_route; struct route strict_route; struct timestamp tstamp; unsigned short security; unsigned short compartment; unsigned short handling; //handling restriction unsigned short stream; //id stream option unsigned tcc; //transmission control code }; //IP 首部定义,参考前文内容不难理解。 struct iphdr { #if defined(LITTLE_ENDIAN_BITFIELD) __u8 ihl:4, version:4; #elif defined (BIG_ENDIAN_BITFIELD) __u8 version:4, ihl:4; #else #error "Please fix " #endif __u8 tos; __u16 tot_len; __u16 id; __u16 frag_off; __u8 ttl; __u8 protocol; __u16 check; __u32 saddr; __u32 daddr; /*The options start here. */ }; #endif /* _LINUX_IP_H */ /* include/linux/ip.h *********************************************************/ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 1.13 ip_fw.h 头文件 该文件定义 IP 层防火墙相关数据结构。该版本网络代码防火墙部分实现较为简单,但可作 为了解防火墙工作原理的基础。可以将防火墙看作为一个过滤器,其过滤进出主机的网络数 据包,根据防火墙预设的规则,对这些数据包进行取舍。防火墙简单的说就是一组规则,内 核网络代码对接收和发送的每个数据包都是用这套规则进行检查,如发现某个数据包满足其 中的规则,则根据该规则的具体规定的措施决定该数据包的去向:有可能被简单的丢弃,也 有可能发送一个 ICMP 数据包给远端,或者继续正常的处理。 软件实现上,每个规则都由一个 ip_fw 数据结构表示,每个 ip_fw 数据结构表示一个规则, 将所有同类型(如转发规则集合)组合在一起形成一个链表,便形成了我们通常意义上的防 火墙规则群。规则的检查就是遍历该链表,根据数据包的相应的头部信息(如 IP 首部或者 TCP,UDP 首部)进行判断该数据包是否匹配其中的规则。这些信息包括数据包发送的源 端 IP 地址,源端端口号,目的端 IP 地址,目的端端口号等。如果数据包的目的端就是本机, 可能这些规则中还包括数据包的接收接口设备。 ip_fw.h 文件如下,其第一部分即对 ip_fw 数据结构的定义,诚如上文所述,每个 ip_fw 结 构代表一个防火墙规则。ip_fw 结构的字段: 1>fw_next:指向 ip_fw 结构的指针类型,该指针的作用是将所有的规则连接成一个链表的 形式,易于管理和规则检查。 2>fw_src, fw_dst:发送端 IP 地址和接收端 IP 地址,用于筛选满足条件的出入站数据包。 3>fw_smsk, fw_dmsk:发送端和接收端 IP 地址掩码,规则检查中用于查看 IP 地址网络部分。 4>fw_via:对于目的端是本机的数据包,该字段指定数据包的接收接口设备 IP 地址。 5>fw_flg:标志位,该字段一方面指定该规则适用的协议范围,另一方面作为控制字段决定 对数据包的具体处理方式。 该字段的可取如下值,这些值定义在在 ip_fw 结构之后。 IP_FW_F_ALL:该规则适用于所有协议类型。 IP_FW_F_TCP:该规则仅针对 TCP 协议。 IP_FW_F_UDP:该规则仅针对 UDP 协议。 IP_FW_F_ICMP:该规则仅针对 ICMP 协议。 IP_FW_F_ACCEPT:满足该规则的数据包的处理方式是放行。 IP_FW_F_SRNG:规则指定的发送端端口值定义在一个范围中,而非具体值(但也可包括)。 IP_FW_F_DRNG:规则指定的目的端端口值定义在一个范围中,而非具体值(但也可包括)。 IP_FW_F_PRN:该标志用于调试,即发现一个满足此规则的数据包时,打印出相关信息。 IP_FW_F_BIDIR:该规则同时适用于出站和进站数据包,默认情况下,仅对进站数据包。 IP_FW_F_TCPSYN:该规则仅针对于使用 TCP 协议的 SYN 数据包。SYN 数据包是 TCP 协 议建立连接过程中同步数据包。 IP_FW_F_ICMPRPL:该规则表示当数据包被丢弃需发送一个 ICMP 目的端不可达错误类型 给该数据报的原始发送端。 6>fw_nsp, fw_ndp:规则定义中指定的发送端和接收端端口数目。 7>fw_pts:short 类型的数组,每个数组元素代表一个端口号,该数组的前 fw_nsp 个元素表 示发送端端口,紧接着之后的 fw_ndp 个元素表示接收端端口。注意 fw_nsp 与 fw_ndp 的之 和不可大于 IP_FW_MAX_PORTS,即数组的容量大小。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 8>fw_pcnt, fw_bcnt:这两个字段用于信息统计,表示满足该规则的数据包的个数,即这些 数据包中包含的数据字节数。 防火墙规则目前定义有三个链表: A. 阻塞(block)规则链表 ip_fw_blk_chain 注意称之为阻塞规则链表并非是指凡是满足该链表中其中之一规则的数据包都要被丢弃,而 是指该链表中规则仅对本机原始发送的数据包以及最终目的端是本机的数据包进行检查。至 于数据包的取舍会根据其满足的具体规则的规定进行相应处理,如果该规则没有定义如何处 理满足该条规则的数据包,则根据全局变量 ip_fw_blk_policy 的值进行取舍。 B. 转发(forward)规则链表 ip_fw_fwd_chain 该链表中规则适用于每个需经过本机转发的数据包。数据包的取舍根据满足的规则规定的方 式进行,如果该规则没有规定如何处理数据包,则根据全局变量 ip_fw_fwd_policy 的值进行 取舍。 C. 信息统计(accouting)规则链表 ip_acct_chain 该链表中的规则用于检查所有类型数据包(无论是转发的还是本机原始发送的或者最终目的 地是本机的),并对该数据包满足的规则进行信息更新。主要是对 ip_fw 结构中 fw_pcnt, fw_bcnt 字段的更新。注意此链表中的规则仅用于信息更新,并不对数据包的去向进行任何 决定,也即该链表中规则没有定义数据包的取舍策略,内核网络代码也不会检查这些策略。 而是在更新信息之后直接返回了。 用于选项设置和获取的标志位: IP_FW_ADD_BLK:添加一个阻塞规则。 IP_FW_ADD_FWD:添加一个转发规则。 IP_FW_CHK_BLK:检查阻塞规则链表中规则的有效性。 IP_FW_CHK_FWD:检查转发规则链表中规则的有效性。 IP_FW_DEL_BLK:从阻塞规则链表中删除一个规则。 IP_FW_DEL_FWD:从转发规则链表中删除一个规则。 IP_FW_FLUSH_BLK:删除阻塞规则链表中的所有规则。 IP_FW_FLUSH_FWD:删除转发规则链表中的所有规则。 IP_FW_ZERO_BLK:清除阻塞规则链表中所有规则的统计信息。 IP_FW_ZERO_FWD:清除转发规则链表中所有规则的统计信息。 IP_FW_POLICY_BLK:设置阻塞链表中所有规则的默认数据包处理策略,但某个具体规没 有定义对满足该规则的数据包的处理策略时,就使用此处设置的策略。 IP_FW_POLICY_FWD:设置转发链表中所有规则的默认数据包处理策略,具体执行同阻塞 链表。 IP_ACCT_ADD:添加一个信息统计规则。 IP_ACCT_DEL:删除一个信息统计规则。 IP_ACCT_FLUSH:删除信息统计规则链表中的所有规则。 IP_ACCT_ZERO:清除信息统计规则链表中所有规则的统计信息。 定义 ip_fwpkt 结构便于处理规则检查。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 文件最后是对一些重要全局变量和函数的声明,这些变量和函数均定义在 net/inet/ip_fw.c 文 件中。这些全局变量的含义前文中已有论说,具体函数的讨论在下文介绍 ip_fw.c 文件时给 出。 /* include/linux/ip_fw.h **********************************************************/ /* * IP firewalling code. This is taken from 4.4BSD. Please note the * copyright message below. As per the GPL it must be maintained * and the licenses thus do not conflict. While this port is subject * to the GPL I also place my modifications under the original * license in recognition of the original copyright. * * Ported from BSD to Linux, * Alan Cox 22/Nov/1994. * Merged and included the FreeBSD-Current changes at Ugen's request * (but hey it's a lot cleaner now). Ugen would prefer in some ways * we waited for his final product but since Linux 1.2.0 is about to * appear it's not practical - Read: It works, it's not clean but please * don't consider it to be his standard of finished work. * Alan. * * All the real work was done by ..... */ /* * Copyright (c) 1993 Daniel Boulet * Copyright (c) 1994 Ugen J.S.Antsilevich * * Redistribution and use in source forms, with and without modification, * are permitted provided that this entire comment appears intact. * * Redistribution in binary form may occur without any restrictions. * Obviously, it would be nice if you gave credit where credit is due * but requiring it would be too onerous. * * This software is provided ``AS IS'' without any warranties of any kind. */ /* * Format of an IP firewall descriptor * * src, dst, src_mask, dst_mask are always stored in network byte order. * flags and num_*_ports are stored in host byte order (of course). * Port numbers are stored in HOST byte order. 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 */ #ifndef _IP_FW_H #define _IP_FW_H struct ip_fw { struct ip_fw *fw_next; /* Next firewall on chain */ struct in_addr fw_src, fw_dst; /* Source and destination IP addr */ struct in_addr fw_smsk, fw_dmsk; /* Mask for src and dest IP addr */ struct in_addr fw_via; /* IP address of interface "via" */ unsigned short fw_flg; /* Flags word */ /* N'of src ports and # of dst ports * in ports array (dst ports follow * src ports; max of 10 ports in all; * count of 0 means match all ports) */ /* fw_nsp, fw_ndp 分别表示本地端口数量和远端端口数量, * 端口号在 fw_pts 数组中,其中本地端口占据数组前面元素空间,远端端口占据 * 数组的后面元素空间。注意通常并非各分一半。 */ unsigned short fw_nsp, fw_ndp; #define IP_FW_MAX_PORTS 10 /* A reasonable maximum */ unsigned short fw_pts[IP_FW_MAX_PORTS]; /* Array of port numbers to match */ unsigned long fw_pcnt,fw_bcnt; /* Packet and byte counters */ }; /* * Values for "flags" field . */ //ip_fw 结构中 fw_flg 字段的可取值。 #define IP_FW_F_ALL 0x000 /* This is a universal packet firewall*/ #define IP_FW_F_TCP 0x001 /* This is a TCP packet firewall */ #define IP_FW_F_UDP 0x002 /* This is a UDP packet firewall */ #define IP_FW_F_ICMP 0x003 /* This is a ICMP packet firewall */ #define IP_FW_F_KIND 0x003 /* Mask to isolate firewall kind */ #define IP_FW_F_ACCEPT 0x004 /* This is an accept firewall (as * * opposed to a deny firewall)* * */ #define IP_FW_F_SRNG 0x008 /* The first two src ports are a min * * and max range (stored in host byte * * order). * * */ #define IP_FW_F_DRNG 0x010 /* The first two dst ports are a min * 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * and max range (stored in host byte * * order). * * (ports[0] <= port <= ports[1]) * * */ #define IP_FW_F_PRN 0x020 /* In verbose mode print this firewall*/ #define IP_FW_F_BIDIR 0x040 /* For bidirectional firewalls */ #define IP_FW_F_TCPSYN 0x080 /* For tcp packets-check SYN only */ #define IP_FW_F_ICMPRPL 0x100 /* Send back icmp unreachable packet */ #define IP_FW_F_MASK 0x1FF /* All possible flag bits mask */ /* * New IP firewall options for [gs]etsockopt at the RAW IP level. * Unlike BSD Linux inherits IP options so you don't have to use * a raw socket for this. Instead we check rights in the calls. */ #define IP_FW_BASE_CTL 64 #define IP_FW_ADD_BLK (IP_FW_BASE_CTL) #define IP_FW_ADD_FWD (IP_FW_BASE_CTL+1) #define IP_FW_CHK_BLK (IP_FW_BASE_CTL+2) #define IP_FW_CHK_FWD (IP_FW_BASE_CTL+3) #define IP_FW_DEL_BLK (IP_FW_BASE_CTL+4) #define IP_FW_DEL_FWD (IP_FW_BASE_CTL+5) #define IP_FW_FLUSH_BLK (IP_FW_BASE_CTL+6) #define IP_FW_FLUSH_FWD (IP_FW_BASE_CTL+7) #define IP_FW_ZERO_BLK (IP_FW_BASE_CTL+8) #define IP_FW_ZERO_FWD (IP_FW_BASE_CTL+9) #define IP_FW_POLICY_BLK (IP_FW_BASE_CTL+10) #define IP_FW_POLICY_FWD (IP_FW_BASE_CTL+11) #define IP_ACCT_ADD (IP_FW_BASE_CTL+16) #define IP_ACCT_DEL (IP_FW_BASE_CTL+17) #define IP_ACCT_FLUSH (IP_FW_BASE_CTL+18) #define IP_ACCT_ZERO (IP_FW_BASE_CTL+19) struct ip_fwpkt { struct iphdr fwp_iph; /* IP header */ union { struct tcphdr fwp_tcph; /* TCP header or */ struct udphdr fwp_udph; /* UDP header */ } fwp_protoh; struct in_addr fwp_via; /* interface address */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 }; /* * Main firewall chains definitions and global var's definitions. */ #ifdef __KERNEL__ #include #ifdef CONFIG_IP_FIREWALL extern struct ip_fw *ip_fw_blk_chain; extern struct ip_fw *ip_fw_fwd_chain; extern int ip_fw_blk_policy; extern int ip_fw_fwd_policy; extern int ip_fw_ctl(int, void *, int); #endif #ifdef CONFIG_IP_ACCT extern struct ip_fw *ip_acct_chain; extern void ip_acct_cnt(struct iphdr *, struct device *, struct ip_fw *); extern int ip_acct_ctl(int, void *, int); #endif extern int ip_fw_chk(struct iphdr *, struct device *rif,struct ip_fw *, int, int); #endif /* KERNEL */ #endif /* _IP_FW_H */ /* include/linux/ip_fw.h *********************************************************/ 1.14 ipx.h 头文件 该文件定义 IPX 协议使用的数据结构。 IPX 是 Internet Packet eXchange(网络报文交换协议)的缩写,其是一种无连接状态,报文 服务协议。对于发送出去的每个数据包,都无需进行确认应答。 每个数据包都被独立的发送到远端。IPX 是 Xerox 网络标准(XNS:Xerox Network Standard) 的一种实现。其它 Netware 协议的服务均建立在 IPX 协议之上,如服务声明(SAP),路由 (RIP),Netware 核心协议(NCP)等等。 IPX 实现了 OSI 模型网络层次中寻址,路由,数据报交换等任务。IPX 尽最大努力传送数据 包,但并不保证一定可以将数据包发送到远端。如果需要提供可靠的数据包传送,则使用 IPX 协议的上层协议必须注意到这一点。 IPX 报文首部需要提供源端和终端地址。IPX 地址模式有三个组成部分。如表 1-7 所示。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 表 1-7 IPX 地址组成 地址组成 长度 说明 网络地址(Network) 4bytes 指定一个具体的网络 节点地址(Node) 6bytes 指定网络中的某个具体的节点 插口号(Socket number) 2bytes 指定节点上运行的某个具体的进程 在 IPX 网络中,每个局域网都被赋予一个网络地址。网络地址被路由器用于转发数据包。 节点地址指定了局域网中一个工作站或者某个计算机。对于客户端而言,节点地址一般是指 网络接口设备的出厂硬件地址。 插口地址则指定了在某个具体节点上运行的进程,这也是数据报的最终目的的。每个进程都 被赋予一个插口地址。进程可以使用众所周知的插口地址,也可以使用临时分配的插口地址。 插口地址是一种在同一个主机上建立多个通信通道的机制。它的功能类似于其它网络协议中 所使用的端口号。 IPX 协议首部格式如图 1-19 所示。 图 1-19 IPX 首部格式 IPX 首部共有 30 个字节,其中: 校验和:2 字节 该字段通常被设置为 0xFFFF,这表示无校验和的计算。 包长度:2 字节 该长度字段包括 IPX 首部及其数据部分总长度。 传输控制:1 字节 该字段表示数据包经过的路由器数目。在发送端该字段被初始化为 0,中间的每个路由器在 转发该数据包时都回递增该字段值。如果该字段值增加到 16,即在第 16 个路由器上该数据 包将被认为无法到达目的端,将被丢弃。 包类型:1 字节 包类型字段表示包所要求或者提供的服务。该字段可取值如表 1-8 所示。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 表 1-8 包类型字段值及其意义 包类型 说明 0x00 未知(SAP 或者 RIP) 0x01 路由信息包类型(RIP:Routing Information Packet) 0x02 回复包类型(Echo Packet) 0x03 错误包类型(Error Packet) 0x04 报文交换协议(PEP:Packet Exchange Protocol) 0x05 分组序列交换协议(SPX:Sequenced Packet eXchange) 0x10 -- 0x1F 在试验协议(Experimental Protocols) 0x11 Netware 核心协议(NCP:Netware Core Protocol) 0x14 NetBIOS 广播 目的端网络地址:4 字节 数据包发送时,该字段必须被设置为目的主机所在网络的网络地址。如果该字段初始化为 0, 表示目的网络即本发送端所在的局域网。 目的端节点地址:6 字节 该字段表示某个局域网中某台主机的网络接口设备的硬件地址,这个地址在设备出厂时都已 设定。节点地址为 0xFFFFFF 表示对由网络地址部分指定的网络上的所有主机进行广播。 目的端端口号:2 字节 该字段主要是用于分别同一目的主机上多个进程。每个进程在使用 IPX 协议进行通信时, 都会被赋予一个端口号,从而实现同一主机上多个进程的同时通信而不互相干扰,这就是通 常所说的多路分用。 一些运行在 IPX 协议之上的服务通常具有固定的端口号。这样使得客户端可以更好的跟踪 并取得服务。 如下端口号被 IPX 协议保留。(以下列表中为避免中文翻译引起歧义,故直接以英文给出) 端口号 说明 --------- -------- 0x02 Echo protocol socket 0x03 Error handler packet 另外 Novell 为特殊目的定义并保留了如下端口号(不全): 端口号 服务 -------- -------- 0x0247 Novell VirtualTerminal (NVT) server 0x0451 Netware Core Protocol (NCP) 0x0452 Netware Service Advertising Protocol (SAP) 0x0453 Netware Routing Protocol (RIP) 0x0456 Netware Diagnostics Protocol (NDP) 0x8063 NVT2 server 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 0x811E Print server 源端网络地址:4 字节 该字段设置为发送端主机所在网络的网络地址。 源端节点地址:6 字节 该字段应设置为发送端主机自身节点地址(其发送接口设备的硬件地址)。 源端端口号:2 字节 如果发送端与远端只有一个通信通道,则该字段可由 IPX 协议内核代码进行初始化,否则 必须由上层应用程序进行初始化操作。 IPX 协议封装格式 1>802.3 IPX 首部紧接在 MAC 首部长度字段之后。 2>IEEE802.2 IPX 首部紧接在 IEEE802.2 首部之后(DSAP,SSAP,Control 字段之后)。 3>Ethernet II IPX 首部紧接在 MAC 首部类型字段之后,对于 IPX 协议,MAC 首部类型字段值为 0x8137。 4>Ethernet SNAP IPX 首部紧接在 SNAP 协议 5 个字节的首部之后。 有关 ipx.h 文件说明请参见以下文件内部注释。 /*include/linux/ipx.h ************************************************************/ #ifndef _IPX_H_ #define _IPX_H_ #include //节点地址长度 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define IPX_NODE_LEN 6 #define IPX_MTU 576 //IPX 协议接口地址定义,类似于 sockaddr_in, sockaddr_un 等结构。 //注意该结构为 16 字节 struct sockaddr_ipx { short sipx_family; //AF_IPX short sipx_port; //端口号 unsigned long sipx_network; //网络地址 unsigned char sipx_node[IPX_NODE_LEN]; //节点地址 unsigned char sipx_type; unsigned char sipx_zero; /* 16 byte fill */ }; /* * So we can fit the extra info for SIOCSIFADDR into the address nicely */ #define sipx_special sipx_port #define sipx_action sipx_zero #define IPX_DLTITF 0 #define IPX_CRTITF 1 //用于设置 IPX 协议的路由表项,注意路由表项并非由此结构表示,而是由 ipx_route 结构 //代表一个 IPX 协议路由表项。ipx_route_definition 结构中的信息只是用于创建一个 ipx_route //结构。这是一个用于信息传替的中间结构。 typedef struct ipx_route_definition { unsigned long ipx_network; unsigned long ipx_router_network; unsigned char ipx_router_node[IPX_NODE_LEN]; } ipx_route_definition; //如同 ipx_route_definition,此结构用于设置或获取主机接口设备信息。在使用 IPX 协议进 //行通信的主机上,每个网络接口均由一个 ipx_interface 结构表示,ipx_route_definition 结构 //的作用也是相当于一个信息传替的中间结构。 typedef struct ipx_interface_definition { unsigned long ipx_network; unsigned char ipx_device[16]; //IPX 协议链路层封装类型,取值如下定义的常量。 //有关内容参考前文中 IPX 协议封装格式一节。 unsigned char ipx_dlink_type; #define IPX_FRAME_NONE 0 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define IPX_FRAME_SNAP 1 #define IPX_FRAME_8022 2 #define IPX_FRAME_ETHERII 3 #define IPX_FRAME_8023 4 unsigned char ipx_special; //该字段取值有如下三个常量。 #define IPX_SPECIAL_NONE 0 #define IPX_PRIMARY 1 #define IPX_INTERNAL 2 unsigned char ipx_node[IPX_NODE_LEN]; } ipx_interface_definition; //ipx_config_data 结构完全是一个用于设置或获取内部变量信息的过渡结构。用于 ioctrl 控制 //函数中。 //ipxcfg_auto_select_primary 字段用于设置内核变量 ipxcfg_auto_select_primary 的值, //同理 ipxcfg_auto_create_interfaces 字段用于设置内核变量 ipxcfg_auto_create_interfaces 的取 //值。这两个内核变量的意义将第二章中对 ipx.c 文件的分析。 typedef struct ipx_config_data { unsigned char ipxcfg_auto_select_primary; unsigned char ipxcfg_auto_create_interfaces; } ipx_config_data; /* * OLD Route Definition for backward compatibility. */ //该结构作用同 ipx_route_definition,该结构是为保持向后兼容性而保留的结构。 struct ipx_route_def { unsigned long ipx_network; unsigned long ipx_router_network; #define IPX_ROUTE_NO_ROUTER 0 unsigned char ipx_router_node[IPX_NODE_LEN]; unsigned char ipx_device[16]; unsigned short ipx_flags; #define IPX_RT_SNAP 8 #define IPX_RT_8022 4 #define IPX_RT_BLUEBOOK 2 #define IPX_RT_ROUTED 1 }; //如下三个标志位用于 ipx_ioctrl 函数中进行相关信息获取和设置。 #define SIOCAIPXITFCRT (SIOCPROTOPRIVATE) #define SIOCAIPXPRISLT (SIOCPROTOPRIVATE+1) #define SIOCIPXCFGDATA (SIOCPROTOPRIVATE+2) #endif /*include/linux/ipx.h ************************************************************/ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 1.15 net.h 头文件 该结构定义 INET 网络层使用的重要数据结构以及一些常量定义。其中常量定义此处不做解 释,读者应该不难理解这些常量的意义。 该文件中定义的 INET 网络层所使用的重要数据结构有: 1>socket 结构 该结构在 INET 层表示一个网络套接字。 主要字段: type:该套接字所用的流类型,可取值有 SOCK_RAW,SOCK_DGRAM,SOCK_STREAM, SOCK_SEQPACKET,SOCK_PACKET. state:该套接字所处的连接状态,可取值有: SS_FREE = 0:这是一个空闲的套接字,“空闲”意为还没有被分配使用。 SS_UNCONNECTED:该 socket 结构已被分配给一个连接,但连接尚未建立。 SS_CONNECTING:正在与远端取得连接(用于 TCP 等有实际连接操作的协议)。 SS_CONNECTED:已与远端取得连接,正在进行数据的正常传送。 SS_DISCONNECTING:正与远端取消连接。 flags:标志字段,该字段目前尚无明确作用。 ops:操作函数集指针,即 INET 层相应域对应的操作函数集,可取值为 unix_proto_ops(UNIX 域:本机进程之间模拟网络操作方式的一种数据交换形式),inet_proto_ops(INET 域:使用 TCP/IP 协议)。 data:用于保存指向“私有”数据结构的指针。此处“私有”是对内核而言的,即对于不同 的域内核使用该指针指向不同的数据结构,如对于 UNIX 域,data 指针指向一个 unix_proto_data 数据结构,而对于 INET 域,其指向 sock 数据结构。 conn,iconn:这两个字段用于 UNIX 域,分别指向建立完全连接的对方 socket 结构以及等 待建立连接的 socket 结构。由于 UNIX 域用于本机进程之间,故 conn,iconn 用于指向对方 进程所对应的 socket 结构方才有意义。对于 INET 域而言,这两个字段没有意义。 next:构成 socket 结构的链表。 wait:等待队列,当进程要求的操作一时无法满足时(如进程进行读数据操作,而接收队列 无数据时,如果进程没有设置为 nonblock 读取形式,内核网络代码会自动安排该进程进入 睡眠状态,即通常所说的阻塞读取),就在该队列上睡眠等待,当系统可满足进程的要求时, 会唤醒该队列上的进程。 wait_queue 结构定义在 include/linux/wait.h 文件中: struct wait_queue { struct task_struct * task; struct wait_queue * next; }; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 inode:指 向 inode 结构的指针。socket 结构中安排这个指针的目的在于使得网络套接字可以 使用普通文件的读写函数进行数据的读取(read)和写入(write)。内核网络代码每当在新建 一个网络套接字连接时(socket 系统调用),都会分配一个 inode 结构,socket 结构中的 inode 字段指向这个 inode 结构,而 inode 结构中的联合类型(union)字段 u 中之 socket_i 字段反 向指向这个 socket 结构,另外还会分配一个 file 结构与 inode 结构对应,最后返回一个整型 类型的文件描述符。之后用户通过该文件描述符进行套接字操作(如读写)时,系统由该文 件描述符得到 file 结构,调用 file 结构 f_op 字段指向的相应的操作函数处理用户命令,而这 个操作函数将根据 file 结构中 f_inode 字段指向的 inode 结构得到相应的 socket 结构 (inode->u.socket_i),接着调用 socket 结构中 ops 字段指向具体函数完成用户要求的命令。当 然事情远远没有结束,这个逐渐调用的过程会继续进行下去,直到到达传输层才会根据具体 的协议进行实质上的处理。 fasync_list:指向 fasync_struct 结构构成的链表。该结构用于同步文件的读写。fasync_struct 结构定义在 include/linux/fs.h 文件中。 struct fasync_struct { int magic; struct fasync_struct *fa_next; /* singly linked list */ struct file *fa_file; }; 2>操作函数集数据结构 proto_ops 该结构类型作为 socket 结构中的一个字段代表了对应操作域的操作函数集合。该集合定义 了网络套接字各种操作的函数指针。例如相应 INET 域对应的函数集合为(net/inet/af_inet.c) static struct proto_ops inet_proto_ops = { AF_INET, inet_create, inet_dup, inet_release, inet_bind, inet_connect, inet_socketpair, inet_accept, inet_getname, inet_read, inet_write, inet_select, inet_ioctl, inet_listen, inet_send, inet_recv, inet_sendto, inet_recvfrom, 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 inet_shutdown, inet_setsockopt, inet_getsockopt, inet_fcntl, }; 用户对网络套接字的所有操作都将经过这些函数的中间处理。例如用户使用 read 函数读取 网络数据时,所经过的函数调用路径为: readÆsys_readÆsock_readÆinet_readÆtcp_read net_proto 结构用于域协议的定义。 其中 name 为域名称如“UNIX”,“INET”等。init_func 函数指针指向域初始化函数如对于 UNIX 域,其初始化函数为 unix_proto_init,而 INET 域为 inet_proto_init 等。目前定义的域 如表 1-5 所示: 表 1-9 域协议 域名称 初始化函数 说明 “UNIX” unix_proto_init 本机进程间模拟网络的数据 一种交换形式 “INET” inet_proto_ini 使用 TCP/IP 协议族的网络数 据包交换协议 “802.2” p8022_proto_init 使用 802.2 协议 “SNAP” snap_proto_init 使用 SNAP ( Subnetwork Access Protocol)协议 “AX.25” ipx_proto_init “IPX” ipx_proto_init 使用 IPX 网络层协议进行数 据包交换(Novell 网络协议), “DDP” atalk_proto_init AppleTalk IEEE802 规范对局域网数据包格式定义了一组标准,以 OSI 模型为例,这组标准规定了物理 层,链路层的数据包首部格式。其中 802.3,802.4,802.5 为物理层标准,802.2 为链路层标 准。802.2 链路层标准定义了 OSI 链路层中 LLC(Logic Link Control:逻辑链路控制)子层。 802 标准首部格式如下图(图 1-20)所示: 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-20 802 标准首部格式 有关使用 802 标准传送数据包的内容参考 RFC-1042。 这些域定义在 net/protocol.c 文件中: struct net_proto protocols[] = { #ifdef CONFIG_UNIX { "UNIX", unix_proto_init }, #endif #if defined(CONFIG_IPX)||defined(CONFIG_ATALK) { "802.2", p8022_proto_init }, { "SNAP", snap_proto_init }, #endif #ifdef CONFIG_AX25 { "AX.25", ax25_proto_init }, #endif #ifdef CONFIG_INET { "INET", inet_proto_init }, #endif #ifdef CONFIG_IPX { "IPX", ipx_proto_init }, #endif #ifdef CONFIG_ATALK { "DDP", atalk_proto_init }, #endif { NULL, NULL } }; 文件结尾是对一些函数的声明。在讨论到具体这些函数的具体定义时再给予解释。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* include/linux/net.h ***********************************************************/ /* * NET An implementation of the SOCKET network access protocol. * This is the master header file for the Linux NET layer, * or, in plain English: the networking handling part of the * kernel. * * Version: @(#)net.h 1.0.3 05/25/93 * * Authors: Orest Zborowski, * Ross Biro, * Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_NET_H #define _LINUX_NET_H #include #include #define NSOCKETS 2000 /* Dynamic, this is MAX LIMIT */ #define NSOCKETS_UNIX 128 /* unix domain static limit */ #define NPROTO 16 /* should be enough for now.. */ #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) */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */ #define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */ typedef enum { SS_FREE = 0, /* not allocated */ SS_UNCONNECTED, /* unconnected to any socket */ SS_CONNECTING, /* in process of connecting */ SS_CONNECTED, /* connected to socket */ SS_DISCONNECTING /* in process of disconnecting */ } socket_state; #define SO_ACCEPTCON (1<<16) /* performed a listen */ #define SO_WAITDATA (1<<17) /* wait data to read */ #define SO_NOSPACE (1<<18) /* no space to write */ #ifdef __KERNEL__ /* * Internal representation of a socket. not all the fields are used by * all configurations: * * server client * conn client connected to server connected to * iconn list of clients -unused- * awaiting connections * wait sleep for clients, sleep for connection, * sleep for i/o sleep for i/o */ struct socket { short type; /* SOCK_STREAM, ... */ socket_state state; long flags; struct proto_ops *ops; /* protocols do most everything */ void *data; /* protocol data */ struct socket *conn; /* server socket connected to */ struct socket *iconn; /* incomplete client conn.s */ struct socket *next; struct wait_queue **wait; /* ptr to place to wait on */ struct inode *inode; struct fasync_struct *fasync_list; /* Asynchronous wake up list */ }; #define SOCK_INODE(S) ((S)->inode) 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 struct proto_ops { int family; int (*create) (struct socket *sock, int protocol); 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); int (*read) (struct socket *sock, char *ubuf, int size, int nonblock); int (*write) (struct socket *sock, char *ubuf, int size, int nonblock); int (*select) (struct socket *sock, int sel_type, select_table *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); int (*listen) (struct socket *sock, int len); int (*send) (struct socket *sock, void *buff, int len, int nonblock, unsigned flags); int (*recv) (struct socket *sock, void *buff, int len, int nonblock, unsigned flags); int (*sendto) (struct socket *sock, void *buff, int len, int nonblock, unsigned flags, struct sockaddr *, int addr_len); int (*recvfrom) (struct socket *sock, void *buff, int len, int nonblock, unsigned flags, struct sockaddr *, int *addr_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); }; struct net_proto { char *name; /* Protocol name */ void (*init_func)(struct net_proto *); /* Bootstrap */ }; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 extern int sock_awaitconn(struct socket *mysock, struct socket *servsock, int flags); extern int sock_wake_async(struct socket *sock, int how); extern int sock_register(int family, struct proto_ops *ops); extern int sock_unregister(int family); extern struct socket *sock_alloc(void); extern void sock_release(struct socket *sock); #endif /* __KERNEL__ */ #endif /* _LINUX_NET_H */ /* include/linux/net.h ************************************************************/ 1.16 netdevice.h 头文件 该文件定义有内核网络栈核心数据结构 device 结构(2.4.0 版本后更名为现在的 net_device)。 另外还有其它重要数据结构的定义如 dev_mac_list (多播地址),packet_type (网络层协议结 构),最后是对部分重要函数的声明,这些函数分布定义在多个文件中。 DEV_NUMBUFFS 数据包发送硬件缓冲队列数目,定义为 3,即存在 3 个级别的发送队列,每个发送队列作为 device 结构中 buffs 数组的一个元素而存在。而 buffs 数组的元素数目即为 DEV_NUMBUFFS。 MAX_ADDR_LEN 硬件地址最大长度,定义为 7,实际上一般为 6 个字节,最后一个字节赋值为 0,便于字符 串处理。 MAX_HEADER 最大链路层首部长度,定义为 18,其中包括了帧尾部的 FCS 字段 4 个字节。 IS_MYADDR:远端发往本机的数据包 IS_LOOPBACK:本机发往本机的数据包 IS_BROADCAST:远端或本机发送的广播数据包 IS_INVBCAST:无效的广播数据包 IS_MULTICAST:多播数据包 所接收数据包的 IP 目的地址相对于本机而言的地址类别。 dev_mac_list 结构(如下所示)用于表示链路层多播地址。 struct dev_mc_list { struct dev_mc_list *next; char dmi_addr[MAX_ADDR_LEN]; unsigned short dmi_addrlen; unsigned short dmi_users; }; next:用于构成一个多播地址链表。 dmi_addr:具体多播地址 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 dmi_addrlen:多播地址长度,一般即为硬件地址长度即 6 个字节。 dmi_users:该多播地址结构的使用数。 device 结构:网络栈代码核心结构,其字段及其含义如下: name 设备名称 rmem_end, rmem_start 设备读缓冲区空间。 mem_end, mem_start 设备总缓冲区首地址和尾地址。注意这两个字段所表示的缓冲区将被分为两个部分:读缓冲 区和写缓冲区。一般写缓冲区占据整缓冲区前面一部分,之后的缓冲区均划为读缓冲区。读 缓冲区将有 rmem_start, rmem_end 字段具体表示。 base_addr 设备寄存器读写 I/O 基地址。 irq 设备所使用中断号。 start,tbusy,interrupt 这两个字段表示设备所处的状态: start=1 表示设备已处于工作状态。 tbusy=1 表示设备正忙于数据包发送,如果再有数据包发送,则需等待。 interrupt=1:软件正在进行设备中断处理。 next 构成一个设备队列,dev_base(drivers/net/Space.c)内核指针即指向这个队列。 init 设备初始化指针,对于系统静态定义的设备,在网络栈初始化时被调用对设备进行相应的初 始化工作。而对于“动态插入”的设备(以动态模块方式加载的设备),则是在注册设备时 (通过调用 register_netdev 函数),该指针指向的函数被调用初始化设备。对于网卡驱动程 序编写而言,如果对应的设备需要初始化,则应将该指针指向对应的函数。 if_port 对于具有多个端口的设备,该字段指定使用的设备端口号。例如设备同时支持铜轴电缆和双 绞线以太网连接时,需要使用该字段。但该字段不常使用,因为大多数设备通常只有一个端 口。 dma 设备所用的 DMA 通道号,如果设备使用 DMA 方式进行数据的传送。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 get_stats 设备信息获取函数指针。所谓信息一般即指经过该设备接收和发送的数据包个数,总字节数, 发送或接收失败的数据包个数等等。具体参考 enet_statistics(if_ether.h)结构的定义。 trans_start 该字段的设置功能用于传输超时计算。每当设备新发送一个数据包时,更新该字段为当前系 统当前时间(以 jiffies 表示),如果传输超时时间为 T,则当 jiffies>trans_start+T 时,表示传 输超时,此时需要对数据包进行重新发送。不过在本版本内核网络代码实现上,并没有使用 该字段判断传输超时,而是在传输层进行超时判断和重传(使用系统定时器实现)。 last_rx 该字段表示上次接收一个数据包的时间,以 jiffies 值表示。该字段仅仅用于表示信息,没有 使用在其它判断条件上。 flags 设备标志位。该标志位一方面表示设备目前所处的状态,另一方面表示设备所具备的功能。 该字段的取值如下(定义在 if.h 文件中): IFF_UP 设备正处正常运行状态。 IFF_BROADCAST 设备支持广播,即设备结构中广播字段地址有效。 IFF_DEBUG 打开设备的调试选项,打印调试信息。 IFF_LOOPBACK 这是一个回环设备,该设备并无实际硬件对应,而仅存在于软件上。 IFF_POINTOPOINT 这是一个点到点的网络设备,此时 device 结构中 pa_dstaddr 地址有效,表示对方 IP 地址。 IFF_NOTRAILERS 不使用网络跟踪选项。 IFF_RUNNING 设备正处于正常运行状态,该字段本版本中的含义同 IFF_UP。 IFF_NOARP 该设备不使用 ARP 协议完成 IP 地址到硬件地址的映射,如回环设备。 IFF_PROMISC 该设备处于混杂接收模式,即接收该设备所连接网络上传输的所有数据包,无论该数据包 发往何处。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 IFF_ALLMULTI 指定该设备接收所有多播数据包。 IFF_MASTER IFF_SLAVE 这两个字段用于主从设备运行模式。一个主设备管理多个从设备。一般主设备是软件上的设 比,而从设备是真正的硬件网卡设备。当发送数据包时,主设备根据某种策略选择一个从设 备完成实际的发送。主设备中对应的 device 结构中 flags 标志位设置 IFF_MASTER 标志, 而从设备设置 IFF_SLAVE 标志。 IFF_MULTICAST 该设备支持多播,此时 device 结构中 mc_list 字段有效,表示该设备所支持的多播地址。 family 该字段表示设备所属的域协议,可取值如下(均定义在 include/linux/socket.h): AF_UNSPEC AF_UNIX AF_INET AF_AX25 AF_IPX AF_APPLETALK metric 代价度量值,现在一般由来表示所经过的路由器数目。本版本没有使用该字段。 mtu 该接口设备的最大传输单元。 type 该设备所属的硬件类型。该字段可取值如下(这些值均定义在 if_arp.h 文件中): ARPHRD_NETROM NET/ROM 是一种被无线电业余爱好者广泛使用的协议。LINUX NET/ROM 协议允许使用类 似网络套接字的方式接收数据。NET/ROM 协议只支持连接模式,这种模式也被使用在 SOCK_SEQPACKET 类型的套接字通信中。用户必须保证发送的数据被适当的打包,接收 的数据也是以一个包的形式接收到缓冲区中。NET/ROM 地址格式由 6 个 ASCII 字符和一个 称为 SSID 的数字构成,sockaddr_ax25 数据结构用于表示这种地址结构形式。 ARPHRD_ETHER 10Mbps 以太网硬件类型。 ARPHRD_EETHER 实验型以太网硬件类型,这是早期的叫法,现在应该称之为 EtherII 类型。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 ARPHRD_AX25 AX.25 是从 X.25 协议族演变而来的一种链路层协议。该协议被设计为业余无线电爱好者使 用。AX.25 协议定义了 OSI 物理层和链路层两个层面,负责在节点之间传输数据,并且探 测通信通道中可能出现的错误。AX.25 支持连接的和无连接通信方式。AX.25 大多被使用在 无线电工作站之间建立直接的点对点连接。AX.25 定义了一个完备的网络协议族,但较少使 用。NET/ROM,ROSE,TexNet 协议通常提供节点之间的路由功能。原理上讲,任何网络 层协议都可使用 AX.25 传输数据包,包括最普遍使用的 IP 协议。 ARPHRD_PRONET Proteon 公司制定的一种令牌环网络协议。该协议本版本内核网络代码没有实现。 ARPHRD_CHAOS Chaosnet 最早是由 MIT AI 实验室的 Thomas Knight 和 Jack Holloway 发展起来的。主要涉及 两方面独立的但又相互联系的技术。第一也是较为广泛使用的是建立在 MIT 校园内部逐渐 流行的 Lisp 主机之间的一系列以包为基础的通信协议技术。第二是早期局域网硬件实现技 术。Chaosnet 协议最初是基于后一种技术实现的,而这种实现又是基于早期 Xerox PARC 3Mbs 以太网和 ARPANET 网以及 TCP 协议。Chaosnet 网络拓扑结构通常是由一系列线性(而 非环形)电缆构成,每条电缆长度可达 1 千米,大约可挂 12 个客户端。Chaosnet 协议后来 被作为以太网协议包结构中负载而实现。Chaosnet 特别的被使用在局域网中,对于广域网的 支持不够。 ARPHRD_IEEE802 IEEE 制定的对应于 OSI 模型物理层和链路层的标准。 ARPHRD_ARCNET ARCNET 是 Attached Resource Computer NETwork 首字母缩写形式,是一种局域网网络协议。 ARCNET 由 Datapoint 公司的首席工程师 John Murphy 于 1976 年发展起来的,最初被使用 在微型机的网络系统中,在 1980 年代办公自动化应用中逐渐流行。自此逐渐在嵌入式系统 中占据一席之地。它是第一个以局域网为基础的聚族解决方式,最初的设计理念是作为更大 更贵计算机系统的一种替代方案。一个应用程序可以被部署在一个具有哑终端的计算机上, 当用户数据超过计算机所允许计算能力时,其它计算机作为一种资源通过 ARCnet 网络连接 进来,运行同一个应用程序以提供更强的计算能力。这种逐渐增强计算能力的方式有些类似 于今天常说的分布式计算方式。在 1970 年代末,全世界有超过 1 万个 ARCNET 局域网被作 为商业应用。为满足更高的宽带要求,一个称为 ARCnet Plus 的标准被开发出来并于 1992 公布。ARCNET 最终被标准化为 ANSI ARCNET 878.1。名字也由 ARCnet 改为 ARCNET。 有关ARCNET的更为详细的内容请访问:http://en.wikipedia.org/wiki/ARCnet。 ARPHRD_APPLETLK AppleTalk 是 Apple 公司为网络计算机开发的一套协议族。在早期 Macintosh 计算机上被使 用,现在 AppleTalk 逐渐淡出,Apple 公司也逐渐转向 TCP/IP 协议族。AppleTalk 是一套协 议族,而并非单个协议。这个协议族包括:AppleTalk 地址解析协议(AARP),AppleTalk 数据系统协议(ADSP),Apple 文件协议(AFP),AppleTalk 会话协议(ASP),AppleTalk 交换协议(ATP),数据报传输协议(DDP),名字绑定协议(NBP),打印机访问协议(PAP), 路由表维护协议(RTMP),区域信息协议(ZIP)。这套协议独成系统完成网络数据交换。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 AppleTalk 的设计紧紧依照 OSI 七层模型分层结构。表 1-6 显示了 AppleTalk 物理实现与 OSI 模型的分层对应关系。 表 1-10 AppleTalk 分层结构与 OSI 模型 OSI 模型 对应的 AppleTalk 分层协议 应用层 Apple 文件协议(AFP) 表示层 Apple 文件协议(AFP) 会话层 区域信息协议(ZIP) AppleTalk 会话协议(ASP) AppleTalk 数据流协议(ADSP) 传输层 AppleTalk 交换协议(ATP) AppleTalk 回复协议(AEP) 名字绑定协议(NBP) 路由表维护协议(RTMP) 网络层 数据包传输协议(DDP) 链路层 EtherTalk 链路访问协议(ELAP) LocalTalk 链路访问协议(LLAP) TokenTalk 链路访问协议(TLAP) 光纤分布式数据接口(FDDI) 物理层 LocalTalk 驱动程序,Ethernet 驱动程序,Token Ring 驱动程序… 以下这些类型均无需 ARP 协议进行 IP 地址到硬件地址的解析。 ARPHRD_SLIP ARPHRD_CSLIP ARPHRD_SLIP6 ARPHRD_CSLIP6 串行线网际协议。 ARPHRD_RSRVD 保留。 ARPHRD_ADAPT 适配模式下的 SLIP 协议。 ARPHRD_PPP 点对点协议。 ARPHRD_TUNNEL IP 隧道协议。 hard_header_len 硬件首部长度,如以太网硬件首部长度为 14 字节。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 priv 私有数据指针,一般使用在网卡驱动程序中用于指向自定义数据结构。 broadcast[MAX_ADDR_LEN] 链路层硬件广播地址。 dev_addr[MAX_ADDR_LEN] 本设备硬件地址。 addr_len 硬件地址长度,如以太网硬件地址长度为 6 个字节。 pa_addr 本地 IP 地址。 pa_brdaddr 网络层广播 IP 地址。 pa_dstaddr 该字段仅使用在点对点网络中指对点 IP 地址。 pa_mask IP 地址网络掩码。 pa_alen IP 地址长度,通常为 4 个字节。 mc_list mc_count MAC 多播地址链表,对于一个多播数据包,如果其多播地址是链表中一项,则接收,否则 丢弃。mc_count 表示链表中多播地址数目。注意这个链表中存储的是 MAC 多播地址,而 ip_mc_list 链表中存储的是 IP 多播地址。MAC 多播地址与 IP 多播地址之间的映射关系在第 二章中分析 igmp.c 文件中给出。到时将对 device 结构,sock 结构中用于多播的字段进行集 中分析。 ip_mc_list 网络层 IP 多播地址链表,处理方式同 mc_list. pkt_queue 该设备缓存的待发送的数据包个数。 slave 用于虚拟主从设备机制中,该字段指向某个从设备。虚拟主从设备机制中,主设备管理多个 从设备,且主设备一般仅存在于软件上,具体的发送和接收数据包由从设备完成。当上层发 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 送某个数据包时,主设备根据某种策略选择其中一个从设备,将数据包交给该从设备完成具 体的发送任务。 buffs[DEV_NUMBUFFS] 设备缓存的待发送的数据包,这些数据包由于某种原因之前没有成功发送,故缓存到 device 结构的 buffs 数组指向的某个队列中。DEV_NUMBUFFS 值为 3,即有三个缓冲队列,分为 三个优先级。每次发送从第一个元素指向的队列中摘取数据包发送,第一个队列空后,继续 发送地址个队列,依次到第三个队列。 open stop 设备打开和关闭时调用的函数,网卡驱动程序需要实现这个函数,并将这个函数指针指向相 应的函数,从而当用户停止或启动网卡工作时进行适当的响应。 hard_start_xmit 数据包发送函数,网卡驱动程序中数据包发送核心函数,网络栈上层代码将调用该函数指针 指向的函数发送数据包,该函数硬件相关,主要将内核缓冲区中用户要发送数据复制到硬件 缓冲区中并启动设备将数据发送到物理传输介质上(如铜轴电缆或双绞线)。 hard_header rebuild_header 硬件首部建立函数。这两个函数用于建立 MAC 首部。hard_header 一般仅在第一次建立硬件 首部时被调用,如果首次建立硬件首部失败(如暂时无法知道远端的硬件地址),则之后可 重复多次调用 rebuild_header 函数重新建立硬件首部。网卡驱动程序必须对这两个字段进行 适当初始化,即便是不使用 ARP 协议。 type_trans 该指针指向的函数用于从接收到的数据包提取 MAC 首部中类型字段值,从而将数据包传送 给适当的协议处理函数进行处理。 set_multicast_list 该指针指向的函数用于设置该设备的多播地址。 set_mac_address 如果设备支持改变其硬件地址,则该指针指向的函数即可被调用改变该设备对应的硬件地 址。 do_ioctl set_config 这两个函数用于设置或获取设备的有关控制或统计信息。如可改变设备 MTU 值大小。 到此为止,device 结构的所有字段均以解释完毕。对于 device 结构字段的深刻理解是编写好 网卡驱动程序的关键。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 packet_type 结构用于表示网络层协议。每个网络层协议对应一个 packet_type 结构。内核支 持的所有网络层协议构成一个链表,系统变量 ptype_base(net/inet/dev.c)指向这个链表。 当系统接收到一个网络数据包时,通过 type_trans 指针指向函数得到数据包所使用的网络层 协议,之后遍历 ptype_base 指向的链表,比较 packet_type 结构中 type 字段与数据包所使用 的协议是否相符,如相符,则调用 packet_type 结构中 func 指针指向的函数,将数据包交给 其处理(即传往上层进行处理)。例如对应 IP 协议,对应的处理函数为 ip_rcv。 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; }; type 对应的网络层协议。该字段可取值如下(include/linux/if_ether.h): ETH_P_LOOP ETH_P_ECHO ETH_P_PUP ETH_P_IP ETH_P_ARP ETH_P_RARP ETH_P_X25 ETH_P_ATALK ETH_P_IPX ETH_P_802_3 ETH_P_AX25 ETH_P_ALL ETH_P_802_2 ETH_P_SNAP dev 数据包接收网络接口设备。一般初始化为 NULL,即并不计较数据包是从哪个网络接口接收 的,只要满足 type 字段,就调用处理函数进行处理。 func 协议处理函数。如对于 IP 协议,该指针指向 ip_rcv 函数。 data 指向特定数据结构以进行某种区分。这在下文分析到具体函数时(dev.c 文件中)在进行交 待。 next 该字段用于构成一个由 packet_type 结构组成的链表。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 该文件最后部分声明了一系列重要函数,在下文中分析到相关部分时进行讨论,现在读者只 需有个认识即可。 /* include/linux/netdevice.h ******************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions for the Interfaces handler. * * Version: @(#)dev.h 1.0.10 08/12/93 * * Authors: Ross Biro, * Fred N. van Kempen, * Corey Minyard * Donald J. Becker, * Alan Cox, * Bjorn Ekwall. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. * * Moved to /usr/include/linux for NET3 */ #ifndef _LINUX_NETDEVICE_H #define _LINUX_NETDEVICE_H #include #include #include /* for future expansion when we will have different priorities. */ #define DEV_NUMBUFFS 3 #define MAX_ADDR_LEN 7 #define MAX_HEADER 18 #define IS_MYADDR 1 /* address is (one of) our own */ #define IS_LOOPBACK 2 /* address is for LOOPBACK */ #define IS_BROADCAST 3 /* address is a valid broadcast */ #define IS_INVBCAST 4 /* Wrong netmask bcast not for us (unused)*/ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define IS_MULTICAST 5 /* Multicast IP address */ /* * We tag these structures with multicasts. */ struct dev_mc_list { struct dev_mc_list *next; char dmi_addr[MAX_ADDR_LEN]; unsigned short dmi_addrlen; unsigned short dmi_users; }; /* * The DEVICE structure. * Actually, this whole structure is a big mistake. It mixes I/O * data with strictly "high-level" data, and it has to know about * almost every data structure used in the INET module. */ struct device { /* * This is the first field of the "visible" part of this structure * (i.e. as seen by users in the "Space.c" file). It is the name * the interface. */ char *name; /* I/O specific fields - FIXME: Merge these and struct ifmap into one */ unsigned long rmem_end; /* shmem "recv" end */ unsigned long rmem_start; /* shmem "recv" start */ unsigned long mem_end; /* sahared mem end */ unsigned long mem_start; /* shared mem start */ unsigned long base_addr; /* device I/O address */ unsigned char irq; /* device IRQ number */ /* Low-level status flags. */ volatile unsigned char start, /* start an operation */ tbusy, /* transmitter busy */ interrupt; /* interrupt arrived */ struct device *next; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* The device initialization function. Called only once. */ int (*init)(struct device *dev); /* Some hardware also needs these fields, but they are not part of the usual set specified in Space.c. */ unsigned char if_port; /* Selectable AUI, TP,..*/ unsigned char dma; /* DMA channel */ struct enet_statistics* (*get_stats)(struct device *dev); /* * This marks the end of the "visible" part of the structure. All * fields hereafter are internal to the system, and may change at * will (read: may be cleaned up at will). */ /* These may be needed for future network-power-down code. */ unsigned long trans_start; /* Time (in jiffies) of last Tx */ unsigned long last_rx; /* Time of last Rx */ unsigned short flags; /* interface flags (a la BSD) */ unsigned short family; /* address family ID (AF_INET) */ unsigned short metric; /* routing metric (not used) */ unsigned short mtu; /* interface MTU value */ unsigned short type; /* interface hardware type */ unsigned short hard_header_len; /* hardware hdr length */ void *priv; /* pointer to private data */ /* Interface address info. */ unsigned char broadcast[MAX_ADDR_LEN]; /* hw bcast add */ unsigned char dev_addr[MAX_ADDR_LEN]; /* hw address */ unsigned char addr_len; /* hardware address length */ unsigned long pa_addr; /* protocol address */ unsigned long pa_brdaddr; /* protocol broadcast addr */ unsigned long pa_dstaddr; /* protocol P-P other side addr */ unsigned long pa_mask; /* protocol netmask */ unsigned short pa_alen; /* protocol address length */ struct dev_mc_list *mc_list; /* Multicast mac addresses */ int mc_count; /* Number of installed mcasts*/ struct ip_mc_list *ip_mc_list; /* IP multicast filter chain */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* For load balancing driver pair support */ unsigned long pkt_queue; /* Packets queued */ struct device *slave; /* Slave device */ /* Pointer to the interface buffers. */ struct sk_buff_head buffs[DEV_NUMBUFFS]; /* Pointers to interface service routines. */ int (*open)(struct device *dev); int (*stop)(struct device *dev); int (*hard_start_xmit) (struct sk_buff *skb, struct device *dev); int (*hard_header) (unsigned char *buff, struct device *dev, unsigned short type, void *daddr, void *saddr, unsigned len, struct sk_buff *skb); int (*rebuild_header)(void *eth, struct device *dev, unsigned long raddr, struct sk_buff *skb); unsigned short (*type_trans) (struct sk_buff *skb, struct device *dev); #define HAVE_MULTICAST void (*set_multicast_list)(struct device *dev, int num_addrs, void *addrs); #define HAVE_SET_MAC_ADDR int (*set_mac_address)(struct device *dev, void *addr); #define HAVE_PRIVATE_IOCTL int (*do_ioctl)(struct device *dev, struct ifreq *ifr, int cmd); #define HAVE_SET_CONFIG int (*set_config)(struct device *dev, struct ifmap *map); }; 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; }; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #ifdef __KERNEL__ #include /* Used by dev_rint */ #define IN_SKBUFF 1 extern volatile char in_bh; extern struct device loopback_dev; extern struct device *dev_base; extern struct packet_type *ptype_base; extern int ip_addr_match(unsigned long addr1, unsigned long addr2); extern int ip_chk_addr(unsigned long addr); extern struct device *ip_dev_check(unsigned long daddr); extern unsigned long ip_my_addr(void); extern unsigned long ip_get_mask(unsigned long addr); extern void dev_add_pack(struct packet_type *pt); extern void dev_remove_pack(struct packet_type *pt); extern struct device *dev_get(char *name); extern int dev_open(struct device *dev); extern int dev_close(struct device *dev); extern void dev_queue_xmit(struct sk_buff *skb, struct device *dev, int pri); #define HAVE_NETIF_RX 1 extern void netif_rx(struct sk_buff *skb); /* The old interface to netif_rx(). */ extern int dev_rint(unsigned char *buff, long len, int flags, struct device * dev); extern void dev_transmit(void); extern int in_net_bh(void); extern void net_bh(void *tmp); extern void dev_tint(struct device *dev); extern int dev_get_info(char *buffer, char **start, off_t offset, int length); extern int dev_ioctl(unsigned int cmd, void *); extern void dev_init(void); /* These functions live elsewhere (drivers/net/net_init.c, but related) */ extern void ether_setup(struct device *dev); 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 extern int ether_config(struct device *dev, struct ifmap *map); /* Support for loadable net-drivers */ extern int register_netdev(struct device *dev); extern void unregister_netdev(struct device *dev); extern int register_netdevice_notifier(struct notifier_block *nb); extern int unregister_netdevice_notifier(struct notifier_block *nb); /* Functions used for multicast support */ extern void dev_mc_upload(struct device *dev); extern void dev_mc_delete(struct device *dev, void *addr, int alen, int all); extern void dev_mc_add(struct device *dev, void *addr, int alen, int newonly); extern void dev_mc_discard(struct device *dev); /* This is the wrong place but it'll do for the moment */ extern void ip_mc_allhost(struct device *dev); #endif /* __KERNEL__ */ #endif /* _LINUX_DEV_H */ /* include/linux/netdevice.h ******************************************************/ 1.17 notifier.h 头文件 该文件定义了一个用于主动通知的策略:notifer_block 数据结构。对于网卡设备而言,网卡 设备的启动和关闭是事件。内核中某些模块需要得到这些事件的通知从而采取相应的措施。 这是通过主动通知的方式完成的。当事件发生时,事件通知者遍历系统中某个队列,调用感 兴趣者注册的函数,从而通知这些感兴趣者。要得到事件的通知,首先必须表示对这些事件 的兴趣,而要表示对某个事件感兴趣,首先必须注册这个“兴趣”,而 notifer_block 结构就 表示一个“兴趣”。一般一个队列本身表示某种类型的事件,对此种类型事件感兴趣者注册 到这个队列中,就表示了其兴趣所在。当事件发生时,该队列中所有的感兴趣者之前注册的 函数将被调用,从而得到对事件发生的通知。 struct notifier_block { int (*notifier_call)(unsigned long, void *); struct notifier_block *next; int priority; }; notifier_call 感兴趣者注册的被调用函数。当注册的感兴趣事件发生时,事件通知者将调用该函数以通知 感兴趣者。 next 该字段用于连接到一个队列中。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 prority 优先级,即该感兴趣者对某种事件感兴趣的程度。优先级越大,表示越关注此种事件,所以 将最先被通知到。其注册时对应的 notifier_block 结构将被插入到队列前部。 NOTIFY_DONE NOTIFY_OK NOTIFY_STOP_MASK NOTIFY_BAD 这四个常量用于表示何时终止对感兴趣者队列中注册函数的调用。如果前一个注册函数返回 NOTIFY_DONE,表示通知完成,此后即便队列还有尚未被通知到的感兴趣者,依然停止通 知操作。返回 NOTIFY_OK 则继续进行通知直到队列尾部。NOTIFY_BAD 表示某个感兴趣 者出现问题,此时也是停止通知队列中其它感兴趣者。NOTIFY_STOP_MASK 是位掩码, 用于判断返回值。 notifier_chain_register 函数用于感兴趣者进行注册事件发生时被调用的函数从而得到事件发 生通知。 其中 list 参数表示插入的队列,诚如上文所述,一般队列本身即表示某种类型的事件,如果 对此类事件感兴趣,就注册到该队列中。 参数 n 中含有事件发生时被调用的函数。 extern __inline__ int notifier_chain_register(struct notifier_block **list, struct notifier_block *n) { while(*list) { if(n->priority > (*list)->priority) break; list= &((*list)->next); } n->next = *list; *list=n; return 0; } 该函数的功能即将表示感兴趣者的 notifier_block 结构根据其优先级插入到系统某个队列中。 notifier_chain_unregister 函数完成的功能与 notifier_chain_register 函数正好相反。其撤销对某 类事件的兴趣,也即将表示兴趣的相应 notifier_block 结构从相应队列中删除。 extern __inline__ int notifier_chain_unregister(struct notifier_block **nl, struct notifier_block *n) { while((*nl)!=NULL) { if((*nl)==n) { *nl=n->next; return 0; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 } nl=&((*nl)->next); } return -ENOENT; } 至于 notifier_call_chain 则是当事件发生时完成对感兴趣者的通知。通知方式即调用感兴趣者 之前注册的函数。 extern __inline__ int notifier_call_chain(struct notifier_block **n, unsigned long val, void *v) { int ret=NOTIFY_DONE; struct notifier_block *nb = *n; while(nb) { ret=nb->notifier_call(val,v); if(ret&NOTIFY_STOP_MASK) return ret; nb=nb->next; } return ret; } 该函数结构简单,此处不多作说明。 文件最后定义了几种事件: NETDEV_UP:某个网卡设备启动 NETDEV_DOWN:某个网卡设备关闭 NETDEV_REBOOT:设备进行重启动 本版本内核网络代码维护一个事件通知队列:由全局变量 netdev_chain 指向的队列。该队列 表示了以上三种事件。定义如下(net/inet/dev.c 文件中): struct notifier_block *netdev_chain=NULL; /* include/linux/notifier.h *******************************************************/ /* * Routines to manage notifier chains for passing status changes to any * interested routines. We need this instead of hard coded call lists so * that modules can poke their nose into the innards. The network devices * needed them so here they are for the rest of you. * * Alan Cox */ #ifndef _LINUX_NOTIFIER_H #define _LINUX_NOTIFIER_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #include struct notifier_block { int (*notifier_call)(unsigned long, void *); struct notifier_block *next; int priority; }; #ifdef __KERNEL__ #define NOTIFY_DONE 0x0000 /* Don't care */ #define NOTIFY_OK 0x0001 /* Suits me */ #define NOTIFY_STOP_MASK 0x8000 /* Don't call further */ #define NOTIFY_BAD (NOTIFY_STOP_MASK|0x0002) /* Bad/Veto action */ extern __inline__ int notifier_chain_register(struct notifier_block **list, struct notifier_block *n) { while(*list) { if(n->priority > (*list)->priority) break; list= &((*list)->next); } n->next = *list; *list=n; return 0; } /* * Warning to any non GPL module writers out there.. these functions are * GPL'd */ extern __inline__ int notifier_chain_unregister(struct notifier_block **nl, struct notifier_block *n) { while((*nl)!=NULL) { if((*nl)==n) { *nl=n->next; return 0; } 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 nl=&((*nl)->next); } return -ENOENT; } /* * This is one of these things that is generally shorter inline */ extern __inline__ int notifier_call_chain(struct notifier_block **n, unsigned long val, void *v) { int ret=NOTIFY_DONE; struct notifier_block *nb = *n; while(nb) { ret=nb->notifier_call(val,v); if(ret&NOTIFY_STOP_MASK) return ret; nb=nb->next; } return ret; } /* * Declared notifiers so far. I can imagine quite a few more chains * over time (eg laptop power reset chains, reboot chain (to clean * device units up), device [un]mount chain, module load/unload chain, * low memory chain, screenblank chain (for plug in modular screenblankers) * VC switch chains (for loadable kernel svgalib VC switch helpers) etc... */ /* netdevice notifier chain */ #define NETDEV_UP 0x0001 /* For now you can't veto a device up/down */ #define NETDEV_DOWN 0x0002 /* Tell a protocol stack a network interface detected a hardware crash and restarted - we can use this eg to kick tcp sessions once done */ #define NETDEV_REBOOT 0x0003 #endif #endif 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* include/linux/notifier.h *******************************************************/ 1.18 ppp.h 头文件 点对点协议(PPP:Point-to-Point Protocol)为在点对点连接上传输多种网络层协议类型的数 据包提供了一种标准方法。该协议修改了 SLIP 协议中所有的缺陷。PPP 协议包括以下三个 功能模块: 1>在串行链路上封装数据包的方法。PPP 既支持数据为 8 位和无奇偶校验的异步模式,还支 持面向比特的同步连接。 2>建立,配置和测试数据链路的链路控制协议(LCP:Link Control Protocol)。它允许通信 双方进行协商决定某些选项的使用,如是否使用压缩,以及进行最大接收单元大小的通报。 3>针对不同网络层协议的网络控制协议(NCP:Network Control Protocol)。如针对 IP 数据 包传输的 IPCP(IP Control Protocol)协议。 PPP 协议定义了在链路层数据包的封装格式,如图 1-21 所示。 图 1-21 PPP 协议报文封装格式 标志字段:1 字节 PPP 协议每帧均是以一个标志字段开始,该字段值固定为 0x7E。所有 PPP 协议的实现必须 检查该字段以进行数据帧的接收同步。在帧与帧之间只需要插入一个标志字段,两个连续的 标志字段表示传输了空帧,对于 PPP 协议的实现而言,传输空帧是合法的,该空帧将被丢 弃而不造成任何影响。 地址字段:1 字节 该字段值固定为 0xFF。因为对于一个点对点连接,数据传送方向只可能有一个,所以该字 段被固定为广播地址。该字段在将来的使用中也可能有其它定义形式。如果一个帧具有一个 非法的地址字段值,该数据帧将被丢弃而不进行任何其它操作。 控制字段:1 字节 该字段值固定为 0x03。该字段在将来的使用中可能被定义其它值。如果一个帧具有一个非 法的控制字段值,则该数据帧也将被丢弃而不造成任何其它影响。 协议字段:1 字节或者 2 字节 该字段可能为 1 字节,也有可能为 2 字节。该字段值指定了该数据帧所使用的传输协议类型。 图 1-20 给出了简单的三种协议值及其对应的数据报类型。该字段值的结构符合 ISO3309 地 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 址域扩展机制。所有的协议字段值均为奇数,即最低字节的最低位为 1。 该字段值所代表的范围表示不同的含义。 0x0***--0x3***:代表网络层协议(如 IP 协议)报文。 0x4***--0x7***:用于低通量链路连接中的相关协议。 0x8***--0xB***:用于网络控制协议(NCP)报文。 0xC***--0xF***:用于链路控制协议(LCP)报文。 数据信息字段,填充字段:长度由 MRU(最大接收单元)决定 数据信息字段有 0 或多个字节构成,代表协议字段部分指定类型的数据报文。数据字段(包 括填充字段,不包括协议字段)的最大长度被称为最大接收单元(MRU:Maximum Receive Unit),该字段默认值一般为 1500 字节。MRU 的值可通过 LCP 协议进行双方协议以使用另 一个值。 帧校验序列字段(FCS:Frame Check Sequence):2 字节(默认)或者 4 字节 该字段用于数据校验。一般为 2 字节长度。4 字节长度的使用可通过双方通过 LCP 协议协 商而定,用于某些特殊情况下。 该字段覆盖的计算范围是:地址字段,控制字段,协议字段,数据字段和填充字段。注意由 于在数据传输中可能会插入一些辅助位(如异步传输中的起始和结束位,以及同步传输中插 入的任何数据位)都不计入校验和的计算。 标志字段:1 字节 同开始处的标志字段,取值固定为 0x7E,该字段与起始的标志字段定义了一帧数据的边界。 帧间填充字节或者下一个地址字段: 如果两个帧的数据是连续的,则该字段应该是下一个地址字段,即取值为 0xFF,之后的封 装如同图 1-20 所示。如果两帧数据不是连续的,则帧与帧之间就会插入填充字节。按照传 输方式的不同,填充字节取值不同。对于同步传输,在此期间即不断的传输标志字段值 0x7E, 直到下一帧数据开始时,则开始传输地址字段值,从而开始下一帧数据的传输。而对于异步 传输方式,则在下一帧数据传输之前的空隙当中,应不停的传输 1(称为 mark-hold 状态) 直到开始传输下一帧数据同步字段(即标志字段)值 0x7E。 PPP 协议链路操作 为了进行建立在点对点物理连接上的数据传输,连接上的每一段必须首先进行 LCP 数据报 文的交换以对链路进行配置和测试。在配置和测试结束之后,根据在此阶段中交换的选项, 下一阶段可能需要进行认证。认证是为了保证数据传输的安全性而采取的一种措施,即向对 方证明本地确实是授权的一方。认证的方法有两种,这是在配置阶段双方进行选项配置交换 时所选取的认证协议决定的。其一是密码认证方式(PAP:Password Authentication Protocol), 使用密码认证协议(对应协议字段值为 0xC023);其二称为查找握手认证方式(CHAP: Challenge Handshake Authentication Protocol),使用 Challenge Handshake Authentication 协议 (对应协议字段值为 0xC223)。 在使用 LCP 协议对链路进行测试以及进行相关选项的配置之后,如果成功进入网络协议配 置阶段,则需要使用具体的 NCP 协议(如 IPCP)进行相关配置。该阶段配置成功之后,即 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 可进行具体协议数据报的交互传输。 在配置,维护及最后关闭 PPP 连接过程中,PPP 链路会经历一系列不同的状态,下图(图 1-22)即显示了这些状态之间的转换。 图 1-22 PPP 协议链路状态转换图 1> Link Dead Phase(链路未就绪阶段) 该状态是一个链路的最初以及最终状态。当某个外部事件(如载路探测)表示物理介质 层可用时,链路就前进到建立阶段。对于使用调制解调器进行连接的链路,则当调制解 调器断开连接时,链路即进入 Link Dead 状态;而对于直接的硬连线连接,则链路处于 该状态的时间极其短暂,在此种情况下,链路通常处于建立阶段。 2> Link Establishment Phase(链路建立阶段) 在该阶段,链路控制协议(LCP:Link Control Protocol)被用来建立链路连接。连接的 建立是通过交换 LCP 配置数据包完成的。当所有的配置完成后,如果在配置阶段使用了 认证选项,则将进入认证阶段,否则直接进入网络协议配置阶段。由此可见,认证阶段 是可选的,该阶段是否存在取决于在链路建立阶段中是否选择了认证选项。需要注意的 是,所有的选项如果在没有进行明确配置的情况下,都有一个默认值。这就使得用户只 需要配置想要改变其默认值的选项,而对于其它选项则可以不进行配置。有关 LCP 配置 数据包的格式以及配置的各种选项将在下文详细介绍。 在该阶段中,所有非 LCP 配置数据包均将被丢弃。 3> Authentication Phase(认证阶段,可选) 诚如上文所述,该阶段是可选的。一旦在链路建立阶段中确认使用认证选项,则在认证 阶段中,必须使用在链路建立阶段中双方协商的认证协议进行身份认证。目前主要有两 种认证协议,其一称为密码认证协议(PAP:Password Authentication Protocol),其二称 为查询握手认证协议(CHAP:Challenge Handshake Authentication Protocol)。 密码认证方式通过传输明码方式验证自己的身份,显而易见,此种方式并不保险,该种 方式的认证过程是: 1> 被认证的一方发送明码形式的用户名和密码给进行认证的一方。 2> 进行认证的一方通过查询其相关文件确定是否有该用户以及密码是否正确。 3> 进行认证的一方发送认证成功或者失败数据包给被认证的一方。 如果认证成功,则进入网络协议配置阶段,否则进入链路关闭阶段断开链路。 查询握手认证方式使用加密方式进行身份认证。该认证方式也是通过 3 个数据包的交换 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 进行身份认证(有时称为 3 路握手认证方式)。 1> 进行认证的一方发送查询(challenge)数据包给被认证者。 2> 被认证者使用某种加密算法对查询报文中有效数据部分进行加密后,返回给对方。 3> 进行认证者也使用加密算法对其发送的查询报文中数据部分进行加密,并将计算后 结果与被认证者发送过来的计算结果进行比较,如果二者相符,则表示认证成功, 否则认证失败。 此种认证方式的关键在于加密算法中使用的密匙。该密匙应仅限于进行认证者和被认证 者知道,否则此种认证方式将失去其意义。 认证一般是单向的,当然也可采用双向认证,此时一般需要两个密匙,分别用于两个方 向上的加密算法。否则攻击者会伪造认证者发送 Challenge 数据包,获得反馈数据包, 从而从中分析出有效信息。 4> Network-layer Protocol Phase(网络协议配置阶段) 当成功进入该阶段,则根据所使用网络层协议的不同将需要使用具体的 NCP 协议进行 有关选项的配置(如对于 IP 协议则对应的 NCP 协议为 IPCP:IP Control Protocol)。为 讨论方便,以下即以 IPCP 协议为依据进行说明。 IPCP 配置包格式同 LCP 配置包格式,LCP 配置包格式及其选项下文中将有详细说明。 以下给出二者不同之处。 A. PPP 协议在链路层使用类 HDLC 格式,对于 IPCP 数据包而言,其首部中协议字段 值为 0x8021。 B. 代码域有效值为 1~7。其它代码值将被认为为无效的,会引起 Code-Rejects 数据包 的发送。 C. IPCP 数据包只在网络协议配置阶段有效。 D. IPCP 使用不同于 LCP 的选项。 对于 IPCP,具有如下选项类型: 1. IP-Addresses(已过时,被 IP-Address 选项替代) 2. IP-Compression-Protocol 3. IP-Address 5> Link Termination Phase(链路关闭阶段) PPP 可以随时终止链路连接。链路关闭可能有如下原因造成:载路丢失,认证失败,空 闲定时器超时或者管理员发出终止命令。 LCP 协议被用于在此阶段进行数据包交互以关闭链路连接。当使用 LCP 协议进行关闭 链路连接数据包交换后,具体实现必须发送信号给物理层以完成实际上的连接关闭。 发送者在发送 Terminate-Request 数据包后,等待对方 Terminate-Ack 数据包,一旦接收 到该数据包或者定时器超时,则断开链路。接收端在接收到 Terminate-Request 数据包后, 应该等待对方断开连接,在发送 Terminate-Ack 数据包给对方后,必须等待一个定时器 超时时间后方能断开本地连接。 此状态结束后,进入 Link Dead 状态。 在此阶段中,所有非 LCP 配置包均将被丢弃。 以下将详细介绍各阶段所使用数据包的格式。 1.链路建立/关闭阶段所使用数据包格式(LCP 数据包) LCP 数据包用于链路建立/关闭阶段,LCP 协议从功能上可分为三类配置包。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 1>用于建立和配置链路连接的 LCP 数据包 Configure-Request,Configure-Ack,Configure-Nak,Configure-Reject 2>用于终止链路连接的 LCP 数据包 Terminate-Request,Terminate-Ack 3>用于维护和测试链路连接的 LCP 数据包 Code-Reject,Protocol-Reject,Echo-Request,Echo-Reply,Discard-Request 为简单起见,LCP 数据包中无版本字段。一个正确的 LCP 协议的实现应当对任何无效 的数据包进行有效的响应。整个 LCP 数据包作为 PPP 封装形式中的信息数据字段(见 前文中说明)。此时对应的帧协议字段值为 0xC021。 下图(图 1-23)显示了 LCP 数据包通用格式。 图 1-23 LCP 数据包通用格式 下图(图 1-24)显示了 LCP 数据包在整个数据帧中的封装形式。 图 1-24 LCP 数据包在整个数据帧中的位置 代码字段:1 字节 该字段表示 LCP 配置包的类型。当一个未知包类型被接收到时,需要响应一个 Code-Reject 数据包。下表显示了该字段值对应的不同包类型。 表 1-11 代码字段值及其意义 代码字段值(Code) 包类型 1 Configure-Request 2 Configure-Ack 3 Configure-Nak 4 Configure-Reject 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 5 Terminate-Request 6 Terminate-Ack 7 Code-Reject 8 Protocol-Reject 9 Echo-Request 10 Echo-Reply 11 Discard-Request 标志符(Identifier)字段:1 字节 该字段用于匹配查询和应答报文。如对于 Configure-Request 查询报文,其对应的应答 报文(如 Configure-Ack)应该返回相同的标志符值以表示这个应答具体是针对哪个查 询报文。因为接收端在接收到一个应答之前不会发送另外的查询报文,所以这种匹配关 系比较简单,即只对前一个发送的查询报文进行匹配,无须记录多个查询报文的标志符 值。如果接收端接收到一个无效的应答报文则应当丢弃。 长度字段:2 字节 该字段值表示 LCP 数据包的长度,该长度包括代码字段,标志符字段,其本身及其之 后的数据部分。在超出长度字段所表示的范围之外的数据被认为是填充数据。如果接收 端接收到一个无效长度字段值的数据包,则将其丢弃。 数据字段:可变长度 对于 LCP 配置数据包而言,该字段大多表示 LCP 选项部分。其具有内部结构,在下文 中阐述 LCP 选项时将给与介绍。而对于其它类型数据包,该字段也表示有效信息。 以下将根据不同的代码字段值分别具体阐述 LCP 数据包类型。 1>Configure-Request:配置请求数据包 2>Configure-Ack:配置应答数据包 3>Configure-Nak:配置否定应答数据包 4>Configure-Reject:配置拒绝数据包 这四种数据包用于链路建立阶段进行选项配置,其封装格式相同,如下图所示。 对于这四种数据包类型,通用格式中数据字段此时即表示通信双方需要交换的选项部 分。选项部分具有其内部结构。LCP 选项配置数据包允许通信双方改变连接中一些默 认特性。如果一个选项在 Configure-Request 没有被指定,则使用其默认值。注意每个 选项都有一个默认值对应。 下图显示了选项字段的内部结构。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 下图(图 1-25)显示了 LCP 选项在整个数据帧中的位置。 图 1-25 LCP 选项封装在整个数据帧中的位置 类型(Type)字段:1 字节 该字段表示选项的具体类型。可取如下值。 类型值 意义 --------- --------- 0 保留 1 最大接收单元 2 认证协议 3 链路质量(稳定性)检测协议 4 幻数协商 5 协议字段压缩 6 地址和控制字段压缩 长度(Length)字段:1 字节 该字段表示选项部分总长度:包括类型字段,其本身及之后的数据部分。 数据部分:长度可变 该字段的长度由长度字段值决定。对于不同的类型值(即对于不同的选项配置)该字段 的格式有所不同,以下根据不同的类型值分别阐述不同的选项。 A. 最大接收长度选项(MRU:Maximum Receive Unit) 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 对于该选项,此时数据部分为最大接收单元的协商值。具体格式如下: 最大接收单元值表示 PPP 封装格式中数据部分和填充部分的数据总长度。 B. 认证协议协商 这个选项协商在需要进行认证时使用的认证协议:PAP 或者 CHAP。缺省的情况是 不进行认证。一个实现不应当在单个 Configure-Request 数据包中包括多个认证协 议,而是分多个数据包进行协商,第一次使用最期望的认证协议,协商失败后,在 选择其它认证协议继续进行协商。如果一个 Configure-Request 数据包中包括认证协 议的选项,则表示一端希望进行身份认证,则如果认证协议协商成功,之后将进入 认证阶段。认证可以是单向或者双向的,对于双向认证,并不要求两个方向上使用 相同的认证协议,换句话说,对于连接的两端,A 端对 B 端进行认证时,可以使用 PAP 协议,而 B 端对 A 端进行认证,可以使用 CHAP 协议。 下图显示了认证协议选项的封装格式。 认证协议字段:2 字节 表示双方进行协商的认证协议类型。 协议字段值 所表示协议 -------------- --------------- 0xC023 PAP 0xC223 CHAP 数据字段部分:长度可变 该字段具体长度有长度字段值可推出。 该字段表示的意义有具体进行协商的认证协议决定。对于 PAP 认证协议其无数据部 分,即格式如下: 对于 CHAP 协议,则数据部分仅有一个字节,表示所使用的加密算法。此时的格式 如下所示。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 算法字段:1 字节 该字段表示所采用的加密算法。一般取值为 5,表示使用 MD5 加密算法。 C. 链路质量检测协议协商 链路连接通道很少有完美的,数据包可能因为各种原因而被丢弃或破坏。 在某些链路连接中,可能需要知道何时以及以多大的频率丢弃部分数据包。这个过 程被称为链路连接质量监控。实现链路连接质量监控的方式有很多种,PPP 协议使 用 Link-Quality-Report 数据包进行监控。PPP 只提供一种检测机制,并没有提供如 果在发现数据包被丢弃或被破坏时采取何种措施,这是实现相关的。质量监控使用 一系列计数器来计数各种类型数据包,这些计数器的定义是依据 SNMP MIB-II 实现 的。 本选项提供了一种机制用于双方协议采用何种监控协议。在通常情况下是不需要对 本选项进行协商的,当用于确定对方是使用 Link-Quality-Report 数据包而非其它形 式数据包或者协商 Link-Quality-Report 发送间隔时间时,本选项的协商才是必要的。 如同认证一样,质量监控可以是双向的。在不同方向上可以使用不同的监控协议。 该选项对应的格式如下所示。 质量监控协议字段:2 字节 该字段即双方进行协商使用的质量监控协议。该字段取值一般为 0xC025,表示使 用“链路连接质量报告”(Link Quality Report)协议。 报告时间间隔:4 字节 对于 LQR 协议,该字段表示两个监控数据包发送之间的最大间隔时间。注意这个 时间是对对方发送 LQR 数据包的间隔时间的要求。如果该字段设置为 0,则表示对 方无需维护一个定时器,此时对方每当接收到一个本地发送的 LQR 数据包时,即 发送一个 LQR 数据包给本地。即对方 LQR 数据包的发送是由本地 LQR 数据包触 发的。 Link-Quality-Report 数据包封装格式,如图 1-26 所示。 以下字段的含义是从该数据包接收者的角度解析的。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-26 LQR 数据包格式及其在整个数据帧中的位置 以下这些字段(图 1-27)并不进行传输,只是逻辑上附加到以上字段之后。一般在 实现中用于本地信息统计。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-27 LQR 统计信息(不进行传输) D. 幻数协商 该配置选项提供了一个检测环路以及其它链路连接异常的机制。默认的,幻数不进 行协商,当使用到幻数时,直接用 0 代替。 幻数的选择推荐使用随机数。而且应每次都具有唯一性,即每次应使用不同的幻数。 幻数协商选项的格式如下: E. 协议字段压缩选项 该选项用于双方协商对协议字段进行压缩。默认的,所有实现必须传输使用两字节 协议的数据包。对于某些协议而言,可以将其压缩为一个字节。该选项用于本端通 知对端本地可以接收单个字节的协议字段,从而减少数据传输量。如果双方协商成 功,则实现上应该是既可以接收单个字节协议字段的数据包,也可以接收原先双字 节协议字段的数据包。注意对于 LCP 配置包而言,不使用协议字段压缩,以避免不 必要的麻烦。当使用协议字段压缩时,FCS 值的计算即使用压缩后的协议字段值, 而非原先的双字节协议字段值。 协议字段压缩选项格式如下: F. 地址和控制字段压缩选项 该选项用于对地址和控制字段进行压缩。如上文中所述,对于 PPP 协议封装格式而 言,地址和控制字段值通常为固定值,所以在双方进行协商的条件下,可以对这两 个字段进行压缩,从而减少数据传输量。 同样的这种压缩不可以使用在 LCP 配置数据包上。当使用地址和控制字段压缩时, FCS 值的计算是使用压缩后的地址和控制字段值。 该选项格式如下: 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 Configure-Request 数据包对于所有要进行协商的选项都包含在选项字段部分。在接 收到一个 Configure-Request 数据包后,接收端必须进行应答。有三种应答数据包类型: 1.Configure-Ack:完全同意发送端发送的 Configure-Request 数据包中所提出的选项值。 2 . Configure-Nak :不完全同意 Configure-Request 数据包中所提供的选项值, Configure-Nak 数据包中将包括那些不同意的选址类型以及对端 提供的该选项的可选值。本端必须选择这些可选值中其一继续发 送 Configure-Request 数据包进行协商直至最后双端达成妥协。 3.Configure-Reject:对于 Configure-Request 中提供的某些选项无法进行辨别。即某些 选项类型不正确,当然此时选项值更无从谈起。Configure-Reject 数据部分将包含这些不认识的选项类型,如此发送 Configure-Request 数据包的一端可以从接收到的 Configure-Reject 数据包中获知对方那些选项无法辨别,从而在 下一个 Configure-Request 数据包中剔除这些选项,从而直接使 用默认值;或者是本地误传某些不正确选项,从而在下次得到改 正。 5>Terminate-Request:请求终止连接数据包 6>Terminate-Ack:终止连接应答数据包 这两个配置包类型使用相同的数据包格式,即上文中给出的通用格式。不过代码字段值 对于 Terminate-Request 配置包而言是 5,而 Terminate-Ack 为 6。 这两个配置包类型用于关闭连接。一个具体的实现如果想关闭链路连接,则首先应该发 送一个 Terminate-Request 数据包给对端,同时设置一个超时定时器,在没有接收到对 端响应的 Terminate-Ack 数据包之前,应该实行超时重传。在接收到 Terminate-Ack 数 据包后或者在进行一定次数的重传后,本端将认为 OSI 网络栈下层(此处即物理层) 已完成关闭。 7>Code-Reject:代码无效应答数据包 顾名思义,该类型 LCP 数据包表示对方接收到一个本地发送的具有无效代码字段的数据 包,此时对方将发送该类型数据包给本地。Code-Reject 数据包的格式如下: 其中原始数据包字段部分只包括原数据包的信息字段部分,不包括链路层帧头和帧尾的 FCS 字段。另外原始数据包的长度如果需要会被截断为 MRU 所允许的长度。 8>Protocol-Reject:协议无效应答数据包 该类型数据包表示对方之前发送的数据包具有无效的协议字段值。 这通常发生在对方在试图使用一个新的传输协议进行数据传输。 该类型数据包的格式如下: 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 其中原始数据包部分所包含的数据同 Code-Reject 对应字段。 9>Echo-Request:请求回复数据包 10>Echo-Reply:回复应答数据包 11>Discard-Request:请求丢弃数据包 这两种类型的数据包用于测试和检测链路连接的状态。其共同使用如下数据包格式: 在接收到 Echo-Request 数据包时,必须相应一个 Echo-Reply 数据包。标志符字段用于 匹配。Echo-Request 和 Echo-Reply 数据包用于检测环路,测试,链路质量检测,传输 效率检测等等各种应用。其中幻数字段占据 4 个字节,该字段主要用于环路检测中。如 果在链路建立阶段没有使用幻数协商选项,则该字段值必须设置为 0。 接收端在接收到一个 Discard-Requst 数据包后立即丢弃而不对其进行任何处理。该种类 型数据包用于环路检测,测试,传说效率检测等等应用中。 封装中数据部分占据 0 个或多个字节(由长度字段可计算出),其中包括发送端发送的 原始数据,这个字段可以包含任何二进制数据。 2.认证阶段所使用数据包封装格式 根据在链路建立阶段使用协商的认证协议的不同,在该阶段将使用不同的协议进行认 证。以下分别以 PAP 协议和 CHAP 协议进行讨论。二者所使用数据包的通用格式同 LCP 配 置数据包,对于 PAP 协议数据包而言,帧协议字段值为 0xC023,对于 CHAP 协议数据包而 言,帧协议字段值为 0xC223,而对于 LCP 配置数据包而言,帧协议字段值为 0xC021。而 对于下文中网络协议配置阶段数据包而言,其帧协议字段值为 0x8021. 1. 密码认证协议(PAP) 如下(图 1-28)显示了使用该协议进行认证的数据包格式及其在整个数据帧中的位置。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 图 1-28 认证数据包格式以及其在整个数据帧中的位置 其中各字段所表示的含义同 LCP 配置数据包。 对于认证数据报而言,代码字段可取如下值: 代码字段值 含义 --------------- ------- 1 Authentication-Request 2 Authentication-Ack 3 Authentication-Nak 对于不同的代码字段取值,其数据部分代表不同的含义。 1> Authentication-Request 该数据包由被认证的一方主动发送,该数据包同时也启动了 PAP 认证的三路握手认 证的第一步,即有被认证的一方发送明码用户名和密码。 进行认证的一方在接收到该数据包后,必须对其进行响应。响应数据包可以是 Authentication-Ack(认证成功)或者 Authentication-Nak(认证失败)。 Authentication-Request 数据包格式如下: 2> Authentication-Ack 3> Authentication-Nak 这两种数据包是对 Authentication-Request 成功和失败时的应答。 当 Authentication-Request 数据包中的 ID 和密码值有效时,认证者即发送 Authentication-Ack 数据包进行应答表示认证成功。否则发送 Authentication-Nak 数 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 据包表示认证失败。二者数据包格式如下: 信息长度字段:1 字节 该字段表示其后所跟的信息字段的长度值。 信息字段:长度可变 该字段可以有 0 个或者多个字节,实现相关。一般为人可读的信息。 2. 查询握手认证协议(CHAP) CHAP 协议所用的数据包通用封装格式及其在整个数据帧中的位置同 PAP 协议。不过对于 CHAP 协议而言,代码字段取值所表示的意义不同。 代码字段值 含义 --------------- ------- 1 Challenge 2 Response 3 Success 4 Failure 其它标志符字段,长度字段,数据字段意义同 PAP 协议。其中数据字段值的含义同代码字 段取值相关。 1> Challenge 2> Response Challenge 数据包用于启动 CHAP 认证的三路握手阶段。不同于 PAP 的是,第一个数据 包(即 Challenge 数据包)由认证者发送(PAP 协议第一个 Authentication-Request 数据 包有被认证者发送)。Challenge 数据包除了正常情况下在认证阶段发送外,也可以在网 络协议配置阶段发送以确定链路连接没有被改变。当被认证者接收到一个 Challenge 数 据包后,其必须发送一个 Response 数据包进行响应并同时返回 Challenge 数据包中包含 的经过加密算法计算后的数据给认证者。当认证者接收到 Response 数据包后,其将 Response 中返回的加密数据与本地计算的加密数据进行比较,如果相符,表示认证成功, 此时发送 Success 数据包给被认证者,否则表示认证失败,此时发送 Failure 数据包给被 认证者。 Challenge 和 Response 数据包采用如下格式。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 值长度字段:1 字节 该字段指出其后值字段的数据长度。 值字段:长度可变 该字段长度由值长度字段标明。被认证使用预先协商的加密算法进行加密:算法所涉及 的域:标志符字段,算法所使用的密匙,值字段所包含的所有数据。计算得到的结果将 作为 Response 数据包的值字段返回给认证者。 标志名称字段:长度可变 该字段的长度由长度字段值可计算出。 该字段所表示的意义是发送该数据包的系统标志名称。原则上对此字段的取值没有限 制。例如可以是 ASCII 字符串。该字段不可以 NULL 字符或者换行符结束。 由于 CHAP 协议可用于多种不同系统的认证,故该字段值可以作为密匙数据库中的一个 关键字用于寻址相应的密匙。 3> Success 4> Failure 如果 Response 数据包中加密数据与本地计算出的值相符,表示认证成功,此时发送 Success 数据包给被认证者,否则表示认证失败,此时发送 Failure 数据包给被认证者。 二者使用如下数据包格式。 信息字段:长度可变 该字段长度由长度字段可计算出。 该字段可以有 0 个或者多个字节,其内容是实现相关的。可以是任何人可读形式的数据。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 3.网络配置阶段数据包格式 通用格式同链路建立阶段和认证阶段数据包格式,但此时帧协议字段值为 0x8021。 图 1-29 网络协议配置数据包在整个数据帧中的位置 在该阶段对于不同的协议将使用不同网络控制协议,本文只讨论针对 IP 协议进行配置 的 IPCP(IP Control Protocol)协议。 对于 IPCP,共有三个配置选项。 1> IP-Addresses 2> IP-Compression-Protocol 3> IP-Address 其中 IP-Addresses 选项已过时,被 IP-Address 选项取代。以下介绍 IP-Compression-Protocol 和 IP-Address 选项。 IP-Address 选项 该配置选项提供了一种方式用于协商本地所使用的 IP 地址。该选项允许 Configure-Request 数据包发送者提供其想使用的 IP 地址;或者要求对方提供一个 IP 地 址给本地使用,此时相应的地址字段设置为 0 即可。 默认情况下,将不分配 IP 地址。 该选项使用如下数据包格式。 IP 地址字段:4 字节 该字段如果不为 0,则表示该 Configure-Request 数据包(注意选项的配置是在 Configure-Request 数据包中指定的,这对于网络协议配置阶段是一样的)的发送者指定 了其想使用的 IP 地址,否则(该字段为 0)则表示需要对方返回一个 IP 地址赋予本地 使用。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 IP-Compression-Protocol 选项 Van Jacobson TCP/IP 首部压缩可以将 TCP/IP 首部长度(最少 40 个字节)减少到 3 个字 节。这对于慢速串行线路而言,大大减少了数据传输量。 IP-Compression-Protocol 配置选项用于表示可以解析压缩数据包的能力。如果一端具有 这种能力,则在 Configure-Request 数据包中应该包括该选项,从而使得对方可以发送 经过压缩后的数据包以优化效率。注意这个选项可以是单向的或者是双向的。 在网络协议配置阶段完成后,当正式传输 IP 数据包时,根据在网络协议配置阶段所使 用的压缩选项,帧协议字段可以有如下取值: 帧协议字段值 含义 ------------------ ------- 0x0021 普通 IP 数据包,非压缩 0x002D 压缩数据包。TCP/IP 首部被压缩后首部替代 0x002F 非压缩数据包,IP 首部协议字段(表示传输层协议) 是一个槽标志符(slot identifier) 使用 Van Jacobson TCP/IP 首部压缩的 IP-Compression-Protocol 选项的数据包格式如下: Max-Slot-Id:1 字节 该字段表示最大槽标志符。注意计数是从零开始的,所以该字段值比槽位数小 1,即槽 标志符从 0 计数到 Max-Slot-Id。 Comp-Slot-Id:1 字节 该字段表示槽标志符字段是否可以被压缩。取值如下: 0:槽标志符不可被压缩 1:槽标志符可以被压缩 对于 IPCP,推荐如下选项: 1> IP-Compression-Protocol---至少具有 4 个槽位,通常 16 个槽位。 2> IP-Address:仅使用在拨号线路。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* include/linux/ppp.h ***********************************************************/ #ifndef _LINUX_PPP_H #define _LINUX_PPP_H /* definitions for kernel PPP module Michael Callahan Nov. 4 1993 */ /* how many PPP units? */ #define PPP_NRUNIT 4 #define PPP_VERSION "0.2.7" /* line discipline number */ #define N_PPP 3 /* Magic value for the ppp structure */ #define PPP_MAGIC 0x5002 //获取配置标志位 #define PPPIOCGFLAGS 0x5490 /* get configuration flags */ //设置配置标志位 #define PPPIOCSFLAGS 0x5491 /* set configuration flags */ //获取异步控制字符映射图 #define PPPIOCGASYNCMAP 0x5492 /* get async map */ //设置异步控制字符映射图 #define PPPIOCSASYNCMAP 0x5493 /* set async map */ #define PPPIOCGUNIT 0x5494 /* get ppp unit number */ #define PPPIOCSINPSIG 0x5495 /* set input ready signal */ #define PPPIOCSDEBUG 0x5497 /* set debug level */ #define PPPIOCGDEBUG 0x5498 /* get debug level */ #define PPPIOCGSTAT 0x5499 /* read PPP statistic information */ #define PPPIOCGTIME 0x549A /* read time delta information */ #define PPPIOCGXASYNCMAP 0x549B /* get async table */ #define PPPIOCSXASYNCMAP 0x549C /* set async table */ #define PPPIOCSMRU 0x549D /* set receive unit size for PPP */ #define PPPIOCRASYNCMAP 0x549E /* set receive async map */ #define PPPIOCSMAXCID 0x549F /* set the maximum compression slot id */ /* special characters in the framing protocol */ #define PPP_ALLSTATIONS 0xff /* All-Stations broadcast address */ #define PPP_UI 0x03 /* Unnumbered Information */ #define PPP_FLAG 0x7E /* frame delimiter -- marks frame boundaries */ #define PPP_ADDRESS 0xFF /* first character of frame <-- (may be */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define PPP_CONTROL 0x03 /* second character of frame <-- compressed)*/ #define PPP_TRANS 0x20 /* Asynchronous transparency modifier */ #define PPP_ESC 0x7d /* escape character -- next character is data, and the PPP_TRANS bit should be toggled. PPP_ESC PPP_FLAG is illegal */ /* protocol numbers */ #define PROTO_IP 0x0021 #define PROTO_VJCOMP 0x002d #define PROTO_VJUNCOMP 0x002f /* FCS support */ #define PPP_FCS_INIT 0xffff #define PPP_FCS_GOOD 0xf0b8 /* initial MTU */ #define PPP_MTU 1500 /* initial MRU */ #define PPP_MRU PPP_MTU /* flags */ #define SC_COMP_PROT 0x00000001 /* protocol compression (output) */ #define SC_COMP_AC 0x00000002 /* header compression (output) */ #define SC_COMP_TCP 0x00000004 /* TCP (VJ) compression (output) */ #define SC_NO_TCP_CCID 0x00000008 /* disable VJ connection-id comp. */ #define SC_REJ_COMP_AC 0x00000010 /* reject adrs/ctrl comp. on input */ #define SC_REJ_COMP_TCP 0x00000020 /* reject TCP (VJ) comp. on input */ #define SC_ENABLE_IP 0x00000100 /* IP packets may be exchanged */ #define SC_IP_DOWN 0x00000200 /* give ip frames to pppd */ #define SC_IP_FLUSH 0x00000400 /* "next time" flag for IP_DOWN */ #define SC_DEBUG 0x00010000 /* enable debug messages */ #define SC_LOG_INPKT 0x00020000 /* log contents of good pkts recvd */ #define SC_LOG_OUTPKT 0x00040000 /* log contents of pkts sent */ #define SC_LOG_RAWIN 0x00080000 /* log all chars received */ #define SC_LOG_FLUSH 0x00100000 /* log all chars flushed */ /* Flag bits to determine state of input characters */ #define SC_RCV_B7_0 0x01000000 /* have rcvd char with bit 7 = 0 */ #define SC_RCV_B7_1 0x02000000 /* have rcvd char with bit 7 = 0 */ #define SC_RCV_EVNP 0x04000000 /* have rcvd char with even parity */ #define SC_RCV_ODDP 0x08000000 /* have rcvd char with odd parity */ #define SC_MASK 0x0fffffff /* bits that user can change */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* flag for doing transmitter lockout */ #define SC_XMIT_BUSY 0x10000000 /* ppp_write_wakeup is active */ /* * This is the format of the data buffer of a LQP packet. The packet data * is sent/received to the peer. */ //Link Quality Protocol 协议数据报文信息统计结构。 //该结构用于在 PPP 链路上进行数据传输。 struct ppp_lqp_packet_hdr { unsigned long LastOutLQRs; /* Copied from PeerOutLQRs */ unsigned long LastOutPackets; /* Copied from PeerOutPackets */ unsigned long LastOutOctets; /* Copied from PeerOutOctets */ unsigned long PeerInLQRs; /* Copied from SavedInLQRs */ unsigned long PeerInPackets; /* Copied from SavedInPackets */ unsigned long PeerInDiscards; /* Copied from SavedInDiscards */ unsigned long PeerInErrors; /* Copied from SavedInErrors */ unsigned long PeerInOctets; /* Copied from SavedInOctets */ unsigned long PeerOutLQRs; /* Copied from OutLQRs, plus 1 */ unsigned long PeerOutPackets; /* Current ifOutUniPackets, + 1 */ unsigned long PeerOutOctets; /* Current ifOutOctets + LQR */ }; /* * This data is not sent to the remote. It is updated by the driver when * a packet is received. */ //该结构用于系统信息统计。 struct ppp_lqp_packet_trailer { unsigned long SaveInLQRs; /* Current InLQRs on reception */ unsigned long SaveInPackets; /* Current ifInUniPackets */ unsigned long SaveInDiscards; /* Current ifInDiscards */ unsigned long SaveInErrors; /* Current ifInErrors */ unsigned long SaveInOctets; /* Current ifInOctects */ }; /* * PPP LQP packet. The packet is changed by the driver immediately prior * to transmission and updated upon reception with the current values. * So, it must be known to the driver as well as the pppd software. */ struct ppp_lpq_packet { 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 unsigned long magic; /* current magic value */ struct ppp_lqp_packet_hdr hdr; /* Header fields for structure */ struct ppp_lqp_packet_trailer tail; /* Trailer fields (not sent) */ }; /* * PPP interface statistics. (used by LQP / pppstats) */ struct ppp_stats { unsigned long rbytes; /* bytes received */ unsigned long rcomp; /* compressed packets received */ unsigned long runcomp; /* uncompressed packets received */ unsigned long rothers; /* non-ip frames received */ unsigned long rerrors; /* received errors */ unsigned long roverrun; /* "buffer overrun" counter */ unsigned long tossed; /* packets discarded */ unsigned long runts; /* frames too short to process */ unsigned long rgiants; /* frames too large to process */ unsigned long sbytes; /* bytes sent */ unsigned long scomp; /* compressed packets sent */ unsigned long suncomp; /* uncompressed packets sent */ unsigned long sothers; /* non-ip frames sent */ unsigned long serrors; /* transmitter errors */ unsigned long sbusy; /* "transmitter busy" counter */ }; /* * Demand dial fields */ struct ppp_ddinfo { unsigned long ip_sjiffies; /* time when last IP frame sent */ unsigned long ip_rjiffies; /* time when last IP frame recvd*/ unsigned long nip_sjiffies; /* time when last NON-IP sent */ unsigned long nip_rjiffies; /* time when last NON-IP recvd */ }; #ifdef __KERNEL__ struct ppp { int magic; /* magic value for structure */ /* Bitmapped flag fields. */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 char inuse; /* are we allocated? */ char sending; /* "channel busy" indicator */ char escape; /* 0x20 if prev char was PPP_ESC*/ char toss; /* toss this frame */ unsigned int flags; /* miscellany */ unsigned long xmit_async_map[8]; /* 1 bit means that given control character is quoted on output*/ unsigned long recv_async_map; /* 1 bit means that given control character is ignored on input*/ int mtu; /* maximum xmit frame size */ int mru; /* maximum receive frame size */ unsigned short fcs; /* FCS field of current frame */ /* Various fields. */ int line; /* PPP channel number */ struct tty_struct *tty; /* ptr to TTY structure */ struct device *dev; /* easy for intr handling */ struct slcompress *slcomp; /* for header compression */ unsigned long last_xmit; /* time of last transmission */ /* These are pointers to the malloc()ed frame buffers. These buffers are used while processing a packet. If a packet has to hang around for the user process to read it, it lingers in the user buffers below. */ unsigned char *rbuff; /* receiver buffer */ unsigned char *xbuff; /* transmitter buffer */ unsigned char *cbuff; /* compression buffer */ /* These are the various pointers into the buffers. */ unsigned char *rhead; /* RECV buffer pointer (head) */ unsigned char *rend; /* RECV buffer pointer (end) */ int rcount; /* PPP receive counter */ unsigned char *xhead; /* XMIT buffer pointer (head) */ unsigned char *xtail; /* XMIT buffer pointer (end) */ /* Structures for interfacing with the user process. */ #define RBUFSIZE 4000 unsigned char *us_rbuff; /* circular incoming packet buf.*/ unsigned char *us_rbuff_end; /* end of allocated space */ unsigned char *us_rbuff_head; /* head of waiting packets */ unsigned char *us_rbuff_tail; /* tail of waiting packets */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 unsigned char us_rbuff_lock; /* lock: bit 0 head bit 1 tail */ int inp_sig; /* input ready signal for pgrp */ int inp_sig_pid; /* process to get notified */ /* items to support the select() function */ struct wait_queue *write_wait; /* queue for reading processes */ struct wait_queue *read_wait; /* queue for writing processes */ /* PPP interface statistics. */ struct ppp_stats stats; /* statistic information */ /* PPP demand dial information. */ struct ppp_ddinfo ddinfo; /* demand dial information */ }; #endif /* __KERNEL__ */ #endif /* _LINUX_PPP_H */ /* include/linux/ppp.h ***********************************************************/ 1.19 route.h 头文件 该文件定义路由表结构。主要定义有两个结构和一些标志位。两个结构表示的意义基本相同。 只是使用在不同的函数调用中。 结构一:old_rentry 结构 struct old_rtentry { unsigned long rt_genmask; struct sockaddr rt_dst; struct sockaddr rt_gateway; short rt_flags; short rt_refcnt; unsigned long rt_use; char *rt_dev; }; 该结构使用于 SIOCADDRTOLD 和 SIOCDELRTOLD 选项。 rt_genmask 路由表项 IP 地址网络掩码。 rt_dst 该字段在进行路由时与数据包目的地址进行匹配。 rt_gateway 中间网关(或路由器)地址,当无法直接到达目的地时,需要经过网关进行转发,此为中间 网关的地址。 rt_flags 该路由表项所处状态标志位。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 rt_refcnt 该路由表项的使用计数。 rt_use 该路由表项是否正在使用。 rt_dev 该路由路径的出站接口设备。 结构二:rentry 结构 struct rtentry { unsigned long rt_hash; /* hash key for lookups */ struct sockaddr rt_dst; /* target address */ struct sockaddr rt_gateway; /* gateway addr (RTF_GATEWAY) */ struct sockaddr rt_genmask; /* target network mask (IP) */ short rt_flags; short rt_refcnt; unsigned long rt_use; struct ifnet *rt_ifp; short rt_metric; /* +1 for binary compatibility! */ char *rt_dev; /* forcing the device at add */ unsigned long rt_mss; /* per route MTU/Window */ unsigned long rt_window; /* Window clamping */ }; 该结构被使用于 SIOCADDRT 和 SIOCDELRT 选项。 rt_hash 该路由表项 HASH 值用于快速寻找相应路由表项。 rt_dst rt_gateway rt_genmask rt_flags rt_refcnt rt_use rt_dev 以上七个字段意义同 old_rentry 结构。 rt_ifp 该字段意义不是很明确,且内核网络代码中无法找到 ifnet 结构定义。 rt_metric 路由代价值。 rt_mss 该路由表项所表示路径的最大传输单元。 rt_window 该路由表项所代表路径的窗口大小。窗口是一种限制数据包发送过快的措施。发送数据包的 个数或总字节数应该在窗口内。 有一点需要注意的是 rentry,old_rentry 只用于用户空间和内核空间传输参数,内核路由表 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 项并非使用这两个结构表示,而是由 rtable 结构表示一个路由表项。rentry,old_rentry 这两 个结构主要用于选项设置或获取。即通过这两个结构传送参数更改或者获取系统路由表项内 容。 该文件结束处定义了路由表项所使用的一些标志值。 RTF_UP 该表项可被使用。 RTF_GATEWAY 该表项通向一个路由器或者网关。 RTF_HOST 该表项提供可直达目的主机的路径。 RTF_REINSTATE 该表项超时后已进行重新确认。每个路由表项(除了永久表项)都有一个超时时间,当超时发 生时,该表项必须经过重新确认,方才认为有效。 RTF_DYNAMIC 该表项是根据接收的数据包信息动态建立的,且内容可变。如 ICMP 送来一个 Redirect 信息, 可表项表示的路径可被更改。 RTF_MODIFIED 该表项内容根据 ICMP Redirect 信息进行了更改。 RTF_MSS 该表项指定了最大传输单元。数据包大小必须满足该规定。 RTF_WINDOW 该表项指定了窗口大小。 /* include/linux/route.h *********************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Global definitions for the IP router interface. * * Version: @(#)route.h 1.0.3 05/27/93 * * Authors: Original taken from Berkeley UNIX 4.3, (c) UCB 1986-1988 * Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_ROUTE_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define _LINUX_ROUTE_H #include /* This structure gets passed by the SIOCADDRTOLD and SIOCDELRTOLD calls. */ struct old_rtentry { unsigned long rt_genmask; struct sockaddr rt_dst; struct sockaddr rt_gateway; short rt_flags; short rt_refcnt; unsigned long rt_use; char *rt_dev; }; /* This structure gets passed by the SIOCADDRT and SIOCDELRT calls. */ struct rtentry { unsigned long rt_hash; /* hash key for lookups */ struct sockaddr rt_dst; /* target address */ struct sockaddr rt_gateway; /* gateway addr (RTF_GATEWAY) */ struct sockaddr rt_genmask; /* target network mask (IP) */ short rt_flags; short rt_refcnt; unsigned long rt_use; struct ifnet *rt_ifp; short rt_metric; /* +1 for binary compatibility! */ char *rt_dev; /* forcing the device at add */ unsigned long rt_mss; /* per route MTU/Window */ unsigned long rt_window; /* Window clamping */ }; /* Route Flags */ #define RTF_UP 0x0001 /* route usable */ #define RTF_GATEWAY 0x0002 /* destination is a gateway */ #define RTF_HOST 0x0004 /* host entry (net otherwise) */ #define RTF_REINSTATE 0x0008 /* reinstate route after tmout */ #define RTF_DYNAMIC 0x0010 /* created dyn. (by redirect) */ #define RTF_MODIFIED 0x0020 /* modified dyn. (by redirect) */ #define RTF_MSS 0x0040 /* specific MSS for this route */ #define RTF_WINDOW 0x0080 /* per route window clamping */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* * REMOVE THESE BY 1.2.0 !!!!!!!!!!!!!!!!! */ #define RTF_MTU RTF_MSS #define rt_mtu rt_mss #endif /* _LINUX_ROUTE_H */ /* include/linux/route.h *********************************************************/ 1.20 skbuff.h 头文件 该文件定义了网络栈代码用于封装网络数据的最重要的数据结构 sk_buff 结构以及部分对其 进行操作的函数定义。 该文件首部的两个常量定义: FREE_READ FREE_WRITE 用于表示释放某个数据包时该数据包时隶属于写缓冲区还是读缓冲区。对于每个套接字内核 分配一定的读写缓冲区,当缓冲区不够时,会发生数据包丢弃行为。所以及时维护缓冲区的 大小信息是至关重要的。而释放一个数据包时就是在得到更多的空闲空间以供其它数据包使 用,而读写缓冲区是不同的,所以释放时需要指示释放的这部分空间是属于哪个缓冲区。 FREE_READ 表示释放的空间被加到读缓冲区中,即现在有更多的读空间。FREE_WRITE 以此类比。 sk_buff_head 结构表示数据包队列的头。注意其字段与 sk_buff 结构的开始几个字段时一样 的,如此处理可以利用指针转化将 sk_buff_head 当作 sk_buff 结构使用,反过来也如此。内 核代码很多地方为方便处理,通常都是这么做的。 sk_buff 是内核网络栈代码的又一核心数据结构。该结构被用来封装网络数据。网络栈代码 对数据的处理都是以 sk_buff 结构为单元进行的。 prev,next 这两个字段用于构成 sk_buff 结构的队列。 magic_debug_cookie 调试之目的。 link3 这个指针也是用于构成 sk_buff 结构的队列。这个队列就是数据包重发队列,用于 TCP 协议 中。重发队列是由 sock 结构的 send_head, send_tail 字段指向的队列。send_head 指向这个队 列的首部,send_tail 指向这个队列的尾部。而队列的连接是使用 sk_buff 结构的 link3 字段完 成的。send_head, send_tail 是指向 sk_buff 结构的指针,而非 sk_buff_head 结构。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 sk 该数据包所属的套接字。 when 该数据包的发送时间。该字段用于计算往返时间 RTT。 stamp 该字段也是记录时间,但目前暂未使用该字段用于任何目的。 dev 对于一个接收的数据包而言,该字段表示接收该数据包的接口设备。对于一个待发送的数据 包而言,该字段如果不为 NULL,则表示将要发送该数据包的接口设备。 mem_addr 该 sk_buff 结构在内存中的基地址,该字段用于释放该 sk_buff 结构。 union { struct tcphdr *th; struct ethhdr *eth; struct iphdr *iph; struct udphdr *uh; unsigned char *raw; unsigned long seq; } h; 该字段一个联合类型,表示了数据包在不同处理层次上所到达的处理位置。如在链路层上, eth 指针有效,指向以太网首部第一个字节位置,在网络层上,iph 指针有效指向 IP 首部第 一个字节位置。raw 指针随层次变化而变化,在链路层上时,其等于 eth,在网络层上时, 其等于 iph。seq 是针对使用 TCP 协议的待发送数据包而言,此时该字段值表示该数据包的 ACK 值。ACK 值等于数据包中第一个数据的序列号加上数据的长度值。 ip_hdr 指向 IP 首部的指针,此处特别的分出一个字段用于指向 IP 首部主要用于 RAW 套接字。 mem_len 该字段表示 sk_buff 结构大小加上数据帧的总长度。 len 该字段只表示数据帧长度。即 len=mem_len – sizeof(sk_buff). fraglen fraglist 这两个字段用于分片数据包。fraglen 表示分片数据包个数,而 fraglist 指向分片数据包队列。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 truesize 意义同 mem_len. saddr 数据包发送的源端 IP 地址。 daddr 数据包最终目的端 IP 地址。 raddr 数据包下一站 IP 地址。 acked, used, free, arp acked=1 表示该数据包已得到确认,可以从重发队列中删除。 used=1 表示该数据包的数据已被应用程序读完,可以进行释放。 free=1 用于数据包发送,当某个待发送数据包 free 标志位等于 1,则表示无论该数据包是否 发送成功,在进行发送操作后立即释放,无需缓存。 arp 用于待发送数据包,该字段等于 1 表示此待发送数据包已完成 MAC 首部的建立。arp=0 表示 MAC 首部中目的端硬件地址尚不知晓,故需使用 ARP 协议询问对方,在 MAC 首部尚 未完全建立之前,该数据包一直处于发送缓冲队列中(device 结构中 buffs 数组元素指向的 某个队列以及 ARP 协议的某个队列中)。 tries, lock, localroute tries 字段表示该数据包已进行 tries 试发送,如果试发送超出域值,则会放弃该数据包的发 送。如对于 TCP 建立连接之 SYN 数据包,发送次数超过 3 次即放弃发送。 lock 表示该数据包是否正在被系统其它部分使用。 localroute 表示进行路由时是使用局域网路由(localroute=1)还是广域网路由。 pkt_type 该数据包的类型,可取如下值: PACKET_HOST 这是一个发往本机的数据包。 PACKET_BROADCAST 广播数据包。 PACKET_MULTICAST 多播数据包。 PACKET_OTHERHOST 该数据包是发往其它机器的,如果本机没有被配置为转发功能,该数据包即被丢弃。 users 使用该数据包的模块数。 pkt_class 意义同 pkt_type. 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 in_dev_queue; 该字段表示该数据包是否正在缓存于设备缓存队列中。 padding[0]; 填充字节。目前定义为 0 字节,即无填充。 data[0]; 指向数据部分。数据一般紧接着该 sk_buff 结构,也有可能在任何地址处。 SK_WMEM_MAX SK_RMEM_MAX 这两个字段定义了套接字读写缓冲区的最大空间大小。 SK_FREED_SKB SK_GOOD_SKB SK_HEAD_SKB 这三个常量用于调试目的。用于 magic_debug_cookie 字段的取值。 该文件中除了声明了一部分函数外,另外还定义了一部分对 sk_buff 结构进行操作的常用函 数,由于这些函数实现上都较为简单,故此处只简单说明这些函数完成的功能,不做一一分 析。 struct sk_buff *skb_peek(struct sk_buff_head *list_) 从 list 指向的队列首部取下一个数据包,但该数据包并不从队列中删除。 void skb_queue_head_init(struct sk_buff_head *list) 对一个 sk_buff_head 结构进行初始化操作。 void skb_queue_head(struct sk_buff_head *list_,struct sk_buff *newsk) 将 newsk 指向的数据包插入到 list 队列中的首部。 void skb_queue_tail(struct sk_buff_head *list_, struct sk_buff *newsk) 将 newsk 指向的数据包插入到 list 队列的尾部。 struct sk_buff *skb_dequeue(struct sk_buff_head *list_) 从 list 所指向队列首部取下一个数据包,并将该数据包从此队列中删除。 void skb_insert(struct sk_buff *old, struct sk_buff *newsk) 将 newsk 指向的数据包插入到 old 指向的数据包的前面。 void skb_append(struct sk_buff *old, struct sk_buff *newsk) 将 newsk 指向的数据包插入到 old 指向的数据包的后面。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 void skb_unlink(struct sk_buff *skb) 将 skb 结构从其链入的队列中删除。 /* include/linux/skbuff.h ********************************************************/ /* * Definitions for the 'struct sk_buff' memory handlers. * * Authors: * Alan Cox, * Florian La Roche, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_SKBUFF_H #define _LINUX_SKBUFF_H #include #include #include #include #undef CONFIG_SKB_CHECK #define HAVE_ALLOC_SKB /* For the drivers to know */ #define FREE_READ 1 #define FREE_WRITE 0 struct sk_buff_head { struct sk_buff * volatile next; struct sk_buff * volatile prev; #if CONFIG_SKB_CHECK int magic_debug_cookie; #endif }; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 struct sk_buff { struct sk_buff * volatile next; struct sk_buff * volatile prev; #if CONFIG_SKB_CHECK int magic_debug_cookie; #endif struct sk_buff * volatile link3; struct sock *sk; volatile unsigned long when; /* used to compute rtt's */ struct timeval stamp; struct device *dev; struct sk_buff *mem_addr; union { struct tcphdr *th; struct ethhdr *eth; struct iphdr *iph; struct udphdr *uh; unsigned char *raw; unsigned long seq; } h; struct iphdr *ip_hdr; /* For IPPROTO_RAW */ unsigned long mem_len; unsigned long len; unsigned long fraglen; struct sk_buff *fraglist; /* Fragment list */ unsigned long truesize; /* equals to mem_len */ unsigned long saddr; unsigned long daddr; /* This packet's eventual destination */ unsigned long raddr; /* next hop addr */ volatile char acked, used, free, arp; unsigned char tries, lock, localroute, pkt_type; /* pkt_type */ #define PACKET_HOST 0 /* To us */ #define PACKET_BROADCAST 1 #define PACKET_MULTICAST 2 #define PACKET_OTHERHOST 3 /* Unmatched promiscuous */ unsigned short users; /* User count - see datagram.c (and soon seqpacket.c/stream.c) */ unsigned short pkt_class;/* For drivers that need to cache the packet type with the skbuff (new PPP) */ #ifdef CONFIG_SLAVE_BALANCING unsigned short in_dev_queue; #endif unsigned long padding[0]; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 unsigned char data[0]; }; #define SK_WMEM_MAX 32767 #define SK_RMEM_MAX 32767 #ifdef CONFIG_SKB_CHECK #define SK_FREED_SKB 0x0DE2C0DE #define SK_GOOD_SKB 0xDEC0DED1 #define SK_HEAD_SKB 0x12231298 #endif #ifdef __KERNEL__ /* * Handling routines are only of interest to the kernel */ #include #if 0 extern void print_skb(struct sk_buff *); #endif extern void kfree_skb(struct sk_buff *skb, int rw); extern void skb_queue_head_init(struct sk_buff_head *list); extern void skb_queue_head(struct sk_buff_head *list,struct sk_buff *buf); extern void skb_queue_tail(struct sk_buff_head *list,struct sk_buff *buf); extern struct sk_buff * skb_dequeue(struct sk_buff_head *list); extern void skb_insert(struct sk_buff *old,struct sk_buff *newsk); extern void skb_append(struct sk_buff *old,struct sk_buff *newsk); extern void skb_unlink(struct sk_buff *buf); extern struct sk_buff * skb_peek_copy(struct sk_buff_head *list); extern struct sk_buff * alloc_skb(unsigned int size, int priority); extern void kfree_skbmem(struct sk_buff *skb, unsigned size); extern struct sk_buff * skb_clone(struct sk_buff *skb, int priority); extern void skb_device_lock(struct sk_buff *skb); extern void skb_device_unlock(struct sk_buff *skb); extern void dev_kfree_skb(struct sk_buff *skb, int mode); extern int skb_device_locked(struct sk_buff *skb); /* * Peek an sk_buff. Unlike most other operations you _MUST_ * be careful with this one. A peek leaves the buffer on the * list and someone else may run off with it. For an interrupt * type system cli() peek the buffer copy the data and sti(); */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 static __inline__ struct sk_buff *skb_peek(struct sk_buff_head *list_) { struct sk_buff *list = (struct sk_buff *)list_; return (list->next != list)? list->next : NULL; } #if CONFIG_SKB_CHECK extern int skb_check(struct sk_buff *skb,int,int, char *); #define IS_SKB(skb) skb_check((skb), 0, __LINE__,__FILE__) #define IS_SKB_HEAD(skb) skb_check((skb), 1, __LINE__,__FILE__) #else #define IS_SKB(skb) #define IS_SKB_HEAD(skb) extern __inline__ void skb_queue_head_init(struct sk_buff_head *list) { list->prev = (struct sk_buff *)list; list->next = (struct sk_buff *)list; } /* * Insert an sk_buff at the start of a list. */ extern __inline__ void skb_queue_head(struct sk_buff_head *list_,struct sk_buff *newsk) { unsigned long flags; struct sk_buff *list = (struct sk_buff *)list_; save_flags(flags); cli(); newsk->next = list->next; newsk->prev = list; newsk->next->prev = newsk; newsk->prev->next = newsk; restore_flags(flags); } /* * Insert an sk_buff at the end of a list. */ extern __inline__ void skb_queue_tail(struct sk_buff_head *list_, struct sk_buff *newsk) { 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 unsigned long flags; struct sk_buff *list = (struct sk_buff *)list_; save_flags(flags); cli(); newsk->next = list; newsk->prev = list->prev; newsk->next->prev = newsk; newsk->prev->next = newsk; restore_flags(flags); } /* * Remove an sk_buff from a list. This routine is also interrupt safe * so you can grab read and free buffers as another process adds them. */ extern __inline__ struct sk_buff *skb_dequeue(struct sk_buff_head *list_) { long flags; struct sk_buff *result; struct sk_buff *list = (struct sk_buff *)list_; save_flags(flags); cli(); result = list->next; if (result == list) { restore_flags(flags); return NULL; } result->next->prev = list; list->next = result->next; result->next = NULL; result->prev = NULL; restore_flags(flags); return result; } 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* * Insert a packet before another one in a list. */ extern __inline__ void skb_insert(struct sk_buff *old, struct sk_buff *newsk) { unsigned long flags; save_flags(flags); cli(); newsk->next = old; newsk->prev = old->prev; old->prev = newsk; newsk->prev->next = newsk; restore_flags(flags); } /* * Place a packet after a given packet in a list. */ extern __inline__ void skb_append(struct sk_buff *old, struct sk_buff *newsk) { unsigned long flags; save_flags(flags); cli(); newsk->prev = old; newsk->next = old->next; newsk->next->prev = newsk; old->next = newsk; restore_flags(flags); } /* * Remove an sk_buff from its list. Works even without knowing the list it * is sitting on, which can be handy at times. It also means that THE LIST * MUST EXIST when you unlink. Thus a list must have its contents unlinked * _FIRST_. */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 extern __inline__ void skb_unlink(struct sk_buff *skb) { unsigned long flags; save_flags(flags); cli(); if(skb->prev && skb->next) { skb->next->prev = skb->prev; skb->prev->next = skb->next; skb->next = NULL; skb->prev = NULL; } restore_flags(flags); } #endif extern struct sk_buff * skb_recv_datagram(struct sock *sk,unsigned flags,int noblock, int *err); extern int datagram_select(struct sock *sk, int sel_type, select_table *wait); extern void skb_copy_datagram(struct sk_buff *from, int offset, char *to,int size); extern void skb_free_datagram(struct sk_buff *skb); #endif /* __KERNEL__ */ #endif /* _LINUX_SKBUFF_H */ /* include/linux/skbuff.h **********************************************************/ 1.21 socket.h 头文件 该头文件定义了 sockaddr 结构,linger 结构以及大量常量,这些常量主要用于编程接口,选 项设置和获取。sockaddr 结构对于网络程序员是再熟悉不过了。其定义了一个通用地址接口。 linger 结构用于设置套接字关闭时的行为。关闭套接字时可以在执行关闭操作后立刻返回, 即不等于关闭操作的最终完成(因为关闭操作的最终完成一般需要经过 4 路数据包的传送)。 如果设置了linger选择,则close函数必须等待直到套接字完全关闭。其中linger结构中l_onoff 表示是否进行等待(l_onoff=1 表示等待完成)。而 l_linger 字段表示等待的时间,如果等待 时间超时,无论实际上是否完成关闭操作,软件处理上都认为已经完成关闭操作。 /*include/linux/socket.h *********************************************************/ #ifndef _LINUX_SOCKET_H #define _LINUX_SOCKET_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #include /* the SIOCxxx I/O controls */ struct sockaddr { unsigned short sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ }; struct linger { int l_onoff; /* Linger active */ int l_linger; /* How long to linger for */ }; /* Socket types. */ /*套接字类型*/ #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. */ /* Supported address families. */ /*支持的地址(域)类型*/ #define AF_UNSPEC 0 #define AF_UNIX 1 #define AF_INET 2 #define AF_AX25 3 #define AF_IPX 4 #define AF_APPLETALK 5 #define AF_MAX 8 /* For now.. */ /* Protocol families, same as address families. */ /*协议类,同地址类型*/ #define PF_UNSPEC AF_UNSPEC #define PF_UNIX AF_UNIX #define PF_INET AF_INET #define PF_AX25 AF_AX25 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define PF_IPX AF_IPX #define PF_APPLETALK AF_APPLETALK #define PF_MAX AF_MAX /* Flags we can use with send/ and recv. */ /*数据处理方式标志位*/ #define MSG_OOB 1 //带外数据,作为紧急数据处理 #define MSG_PEEK 2 #define MSG_DONTROUTE 4 /* Setsockoptions(2) level. Thanks to BSD these must match IPPROTO_xxx */ /*选项设置级别(层次)*/ #define SOL_SOCKET 1 #define SOL_IP 0 #define SOL_IPX 256 #define SOL_AX25 257 #define SOL_ATALK 258 #define SOL_TCP 6 #define SOL_UDP 17 /* For setsockoptions(2) */ /*具体选项类型*/ #define SO_DEBUG 1 #define SO_REUSEADDR 2 #define SO_TYPE 3 #define SO_ERROR 4 #define SO_DONTROUTE 5 #define SO_BROADCAST 6 #define SO_SNDBUF 7 #define SO_RCVBUF 8 #define SO_KEEPALIVE 9 #define SO_OOBINLINE 10 #define SO_NO_CHECK 11 #define SO_PRIORITY 12 #define SO_LINGER 13 /* To add :#define SO_REUSEPORT 14 */ /* IP options */ /*IP 选项*/ #define IP_TOS 1 #define IPTOS_LOWDELAY 0x10 #define IPTOS_THROUGHPUT 0x08 #define IPTOS_RELIABILITY 0x04 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define IP_TTL 2 #ifdef V1_3_WILL_DO_THIS_FUNKY_STUFF #define IP_HRDINCL 3 #define IP_OPTIONS 4 #endif #define IP_MULTICAST_IF 32 #define IP_MULTICAST_TTL 33 #define IP_MULTICAST_LOOP 34 #define IP_ADD_MEMBERSHIP 35 #define IP_DROP_MEMBERSHIP 36 /* These need to appear somewhere around here */ #define IP_DEFAULT_MULTICAST_TTL 1 #define IP_DEFAULT_MULTICAST_LOOP 1 #define IP_MAX_MEMBERSHIPS 20 /* IPX options */ #define IPX_TYPE 1 /* TCP options - this way around because someone left a set in the c library includes */ /*TCP 选项*/ #define TCP_NODELAY 1 #define TCP_MAXSEG 2 /* The various priorities. */ /*数据包缓存到设备队列中的优先级*/ #define SOPRI_INTERACTIVE 0 #define SOPRI_NORMAL 1 #define SOPRI_BACKGROUND 2 #endif /* _LINUX_SOCKET_H */ /* include/linux/socket.h*********************************************************/ 1.22 sockios.h 头文件 该文件定义了用于各种层次(协议)IO 控制操作选项类型。 /*include/linux/sockios.h*******************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * * Definitions of the socket-level I/O control calls. * * Version: @(#)sockios.h 1.0.2 03/09/93 * * Authors: Ross Biro, * Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_SOCKIOS_H #define _LINUX_SOCKIOS_H /* This section will go away soon! */ /* Socket-level I/O control calls. */ /*套接字层次 IO 控制*/ #define FIOSETOWN 0x8901 #define SIOCSPGRP 0x8902 #define FIOGETOWN 0x8903 #define SIOCGPGRP 0x8904 #define SIOCATMARK 0x8905 #define SIOCGSTAMP 0x8906 /* Get stamp */ /* Routing table calls. */ /*路由表项控制*/ #define SIOCADDRT 0x890B /* add routing table entry */ #define SIOCDELRT 0x890C /* delete routing table entry */ /* Socket configuration controls. */ /*套接字配置控制*/ #define SIOCGIFNAME 0x8910 /* get iface name */ #define SIOCSIFLINK 0x8911 /* set iface channel */ #define SIOCGIFCONF 0x8912 /* get iface list */ #define SIOCGIFFLAGS 0x8913 /* get flags */ #define SIOCSIFFLAGS 0x8914 /* set flags */ #define SIOCGIFADDR 0x8915 /* get PA address */ #define SIOCSIFADDR 0x8916 /* set PA address */ #define SIOCGIFDSTADDR 0x8917 /* get remote PA address */ #define SIOCSIFDSTADDR 0x8918 /* set remote PA address */ #define SIOCGIFBRDADDR 0x8919 /* get broadcast PA address */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define SIOCSIFBRDADDR 0x891a /* set broadcast PA address */ #define SIOCGIFNETMASK 0x891b /* get network PA mask */ #define SIOCSIFNETMASK 0x891c /* set network PA mask */ #define SIOCGIFMETRIC 0x891d /* get metric */ #define SIOCSIFMETRIC 0x891e /* set metric */ #define SIOCGIFMEM 0x891f /* get memory address (BSD) */ #define SIOCSIFMEM 0x8920 /* set memory address (BSD) */ #define SIOCGIFMTU 0x8921 /* get MTU size */ #define SIOCSIFMTU 0x8922 /* set MTU size */ #define OLD_SIOCGIFHWADDR 0x8923 /* get hardware address */ #define SIOCSIFHWADDR 0x8924 /* set hardware address (NI) */ #define SIOCGIFENCAP 0x8925 /* get/set slip encapsulation */ #define SIOCSIFENCAP 0x8926 #define SIOCGIFHWADDR 0x8927 /* Get hardware address */ #define SIOCGIFSLAVE 0x8929 /* Driver slaving support */ #define SIOCSIFSLAVE 0x8930 /* begin multicast support change */ #define SIOCADDMULTI 0x8931 #define SIOCDELMULTI 0x8932 /* end multicast support change */ /* Routing table calls (oldrtent - don't use) */ /*路由表项控制,使用 old_rentry 结构*/ #define SIOCADDRTOLD 0x8940 /* add routing table entry */ #define SIOCDELRTOLD 0x8941 /* delete routing table entry */ /* ARP cache control calls. */ /*ARP 缓存表项控制*/ #define SIOCDARP 0x8950 /* delete ARP table entry */ #define SIOCGARP 0x8951 /* get ARP table entry */ #define SIOCSARP 0x8952 /* set ARP table entry */ /* RARP cache control calls. */ /*RARP 缓存表项控制*/ #define SIOCDRARP 0x8960 /* delete RARP table entry */ #define SIOCGRARP 0x8961 /* get RARP table entry */ #define SIOCSRARP 0x8962 /* set RARP table entry */ /* Driver configuration calls */ /*设备配置选项*/ #define SIOCGIFMAP 0x8970 /* Get device parameters */ #define SIOCSIFMAP 0x8971 /* Set device parameters */ /* Device private ioctl calls */ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 /* * These 16 ioctls are available to devices via the do_ioctl() device * vector. Each device should include this file and redefine these names * as their own. Because these are device dependent it is a good idea * _NOT_ to issue them to random objects and hope. */ #define SIOCDEVPRIVATE 0x89F0 /* to 89FF */ /* * These 16 ioctl calls are protocol private */ #define SIOCPROTOPRIVATE 0x89E0 /* to 89EF */ #endif /* _LINUX_SOCKIOS_H */ /*include/linux/sockios.h**********************************************************/ 1.23 tcp.h 头文件 该文件定义了 TCP 首部格式,使用 TCP 套接字所可能处的各种状态。 TCP 协议是一种面向连接的可靠的端到端协议,为互连于计算机通信网络中的两台主机之 间提供可靠的进程间通信。TCP 协议是传输层协议,工作在网络层协议之上(如 IP 协议)。 网络层协议为 TCP 提供发送和接收变长报文段信息的一种方式,这些报文通常被封装成数 据报的形式传送。数据报提供一种方式用于寻址源端和目的端主机地址。另外网络层还为 TCP 协议提供报文分片和重组服务,从而使报文满足某些中间路由器或者网关的传送要求。 诚如上述,TCP 协议之首要目的在于为运行于网络中不同主机上的进程提供可靠的通信, 要达到这个要求,TCP 协议需要提供如下的服务: 1>数据基本传输要求 2>可靠性保证 3>流量控制 4>多路分用 5>连接 6>优先级和安全性 以下简单介绍这些服务。 1>数据基本传输要求 TCP 提供了一种流式服务。通常 TCP 协议会自行判定何时节制发送数据。有时,上层应用 程序需要自己确定其使用 TCP 发送的数据确实已被发送给远端主机。为了达到此目的,系 统提供了 push 函数。push 函数会导致 TCP 协议立刻将应用程序数据转发给远端,以及远端 在接收到该数据时,立刻将数据传送给上层应用程序。 2>可靠性保证 TCP通过下列方式来提供可靠性: 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 •应用数据被分割成TCP认为最适合发送的数据块。这和UDP完全不同,应用程序产生的 数据报长度将保持不变。由TCP传递给IP的信息单位称为报文段或段(segment)当TCP发出 一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确 认,将重发这个报文段。 •当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常 将推迟几分之一秒。 •TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过 程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文 段(希望发端超时并重发)。 •既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到 达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺 序交给应用层。 •既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。 TCP 协议必须能够应对数据丢失,数据重复和数据乱序等问题。这些问题的解决方案是赋 予每个数据一个序列号,且使用数据接收确认机制。发送端发送的数据,接收端在接收到这 些数据后必须给发送端发送一个确认报文以通知发送端这些数据已被接收。如果发送端在发 送数据并等待系统定义的一段时间后,没有受到确认,则会重新发送这些数据。由此如果之 前发送的同样数据并没有丢失,只是延迟到达,则接收端有可能接收到相同的两份数据,这 就产生了数据重复。而发送的数据包如果经过不同的路径到达接收端,则也有可能出现先发 送的数据后于后发送的数据到达,从而造成数据的乱序。序列号的引入可以有效地解决这些 问题。如果接收端接收到两份相同的数据,首先期会根据其相同的(或者部分重叠的)序列 号判别出有重复数据的问题产生,此时采取的措施是丢弃其中的一份,只保留下一份供上层 使用;而数据乱序的问题则可以通过数据的序列号进行排序从而得到正确的数据序列。TCP 协议区别于其它协议的本质之处即在于其传输的每个数据都被绑定一个序列号。这是保证 TCP 所声明的功能必不可少的措施。有关序列号的问题在下文中还将有讨论。 3>流量控制 TCP 使用一种称为窗口的机制来节制发送端发送数据的速率。在 TCP 数据交换中,其中传 送每个数据报文中 TCP 首部(TCP 首部格式见下文)中 ACK 标志位设置为 1 的报文都会进 行接收端窗口大小的通报。由于 TCP 使用全双工方式进行数据传送,故通信的双方都会向 对方通报本端的窗口大小,以使得对方合理安排其发送速率。刚刚提到,窗口大小在每个应 答报文中都进行通报,而事实上,对于 TCP 协议,一旦建立起连接之后,到连接关闭之前, 其间双方传送的所有正常数据包中 ACK 标志位都设置为 1(减少网络中传输数据量),即 窗 口通报基本贯穿于整个 TCP 连接阶段。窗口大小规定了发送端(即对端)当前所能够发送 的最大数据量。本质上讲,窗口大小表示了当前本地接收端接收缓存的空闲空间大小,即可 容纳的数据量。所以可以理解一旦不进行窗口通报,即便发送端发送大量数据,在本地也会 被丢弃,因为没有足够的内存容纳这些数据。但由此引起的一个问题是,发送端并不知道接 收端接收缓存不够,其认为发送的数据在网络中丢失,从而不断进行重发,这导致这个网络 的负载加大,在特别情况下,会造成网络的崩溃,而且也会加大接收端的资源消耗(注意在 丢弃数据包之前的所有处理都占用 CPU 时间)。通过窗口通报的方式将从根本上杜绝这些问 题,因为数据包不会在网络上出现,占用有限的网络带宽。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 4>多路分用 为了在单个主机上实现多个进程的同时通信,TCP 协议使用地址和端口号的组合形式来表 示一个通信单元。这种地址加端口的构成形式通常称为套接字(socket)或插口。一个通信 通道即有两台主机各自的地址和端口号构成。这种构成形式中,地址部分表示进行通信的主 机,而端口号则表示主机上某个进行通信的进程。通常端口号与进程之间的绑定是随机的, 但将某个服务绑定到固定的端口上并约定俗成可以有效地为其它主机提供服务(如 FTP 服 务使用端口 21)。 5>连接 TCP 提供的可靠性以及流量控制功能要求其必须保存通信双方的状态。我们将将通信双方 的状态,加上地址和端口组合(即套接字),加上序列号,加上窗口大小等等所有这些构成 通常所说的 TCP 连接。当不同主机间需要进行数据传输时(即进行通信时),首先必须建立 起一个连接,而当通信结束后,则必须关闭这个连接以释放资源。TCP 协议是构建在不可 靠的协议之上,故需要一种“握手”机制来建立连接。通常我们将 TCP 这种建立连接的方 式称为“三路握手”机制。TCP 连接中的各种状态及相互之间的转换在下文中将有专门讨 论。 6>优先级和安全性 使用 TCP 协议的上层协议可以表示出对通信优先级和安全性的“关注”。如果没有,则系统 会提供默认值。 TCP 协议中序列号的使用 TCP 协议设计中一个最基本的概念即是其传输的每个字节都被赋予一个序列号。序列号的 使用是保证 TCP 协议实现可靠传输的重要措施。因为每个字节均被编号,故接收端可对接 收到的每个字节进行确认应答。不过为了提供网络利用率以及数据传输效率,确认应答采用 累加形式,即一次可以对一批数据进行确认,而非每次单个字节。此处“一批”数据通常是 指一个包或者多个包的数据。我们通常说的 TCP 是面向流的协议,这是从使用 TCP 协议传 送数据的上层协议而言的,而对于 TCP 协议本身及其下层协议(如 IP 协议)而言,数据传 送是以包的形式进行的。TCP 协议从其下层接收的数据都是封装在一个包中,所以进行数 据确认应答通常是以一个包的数据为单位,当然很多算法为节省网络带宽,会延迟对所接收 数据的确认应答,此时可能对多个包(一般两个包)的数据进行一次性确认应答。因为确认 应答是以包为单位(无论是单个包还是多个包,不影响此处讨论的问题本质),所以应答序 列号应是在包的边界上,换句话说,如果一切正常,确认应答序列号应该是发送端将要发送 的下一个数据包中所包含数据的第一个字节数据的序列号。这一点很重要,因为对于 TCP 协议而言,为保证可靠传输,其对每个发送出去的数据包并不进行立即释放,而是缓存在重 发队列中,以便在一段时间后,如果没有接收到对端的确认应答,则需要重新发送这些数据 包。而当一个确认应答到达时,发送端需要根据这个应答的序列号释放重发队列中缓存的部 分数据包,因为一个应答的到来表示之前发送的某些数据包已经成功发送到对方,从而无需 继续在重发队列中缓存这些数据包,如果应答序列号不是在包中数据序列号的边界上,那么 将会出现包中数据一部分被确认,另一部分没有被确认。这就让发送端“左右为难”了。而 实际上这种情况是不会发生的。即数据的接收是以包为单位的,应答的序列号一定是在包的 边界上。不会出现一个包中数据只有一部分被应答的情况(即换句话说,一个包中的数据只 有一部分被接收,而这是不可能的)。当然此处可能有人会提出异议:即在数据包传送过程 中有可能原来的一个数据包备份片成为多个数据包,这样不是会造成原先的包中一部分数据 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 可能被接收到,而另一部分数丢失的情况?事实上,这种考虑本身没有问题,但对于接收端 而言,如果数据包在中间路由器或者网关上被分片,则接收端会在网络层进行组装,在组装 完成之后才会传送给上一层及传输层协议使用。即数据包在传输过程中的分片对于 TCP 协 议而言是不可见的。而发送端的分片并不影响问题的本质,因为对于 TCP 协议而言,无所 谓发送端分片,每个分片都是被作为一个正常的数据包处理的,因为 TCP 提供的是流式数 据传输服务。 序列号简单的说就是一个 32 位的整型数。由于只有 32 位表示,故在经过一段时间后,这个 数字会进行回环。所以对于一个高速率以太网而言,序列号很快会绕回来。此时对序列号大 小的判断需要谨慎处理,特别是在靠近 2**32-1 附近的序列号。 TCP 首部格式如图 1-30 所示。 图 1-30 TCP 协议首部格式 发送端端口号:16 比特 标识该数据包源端数据发送进程。 目的端端口号:16 比特 标识该数据包目的端数据接收进程。 序列号:32 比特 本端发送的数据包中所包含数据的第一个字节的序列号。 应答序列号:32 比特 该序列号表示本端想要接收的的下一个数据包中所包含数据中第一个字节数据的序列号。 注意不要混淆序列号和应答序列号这两个字段的意义。序列号字段时对本地发送数据的编 号,而应答序列号是对对方发送数据的计号:即计算本地已接收到的对方发送的数据的编号。 数据偏移:4 比特 该字段表示 TCP 首部以 4 字节为单位的长度(包括 TCP 选项,如果存在)。换句话说即用 户数据的开始起点(从 TCP 首部第一个字节算起)。由此可见如同 IP 首部,TCP 首部必是 4 字节的整数倍,有时这需要在首部结尾处加填充字节。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 保留字段:6 比特 必须初始化为 0。 控制位:6 比特 URG:URG=1 表示紧急指针字段值有效,否则无效。紧急数据是将接收的数据迅速传替给 应用程序的一种手段。 ACK:ACK=1 表示这是一个应答报文(注意应答报文中可以包含本端发送的数据,事实上, 很少只发送一个应答报文而其中不附带数据的,换句话说,应答报文一般都是随数据 顺便发送的。 PSH:PSH=1 时的意义如同 URG=1 表示需要将数据立刻交给应用程序。软件处理上是给应 用程序发送信号(SIGURG)。 RST:复位标志。当一个报文中 RST=1 时,表示对方要求本地重新建立与对方的连接。该 标志位一般用于非严重错误恢复中。 SYN:同步标志。当 SYN=1 时表示这是一个同步报文,同步报文只用在建立连接的过程中。 FIN:结束连接报文。当 FIN=1 时表示本地已无数据发送给对端,但可以继续接收对方发送 的数据。FIN 报文通常使用在关闭连接中。 窗口大小:16 比特 该字段用于流量节制:表示当前对方所能容忍的最大数据接收量。本地必须注意到该要求, 发送的数据量必须小于该字段所声明的值。 校验和:16 比特 该字段计算 TCP 首部及其之后的所有用户数据部分的校验和。 注意在计算 TCP 校验和(UDP 也如此)还包含一个伪首部的计算。图 1-31 显示了计算 TCP 校验和时所需计入的伪首部格式。 图 1-31TCP 校验和计算中需计入的伪首部格式 紧急指针:16 比特 该字段表示紧急数据部分,其值是一个偏移量,该偏移量是从该数据包中第一个数据字节 算起。只当 URG 标志位为 1 时,紧急指针字段值方才有效。 TCP 选项:长度可变 TCP 选项较少,从结构来分,可分为单字节选项和多字节选项。单字节选项只有一个表示 选项类型的字节。多字节选项第一个字节为类型字节,第二个字节为长度字节,长度包括类 型字节及其本身以及后续的数据字节,第三个字节及其之后字节为数据字节部分。 此处介绍常用的三种选项,如下所示。 类型 长度 意义 ------ ------ ------ 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 0 -- 选项结束 1 -- 无操作 2 4 最大报文长度 1>选项结束 该选项标志着所有选项的结束。在此选项之后不可再有其它选项类型。 2>无操作选项 该选项不表示任何意义,只是作为其它选项之间的一种填充。有时为了将下一个选项安排在 4 字节边界上,有必要进行填充,但又不可简单的填充 0,因为这样可能被解析为上一个选 项的数据部分或其它后续部分,而使用无操作选项则无此疑义。 3>最大报文长度(MSS:Maximum Segment Size)选项 该选项只是用 TCP 建立连接过程的报文交换中。交换的得到的对方最大报文长度值在整个 后续连接中均不作改变。最大报文长度是指用户可一次性发送的数据最大长度。其与 MTU (Maximum Transmission Unit)即最大传输单元的差别在于(假设网络层使用 IP 协议): MSS=MTU-(TCP 首部长度+IP 首部长度+MAC 首部长度) 填充值 该字段是为保证 TCP 首部的长度为 4 字节的倍数。填充值一般取 0。 数据部分 此之后即用户实际需要传送的数据。 TCP 连接状态 按 TCP 协议的标准表示法,TCP 可具有如下几种状态: 为讨论方便,如下讨论中区分服务端和客户端,实际软件处理上对二者一视同仁。 CLOSED 关闭状态。在两个通信端使用“三路握手”机制建立连接之前即处于该状态。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 LISTEN 监听状态。此状态是对服务器端而言的。处于此状态的套接字正在等待客户端的连接请求。 SYN-SENT 客户端发送一个 SYN 报文请求建立与服务器端的连接后,设置为该状态。注意处于此状态 的套接字仅仅表示发送了 SYN 请求报文,但尚未得到对方应答。如果已得到对方应答,则 就会进入 ESTABLISHED 状态。 SYN-RECV 一般服务端在接收到客户端发送的 SYN 报文后,会发送一个附带 ACK 的 SYN 报文进行与 对方的同步,之后将其状态设置为 SYN-RECV。 ESTABLISHED 客户端接收到其之前发送的 SYN 报文的 ACK 后,设置为此状态。并且紧接着发送一个 ACK 报文给服务端从而建立正式连接。而此时处于 SYN-RECV 状态的服务端在接收到此 ACK 后也会将自己的状态设置为 ESTABLISHED。到此为止,连接完全建立,之后二者既可进行 数据的传输。 以下状态将无法区分服务端和客户端,主要视哪端首先发起关闭操作。我们将首先发起关闭 操作的一端视为客户端,对应的另一端视为服务端。 FIN-WAIT-1 客户端发起关闭操作,此时客户端将发送一个 FIN 报文给对方,并将其状态设置为 FIN-WAIT-1。 FIN-WAIT-2 处于 FIN-WAIT-1 状态的客户端在接收到之前发送的 FIN 的 ACK 报文后,将其状态设置为 FIN-WAIT-2,然后一直处于该状态,直到接收到对方发送 FIN 报文。注意在接收到 FIN 报 文之前,本端仍然可以继续接收对方发送的数据。因为发送 FIN 报文给对方仅仅表示本地 不再有数据发送给对方,并不表示本地不再接收对方的数据。这是两回事。处于此状态的套 接字接收到对方发送 FIN 时,将进入 TIME-WAIT 状态。 CLOSE-WAIT 服务端在接收到对方的 FIN 报文时,将本地状态设置为 CLOSE-WAIT,并即刻发送 ACK 报 文给对方。此后本地依然可以发送尚未发送的数据直到数据发送完。 CLOSING 当连接双方同时发送 FIN 时,会进入此状态。在前面的讨论中,如果本地发送关闭操作, 则会发送一个 FIN 报文给对方,并将自己状态设置为 FIN-WAIT-1,正常情况下,紧接着应 该接收到对方响应的 ACK 报文这样本地就可以进入 FIN-WAIT-2 状态,然后一直等待对方 的 FIN 报文。而如果在等待 ACK 报文时,接收到对方的 FIN 报文,则表示对方同时发起了 关闭操作,此时本地并不再进入 FIN-WAIT-2,而是发送一个对此 FIN 的 ACK 报文,并且 进入 CLOSING 状态,如果之后接收到对本地之前发送的 FIN 的 ACK 报文,则直接进入 TIME-WAIT 状态。 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 LAST-ACK 此状态从 CLOSE-WAIT 状态变化而来。承接上文中对 CLOSE-WAIT 状态的讨论,当服务端 发送完用户数据后,进行关闭操作,此时便可发送一个 FIN 报文完成通信通道的完全关闭。 在服务端发送这个 FIN 报文后,有两个方面状态的改变,对于服务端,则将状态设置为 LAST-ACK,即接下来就等待最后一个客户端响应的 ACK 报文了,一旦接收到该 ACK 报 文,则连接便完全关闭,服务端将状态设置为 CLOSED。而对于客户端而言,当接收到服 务端发送的 FIN 时,其首先发送一个 ACK 报文给服务端,之后将状态从 FIN-WAIT-2 设置 为 TIME-WAIT,表示等待 2MSL 时间(此称为静等待时间)。 TIME-WAIT 静等待状态。这是对于首先发送关闭操作的一端(一般即客户端)所最后经历的状态。应该 说在接收到服务端发送的 FIN 后,本地既可设置状态为 CLOSED。之所以需要等待一段时 间(通常为 2MSL:Maximum Segment Timelife)一方面是因为避免对服务端 ACK 信号的丢 失,这样当服务端重传 FIN 时,客户端可以再响应一个 ACK。这个原因是次要的,因为一 般对于 FIN 数据包的超时,会直接将状态设置为 CLOSED,这样处理不会造成任何有效的 影响。另一个方面的主要原因是防止之后重新建立的“化生”套接字接收到老的套接字在网 络中延迟的数据包。所谓“化生”套接字是指通信双方是之前的同一个信道:即具有相同的 源端,目的端 IP 地址和端口号。如果之前的套接字有数据包延迟在网络中,有可能这个新 建立的套接字会接收到该数据包从而造成数据误传。等待时间 2MSL 称为报文最大生存时 间,顾名思义即数据包可在网络中的最大停留时间。等待这么一段时间后,可以保证“肃清” 老的套接字的所有信息。2MSL 等待时间对于应用程序编程的影响即一般在关闭一个客户端 套接字后,系统不会允许立刻建立一个同样通道的套接字连接,除非设置诸如 REUSEADDR 的选项。 在对以上内容理解后,读者应该不难理解如下 tcp.h 文件中内容,此处不再作论述。 /*include/linux/tcp.h ***********************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions for the TCP protocol. * * Version: @(#)tcp.h 1.0.2 04/28/93 * * Author: Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_TCP_H 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 #define _LINUX_TCP_H #define HEADER_SIZE 64 /* maximum header size */ //res 字段表示保留(reserved)。 struct tcphdr { __u16 source; __u16 dest; __u32 seq; __u32 ack_seq; #if defined(LITTLE_ENDIAN_BITFIELD) __u16 res1:4, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, res2:2; #elif defined(BIG_ENDIAN_BITFIELD) __u16 res2:2, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1, doff:4, res1:4; #else #error "Adjust your defines" #endif __u16 window; __u16 check; __u16 urg_ptr; }; //TCP 连接可能的状态 enum { TCP_ESTABLISHED = 1, TCP_SYN_SENT, TCP_SYN_RECV, 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 TCP_FIN_WAIT1, TCP_FIN_WAIT2, TCP_TIME_WAIT, TCP_CLOSE, TCP_CLOSE_WAIT, TCP_LAST_ACK, TCP_LISTEN, TCP_CLOSING /* now a valid state */ }; #endif /* _LINUX_TCP_H */ /*include/linux/tcp.h ***********************************************************/ 1.24 timer.h 头文件 该文件定义了两个系统定时器结构 timer_struct 和 timer_list 结构。timer_struct 主要用于实现 内核本身所必需的定时器,而 timer_list 用于内核随需要动态添加的定时器。 /*include/linux/timer.h 头文件**************************************************/ #ifndef _LINUX_TIMER_H #define _LINUX_TIMER_H /* * DON'T CHANGE THESE!! Most of them are hardcoded into some assembly language * as well as being defined here. */ /* * The timers are: * * BLANK_TIMER console screen-saver timer * * BEEP_TIMER console beep timer * * RS_TIMER timer for the RS-232 ports * * HD_TIMER harddisk timer * * HD_TIMER2 (atdisk2 patches) * * FLOPPY_TIMER floppy disk timer (not used right now) * * SCSI_TIMER scsi.c timeout timer * 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * NET_TIMER tcp/ip timeout timer * * COPRO_TIMER 387 timeout for buggy hardware.. * * QIC02_TAPE_TIMER timer for QIC-02 tape driver (it's not hardcoded) * * MCD_TIMER Mitsumi CD-ROM Timer * */ //这些系统硬编码的定时器是为完成内核必须功能而设置的定时器。 #define BLANK_TIMER 0 //文字输入竖标闪烁定时器 #define BEEP_TIMER 1 //系统铃声 #define RS_TIMER 2 //RS 串口使用 #define HD_TIMER 16 //硬盘使用 #define FLOPPY_TIMER 17 //软件使用 #define SCSI_TIMER 18 //SCSI 接口使用 #define NET_TIMER 19 //网络接口使用 #define SOUND_TIMER 20 //声卡使用 #define COPRO_TIMER 21 #define QIC02_TAPE_TIMER 22 /* hhb */ #define MCD_TIMER 23 #define HD_TIMER2 24 //第二块硬盘使用 //系统本身使用的定时器结构 struct timer_struct { unsigned long expires; void (*fn)(void); }; /* kernel/sched.c */ extern unsigned long timer_active; extern struct timer_struct timer_table[32]; /* * This is completely separate from the above, and is the * "new and improved" way of handling timers more dynamically. * Hopefully efficient and general enough for most things. * * The "hardcoded" timers above are still useful for well- * defined problems, but the timer-list is probably better 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 * when you need multiple outstanding timers or similar. * * The "data" field is in case you want to use the same * timeout function for several timeouts. You can use this * to distinguish between the different invocations. */ //动态定时器结构 struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long expires; unsigned long data; void (*function)(unsigned long); }; extern void add_timer(struct timer_list * timer); extern int del_timer(struct timer_list * timer); extern inline void init_timer(struct timer_list * timer) { timer->next = NULL; timer->prev = NULL; } #endif /* include/linux/timer.h********************************************************/ 系统在 sched.c 文件中定义了 timer_head 变量: static struct timer_list timer_head = { &timer_head, &timer_head, ~0, 0, NULL }; 以及 timer_table 数组: unsigned long timer_active = 0; struct timer_struct timer_table[32]; 这两个部分的处理均是在 timer_bh 函数中进行的,而 timer_bh 函数作为 TIMER_BH 标志的 下半部分处理函数是在 do_bottom_half 函数(softirq.c)中被调用的。 换句话说,系统定时器虽然有几种类型:如 timer_struct 结构代表的定时器和 timer_list 结构 代表的定时器,不过系统在处理他们时并不分类,都是作为下半部分进行处理的,即统一在 timer_bh 函数中被调用执行。有关下半部分的数据结构定义在 linux/interrupt.h, kernel/softirq.c 文件中。每个下半部分都是由一个 bh_struct 结构表示: struct bh_struct { void (*routine)(void *); void *data; }; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 而系统目前定义的下半部分共有如下类型: enum { TIMER_BH = 0, //处理函数为 timer_bh(kernel/sched.c) CONSOLE_BH, TQUEUE_BH, //处理函数为 tqueue_bh(kernel/sched.c) SERIAL_BH, NET_BH, //处理函数为 net_bh(net/dev.c) IMMEDIATE_BH, //处理函数为 immediate_bh(kernel/sched.c) KEYBOARD_BH, YCLADES_BH }; 所有下半部分处理函数的调用执行都是在 do_bottom_half 函数中进行的,其它函数(如 do_timer, netif_rx 等等)只是对相应的标志位进行设置(每个标志位作为 bh_active 变量中的 一位,使用 mark_bh 函数(linux/interrupt.h)进行设置),从而使得相关的处理函数具有执行资 格。从中可以看出,网络部分数据包接收处理函数 net_bh(dev.c)是作为 NET_BH 类型的处理 函数执行的。综合而言,系统定义的所有定时器,无论是 timer_table 数组中的,还是 timer_head 链表中的,都是在 timer_bh 中被调用,do_timer 函数在每次时钟中断时被调用,其查看 timer_table 和 timer_head 中是否有定时器到期,如有,则调用 mark_bh(TIMER_BH)函数设 置 TIMER_BH 标志位,从而在 do_bottom_half 函数中处理下半部分时调用 timer_bh 函数进 行具体的函数处理。而 netif_rx 在接收到一个数据包时,通过调用 mark_bh(NET_BH)函数设 置 NET_BH 标志位,达到如上所述同样的目的,即在 do_bottom_half 函数中调用 net_bh 处 理函数进行数据包的向上传送。* 由此可见,下半部分在系统中占据着极其重要的地位。 do_bottom_half 函数的被调用之处是在 entry.S 文件中,在每次系统调用返回时,都会对下半 部分进行处理。从而不会影响定时器的有效性以及相关操作上的及时性。 1.25 udp.h 头文件 该文件定义 UDP 协议首部格式。 UDP 协议是无连接协议,其不提供可靠的传输,只是尽可能将数据送往接收端。在接收端 没有接收到数据包时,也不提供任何手段通知发送端。如果需要提供可靠保证的数据传输, 必须使用 TCP 协议。如同 TCP 协议,UDP 协议是一种传输层协议,工作在网络层协议之上 (如 IP 协议)。UDP 协议是面向包的,即接收端每次都以包为单位进行接收,应用程序也 是以包为单位进行数据接收,如果所分配的用户缓冲区不足,则包中多余的数据就会被直接 丢弃,而非等待下一次的读取。 UDP 首部格式,如图 1-32 所示。 图 1-32 UDP 协议首部格式 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 发送端端口号:16 比特 目的端端口号:16 比特 标识该数据包源端进程和目的端进程。 长度:16 比特 该字段表示 UDP 首部与其数据部分的总长度。 校验和:16 比特 该字段计算的是 UDP 首部,数据部分和伪首部的校验和。 伪首部格式与计算 TCP 校验和时使用的格式相同,只是协议字段有所不同,如图 1-33 所示。 图 1-33 UDP 校验和计算中使用的伪首部格式 udp.h 文件定义了 UDP 首部结构,参考图 1-22 应不难理解。 /*include/linux/udp.h ************************************************************/ /* * INET An implementation of the TCP/IP protocol suite for the LINUX * operating system. INET is implemented using the BSD Socket * interface as the means of communication with the user level. * * Definitions for the UDP protocol. * * Version: @(#)udp.h 1.0.2 04/28/93 * * Author: Fred N. van Kempen, * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ #ifndef _LINUX_UDP_H #define _LINUX_UDP_H //UDP 首部格式定义 struct udphdr { unsigned short source; unsigned short dest; unsigned short len; 第一章 网络协议头文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 unsigned short check; }; #endif /* _LINUX_UDP_H */ /*include/linux/udp.h ************************************************************/ 1.26 un.h 头文件 该文件定义了 UNIX 域使用的地址结构:sockaddr_un 结构。 /* include/linux/un.h**********************************************************/ #ifndef _LINUX_UN_H #define _LINUX_UN_H #define UNIX_PATH_MAX 108 struct sockaddr_un { unsigned short sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ }; #endif /* _LINUX_UN_H */ /* include/linux/un.h**********************************************************/ 1.27 本章小结 本章着重对网络协议相关头文件进行了分析,对其中不明白的部分,读者可暂时跳过,在第 二章阅读相关协议具体实现代码时可回头进行查看和理解。对于头文件中绝大部分变量和结 构定义本章都作了较为详细的解释,某些无法详细说明的字段将在第二章具体代码实现中给 与阐述。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 第二章 网络协议实现文件分析 网络协议具体实现文件均位于 net 文件夹下,该文件夹下文件组织形式如下图(图 2-1)所示。 图 2-1 网络协议实现文件组织形式 其中 unix 子文件夹中三个文件是有关 UNIX 域代码,UNIX 域是模拟网络传输方式在本机范围 内用于进程间数据传输的一种机制,由于不涉及网络部分且相对较为简单,本文将不对该部分 进行分析。下文将集中于对 inet 子文件夹中文件进行详细介绍。不过首先需要对 net 文件夹下 两个文件进行分析。 下图(图 2-2)显示了 Linux 网络栈实现与 ISO/OSI 网络栈七层分层之间的对应关系。图中另外 简单给出了本版本网络栈实现中各层次的主要对应文件。如 BSD socket 层对应函数集定义在 socket.c 文件中,其中函数将作为对 socket,bind,accept 等系统调用的直接下层响应函数 (sock_socket, sock_bind, sock_accept)。不过在下文对 socket.c 文件的分析中可以看出所有的网 络调用函数都具有共同的入口函数 sys_socket,由该入口函数调用具体的处理函数(如 sock_socket, sock_bind 等)。BSD socket 层之下是 INET socket 层,文件 af_inet.c 对应该层次。 这种上下层次的表示从函数的嵌套调用关系上体现出来。socket.c 文件中函数的实现绝大多数都 是简单调用下层函数,而这些下层函数就是 af_inet.c 文件中定义的函数。例如 sock_bind 调用 inet_bind, sock_accept 调用 inet_accept 等等。之所以取名为 INET 层,恐怕与该层函数大多是以 inet 开头有关。 INET 层之下即传输层,在该层不同的协议具有不同的函数集,如 TCP 协议处理函数集定义在 tcp.c 文件中。从下文的分析中,读者可以看到,内核对一个系统调用的响应是层层下放的,而 传输层才会真正进行实质上的处理,在 BSD 层和 INET 层并不进行实际处理,而是仅仅进行某 些检查后,调用下一层函数:BSD 层调用 INET 层,INET 层调用传输层。这种调用在实现上是 通过函数指针的方式进行的,一个套接字在不同层次有不同的数据结构表示,如在 BSD 层,用 socket 结构表示,而在 INET 层有 sock 结构表示(虽然 sock 结构不止使用在 INET 层)。BSD 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 层调用 INET 层函数通过 socket 结构中 ops 字段完成的。ops 字段是一个 proto_ops 结构类型, 该 proto_ops 结构(下文中介绍)主要由函数指针组成。所以在 socket.c 文件中定义的函数大部 分都有如下形式的调用语句:socket->ops->bind 或者 socket->ops->accept。而实际上这些函数指 针指向 af_inet.c 文件中定义的函数,如 socket->ops->bind 实际上是 inet_bind, socket->ops->accept 实际上对应 inet_accept,其它类同。同理 INET 层对传输层函数的调用也是通过函数指针的形式 完成,只不过在此起桥梁作用的是 sock 结构而已。sock 结构中 prot 字段是一个 proto 结构类型。 该 proto 结构也是主要由一些函数指针组成。另外由于传输层所使用协议的不同,故将对应不同 的函数集。如果使用 TCP 协议则 prot 字段指向 TCP 协议操作函数集,即 INET 层将调用 tcp.c 文件中定义的函数,或者如果使用 UDP 协议,则将调用 udp.c 文件中定义的函数。使用函数指 针这种形式为此根据使用协议的不同调用不同的函数集合提供了很大的灵活性。在下文中对 inet_create 函数的分析中,读者可以看到,根据所使用协议的不同以及套接字类型的不同(流式? 报文形式?),sock 结构的 prot 字段将初始化为不同的函数操作集合,从而实现了操作函数集 合的动态分配。对于 af_inet.c 文件中定义的大多数函数都有如下的对其下层(传输层)函数的 调用形式:sock->prot->bind, sock->prot->accept, 对于 TCP 协议,则这些函数指针实际上指向 tcp.c 文件中定义的 tcp_bind, tcp_accept, 其它函数调用类似。 图 2-2 Linux 网络栈实现及其对应文件 2.1 net/protocols.c 文件 该文件定义了链路层所使用协议的初始化函数。主要是对一个 net_proto 数组的定义。 net_proto 结构定义在 linux/net.h 文件中。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 /*include/linux/net.h*/ 115 struct net_proto { 116 char *name; /* Protocol name */ 117 void (*init_func)(struct net_proto *); /* Bootstrap */ 118 }; 该文件内容如下: /*net/protocols.c ****************************************************************/ 1 /* 2 * Protocol initializer table. Here separately for convenience 3 * 4 */ 5 #include 6 #include 7 #include 8 #include 9 #define CONFIG_UNIX /* always present... */ 10 #ifdef CONFIG_UNIX 11 #include "unix/unix.h" 12 #endif 13 #ifdef CONFIG_INET 14 #include 15 #endif 16 #ifdef CONFIG_IPX 17 #include "inet/ipxcall.h" 18 #include "inet/p8022call.h" 19 #endif 20 #ifdef CONFIG_AX25 21 #include "inet/ax25call.h" 22 #endif 23 #ifdef CONFIG_ATALK 24 #ifndef CONFIG_IPX 25 #include "inet/p8022call.h" 26 #endif 27 #include "inet/atalkcall.h" 28 #endif 29 #include "inet/psnapcall.h" 30 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 31 * Protocol Table 32 */ 33 struct net_proto protocols[] = { 34 #ifdef CONFIG_UNIX 35 { "UNIX", unix_proto_init },//Unix 域,本机内模拟网络通信方式进行进程间数据传送 36 #endif 37 #if defined(CONFIG_IPX)||defined(CONFIG_ATALK) 38 { "802.2", p8022_proto_init }, //802.2,SNAP 是链路层子协议,与 802.3 头部配合使用 39 { "SNAP", snap_proto_init }, 40 #endif 41 #ifdef CONFIG_AX25 42 { "AX.25", ax25_proto_init }, 43 #endif 44 #ifdef CONFIG_INET 45 { "INET", inet_proto_init }, //INET 域,网络栈常用协议初始化 46 #endif 47 #ifdef CONFIG_IPX 48 { "IPX", ipx_proto_init },//IPX 协议,网络层协议,ipx_proto_init 用于初始化 49 #endif 50 #ifdef CONFIG_ATALK 51 { "DDP", atalk_proto_init },//Appletalk 协议初始化 52 #endif 53 { NULL, NULL } 54 }; /*net/protocols.c ***************************************************************/ 该文件定义了一系列条件语句,根据某些变量的声明与否定义相关的初始化函数。protocols 数 组包含了一系列初始化函数声明。在网络栈初始化阶段,protocols 数组定义的这些初始化函数 将被调用。 2.2 net/socket.c 文件 该文件的定义的函数集,我们通常意义上可以将其 BSD socket 层对应的函数来看待。它是系统 调用与下层网络栈实现函数之间的桥梁。系统调用通过 INT $0x80 进入内核执行函数,该函数 根据 AX 寄存器中的系统调用号,进一步调用内核网络栈相应的实现函数。对于 socket,bind, connect,sendto,recvfrom 等这些网络栈直接操作函数而言,则 socket.c 文件中定义的函数是作 为网络栈的最上层实现函数,即第一层被调用的函数。对于不同的协议具有的不同操作方式, 只有到达传输层才可以处理具体的请求,所以在传输层之上的所有层面操作函数集,其实现的 功能较为直接:只是检查某些字段或者标志位,然后将请求进一步传替给下层处理函数。对于 标准的 TCP/IP 四层结构,我们通常将链路层称为 L2 层,依次类推,传输层即为 L4 层,应用 层为 L5 层,则 socket.c 文件中定义的函数集合可以称之为 L6 层,即该文件中定义函数是一个 通用的层面,它只是系统调用层与网络栈函数集合的接口层,所以可以预见该文件中定义的函 数实现均较为简单,因为它们大多数实现的功能只是调用下一层函数进行处理。 虽然 Linux 中几乎所有的接口都是以文件形式组织的,但对于网络栈在/dev 目录下却无这样的 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 对应关系。不过内核网络栈实现仍然提供了对于网络数据的普通文件操作方式,如 write,read 函数可直接用于读写网络数据,在 socket.c 文件中可以看到内核提供的针对网络数据的文件操 作函数集合的实现。 由于 socket.c 文件相当长,故我们分段进行分析。 1 /* 2 * NET An implementation of the SOCKET network access protocol. 3 * 4 * Version: @(#)socket.c 1.1.93 18/02/95 5 * 6 * Authors: Orest Zborowski, 7 * Ross Biro, 8 * Fred N. van Kempen, 9 * 10 * Fixes: 11 * Anonymous : NOTSOCK/BADF cleanup. Error fix in 12 * shutdown() 13 * Alan Cox : verify_area() fixes 14 * Alan Cox : Removed DDI 15 * Jonathan Kamens : SOCK_DGRAM reconnect bug 16 * Alan Cox : Moved a load of checks to the very 17 * top level. 18 * Alan Cox : Move address structures to/from user 19 * mode above the protocol layers. 20 * Rob Janssen : Allow 0 length sends. 21 * Alan Cox : Asynchronous I/O support (cribbed from the 22 * tty drivers). 23 * Niibe Yutaka : Asynchronous I/O for writes (4.4BSD style) 24 * Jeff Uphoff : Made max number of sockets command-line 25 * configurable. 26 * Matti Aarnio : Made the number of sockets dynamic, 27 * to be allocated when needed, and mr. 28 * Uphoff's max is used as max to be 29 * allowed to allocate. 30 * Linus : Argh. removed all the socket allocation 31 * altogether: it's in the inode now. 32 * Alan Cox : Made sock_alloc()/sock_release() public 33 * for NetROM and future kernel nfsd type 34 * stuff. 35 * 36 * 37 * This program is free software; you can redistribute it and/or 38 * modify it under the terms of the GNU General Public License 39 * as published by the Free Software Foundation; either version 40 * 2 of the License, or (at your option) any later version. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 41 * 42 * 43 * This module is effectively the top level interface to the BSD socket 44 * paradigm. Because it is very simple it works well for Unix domain sockets, 45 * but requires a whole layer of substructure for the other protocols. 46 * 47 * In addition it lacks an effective kernel -> kernel interface to go with 48 * the user one. 49 */ 50 #include 51 #include 52 #include 53 #include 54 #include 55 #include 56 #include 57 #include 58 #include 59 #include 60 #include 61 #include 62 #include 63 #include 64 #include 以上为 socket.c 对其引入头文件的声明。紧接着即是对网络数据提供普通文件操作接口函数的 声明以及相关数据结构(file_operations)的初始化。file_operations 结构定义了普通文件操作函 数集。系统中每个文件对应一个 file 结构,file 结构中有一个 file_operations 变量,当使用 write, read 函数对某个文件描述符进行读写操作时,系统首先根据文件描述符索引到其对应的 file 结 构,然后调用其成员变量 file_operations 中对应函数完成请求。 65 static int sock_lseek(struct inode *inode, struct file *file, off_t offset, 66 int whence); 67 static int sock_read(struct inode *inode, struct file *file, char *buf, 68 int size); 69 static int sock_write(struct inode *inode, struct file *file, char *buf, 70 int size); 71 static int sock_readdir(struct inode *inode, struct file *file, 72 struct dirent *dirent, int count); 73 static void sock_close(struct inode *inode, struct file *file); 74 static int sock_select(struct inode *inode, struct file *file, int which, select_table *seltable); 75 static int sock_ioctl(struct inode *inode, struct file *file, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 76 unsigned int cmd, unsigned long arg); 77 static int sock_fasync(struct inode *inode, struct file *filp, int on); 78 /* 79 * Socket files have a set of 'special' operations as well as the generic file ones. These don't appear 80 * in the operation structures but are done directly via the socketcall() multiplexor. 81 */ 82 static struct file_operations socket_file_ops = { 83 sock_lseek, 84 sock_read, 85 sock_write, 86 sock_readdir, 87 sock_select, 88 sock_ioctl, 89 NULL, /* mmap */ 90 NULL, /* no special open code... */ 91 sock_close, 92 NULL, /* no fsync */ 93 sock_fasync 94 }; 以上 socket_file_ops 变量中声明的函数即是网络协议对应的普通文件操作函数集合。从而使得 read,write,ioctl 等这些常见普通文件操作函数也可以被使用在网络接口的处理上。 值得注意的是,这些提供的普通文件接口中并没有定义 open 函数,因为对于网络而言,socket 函数已经完成了类似的功能,并且 open 函数基本调用语义是通过文件路径进行操作,然而诚如 上文中所述,对于网络栈内核并无这样的对应文件。 95 /* 96 * The protocol list. Each protocol is registered in here. 97 */ 98 static struct proto_ops *pops[NPROTO]; 99 /* 100 * Statistics counters of the socket lists 101 */ 102 static int sockets_in_use = 0; proto_ops 结构定义在 include/linux/net.h 文件中,该结构中声明了一系列操作函数作为 L6 层处 理函数。pops 数组将在 sock_register 函数中被初始化,对于不同的操作域,具有不同的操作函 数集,如对应 INET 域的 inet_proto_ops 操作函数集,UNIX 域对应 unix_proto_ops 操作函数集。 socket_in_use 变量定义了系统当前正在使用的套接字总数目。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 103 /* 104 * Support routines. Move socket addresses back and forth across the kernel/user 105 * divide and look after the messy bits. 106 */ 107 #define MAX_SOCK_ADDR 128 /* 108 for Unix domain - 16 for IP, 16 for IPX, about 80 for AX.25 */ 108 static int move_addr_to_kernel(void *uaddr, int ulen, void *kaddr) 109 { 110 int err; 111 if(ulen<0||ulen>MAX_SOCK_ADDR) 112 return -EINVAL; 113 if(ulen==0) 114 return 0; 115 if((err=verify_area(VERIFY_READ,uaddr,ulen))<0) 116 return err; 117 memcpy_fromfs(kaddr,uaddr,ulen); 118 return 0; 119 } 120 static int move_addr_to_user(void *kaddr, int klen, void *uaddr, int *ulen) 121 { 122 int err; 123 int len; 124 if((err=verify_area(VERIFY_WRITE,ulen,sizeof(*ulen)))<0) 125 return err; 126 len=get_fs_long(ulen); 127 if(len>klen) 128 len=klen; 129 if(len<0 || len> MAX_SOCK_ADDR) 130 return -EINVAL; 131 if(len) 132 { 133 if((err=verify_area(VERIFY_WRITE,uaddr,len))<0) 134 return err; 135 memcpy_tofs(uaddr,kaddr,len); 136 } 137 put_fs_long(len,ulen); 138 return 0; 139 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 move_addr_to_kernel, move_addr_to_user 这两个函数实现的功能较为直接,即实现地址用户空间 和内核空间之间的相互移动,主要是通过调用底层函数 memcpy_tofs 和 memcpy_fromfs 实现的。 140 /* 141 * Obtains the first available file descriptor and sets it up for use. 142 */ 143 static int get_fd(struct inode *inode) 144 { 145 int fd; 146 struct file *file; 147 /* 148 * Find a file descriptor suitable for return to the user. 149 */ 150 file = get_empty_filp(); 151 if (!file) 152 return(-1); 153 for (fd = 0; fd < NR_OPEN; ++fd) 154 if (!current->files->fd[fd]) 155 break; 156 if (fd == NR_OPEN) 157 { 158 file->f_count = 0; 159 return(-1); 160 } 161 FD_CLR(fd, ¤t->files->close_on_exec); 162 current->files->fd[fd] = file; 163 file->f_op = &socket_file_ops; 164 file->f_mode = 3; 165 file->f_flags = O_RDWR; 166 file->f_count = 1; 167 file->f_inode = inode; 168 if (inode) 169 inode->i_count++; 170 file->f_pos = 0; 171 return(fd); 172 } 函数 get_fd 是为网络套接字分配一个文件描述符,在 socket 系统调用处理函数 sys_socket 实现 中,内核在分配 inode,socket,sock 结构后,调用 get_fd 获得一个文件描述符作为 socket 系统 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 调用的返回值。注意函数的参数是一个 inode 结构,因为分配文件描述符的同时需要一个 file 结构,file 结构中 f_inode 字段即指向这个 inode 结构,每个 file 结构都需要有一个 inode 结构对 应。注意函数中 f_op 字段的赋值即前文中定义的 socket_file_ops,通过此处的赋值即实现了网 络操作的普通文件接口。如果对文件描述符调用 write,read 函数进行操作时,即调用 socket_file_ops 变量中对应的 sock_read, sock_write 函数。 file 结构的获取是通过 get_empty_flip 函数实现的。内核维护一个 file 结构的数组,get_empty_filp 函数即通过检查该数组,获取一个闲置的成员。 173 /* 174 * Go from an inode to its socket slot. 175 * 176 * The original socket implementation wasn't very clever, which is 177 * why this exists at all.. 178 */ 179 inline struct socket *socki_lookup(struct inode *inode) 180 { 181 return &inode->u.socket_i; 182 } socki_lookup 函数即通过 inode 结构查找对应的 socket 结构,socket 结构可以看作是网络栈操作 在 L6,L5 层的表示。从实现来看,socket 结构是作为 inode 结构中一个变量。这点可以从 inode 结构的具体定义看出,此处给出 inode 结构的设计此部分的定义。 /*include/linux/fs.h*/ 172 struct inode { …… 206 union { 207 struct pipe_inode_info pipe_i; 208 struct minix_inode_info minix_i; 209 struct ext_inode_info ext_i; 210 struct ext2_inode_info ext2_i; 211 struct hpfs_inode_info hpfs_i; 212 struct msdos_inode_info msdos_i; 213 struct umsdos_inode_info umsdos_i; 214 struct iso_inode_info isofs_i; 215 struct nfs_inode_info nfs_i; 216 struct xiafs_inode_info xiafs_i; 217 struct sysv_inode_info sysv_i; 218 struct socket socket_i; 219 void * generic_ip; 220 } u; 221 }; 从中可以看出 inode 结构中最后一个变量是一个 union 结构,该结构中又定义了一系列针对不同 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 文件系统的信息结构如对应 ext2 文件系统的 ext2_i 结构,而对于网络而言则对应 socket_i,此 时这是一个 socket 结构。 183 /* 184 * Go from a file number to its socket slot. 185 */ 186 static inline struct socket *sockfd_lookup(int fd, struct file **pfile) 187 { 188 struct file *file; 189 struct inode *inode; 190 if (fd < 0 || fd >= NR_OPEN || !(file = current->files->fd[fd])) 191 return NULL; 192 inode = file->f_inode; 193 if (!inode || !inode->i_sock) 194 return NULL; 195 if (pfile) 196 *pfile = file; 197 return socki_lookup(inode); 198 } sockfd_lookup 函数实现较为直接,从文件描述符得到其对应的 file 结构,进而得到 inode 结构, 然后调用 socki_lookup 函数返回 socket 结构。 199 /* 200 * Allocate a socket. 201 */ 202 struct socket *sock_alloc(void) 203 { 204 struct inode * inode; 205 struct socket * sock; 206 inode = get_empty_inode(); 207 if (!inode) 208 return NULL; 209 inode->i_mode = S_IFSOCK; 210 inode->i_sock = 1; 211 inode->i_uid = current->uid; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 212 inode->i_gid = current->gid; 213 sock = &inode->u.socket_i; 214 sock->state = SS_UNCONNECTED; 215 sock->flags = 0; 216 sock->ops = NULL; 217 sock->data = NULL; 218 sock->conn = NULL; 219 sock->iconn = NULL; 220 sock->next = NULL; 221 sock->wait = &inode->i_wait; 222 sock->inode = inode; /* "backlink": we could use pointer arithmetic instead */ 223 sock->fasync_list = NULL; 224 sockets_in_use++; 225 return sock; 226 } sock_alloc 函数用于分配一个 socket 结构,从实现来看这个函数实质上是获取一个 inode 结构, get_empty_inode 结构实现的功能基本类同于 get_empty_filp,该函数通过检查系统维护的一个 inode 结构数组,获取一个空闲的 inode 结构返回。之后对此 inode 结构的部分字段进行初始化, 之后返回需要的 socket 结构。 227 /* 228 * Release a socket. 229 */ 230 static inline void sock_release_peer(struct socket *peer) 231 { 232 peer->state = SS_DISCONNECTING; 233 wake_up_interruptible(peer->wait); 234 sock_wake_async(peer, 1); 235 } 236 void sock_release(struct socket *sock) 237 { 238 int oldstate; 239 struct socket *peersock, *nextsock; 240 if ((oldstate = sock->state) != SS_UNCONNECTED) 241 sock->state = SS_DISCONNECTING; 242 /* 243 * Wake up anyone waiting for connections. 244 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 245 for (peersock = sock->iconn; peersock; peersock = nextsock) 246 { 247 nextsock = peersock->next; 248 sock_release_peer(peersock); 249 } 250 /* 251 * Wake up anyone we're connected to. First, we release the 252 * protocol, to give it a chance to flush data, etc. 253 */ 254 peersock = (oldstate == SS_CONNECTED) ? sock->conn : NULL; 255 if (sock->ops) 256 sock->ops->release(sock, peersock); 257 if (peersock) 258 sock_release_peer(peersock); 259 --sockets_in_use; /* Bookkeeping.. */ 260 iput(SOCK_INODE(sock)); 261 } sock_release_peer 仅用于 UNIX 域,对于 INET 域 socket 结构中 conn,iconn 字段为 NULL。因 为对于 INET 域而言,与远端主机的连接是一一对应的。 sock_release 函数用于释放(关闭)一个套接字。其中需要注意的是对 sock->ops->release 函数 的调用,sock->ops 字段是一个 proto_ops 结构,proto_ops 结构是一个操作函数集合定义,这个 集合实质上是一个接口函数集,换句话说,函数本身并不完成具体功能,而是将请求送往下层 函数。对于 INET 域而言,此时 sock->ops 字段将被赋值为 inet_proto_ops 变量,该变量在 net/inet/af_inet.c 文件中定义: /*net/inet/af_inet.c*/ 1313 static struct proto_ops inet_proto_ops = { 1314 AF_INET, 1315 inet_create, 1316 inet_dup, 1317 inet_release, 1318 inet_bind, 1319 inet_connect, 1320 inet_socketpair, 1321 inet_accept, 1322 inet_getname, 1323 inet_read, 1324 inet_write, 1325 inet_select, 1326 inet_ioctl, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1327 inet_listen, 1328 inet_send, 1329 inet_recv, 1330 inet_sendto, 1331 inet_recvfrom, 1332 inet_shutdown, 1333 inet_setsockopt, 1334 inet_getsockopt, 1335 inet_fcntl, 1336 }; 从中可以看出实际上对于 release 函数的调用具体是 inet_release。而 inet_release 也仅仅是一个接 口函数,其将通过进一步调用下层函数完成具体的请求。在分析 af_inet.c 文件时再作具体说明。 353 void sock_close(struct inode *inode, struct file *filp) 354 { 355 struct socket *sock; 356 /* 357 * It's possible the inode is NULL if we're closing an unfinished socket. 358 */ 359 if (!inode) 360 return; 361 if (!(sock = socki_lookup(inode))) 362 { 363 printk("NET: sock_close: can't find socket for inode!\n"); 364 return; 365 } 366 sock_fasync(inode, filp, 0); 367 sock_release(sock); 368 } 此处将 sock_close 函数提前,因为 sock_close,sock_release, sock_release_peer 这三个函数都是 用于套接字的关闭操作且相互嵌套。sock_close 调用 sock_release , sock_release 调用 sock_release_peer. sock_fasync 函数如下所示。 369 /* 370 * Update the socket async list 371 */ 372 static int sock_fasync(struct inode *inode, struct file *filp, int on) 373 { 374 struct fasync_struct *fa, *fna=NULL, **prev; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 375 struct socket *sock; 376 unsigned long flags; 377 if (on) 378 { 379 fna=(struct fasync_struct *)kmalloc(sizeof(struct fasync_struct), GFP_KERNEL); 380 if(fna==NULL) 381 return -ENOMEM; 382 } 383 sock = socki_lookup(inode); 384 prev=&(sock->fasync_list); 385 save_flags(flags); 386 cli(); 387 for(fa=*prev; fa!=NULL; prev=&fa->fa_next,fa=*prev) 388 if(fa->fa_file==filp) 389 break; 390 if(on) 391 { 392 if(fa!=NULL) 393 { 394 kfree_s(fna,sizeof(struct fasync_struct)); 395 restore_flags(flags); 396 return 0; 397 } 398 fna->fa_file=filp; 399 fna->magic=FASYNC_MAGIC; 400 fna->fa_next=sock->fasync_list; 401 sock->fasync_list=fna; 402 } 403 else 404 { 405 if(fa!=NULL) 406 { 407 *prev=fa->fa_next; 408 kfree_s(fa,sizeof(struct fasync_struct)); 409 } 410 } 411 restore_flags(flags); 412 return 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 413 } 该函数将根据输入参数 on 的取值决定是分配还是释放一个 fasync_struct 结构。 /*include/linux/fs.h*/ 246 struct fasync_struct { 247 int magic; 248 struct fasync_struct *fa_next; /* singly linked list */ 249 struct file *fa_file; 250 }; 251 #define FASYNC_MAGIC 0x4601 fasync_struct 结构用于异步唤醒。结构中 fa_file 是一个 file 结构,file 结构中 f_owner 指向该 file 结构对应文件所属的属主,所谓属主即打开该文件进行操作的某个进程,f_owner 即指向该进程 进程号。sock_fasync 函数虽然较长,但实现功能简单,此处不多作表述。 socket 结构中最后一个字段 fasync_list 是一个 fasync_struct 结构。如上文所述,该结构用于异步 唤醒,对应的唤醒函数为 sock_wake_async。 /*include/linux/net.h*/ 62 struct socket { … 73 struct fasync_struct *fasync_list; /* Asynchronous wake up list */ 74 }; sock_wake_async 函数实现如下文所示。 /*如无特别指出,函数定义在 socket.c 文件中*/ 414 int sock_wake_async(struct socket *sock, int how) 415 { 416 if (!sock || !sock->fasync_list) 417 return -1; 418 switch (how) 419 { 420 case 0: 421 kill_fasync(sock->fasync_list, SIGIO); 422 break; 423 case 1: 424 if (!(sock->flags & SO_WAITDATA)) 425 kill_fasync(sock->fasync_list, SIGIO); 426 break; 427 case 2: 428 if (sock->flags & SO_NOSPACE) 429 { 430 kill_fasync(sock->fasync_list, SIGIO); 431 sock->flags &= ~SO_NOSPACE; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 432 } 433 break; 434 } 435 return 0; 436 } sock_wake_async 函数在 sock_release_peer, sock_awaitconn 函数中调用。sock_release_peer 在释 放一个套接字被调用,sock_awaitconn 在试图建立一个连接时被调用,这些调用或者涉及到套 接字状态的改变,所以如果有进程等待套接字的状态改变,则需要在改变时通知相应进程或者 需要唤醒相关进程进行某种处理,如唤醒服务器端监听进程进行客户端套接字连接的处理。该 函数通过遍历 socket 结构中 fasync_list 变量指向的队列,对队列中每个元素调用 kill_fasync 函 数,kill_fasync 函数如下所示。 /*fs/fcntl.c*/ void kill_fasync(struct fasync_struct *fa, int sig) { while (fa) { if (fa->magic != FASYNC_MAGIC) { printk("kill_fasync: bad magic number in " "fasync_struct!\n"); return; } if (fa->fa_file->f_owner > 0) kill_proc(fa->fa_file->f_owner, sig, 1); else kill_pg(-fa->fa_file->f_owner, sig, 1); fa = fa->fa_next; } } 从 kill_fasync 函数的实现来看,所谓的异步唤醒功能就一目了然了,即通过向相应的进程发送 信号。 以下 6 个函数用于建立网络数据的普通文件操作接口,分别为 sock_lseek, sock_read, sock_write, sock_readdir, sock_select, sock_ioctl. 其中 sock_lseek, sock_readdir 未实现。sock_read, sock_write 这两个函数用于读写网络数据,在具体实现上,均是在完成部分检查后,调用下层函数(inet_read, inet_write)完成具体功能。而 sock_select, sock_ioctl 也是类似的实现,通过调用下层函数 inet_select, inet_ioctl 完成请求。 262 /* 263 * Sockets are not seekable. 264 */ 265 static int sock_lseek(struct inode *inode, struct file *file, off_t offset, int whence) 266 { 267 return(-ESPIPE); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 268 } 269 /* 270 * Read data from a socket. ubuf is a user mode pointer. We make sure the user 271 * area ubuf...ubuf+size-1 is writable before asking the protocol. 272 */ 273 static int sock_read(struct inode *inode, struct file *file, char *ubuf, int size) 274 { 275 struct socket *sock; 276 int err; 277 if (!(sock = socki_lookup(inode))) 278 { 279 printk("NET: sock_read: can't find socket for inode!\n"); 280 return(-EBADF); 281 } 282 if (sock->flags & SO_ACCEPTCON) 283 return(-EINVAL); 284 if(size<0) 285 return -EINVAL; 286 if(size==0) 287 return 0; 288 if ((err=verify_area(VERIFY_WRITE,ubuf,size))<0) 289 return err; 290 return(sock->ops->read(sock, ubuf, size, (file->f_flags & O_NONBLOCK))); 291 } 292 /* 293 * Write data to a socket. We verify that the user area ubuf..ubuf+size-1 is 294 * readable by the user process. 295 */ 296 static int sock_write(struct inode *inode, struct file *file, char *ubuf, int size) 297 { 298 struct socket *sock; 299 int err; 300 if (!(sock = socki_lookup(inode))) 301 { 302 printk("NET: sock_write: can't find socket for inode!\n"); 303 return(-EBADF); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 304 } 305 if (sock->flags & SO_ACCEPTCON) 306 return(-EINVAL); 307 if(size<0) 308 return -EINVAL; 309 if(size==0) 310 return 0; 311 if ((err=verify_area(VERIFY_READ,ubuf,size))<0) 312 return err; 313 return(sock->ops->write(sock, ubuf, size,(file->f_flags & O_NONBLOCK))); 314 } 315 /* 316 * You can't read directories from a socket! 317 */ 318 static int sock_readdir(struct inode *inode, struct file *file, struct dirent *dirent, 319 int count) 320 { 321 return(-EBADF); 322 } 323 /* 324 * With an ioctl arg may well be a user mode pointer, but we don't know what to do 325 * with it - thats up to the protocol still. 326 */ 327 int sock_ioctl(struct inode *inode, struct file *file, unsigned int cmd, 328 unsigned long arg) 329 { 330 struct socket *sock; 331 if (!(sock = socki_lookup(inode))) 332 { 333 printk("NET: sock_ioctl: can't find socket for inode!\n"); 334 return(-EBADF); 335 } 336 return(sock->ops->ioctl(sock, cmd, arg)); 337 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 338 static int sock_select(struct inode *inode, struct file *file, int sel_type, select_table * wait) 339 { 340 struct socket *sock; 341 if (!(sock = socki_lookup(inode))) 342 { 343 printk("NET: sock_select: can't find socket for inode!\n"); 344 return(0); 345 } 346 /* 347 * We can't return errors to select, so it's either yes or no. 348 */ 349 if (sock->ops && sock->ops->select) 350 return(sock->ops->select(sock, sel_type, wait)); 351 return(0); 352 } sock_awaitconn 函数只用于 UNIX 域,用于处理一个客户端连接请求。socket 结构中 iconn,conn 结构用于 UNIX 域中连接操作,其中 iconn 只用于服务器端,表示等待连接但尚未完成连接的 客户端 socket 结构链表。 437 /* 438 * Wait for a connection. 439 */ 440 int sock_awaitconn(struct socket *mysock, struct socket *servsock, int flags) 441 { 442 struct socket *last; 443 /* 444 * We must be listening 445 */ //检查服务器端是否是处于侦听状态,即可以进行连接。 446 if (!(servsock->flags & SO_ACCEPTCON)) 447 { 448 return(-EINVAL); 449 } 450 /* 451 * Put ourselves on the server's incomplete connection queue. 452 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 453 mysock->next = NULL; 454 cli(); //将本次客户端连接的套接字(socket 结构)插入服务器端 socket 结构 iconn 字段指向 //的链表,即表示客户端正等待连接。 455 if (!(last = servsock->iconn)) 456 servsock->iconn = mysock; 457 else 458 { 459 while (last->next) 460 last = last->next; 461 last->next = mysock; 462 } 463 mysock->state = SS_CONNECTING; 464 mysock->conn = servsock; 465 sti(); 466 /* 467 * Wake up server, then await connection. server will set state to 468 * SS_CONNECTED if we're connected. 469 */ //唤醒服务器端进程,以处理本地客户端连接。 470 wake_up_interruptible(servsock->wait); //注意对 sock_wake_async 函数的调用,sock_wake_async 函数将唤醒 servsock 结构中 //fasync_list 指向的队列中每个元素对应的进程。 471 sock_wake_async(servsock, 0); //由于唤醒服务器端需要一定延迟时间,所以这个 if 判断条件一般为 true。 472 if (mysock->state != SS_CONNECTED) 473 { 474 if (flags & O_NONBLOCK) 475 return -EINPROGRESS; //等待服务器端处理本次连接。 476 interruptible_sleep_on(mysock->wait); //当本地客户端被唤醒后,继续检查连接状态,以察看是否已完成与服务器的连接。 477 if (mysock->state != SS_CONNECTED && 478 mysock->state != SS_DISCONNECTING) 479 { 480 /* 481 * if we're not connected we could have been 482 * 1) interrupted, so we need to remove ourselves 483 * from the server list 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 484 * 2) rejected (mysock->conn == NULL), and have 485 * already been removed from the list 486 */ //如果仍然没有建立连接,则原因有二,一是服务器端拒绝连接,此时本地 //socket 结构已被服务器端从其等待队列 iconn 中删除;二是在睡眠中被其它 //情况中断但并未建立连接,此时需要主动将本地 socket 结构从对方服务器端 //等待连接队列 iconn 中删除。 487 if (mysock->conn == servsock) 488 { 489 cli(); 490 if ((last = servsock->iconn) == mysock) 491 servsock->iconn = mysock->next; 492 else 493 { 494 while (last->next != mysock) 495 last = last->next; 496 last->next = mysock->next; 497 } 498 sti(); 499 } //如果 mysock->conn 为 NULL,则表示服务器拒绝连接,返回 EACCESS。 //否则返回被中断标志:EINTR。 500 return(mysock->conn ? -EINTR : -EACCES); 501 } 502 } 503 return(0); 504 } 以下主要是对网络套接字专用函数的底层对应函数的实现。 /* 505 /* 506 * Perform the socket system call. we locate the appropriate 507 * family, then create a fresh socket. 508 */ 509 static int sock_socket(int family, int type, int protocol) 510 { 511 int i, fd; 512 struct socket *sock; 513 struct proto_ops *ops; 514 /* Locate the correct protocol family. */ 515 for (i = 0; i < NPROTO; ++i) 516 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 517 if (pops[i] == NULL) continue; 518 if (pops[i]->family == family) 519 break; 520 } 521 if (i == NPROTO) 522 { 523 return -EINVAL; 524 } 525 ops = pops[i]; 526 /* 527 * Check that this is a type that we know how to manipulate and 528 * the protocol makes sense here. The family can still reject the 529 * protocol later. 530 */ //此下判断参数的有效性。这些标志均定义在 linux/socket.h 文件中。 //SOCK_STREAM:使用流式数据交换,如 TCP //SOCK_DGRAM:使用报文数据交换,如 UDP //SOCK_SEQPACKET:序列报文套接字格式,内核处理方式如同流式报文。 //SOCK_RAW:原始套接字,直接在传输层收发数据,应用程序须自行建立传输层首部 //SOCK_PACKET:包类型套接字,直接在网络层收发数据,应用程序须自行建立网络层, //传输层首部 531 if ((type != SOCK_STREAM && type != SOCK_DGRAM && 532 type != SOCK_SEQPACKET && type != SOCK_RAW && 533 type != SOCK_PACKET) || protocol < 0) 534 return(-EINVAL); 535 /* 536 * Allocate the socket and allow the family to set things up. if 537 * the protocol is 0, the family is instructed to select an appropriate 538 * default. 539 */ 540 if (!(sock = sock_alloc())) 541 { 542 printk("NET: sock_socket: no more sockets\n"); 543 return(-ENOSR); /* Was: EAGAIN, but we are out of 544 system resources! */ 545 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 546 sock->type = type; 547 sock->ops = ops; 548 if ((i = sock->ops->create(sock, protocol)) < 0) 549 { 550 sock_release(sock); 551 return(i); 552 } 553 if ((fd = get_fd(SOCK_INODE(sock))) < 0) 554 { 555 sock_release(sock); 556 return(-EINVAL); 557 } 558 return(fd); 559 } sock_socket 函数对应 socket 系统调用。该函数首先根据 family 输入参数决定域操作函数集用于 socket结构中ops字段的赋值,如inet_proto_ops或者unix_proto_ops。之后分配inode结构和socket 结构(通过调用 sock_alloc),并对 socket 结构中 ops 字段赋值。之后调用 sock->ops->create 指 向的函数(如 inet_create,该函数在本书下文中对应部分进行分析)分配下层 sock 结构,sock 结构是比 socket 结构更底层的表示一个套接字连接的结构。最后调用 get_fd 分配一个文件描述 符(及其对应的 file 结构)并返回。 故 sock_socket 函数完成如下工作: 1>分配 socket,sock 结构,这两个结构在网络栈的不同层次表示一个套接字连接。 2>分配 inode,file 结构用于普通文件操作。 3>分配一个文件描述符并返回给应用程序作为以后的操作句柄。 560 /* 561 * Create a pair of connected sockets. 562 */ 563 static int sock_socketpair(int family, int type, int protocol, unsigned long usockvec[2]) 564 { 565 int fd1, fd2, i; 566 struct socket *sock1, *sock2; 567 int er; 568 /* 569 * Obtain the first socket and check if the underlying protocol 570 * supports the socketpair call. 571 */ 572 if ((fd1 = sock_socket(family, type, protocol)) < 0) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 573 return(fd1); 574 sock1 = sockfd_lookup(fd1, NULL); 575 if (!sock1->ops->socketpair) 576 { 577 sys_close(fd1); 578 return(-EINVAL); 579 } 580 /* 581 * Now grab another socket and try to connect the two together. 582 */ 583 if ((fd2 = sock_socket(family, type, protocol)) < 0) 584 { 585 sys_close(fd1); 586 return(-EINVAL); 587 } 588 sock2 = sockfd_lookup(fd2, NULL); 589 if ((i = sock1->ops->socketpair(sock1, sock2)) < 0) 590 { 591 sys_close(fd1); 592 sys_close(fd2); 593 return(i); 594 } 595 sock1->conn = sock2; 596 sock2->conn = sock1; 597 sock1->state = SS_CONNECTED; 598 sock2->state = SS_CONNECTED; 599 er=verify_area(VERIFY_WRITE, usockvec, 2 * sizeof(int)); 600 if(er) 601 { 602 sys_close(fd1); 603 sys_close(fd2); 604 return er; 605 } 606 put_fs_long(fd1, &usockvec[0]); 607 put_fs_long(fd2, &usockvec[1]); 608 return(0); 609 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 sock_socketpair 只用于 UNIX 域,用于在两个进程间通过套接字建立一个连接进行数据传送。 该函数通过指针变量返回描述套接字两通信端的文件描述符。一般在父子进程间通信时用该种 方式。而且这种通信方式有点类似管道通信方式。sock_socketpair 函数首先调用 sock_socket 分 别在模拟通信的两端建立套接字,然后调用 sock1->ops->socketpair 指针指向的函数初始化相关 变量,此后初始化套接字各自的 conn 字段指向对方并修改套接字状态为 SS_CONNECTED 表示 完成连接的建立。函数最后返回分别表示通信两端的文件描述符。 610 /* 611 * Bind a name to a socket. Nothing much to do here since it's 612 * the protocol's responsibility to handle the local address. 613 * 614 * We move the socket address to kernel space before we call 615 * the protocol layer (having also checked the address is ok). 616 */ 617 static int sock_bind(int fd, struct sockaddr *umyaddr, int addrlen) 618 { 619 struct socket *sock; 620 int i; 621 char address[MAX_SOCK_ADDR]; 622 int err; 623 if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) 624 return(-EBADF); 625 if (!(sock = sockfd_lookup(fd, NULL))) 626 return(-ENOTSOCK); 627 if((err=move_addr_to_kernel(umyaddr,addrlen,address))<0) 628 return err; 629 if ((i = sock->ops->bind(sock, (struct sockaddr *)address, addrlen)) < 0) 630 { 631 return(i); 632 } 633 return(0); 634 } sock_bind 函数是系统调用 bind 函数的 BSD 层对应函数,用于绑定一个本地地址。参数 umyaddr 表示需要绑定的地址。sock_bind 函数实现首先通过文件描述符获取其对应的 socket 结构,然后 调用 move_addr_to_kernel 将地址从用户缓冲区复制到内核缓冲区,最后调用 sock->ops->bind 指针指向的函数(如 inet_bind)完成具体的操作。 635 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 636 * Perform a listen. Basically, we allow the protocol to do anything 637 * necessary for a listen, and if that works, we mark the socket as 638 * ready for listening. 639 */ 640 static int sock_listen(int fd, int backlog) 641 { 642 struct socket *sock; 643 if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) 644 return(-EBADF); 645 if (!(sock = sockfd_lookup(fd, NULL))) 646 return(-ENOTSOCK); 647 if (sock->state != SS_UNCONNECTED) 648 { 649 return(-EINVAL); 650 } 651 if (sock->ops && sock->ops->listen) 652 sock->ops->listen(sock, backlog); 653 sock->flags |= SO_ACCEPTCON; 654 return(0); 655 } sock_listen 函数是系统调用 listen 函数 BSD 层对应的函数。该函数用于服务器端为接收客户端 连接作准备。该函数通过调用 sock->ops->listen 函数完成具体的操作,listen 函数的底层实现函 数主要是几个标志字段的设置,如 sock->flags。这样在接下来的操作中会检查这些标志位的设 置。 656 /* 657 * For accept, we attempt to create a new socket, set up the link 658 * with the client, wake up the client, then return the new 659 * connected fd. We collect the address of the connector in kernel 660 * space and move it to user at the very end. This is buggy because 661 * we open the socket then return an error. 662 */ 663 static int sock_accept(int fd, struct sockaddr *upeer_sockaddr, int *upeer_addrlen) 664 { 665 struct file *file; 666 struct socket *sock, *newsock; 667 int i; 668 char address[MAX_SOCK_ADDR]; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 669 int len; 670 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 671 return(-EBADF); 672 if (!(sock = sockfd_lookup(fd, &file))) 673 return(-ENOTSOCK); 674 if (sock->state != SS_UNCONNECTED) 675 { 676 return(-EINVAL); 677 } 678 if (!(sock->flags & SO_ACCEPTCON)) 679 { 680 return(-EINVAL); 681 } 682 if (!(newsock = sock_alloc())) 683 { 684 printk("NET: sock_accept: no more sockets\n"); 685 return(-ENOSR); /* Was: EAGAIN, but we are out of system 686 resources! */ 687 } 688 newsock->type = sock->type; 689 newsock->ops = sock->ops; 690 if ((i = sock->ops->dup(newsock, sock)) < 0) 691 { 692 sock_release(newsock); 693 return(i); 694 } 695 i = newsock->ops->accept(sock, newsock, file->f_flags); 696 if ( i < 0) 697 { 698 sock_release(newsock); 699 return(i); 700 } 701 if ((fd = get_fd(SOCK_INODE(newsock))) < 0) 702 { 703 sock_release(newsock); 704 return(-EINVAL); 705 } 706 if (upeer_sockaddr) 707 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 708 newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 1); 709 move_addr_to_user(address,len, upeer_sockaddr, upeer_addrlen); 710 } 711 return(fd); 712 } sock_accept 函数是系统调用 accept 函数的 BSD 层对应函数,用于服务器端接受一个客户端的连 接请求。从该函数实现可以看出我们经常讨论的监听套接字与实际处理数据传送的套接字是不 同的。监听套接字在处理一个新的连接时会另外建立一个新的套接字结构专门用于此次数据传 输,而监听套接字仍然进行监听。注意函数中对 SO_ACCEPTCON 标志位的检测,如果没有设 置该标志位则表示对一个非监听套接字调用了该函数,此时错误返回。在确定是对一个监听套 接字处理后,继续调用 sock_alloc 函数创建一个新的套接字用于数据传送,而后需要对此信的 套接字进行初始化,初始化信息主要来自监听套接字中原有信息,sock->ops->dup 函数(如 inet_dup)即用于此目的。在对新创建的通信套接字进行初始化后,调用 sock->ops->accept 函数 (如 inet_accept)完成与远端的连接建立过程。最后分配一个新文件描述符并返回用于以后的 数据传送。函数最后,如果传入的 upeer_sockaddr 指针参数不为 NULL,表示应用程序需要返 回通信远端的地址,此时调用 sock->ops->getname 函数从连接请求数据包中取得远端地址并使 用 move_addr_to_user 函数将地址复制到用户缓冲区中。 713 /* 714 * Attempt to connect to a socket with the server address. The address 715 * is in user space so we verify it is OK and move it to kernel space. 716 */ 717 static int sock_connect(int fd, struct sockaddr *uservaddr, int addrlen) 718 { 719 struct socket *sock; 720 struct file *file; 721 int i; 722 char address[MAX_SOCK_ADDR]; 723 int err; 724 if (fd < 0 || fd >= NR_OPEN || (file=current->files->fd[fd]) == NULL) 725 return(-EBADF); 726 if (!(sock = sockfd_lookup(fd, &file))) 727 return(-ENOTSOCK); 728 if((err=move_addr_to_kernel(uservaddr,addrlen,address))<0) 729 return err; 730 switch(sock->state) 731 { 732 case SS_UNCONNECTED: 733 /* This is ok... continue with connect */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 734 break; 735 case SS_CONNECTED: 736 /* Socket is already connected */ 737 if(sock->type == SOCK_DGRAM) /* Hack for now - move this all into the protocol */ 738 break; 739 return -EISCONN; 740 case SS_CONNECTING: 741 /* Not yet connected... we will check this. */ 742 /* 743 * FIXME: for all protocols what happens if you start 744 * an async connect fork and both children connect. Clean 745 * this up in the protocols! 746 */ 747 break; 748 default: 749 return(-EINVAL); 750 } 751 i = sock->ops->connect(sock, (struct sockaddr *)address, addrlen, file->f_flags); 752 if (i < 0) 753 { 754 return(i); 755 } 756 return(0); 757 } sock_connect 函数是系统调用 connect 函数 BSD 层的对应函数。该函数首先将要连接的远端地 址从用户缓冲区复制到内核缓冲区,之后根据套接字目前所处的状态采取对应的措施,如对已 经完成连接的套接字调用该函数,则简单返回 EISCONN 。如果状态有效,则调用 sock->ops->connect 函数完成具体的连接,底层实现函数将涉及到网络数据的传输。 758 /* 759 * Get the local address ('name') of a socket object. Move the obtained 760 * name to user space. 761 */ 762 static int sock_getsockname(int fd, struct sockaddr *usockaddr, int *usockaddr_len) 763 { 764 struct socket *sock; 765 char address[MAX_SOCK_ADDR]; 766 int len; 767 int err; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 768 if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) 769 return(-EBADF); 770 if (!(sock = sockfd_lookup(fd, NULL))) 771 return(-ENOTSOCK); 772 err=sock->ops->getname(sock, (struct sockaddr *)address, &len, 0); 773 if(err) 774 return err; 775 if((err=move_addr_to_user(address,len, usockaddr, usockaddr_len))<0) 776 return err; 777 return 0; 778 } 779 /* 780 * Get the remote address ('name') of a socket object. Move the obtained 781 * name to user space. 782 */ 783 static int sock_getpeername(int fd, struct sockaddr *usockaddr, int *usockaddr_len) 784 { 785 struct socket *sock; 786 char address[MAX_SOCK_ADDR]; 787 int len; 788 int err; 789 if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) 790 return(-EBADF); 791 if (!(sock = sockfd_lookup(fd, NULL))) 792 return(-ENOTSOCK); 793 err=sock->ops->getname(sock, (struct sockaddr *)address, &len, 1); 794 if(err) 795 return err; 796 if((err=move_addr_to_user(address,len, usockaddr, usockaddr_len))<0) 797 return err; 798 return 0; 799 } sock_getsockname 和 sock_getpeername 用于获取通信双端本地和远端的地址。注意二者调用同 一个下层函数,下层函数将根据其传入的第三个函数做出判断返回本地地址或者远端地址。地 址的获取是相当简单的,因为对于每个套接字连接,下层都有一个 sock 结构对应,该结构中将 保存通信双方的地址信息,所以返回地址信息只是相应字段的简单复制或移动。 800 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 801 * Send a datagram down a socket. The datagram as with write() is 802 * in user space. We check it can be read. 803 */ 804 static int sock_send(int fd, void * buff, int len, unsigned flags) 805 { 806 struct socket *sock; 807 struct file *file; 808 int err; 809 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 810 return(-EBADF); 811 if (!(sock = sockfd_lookup(fd, NULL))) 812 return(-ENOTSOCK); 813 if(len<0) 814 return -EINVAL; 815 err=verify_area(VERIFY_READ, buff, len); 816 if(err) 817 return err; 818 return(sock->ops->send(sock, buff, len, (file->f_flags & O_NONBLOCK), flags)); 819 } 820 /* 821 * Send a datagram to a given address. We move the address into kernel 822 * space and check the user space data area is readable before invoking 823 * the protocol. 824 */ 825 static int sock_sendto(int fd, void * buff, int len, unsigned flags, 826 struct sockaddr *addr, int addr_len) 827 { 828 struct socket *sock; 829 struct file *file; 830 char address[MAX_SOCK_ADDR]; 831 int err; 832 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 833 return(-EBADF); 834 if (!(sock = sockfd_lookup(fd, NULL))) 835 return(-ENOTSOCK); 836 if(len<0) 837 return -EINVAL; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 838 err=verify_area(VERIFY_READ,buff,len); 839 if(err) 840 return err; 841 if((err=move_addr_to_kernel(addr,addr_len,address))<0) 842 return err; 843 return(sock->ops->sendto(sock, buff, len, (file->f_flags & O_NONBLOCK), 844 flags, (struct sockaddr *)address, addr_len)); 845 } send 和 sendto 函数用于发送数据,不同的是 sendto 可以指定远端地址,对于 TCP 协议而言,指 定的远端地址必须是之前与之建立连接的远端的地址。否则错误返回。而对于 UDP 协议则可以 在每次发送数据时指定不同的远端地址。这两个函数的实现都是通过调用更底层函数完成具体 的发送功能。对于 INET 域而言,下层函数分别为 inet_send, inet_sendto。这些函数将在 net/inet/af_inet.c 文件中分析。 846 /* 847 * Receive a datagram from a socket. This isn't really right. The BSD manual 848 * pages explicitly state that recv is recvfrom with a NULL to argument. The 849 * Linux stack gets the right results for the wrong reason and this need to 850 * be tidied in the inet layer and removed from here. 851 * We check the buffer is writable and valid. 852 */ 853 static int sock_recv(int fd, void * buff, int len, unsigned flags) 854 { 855 struct socket *sock; 856 struct file *file; 857 int err; 858 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 859 return(-EBADF); 860 if (!(sock = sockfd_lookup(fd, NULL))) 861 return(-ENOTSOCK); 862 if(len<0) 863 return -EINVAL; 864 if(len==0) 865 return 0; 866 err=verify_area(VERIFY_WRITE, buff, len); 867 if(err) 868 return err; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 869 return(sock->ops->recv(sock, buff, len,(file->f_flags & O_NONBLOCK), flags)); 870 } 871 /* 872 * Receive a frame from the socket and optionally record the address of the 873 * sender. We verify the buffers are writable and if needed move the 874 * sender address from kernel to user space. 875 */ 876 static int sock_recvfrom(int fd, void * buff, int len, unsigned flags, 877 struct sockaddr *addr, int *addr_len) 878 { 879 struct socket *sock; 880 struct file *file; 881 char address[MAX_SOCK_ADDR]; 882 int err; 883 int alen; 884 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 885 return(-EBADF); 886 if (!(sock = sockfd_lookup(fd, NULL))) 887 return(-ENOTSOCK); 888 if(len<0) 889 return -EINVAL; 890 if(len==0) 891 return 0; 892 err=verify_area(VERIFY_WRITE,buff,len); 893 if(err) 894 return err; 895 len=sock->ops->recvfrom(sock, buff, len, (file->f_flags & O_NONBLOCK), 896 flags, (struct sockaddr *)address, &alen); 897 if(len<0) 898 return len; 899 if(addr!=NULL && (err=move_addr_to_user(address,alen, addr, addr_len))<0) 900 return err; 901 return len; 902 } sock_recv 和 sock_recvfrom 用于接收远端发送的数据,不同的是 sock_recvfrom 可以同时返回远 端地址,所以 sock_recvfrom 函数主要用于 UDP 协议。从二者的实现来看,与之前其它函数的 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 实现无异:即通过调用下层函数实现。对于 INET 域而言,二者分别对应 inet_recv, inet_recvfrom。 903 /* 904 * Set a socket option. Because we don't know the option lengths we have 905 * to pass the user mode parameter for the protocols to sort out. 906 */ 907 static int sock_setsockopt(int fd, int level, int optname, char *optval, int optlen) 908 { 909 struct socket *sock; 910 struct file *file; 911 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 912 return(-EBADF); 913 if (!(sock = sockfd_lookup(fd, NULL))) 914 return(-ENOTSOCK); 915 return(sock->ops->setsockopt(sock, level, optname, optval, optlen)); 916 } 917 /* 918 * Get a socket option. Because we don't know the option lengths we have 919 * to pass a user mode parameter for the protocols to sort out. 920 */ 921 static int sock_getsockopt(int fd, int level, int optname, char *optval, int *optlen) 922 { 923 struct socket *sock; 924 struct file *file; 925 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 926 return(-EBADF); 927 if (!(sock = sockfd_lookup(fd, NULL))) 928 return(-ENOTSOCK); 929 if (!sock->ops || !sock->ops->getsockopt) 930 return(0); 931 return(sock->ops->getsockopt(sock, level, optname, optval, optlen)); 932 } sock_setsockopt 和 sock_getsockopt 函数分别用于设置和获取套接字选项。根据选项类型,这个 选项可以处在任何一个层次上。这两个函数的实现通过调用下层函数完成,对于 INET 域而言, 分别对应 inet_setsockopt 和 inet_getsockopt。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 933 /* 934 * Shutdown a socket. 935 */ 936 static int sock_shutdown(int fd, int how) 937 { 938 struct socket *sock; 939 struct file *file; 940 if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL)) 941 return(-EBADF); 942 if (!(sock = sockfd_lookup(fd, NULL))) 943 return(-ENOTSOCK); 944 return(sock->ops->shutdown(sock, how)); 945 } 该函数是系统调用 shutdown 函数的 BSD 对应实现函数,用于套接字半关闭操作。半关闭操作 在一些应用中非常常见,因为有时需要以本地半关闭的形式通知对方本地数据已完全传送完毕。 半关闭即只关闭发送通道(接收通道不可单独关闭)。一般在完全发送完本地数据后,可以使用 shutdown 关闭本地发送通道。该函数实现通过调用下层函数 inet_shutdown 完成,inet_shutdown 调用 tcp_shutdown,这些实现均不涉及网络数据的传输,主要是相关标志位的设置,所以实现上 均较为简单。 946 /* 947 * Perform a file control on a socket file descriptor. 948 */ 949 int sock_fcntl(struct file *filp, unsigned int cmd, unsigned long arg) 950 { 951 struct socket *sock; 952 sock = socki_lookup (filp->f_inode); 953 if (sock != NULL && sock->ops != NULL && sock->ops->fcntl != NULL) 954 return(sock->ops->fcntl(sock, cmd, arg)); 955 return(-EINVAL); 956 } 该函数用于控制套接字行为。如果对普通文件的控制操作一样,使用 fcntl 可以修改套接字对应 结构(如 socket,sock 结构)的某些字段,这是最直接的控制套接字的方式。另外 sock_ioctl 函数也用于类似的目的。目前此函数只用于设置和获取套接字属主(sock 结构的 proc 字段值)。 957 /* 958 * System call vectors. Since I (RIB) want to rewrite sockets as streams, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 959 * we have this level of indirection. Not a lot of overhead, since more of 960 * the work is done via read/write/select directly. 961 * 962 * I'm now expanding this up to a higher level to separate the assorted 963 * kernel/user space manipulations and global assumptions from the protocol 964 * layers proper - AC. 965 */ 966 asmlinkage int sys_socketcall(int call, unsigned long *args) 967 { 968 int er; 969 switch(call) 970 { 971 case SYS_SOCKET: 972 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 973 if(er) 974 return er; 975 return(sock_socket(get_fs_long(args+0), 976 get_fs_long(args+1), 977 get_fs_long(args+2))); 978 case SYS_BIND: 979 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 980 if(er) 981 return er; 982 return(sock_bind(get_fs_long(args+0), 983 (struct sockaddr *)get_fs_long(args+1), 984 get_fs_long(args+2))); 985 case SYS_CONNECT: 986 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 987 if(er) 988 return er; 989 return(sock_connect(get_fs_long(args+0), 990 (struct sockaddr *)get_fs_long(args+1), 991 get_fs_long(args+2))); 992 case SYS_LISTEN: 993 er=verify_area(VERIFY_READ, args, 2 * sizeof(long)); 994 if(er) 995 return er; 996 return(sock_listen(get_fs_long(args+0), 997 get_fs_long(args+1))); 998 case SYS_ACCEPT: 999 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 1000 if(er) 1001 return er; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1002 return(sock_accept(get_fs_long(args+0), 1003 (struct sockaddr *)get_fs_long(args+1), 1004 (int *)get_fs_long(args+2))); 1005 case SYS_GETSOCKNAME: 1006 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 1007 if(er) 1008 return er; 1009 return(sock_getsockname(get_fs_long(args+0), 1010 (struct sockaddr *)get_fs_long(args+1), 1011 (int *)get_fs_long(args+2))); 1012 case SYS_GETPEERNAME: 1013 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); 1014 if(er) 1015 return er; 1016 return(sock_getpeername(get_fs_long(args+0), 1017 (struct sockaddr *)get_fs_long(args+1), 1018 (int *)get_fs_long(args+2))); 1019 case SYS_SOCKETPAIR: 1020 er=verify_area(VERIFY_READ, args, 4 * sizeof(long)); 1021 if(er) 1022 return er; 1023 return(sock_socketpair(get_fs_long(args+0), 1024 get_fs_long(args+1), 1025 get_fs_long(args+2), 1026 (unsigned long *)get_fs_long(args+3))); 1027 case SYS_SEND: 1028 er=verify_area(VERIFY_READ, args, 4 * sizeof(unsigned long)); 1029 if(er) 1030 return er; 1031 return(sock_send(get_fs_long(args+0), 1032 (void *)get_fs_long(args+1), 1033 get_fs_long(args+2), 1034 get_fs_long(args+3))); 1035 case SYS_SENDTO: 1036 er=verify_area(VERIFY_READ, args, 6 * sizeof(unsigned long)); 1037 if(er) 1038 return er; 1039 return(sock_sendto(get_fs_long(args+0), 1040 (void *)get_fs_long(args+1), 1041 get_fs_long(args+2), 1042 get_fs_long(args+3), 1043 (struct sockaddr *)get_fs_long(args+4), 1044 get_fs_long(args+5))); 1045 case SYS_RECV: 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1046 er=verify_area(VERIFY_READ, args, 4 * sizeof(unsigned long)); 1047 if(er) 1048 return er; 1049 return(sock_recv(get_fs_long(args+0), 1050 (void *)get_fs_long(args+1), 1051 get_fs_long(args+2), 1052 get_fs_long(args+3))); 1053 case SYS_RECVFROM: 1054 er=verify_area(VERIFY_READ, args, 6 * sizeof(unsigned long)); 1055 if(er) 1056 return er; 1057 return(sock_recvfrom(get_fs_long(args+0), 1058 (void *)get_fs_long(args+1), 1059 get_fs_long(args+2), 1060 get_fs_long(args+3), 1061 (struct sockaddr *)get_fs_long(args+4), 1062 (int *)get_fs_long(args+5))); 1063 case SYS_SHUTDOWN: 1064 er=verify_area(VERIFY_READ, args, 2* sizeof(unsigned long)); 1065 if(er) 1066 return er; 1067 return(sock_shutdown(get_fs_long(args+0), 1068 get_fs_long(args+1))); 1069 case SYS_SETSOCKOPT: 1070 er=verify_area(VERIFY_READ, args, 5*sizeof(unsigned long)); 1071 if(er) 1072 return er; 1073 return(sock_setsockopt(get_fs_long(args+0), 1074 get_fs_long(args+1), 1075 get_fs_long(args+2), 1076 (char *)get_fs_long(args+3), 1077 get_fs_long(args+4))); 1078 case SYS_GETSOCKOPT: 1079 er=verify_area(VERIFY_READ, args, 5*sizeof(unsigned long)); 1080 if(er) 1081 return er; 1082 return(sock_getsockopt(get_fs_long(args+0), 1083 get_fs_long(args+1), 1084 get_fs_long(args+2), 1085 (char *)get_fs_long(args+3), 1086 (int *)get_fs_long(args+4))); 1087 default: 1088 return(-EINVAL); 1089 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1090 } 本函数是网络栈专用操作函数集的总入口函数。该函数功能单一,主要是将请求分配,调用具 体的底层函数进行处理。其中参数 call 表示具体的请求,args 为相应请求所需要提供的信息。 这些信息是与应用程序接口函数中定义的参数一一对应的。 1091 /* 1092 * This function is called by a protocol handler that wants to 1093 * advertise its address family, and have it linked into the 1094 * SOCKET module. 1095 */ 1096 int sock_register(int family, struct proto_ops *ops) 1097 { 1098 int i; 1099 cli(); 1100 for(i = 0; i < NPROTO; i++) 1101 { //如果 pops[i]==NULL,则表示对应表项闲置,即可用,否则继续检查下一个表项。 1102 if (pops[i] != NULL) 1103 continue; //找到一个闲置表项后,用输入的参数对其进行初始化即完成操作函数集的注册。 //从下文 sock_unregister 函数的实现来看,操作函数集合的注销是通过 family 字段 //匹配的。 1104 pops[i] = ops; 1105 pops[i]->family = family; 1106 sti(); 1107 return(i); 1108 } 1109 sti(); 1110 return(-ENOMEM); 1111 } 1112 /* 1113 * This function is called by a protocol handler that wants to 1114 * remove its address family, and have it unlinked from the 1115 * SOCKET module. 1116 */ 1117 int sock_unregister(int family) 1118 { 1119 int i; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1120 cli(); 1121 for(i = 0; i < NPROTO; i++) 1122 { //如果是一个闲置表项,则显然不需要注销。 1123 if (pops[i] == NULL) 1124 continue; //发现一个非空闲表项,检查其 family 字段值,如果和传入的参数相等,则满足 //条件,将该表项重新设置为空闲(即完成注销操作)。 1125 if(pops[i]->family == family) 1126 { 1127 pops[i]=NULL; 1128 sti(); 1129 return(i); 1130 } 1131 } 1132 sti(); 1133 return(-ENOENT); 1134 } sock_register 和 sock_unregister 用于注册和注销一个域操作集。sock_register 函数首先在 pops 数 组中寻找到一个空闲元素,然后根据传入的参数初始化该元素即完成域操作集的注册。 sock_unregister 函数完成相应操作函数集的注销,即设置对应表项重新为空闲状态即可。 1135 void proto_init(void) 1136 { 1137 extern struct net_proto protocols[]; /* Network protocols */ 1138 struct net_proto *pro; 1139 /* Kick all configured protocols. */ 1140 pro = protocols; 1141 while (pro->name != NULL) 1142 { 1143 (*pro->init_func)(pro); 1144 pro++; 1145 } 1146 /* We're all done... */ 1147 } proto_init 函数完成相关协议初始化操作,protocols 数组定义在 protocols.c 文件中,其中有 IPX, 802.2,SNAP,AX.25 等协议的初始化函数,本函数即依次遍历该数据,调用每个初始化函数 完成对应协议的初始化工作。 1148 void sock_init(void) 1149 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1150 int i; 1151 printk("Swansea University Computer Society NET3.019\n"); 1152 /* 1153 * Initialize all address (protocol) families. 1154 */ //对 pops 数组进行清空处理。为下面对该数组的初始化作准备。 1155 for (i = 0; i < NPROTO; ++i) pops[i] = NULL; 1156 /* 1157 * Initialize the protocols module. 1158 */ 1159 proto_init(); 1160 #ifdef CONFIG_NET 1161 /* 1162 * Initialize the DEV module. 1163 */ 1164 dev_init(); 1165 /* 1166 * And the bottom half handler 1167 */ 1168 bh_base[NET_BH].routine= net_bh; 1169 enable_bh(NET_BH); 1170 #endif 1171 } 本函数在 start_kernel 函数中被调用,用于整个网络栈的初始化操作。该函数主要完成相关协议 初始化(proto_init),部分网卡驱动程序初始化(dev_init, net/inet/dev.c),用于网络栈操作的下半 部分(Bottom Half)初始化。下半部分的初始化主要是对 bh_base 数组 NET_BH 对应表项函数 指针进行了赋值(net_bh 函数,net/inet/dev.c)以及使能该下半部分。有关下半部分的功能及相 关结构的介绍请参考第一章中相关内容。 socket.c 文件最后定义了一个信息获取函数 socket_get_info。此处指的信息即系统当前使用的套 接字总数目(sockets_in_use,这是定义在本文件头部一个变量,用于表示系统正在使用的套接 字总数量)。 1172 int socket_get_info(char *buffer, char **start, off_t offset, int length) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1173 { 1174 int len = sprintf(buffer, "sockets: used %d\n", sockets_in_use); 1175 if (offset >= len) 1176 { 1177 *start = buffer; 1178 return 0; 1179 } 1180 *start = buffer + offset; 1181 len -= offset; 1182 if (len > length) 1183 len = length; 1184 return len; 1185 } socket.c 文件小结 socket.c 文件中定义的函数作为 BSD 层的函数集,对于 INET 正常网络操作而言,其下层即对 应 inet/af_inet.c 文件中定义的函数。而且这种对应关系几乎是一一对应的。从对 socket.c 文件中 函数实现来看,绝大部分函数并没有进行具体所请求功能的实现,而是将请求传给了下一层函 数,这个下一层函数即指 af_inet.c 文件中定义的函数。下文将对该文件进行分析,不过从对 af_inet.c 文件的分析来看,其中定义的函数实际上也没有进行具体功能的实现,而是将请求继 续传递给下一层(此时即传输层)。这是可以理解的,因为对于大部分操作,根据传输层所使用 协议的不同,具体对应的操作也不同,所以请求不传递到这一层,上层函数无法进行具体的处 理。而上层函数(socket.c, af_inet.c 文件中定义的函数均是作为上层函数而言)主要是对一些标 志位进行检查后即调用下层函数进行处理。 2.3 net/inet/af_inet.c 文件 该文件定义的函数集作为网络栈中 INET 层而存在。之所以称为 INET 层,一是因为 af_inet.c 文件中几乎所有的函数均是以 inet 这四个字符打头;二是该文件中定义的函数作为 INET 域表 示层的操作接口而存在。我们可以将该文件定义之函数集合作为 L5 层(也即表示层)的实现。 以下将对该文件中定义函数一一进行分析。读者可以发现,作为正常的网络栈实现,该文件中 定义函数是作为 socket.c 文件中定义函数的下层函数而存在的。如 socket.c 文件中 sock_bind 对 应 af_inet.c 中 inet_bind, sock_accept 对应 inet_accept,sock_connect 对应 inet_connect 等等。 由于该文件函数在进一步调用下层函数时需要都需要使用到 sock 结构,故首先有必须对此结构 进行以下说明。由于在 L5 层被大量使用,所以将 sock 结构作为一个套接字连接在 L5 层表示 (socket 结构作为 L6 层对于一个套接字连接的表示),当然从实际网络栈的实现来看,sock 结 构的使用范围相比 socket 结构要广泛的多,socket 结构主要使用在 L6 层,L5 层作为 L6 层下层, 也会有部分地方使用到 socket 结构,但很少。但 sock 结构的使用基本贯穿 L2,L3,L4,L5 层, 而且是作为各层的之间的一个联系。这主要是因为无论是发送的还是接收的数据包都要被缓存 到 sock 结构中的缓冲队列中。在下面对 sock 结构的介绍中也可以看出这一点。由于 sock 结构 相当庞大,故对其进行分段分析。 /*net/inet/sock.h*/ /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 * This structure really needs to be cleaned up. * Most of it is for TCP, and not used by any of * the other protocols. */ struct sock { struct options *opt; //IP 选项缓存于此处。 volatile unsigned long wmem_alloc; //当前写缓冲区大小,该值不可大于系统规定的最大值。 volatile unsigned long rmem_alloc; //当前读缓冲区大小,该值不可大于系统规定最大值。 //以下三个字段用于 TCP 协议中为保证可靠数据传输而使用的序列号。 //其中 write_seq 表示应用程序下一次写数据时所对应的第一个字节的序列号。 //sent_seq 表示本地将要发送的下一个数据包中第一个字节对应的序列号。该字段对应//TCP 首部中序列号字段,表示本数据包中所包含数据第一个字节的序列号。 //acked_seq 表示本地希望从远端接收的下一个数据的序列号。该字段对应 TCP 首部中应//答 序列号字段。 //有关 TCP 协议为保证可靠性数据传输底层实现方法的论述请参考附录一,其中对序列号 //的使用的阐述较为详细,读者可以借此理解 sock 结构中这几个字段的意义。 unsigned long write_seq; unsigned long sent_seq; unsigned long acked_seq; /* 应用程序有待读取(但尚未读取)数据的第一个序列号。*/ unsigned long copied_seq; //rcv_ack_seq 表示目前本地接收到的对本地发送数据的应答序列号。如 acked_seq=12345, //则表示本地之前发送的序列号小于 12345 的所有数据已被远端成功接收。 unsigned long rcv_ack_seq; //窗口大小,是一个绝对值,表示本地将要发送数据包中所包含最后一个数据的序列号不//可 大于 window_seq. window_seq 的初始化为 sent_seq 加上远端当前同胞的窗口大小,这//个窗口 大小是在 TCP 首部中窗口字段指定的。 unsigned long window_seq; //该字段在对方发送 FIN 数据包时使用,在接收到远端发送的 FIN 数据包后,fin_seq 被 //初始化为对方的 FIN 数据包最后一个字节的序列号加 1,表示本地对对方此 FIN 数据包//进 行应答的序列号。 unsigned long fin_seq; //以下两个字段用于紧急数据处理,urg_seq 表示紧急数据最大序列号。urg_data 是一个标//志 位,当设置为 1 时,表示接收到紧急数据。 unsigned long urg_seq; unsigned long urg_data; /* * Not all are volatile, but some are, so we * might as well say they all are. */ volatile char inuse, //inuse=1 表示其它进程正在使用该 sock 结构,本进程需等待。 dead, //dead=1表示该 sock 结构已处于释放状态。 urginline, //urginline=1 表示紧急数据将被当作普通数据处理。 intr, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 blog, //blog=1 表示对应套接字处于节制状态,此时接收的数据包均被丢弃 done, reuse, keepopen,//keepopen=1 表示使用保活定时器 linger,//linger=1表示在关闭套接字时需要等待一段时间以确认其已关闭。 delay_acks, //delay_acks=1 表示延迟应答,可一次对多个数据包进行应答 destroy, //destroy=1表示该 sock 结构等待销毁 ack_timed, no_check, zapped, /* In ax25 & ipx means not linked */ broadcast, nonagle; //noagle=1 表示不使用 NAGLE 算法,Nagle 算法指每次最多只有// 一个数据包未被应答,换句话说, //在前一个发送的数据包被应答之前, //不可再继续发送其它数据包。 //lingertime 字段表示等待关闭操作的时间,只有当 linger 标志位为 1 时,该字段才有意义。 unsigned long lingertime; int proc; //该 sock 结构(即该套接字)所属的进程的进程号。 //以下三个字段用于 sock 的连接。 struct sock *next; struct sock *prev; /* Doubly linked chain.. */ struct sock *pair; //send_head, send_tail 用于 TCP 协议重发队列。 struct sk_buff * volatile send_head; struct sk_buff * volatile send_tail; //back_log 为接收的数据包缓存队列。 struct sk_buff_head back_log; //partial 字段用于创建最大长度的待发送数据包。 struct sk_buff *partial; //partial_timer 定时器用于按时发送 partial 指针指向的数据包,以免缓存(等待)时间过长。 struct timer_list partial_timer; //重发次数 long retransmits; //write_queue 指向待发送数据包,其与 send_head,send_tail 队列的不同之处在于 //send_head,send_tail 队列中数据包均已经发送出去,但尚未接收到应答。而 write_queue //中数据包尚未发送。 //receive-queue 为读队列,其不同于 back_log 队列之处在于 back_log 队列缓存从网络层传 //上来的数据包,在用户进行读取操作时,不可操作 back_log 队列,而是从 receive_queue //队列中去数据包读取其中的数据,即数据包首先缓存在 back_log 队列中,然后从 back_log //队列中移动到 receive_queue 队列中方可被应用程序读取。而并非所有 back_log 队列中缓 //存的数据包都可以成功的被移动到 receive_queue 队列中,如果此刻读缓存区太小,则当 //前从 back_log 队列中被取下的被处理的数据包将被直接丢弃,而不会被缓存到 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 //receive_queue 队列中。如果从应答的角度看,在 back_log 队列中的数据包由于有可能被 //丢弃,故尚未应答,而将一个数据包从 back_log 移动到 receive_queue 时,表示该数据包 //已被正式接收,即会发送对该数据包的应答给远端表示本地已经成功接收该数据包。 struct sk_buff_head write_queue, receive_queue; struct proto *prot; //该字段十分重要,指向传输层处理函数集。 struct wait_queue **sleep; unsigned long daddr; //sock 结构所代表套接字的远端地址。 unsigned long saddr; //本地地址 unsigned short max_unacked; //最大未处理请求连接数(应答数) unsigned short window; //远端窗口大小 unsigned short bytes_rcv; //已接收字节总数 /* mss is min(mtu, max_window) */ //最大传输单元 unsigned short mtu; /* mss negotiated in the syn's */ //最大报文长度:MSS=MTU-IP 首部长度-TCP 首部长度 volatile unsigned short mss; /* current eff. mss - can change */ //用户指定的 MSS 值 volatile unsigned short user_mss; /* mss requested by user in ioctl */ //最大窗口大小和窗口大小钳制值 volatile unsigned short max_window; unsigned long window_clamp; //本地端口号 unsigned short num; //以下三个字段用于拥塞算法 volatile unsigned short cong_window; volatile unsigned short cong_count; volatile unsigned short ssthresh; //本地已发送出去但尚未得到应答的数据包数目 volatile unsigned short packets_out; //本地关闭标志位,用于半关闭操作 volatile unsigned short shutdown; //往返时间估计值 volatile unsigned long rtt; //RTTS volatile unsigned long mdev; //mean deviation, 即 RTTD, 绝对偏差 //RTO是用 RTT 和 mdev 用算法计算出的延迟时间值。 volatile unsigned long rto; //RTO /* currently backoff isn't used, but I'm maintaining it in case * we want to go back to a backoff formula that needs it */ volatile unsigned short backoff; //退避算法度量值 volatile short err; //错误标志值 unsigned char protocol; //传输层协议值 volatile unsigned char state; //套接字状态值,如 TCP_ESTABLISHED 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 volatile unsigned char ack_backlog; //缓存的未应答数据包个数 unsigned char max_ack_backlog; //最大缓存的未应答数据包个数 unsigned char priority; //该套接字优先级,在硬件缓存发送数据包时使用 unsigned char debug; //这两个字段的初始化将下文中对 inet_create 函数的分析。 unsigned short rcvbuf; //最大接收缓冲区大小 unsigned short sndbuf; //最大发送缓冲区大小 unsigned short type; //类型值如 SOCK_STREAM //localroute=1 表示只使用本地路由,一般目的端在相同子网时使用。 unsigned char localroute; /* Route locally only */ //此处暂时省略有关 IPX,AX.25,Appletalk 协议相关字段的声明。 #ifdef CONFIG_IPX …… #endif #ifdef CONFIG_AX25 ……. #endif #ifdef CONFIG_ATALK …… #endif /* IP 'private area' or will be eventually */ //IP首部 TTL 字段值,实际上表示路由器跳数。 int ip_ttl; /* TTL setting */ //IP首部 TOS 字段值,服务类型值。 int ip_tos; /* TOS */ //缓存的 TCP 首部,在 TCP 协议中创建一个发送数据包时可以利用此字段快速创建 TCP //首部。 struct tcphdr dummy_th; //保活定时器,用于探测对方窗口大小,防止对方通报窗口大小的数据包丢弃,从而造成 //本地发送通道被阻塞。 struct timer_list keepalive_timer; /* TCP keepalive hack */ //重发定时器,用于数据包超时重发。 struct timer_list retransmit_timer; /* TCP retransmit timer */ //延迟应答定时器,延迟应答可以减少应答数据包的个数,但不可无限延迟以免造成远端 //重发,所以设置定时器定期发送应答数据包。 struct timer_list ack_timer; /* TCP delayed ack timer */ //该字段为标志位组合字段,用于表示下文中 timer 定时器超时的原因。 int ip_xmit_timeout; /* Why the timeout is running */ //如下 4 个字段用于 IP 多播。 #ifdef CONFIG_IP_MULTICAST int ip_mc_ttl; /* Multicasting TTL */ int ip_mc_loop; /* Loopback (not implemented yet) */ char ip_mc_name[MAX_ADDR_LEN]; /* Multicast device name */ struct ip_mc_socklist *ip_mc_list; /* Group array */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 #endif //以下两个字段用于通用定时,timeout 表示定时时间值,ip_xmit_timeout 表示此次定时的 //原因,timer 为定时器。 /* This part is used for the timeout functions (timer.c). */ int timeout; /* What are we waiting for? */ struct timer_list timer; /* This is the TIME_WAIT/receive timer when we are doing IP */ //时间戳 struct timeval stamp; /* identd */ //一个套接字在不同的层次上分别由 socket 结构和 sock 结构表示,该 socket 字段用于指向// 其对应的 socket 结构以便对某些信息的快速获取。同理在 socket 结构中也有指向 sock //结构的指针。 struct socket *socket; //以下四个函数指针字段指向回调函数。这些字段的设置为自定义回调函数提供的很大的 //灵活性,内核在发生某些时间时,会调用这些函数,如此可以实现自定义响应。目前这 //种自定义响应还是完全有内核控制。 /* 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); }; 在介绍完 sock 结构之后,下面我们开始对 af_inet.c 文件分析,前文中已说明该文件中定义函数 也是作为接口函数而存在的。其绝大部分函数实现均是调用传输层函数(sock 结构中 prot 字段 指向的函数集)完成具体的功能,因为诚如前文所述,对于不同的传输层协议完成一个请求的 操作方式是很不同的,所以必须将请求传递到传输层方可进行具体的处理,所以 af_inet.c 中定 义的函数集作为表示层操作所做的工作也仅限于对某些字段进行检查后,向下层继续传递请求。 故可以预见 af_inet.c 文件中大部分函数实现都较为简单,而诸如 tcp.c,udp.c 文件中定义的函数 (传输层函数集合)都较为复杂庞大。 /*net/inet/af_inet.c*/ 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * AF_INET protocol family socket handler. 7 * 8 * Version: @(#)af_inet.c (from sock.c) 1.0.17 06/02/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * Florian La Roche, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 13 * Alan Cox, 14 * 15 * Changes (see also sock.c) 16 * 17 * A.N.Kuznetsov : Socket death error in accept(). 18 * John Richardson : Fix non blocking error in connect() 19 * so sockets that fail to connect 20 * don't return -EINPROGRESS. 21 * Alan Cox : Asynchronous I/O support 22 * Alan Cox : Keep correct socket pointer on sock structures 23 * when accept() ed 24 * Alan Cox : Semantics of SO_LINGER aren't state moved 25 * to close when you look carefully. With 26 * this fixed and the accept bug fixed 27 * some RPC stuff seems happier. 28 * Niibe Yutaka : 4.4BSD style write async I/O 29 * Alan Cox, 30 * Tony Gale : Fixed reuse semantics. 31 * Alan Cox : bind() shouldn't abort existing but dead 32 * sockets. Stops FTP netin:.. I hope. 33 * Alan Cox : bind() works correctly for RAW sockets. Note 34 * that FreeBSD at least is broken in this respect 35 * so be careful with compatibility tests... 36 * 37 * This program is free software; you can redistribute it and/or 38 * modify it under the terms of the GNU General Public License 39 * as published by the Free Software Foundation; either version 40 * 2 of the License, or (at your option) any later version. 41 */ 42 #include 43 #include 44 #include 45 #include 46 #include 47 #include 48 #include 49 #include 50 #include 51 #include 52 #include 53 #include 54 #include 55 #include 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 56 #include 57 #include 58 #include 59 #include 60 #include 61 #include "ip.h" 62 #include "protocol.h" 63 #include "arp.h" 64 #include "rarp.h" 65 #include "route.h" 66 #include "tcp.h" 67 #include "udp.h" 68 #include 69 #include "sock.h" 70 #include "raw.h" 71 #include "icmp.h" 72 #define min(a,b) ((a)<(b)?(a):(b)) 73 extern struct proto packet_prot; 以上是对文件中头文件的声明,另外是一个宏定义用于取两数中较小者。此后是对 packet_prot 变量的一个声明,注意 extern 关键字,该关键字表示 packet_prot 变量声明在其它文件中,此处 只是引用该变量。 74 /* 75 * See if a socket number is in use. 76 */ 77 static int sk_inuse(struct proto *prot, int num) 78 { 79 struct sock *sk; 80 for(sk = prot->sock_array[num & (SOCK_ARRAY_SIZE -1 )]; 81 sk != NULL; sk=sk->next) 82 { 83 if (sk->num == num) 84 return(1); 85 } 86 return(0); 87 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 sk_inuse 函数用于检测一个端口号是否已被使用。注意到 proto 结构是表示传输层操作函数集的 一个结构,该结构也是定义在 net/inet/sock.h 文件中。其完整定义如下所示。 /*net/inet/sock.h*/ 187 struct proto { 188 struct sk_buff * (*wmalloc)(struct sock *sk, 189 unsigned long size, int force, 190 int priority); 191 struct sk_buff * (*rmalloc)(struct sock *sk, 192 unsigned long size, int force, 193 int priority); 194 void (*wfree)(struct sock *sk, struct sk_buff *skb, 195 unsigned long size); 196 void (*rfree)(struct sock *sk, struct sk_buff *skb, 197 unsigned long size); 198 unsigned long (*rspace)(struct sock *sk); 199 unsigned long (*wspace)(struct sock *sk); 200 void (*close)(struct sock *sk, int timeout); 201 int (*read)(struct sock *sk, unsigned char *to, 202 int len, int nonblock, unsigned flags); 203 int (*write)(struct sock *sk, unsigned char *to, 204 int len, int nonblock, unsigned flags); 205 int (*sendto)(struct sock *sk, 206 unsigned char *from, int len, int noblock, 207 unsigned flags, struct sockaddr_in *usin, 208 int addr_len); 209 int (*recvfrom)(struct sock *sk, 210 unsigned char *from, int len, int noblock, 211 unsigned flags, struct sockaddr_in *usin, 212 int *addr_len); 213 int (*build_header)(struct sk_buff *skb, 214 unsigned long saddr, 215 unsigned long daddr, 216 struct device **dev, int type, 217 struct options *opt, int len, int tos, int ttl); 218 int (*connect)(struct sock *sk, 219 struct sockaddr_in *usin, int addr_len); 220 struct sock * (*accept) (struct sock *sk, int flags); 221 void (*queue_xmit)(struct sock *sk, 222 struct device *dev, struct sk_buff *skb, 223 int free); 224 void (*retransmit)(struct sock *sk, int all); 225 void (*write_wakeup)(struct sock *sk); 226 void (*read_wakeup)(struct sock *sk); 227 int (*rcv)(struct sk_buff *buff, struct device *dev, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 228 struct options *opt, unsigned long daddr, 229 unsigned short len, unsigned long saddr, 230 int redo, struct inet_protocol *protocol); 231 int (*select)(struct sock *sk, int which, 232 select_table *wait); 233 int (*ioctl)(struct sock *sk, int cmd, 234 unsigned long arg); 235 int (*init)(struct sock *sk); 236 void (*shutdown)(struct sock *sk, int how); 237 int (*setsockopt)(struct sock *sk, int level, int optname, 238 char *optval, int optlen); 239 int (*getsockopt)(struct sock *sk, int level, int optname, 240 char *optval, int *option); 241 unsigned short max_header; 242 unsigned long retransmits; 243 struct sock * sock_array[SOCK_ARRAY_SIZE]; 244 char name[80]; 245 int inuse, highestinuse; 246 }; proto 结构声明用于传输层操作的一系列函数指针,以及几个辅助字段。对于每个传输层协议都 有一个 proto 结构对应,如 TCP 协议的 tcp_proto, UDP 协议的 udp_proto。 这些 proto 结构变量都初始化为各自协议对应的具体操作。从 proto 结构的字段名称可以看出, 很多函数作为上层函数调用的对象将完成具体的功能,如 connect 指针指向的函数将实际完成连 接操作,而 socket.c 文件中 sock_connect, 到 af_inet.c 文件中的 inet_connect 最后都归结于对此 处 connect 指针所指向函数的调用。如对于 TCP 协议该指针字段对应值为 tcp_connect, 则调用 队列如下: 应用程序调用 connect 函数,connect 函数的内核处理函数为 sys_socket 入口函数,sys_socket 入口函数调用对应的 sock_connect 函数,sock_connect 调用 inet_connect, inet_connect 调用 tcp_connect, tcp_connect 根据 TCP 协议的规范完成具体的连接操作(即进行三路握手连接)。 从这个调用过程可以看出,直到 tcp_connect 函数才进行实际的处理。而之前的函数均进行相应 层次的检查工作。当然有一点需要指出的是,上面的调用过程并非直接调用,如 sock_connect 并非直接调用 inet_connect 函数,而是通过函数指针,即 socket->ops->connect 指针指向的函数 (此处 socket 代表一个 socket 结构变量),对于 INET 域而言,这个指针即指向 inet_connect 函 数,而对于 UNIX 域,则指向 unix_connect。这种使用函数指针的方式可以有效地实现灵活性和 进行层次之间的隔离。另外 inet_connect 函数对 tcp_connect 此类函数的调用也是通过函数指针 的方式即:sock->proto->connect 指针指向的函数(同理,sock 表示一个 sock 结构变量),所以 对于 UDP 协议,这个函数指针就指向了 udp_connect, 从而避免了硬编码方式调用函数,也实现 了内核程序的灵活性。在下文的分析中,读者可以大量看到 af_inet.c 文件中函数使用像 sock->proto->connect 这样的调用方式将请求传递给传输层处理。 刚才说到,对于每个传输层协议都有一个 proto 结构对应,并且只有一个 proto 结构对应,如 TCP 协议对应 tcp_proto 变量,UDP 对应 udp_proto。从这种意义上说,tcp_proto, udp_proto 分别是 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 TCP 协议,UDP 协议各自的代表。所以在很多函数中直接传入一个 proto 结构表示对应使用的 协议。 如果同时有多个套接字使用 TCP 协议,就会有多个 sock 结构使用 tcp_proto 变量中字段指向的 函数。而内核对于这种情况的管理是通过在 proto 结构定义一个 sock_array 数组来实现的,所有 使用该种协议(如 TCP)协议的套接字(对应的 sock 结构)均被插入到 sock_array 数组中元素指 向的链表中。sock_array 含有 256 个元素,即同时可以存在 256 个链表。至于一个 sock 结构插 入到那个元素指向的链表是由该套接字所使用的本地端口号进行索引的。由于只有 256 个链表, 而端口号范围可以达到 65535,所以需要使用求模(%)操作。这样所有使用 TCP 协议的套接 字所对应的 sock 结构均被插入到 tcp_proto 变量之 sock_array 数组元素所指向的链表中。 如此 sk_inuse 函数中 for 循环的作用也就很好理解了。用端口号寻址对应 proto 结构中 sock_array 所对应元素所指向的队列,然后遍历该队列中所有 sock 结构,检查 sock 结构 num 字段与参数 num 值是否相同,如果都不同,则表示参数 num 表示的端口号并未使用,可以分批给新的套接 字使用。由此亦可看出,对于不同的协议使用相同的端口号不会引起任何问题。 88 /* 89 * Pick a new socket number 90 */ 91 unsigned short get_new_socknum(struct proto *prot, unsigned short base) 92 { 93 static int start=0; 94 /* 95 * Used to cycle through the port numbers so the 96 * chances of a confused connection drop. 97 */ 98 int i, j; 99 int best = 0; 100 int size = 32767; /* a big num. */ 101 struct sock *sk; 102 if (base == 0) 103 base = PROT_SOCK+1+(start % 1024); 104 if (base <= PROT_SOCK) 105 { 106 base += PROT_SOCK+(start % 1024); 107 } 108 /* Now look through the entire array and try to find an empty ptr. */ 109 for(i=0; i < SOCK_ARRAY_SIZE; i++) 110 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 111 j = 0; 112 sk = prot->sock_array[(i+base+1) &(SOCK_ARRAY_SIZE -1)]; 113 while(sk != NULL) 114 { 115 sk = sk->next; 116 j++; 117 } 118 if (j == 0) 119 { 120 start =(i+1+start )%1024; 121 return(i+base+1); 122 } 123 if (j < size) 124 { 125 best = i; 126 size = j; 127 } 128 } 129 /* Now make sure the one we want is not in use. */ 130 while(sk_inuse(prot, base +best+1)) 131 { 132 best += SOCK_ARRAY_SIZE; 133 } 134 return(best+base+1); 135 } get_new_socknum 用于获取一个新的未使用端口号。参数 prot 表示所使用的协议,base 表示最 小起始端口号。该函数实现的基本思想是: 1>首先检查 sock_array 数组中具有最少 sock 结构的链表所对应表项。 2>从<1>中结果计算出一个端口号。计算方法如下。 假设由步骤<1>获取的表项索引为 N(从 0 计数)。则表示分配的端口号 n 满足: n% SOCK_ARRAY_SIZE=N 即 n=N+m* SOCK_ARRAY_SIZE,(m=0,1,2,……,1024num = num; 146 sk->next = NULL; 147 num = num &(SOCK_ARRAY_SIZE -1); 148 /* We can't have an interrupt re-enter here. */ 149 save_flags(flags); 150 cli(); 151 sk->prot->inuse += 1; 152 if (sk->prot->highestinuse < sk->prot->inuse) 153 sk->prot->highestinuse = sk->prot->inuse; 154 if (sk->prot->sock_array[num] == NULL) 155 { 156 sk->prot->sock_array[num] = sk; 157 restore_flags(flags); 158 return; 159 } 160 restore_flags(flags); //这个 for 语句用于估计本地地址子网反掩码。 161 for(mask = 0xff000000; mask != 0xffffffff; mask = (mask >> 8) | mask) 162 { 163 if ((mask & sk->saddr) && 164 (mask & sk->saddr) != (mask & 0xffffffff)) 165 { 166 mask = mask << 8; 167 break; 168 } 169 } 170 cli(); 171 sk1 = sk->prot->sock_array[num]; 172 for(sk2 = sk1; sk2 != NULL; sk2=sk2->next) 173 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 174 if (!(sk2->saddr & mask)) 175 { 176 if (sk2 == sk1) 177 { 178 sk->next = sk->prot->sock_array[num]; 179 sk->prot->sock_array[num] = sk; 180 sti(); 181 return; 182 } 183 sk->next = sk2; 184 sk1->next= sk; 185 sti(); 186 return; 187 } 188 sk1 = sk2; 189 } 190 /* Goes at the end. */ 191 sk->next = NULL; 192 sk1->next = sk; 193 sti(); 194 } put_sock 函数用于将一个新 sock 结构插入到对应的链表中。该链表有端口号在 sock->prot->sock_array 数组中进行寻址。实现中使用了本地地址掩码进行了地址排列。但实际 上这种排列毫无必要!因为这不是路由表,这种排列方式并没有在其它地方得到利用。所以实 际上该函数可以实现的相当简单,而现在弄得比较复杂。不过既然代码这样编写,此处还是有 必要进行一下说明。函数中实现的插入顺序有效位(即非零位)从多到少排列的,如 10.16.1.23 排在 10.16.1.0 之前,10.16.1.0 排在 10.16.0.0 之前,依次类推。 195 /* 196 * Remove a socket from the socket tables. 197 */ 198 static void remove_sock(struct sock *sk1) 199 { 200 struct sock *sk2; 201 unsigned long flags; 202 if (!sk1->prot) 203 { 204 printk("sock.c: remove_sock: sk1->prot == NULL\n"); 205 return; 206 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 207 /* We can't have this changing out from under us. */ 208 save_flags(flags); 209 cli(); 210 sk2 = sk1->prot->sock_array[sk1->num &(SOCK_ARRAY_SIZE -1)]; 211 if (sk2 == sk1) 212 { 213 sk1->prot->inuse -= 1; 214 sk1->prot->sock_array[sk1->num &(SOCK_ARRAY_SIZE -1)] = sk1->next; 215 restore_flags(flags); 216 return; 217 } 218 while(sk2 && sk2->next != sk1) 219 { 220 sk2 = sk2->next; 221 } 222 if (sk2) 223 { 224 sk1->prot->inuse -= 1; 225 sk2->next = sk1->next; 226 restore_flags(flags); 227 return; 228 } 229 restore_flags(flags); 230 } remove_sock 函数用于从链表中删除一个指定端口号的 sock 结构。该函数实现较为简单,此处 不再作说明。 如下 destroy_sock 函数较长,但实现的功能并不复杂,该函数用于销毁一个套接字连接,包括 该套接字对应的 sock 结构。而在销毁之前需要对其中缓存的数据包进行释放操作以避免造成内 存泄漏。 231 /* 232 * Destroy an AF_INET socket 233 */ 234 void destroy_sock(struct sock *sk) 235 { 236 struct sk_buff *skb; 237 sk->inuse = 1; /* just to be safe. */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 //检查 dead 标志位,对于销毁的 sock 结构,其 dead 字段必须首先设置为 1。 238 /* In case it's sleeping somewhere. */ 239 if (!sk->dead) 240 sk->write_space(sk); //remove_sock 函数用于将 sock 结构从其对应链表中删除。 241 remove_sock(sk); //将 sock 结构中各定时器从相应链表中删除。 242 /* Now we can no longer get new packets. */ 243 delete_timer(sk); 244 /* Nor send them */ 245 del_timer(&sk->retransmit_timer); //释放 partial 指针指向的数据包。该 partial 指针用于创建大容量数据包(即使用最大 //MTU 值创建的数据包,以便减少数据包的数量。此处的操作是释放该数据包。 246 while ((skb = tcp_dequeue_partial(sk)) != NULL) { 247 IS_SKB(skb); 248 kfree_skb(skb, FREE_WRITE); 249 } //清空写队列。 250 /* Cleanup up the write buffer. */ 251 while((skb = skb_dequeue(&sk->write_queue)) != NULL) { 252 IS_SKB(skb); 253 kfree_skb(skb, FREE_WRITE); 254 } 255 /* 256 * Don't discard received data until the user side kills its 257 * half of the socket. 258 */ 259 if (sk->dead) 260 { //清空已缓存的待读取数据包。 261 while((skb=skb_dequeue(&sk->receive_queue))!=NULL) 262 { 263 /* 264 * This will take care of closing sockets that were 265 * listening and didn't accept everything. 266 */ 267 if (skb->sk != NULL && skb->sk != sk) 268 { 269 IS_SKB(skb); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 270 skb->sk->dead = 1; 271 skb->sk->prot->close(skb->sk, 0); 272 } 273 IS_SKB(skb); 274 kfree_skb(skb, FREE_READ); 275 } 276 } 277 /* Now we need to clean up the send head. */ 278 cli(); //清空重发队列中缓存的数据包。 279 for(skb = sk->send_head; skb != NULL; ) 280 { 281 struct sk_buff *skb2; 282 /* 283 * We need to remove skb from the transmit queue, 284 * or maybe the arp queue. 285 */ 286 if (skb->next && skb->prev) { 287 /* printk("destroy_sock: unlinked skb\n");*/ 288 IS_SKB(skb); 289 skb_unlink(skb); 290 } 291 skb->dev = NULL; 292 skb2 = skb->link3; 293 kfree_skb(skb, FREE_WRITE); 294 skb = skb2; 295 } 296 sk->send_head = NULL; 297 sti(); //清空数据包接收缓存队列。 298 /* And now the backlog. */ 299 while((skb=skb_dequeue(&sk->back_log))!=NULL) 300 { 301 /* this should never happen. */ 302 /* printk("cleaning back_log\n");*/ 303 kfree_skb(skb, FREE_READ); 304 } 305 /* Now if it has a half accepted/ closed socket. */ 306 if (sk->pair) 307 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 308 sk->pair->dead = 1; 309 sk->pair->prot->close(sk->pair, 0); 310 sk->pair = NULL; 311 } 312 /* 313 * Now if everything is gone we can free the socket 314 * structure, otherwise we need to keep it around until 315 * everything is gone. 316 */ //如果 sock 结构 dead 标志位为 1,且其读写缓冲区均已被释放,则可以对该 sock 结 //构本身进行释放操作。 317 if (sk->dead && sk->rmem_alloc == 0 && sk->wmem_alloc == 0) 318 { 319 kfree_s((void *)sk,sizeof(*sk)); 320 } //否则设置定时器,等待其它进程释放其读写缓冲区后进行 sock 结构的释放。 321 else 322 { 323 /* this should never happen. */ 324 /* actually it can if an ack has just been sent. */ 325 sk->destroy = 1; 326 sk->ack_backlog = 0; 327 sk->inuse = 0; 328 reset_timer(sk, TIME_DESTROY, SOCK_DESTROY_TIME); 329 } 330 } destroy_sock 函数虽然较长,但功能单一,从上文中对函数的注释可以看出,该函数主要完成对 各个队列中缓存数据包的释放操作。并在读写缓冲区均已释放的条件下释放 sock 结构本身,释 放 sock 结构后,这个套接字完全消失。 331 /* 332 * The routines beyond this point handle the behaviour of an AF_INET 333 * socket object. Mostly it punts to the subprotocols of IP to do 334 * the work. 335 */ 336 static int inet_fcntl(struct socket *sock, unsigned int cmd, unsigned long arg) 337 { 338 struct sock *sk; 339 sk = (struct sock *) sock->data; 340 switch(cmd) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 341 { 342 case F_SETOWN: 343 /* 344 * This is a little restrictive, but it's the only 345 * way to make sure that you can't send a sigurg to 346 * another process. 347 */ 348 if (!suser() && current->pgrp != -arg && 349 current->pid != arg) return(-EPERM); 350 sk->proc = arg; 351 return(0); 352 case F_GETOWN: 353 return(sk->proc); 354 default: 355 return(-EINVAL); 356 } 357 } inet_fcntl 函数用于设置和获取套接字的有关信息。从该函数实现来看,目前仅提供设置和读取 套接字属主的功能,即通过改变 sock 结构中 proc 字段值来完成。从底层实现来看,对于此类信 息设置或获取函数而言,在实现上都是非常直接的,即通过修改相关结构的字段值来完成这些 功能。下文中也有对于一些选项的设置和读取,无论函数本身有多长,但都比较简单。注意 inet_fcntl 函数实现中对于字段值的改变需要超级用户权限,这一点对于所有关键信息的设置都 是一样的。 358 /* 359 * Set socket options on an inet socket. 360 */ 361 static int inet_setsockopt(struct socket *sock, int level, int optname, 362 char *optval, int optlen) 363 { 364 struct sock *sk = (struct sock *) sock->data; 365 if (level == SOL_SOCKET) 366 return sock_setsockopt(sk,level,optname,optval,optlen); 367 if (sk->prot->setsockopt==NULL) 368 return(-EOPNOTSUPP); 369 else 370 return sk->prot->setsockopt(sk,level,optname,optval,optlen); 371 } 372 /* 373 * Get a socket option on an AF_INET socket. 374 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 375 static int inet_getsockopt(struct socket *sock, int level, int optname, 376 char *optval, int *optlen) 377 { 378 struct sock *sk = (struct sock *) sock->data; 379 if (level == SOL_SOCKET) 380 return sock_getsockopt(sk,level,optname,optval,optlen); 381 if(sk->prot->getsockopt==NULL) 382 return(-EOPNOTSUPP); 383 else 384 return sk->prot->getsockopt(sk,level,optname,optval,optlen); 385 } inet_setsockopt 和 inet_getsockopt 用于选项的设置和读取。所谓选项简单的说就是套接字对应结 构中(sock 结构)中某些字段。这两个函数实现根据选项的层次将分别调用不同的下层处理函 数。目前只进行两个层次的区分:SOL_SOCKET 和其它层次。分别调用 sock_xetsockopt 和 sk->prot->xetsockopt 函数(其中 x 表示 s 或者 g,下同)。对于 TCP 协议而言,sock 结构 prot 字段将初始化为 tcp_proto, UDP 协议初始化为 udp_proto。所以对应的函数为 tcp_xetsockopt 和 udp_xegsockopt。sock_xetsockopt 定义在 net/inet/sock.c 文件中。 386 /* 387 * Automatically bind an unbound socket. 388 */ 389 static int inet_autobind(struct sock *sk) 390 { 391 /* We may need to bind the socket. */ 392 if (sk->num == 0) 393 { 394 sk->num = get_new_socknum(sk->prot, 0); 395 if (sk->num == 0) 396 return(-EAGAIN); 397 put_sock(sk->num, sk); 398 sk->dummy_th.source = ntohs(sk->num); 399 } 400 return 0; 401 } inet_autobind 函数用于自动绑定一个本地端口号。例如应用程序客户端在使用 connect 函数之前 没有使用 bind 函数进行绑定,则 connect 函数的底层实现将需要邦定一个本地端口后方可与对 方建立连接。注意此处的邦定主要是分配一个用于连接的本地端口号。此即我们通常所说的自 动邦定。get_new_socknum 函数用于分配一个未使用端口号,put_sock 函数将根据端口将 sock 结构插入的相应的队列中,而 sock 结构 dummy_th 字段是一个 tcphdr 结构(即 TCP 首部结构), 其 source 字段表示本地端口号(注意该字段使用网络字节序)。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 402 /* 403 * Move a socket into listening state. 404 */ 405 static int inet_listen(struct socket *sock, int backlog) 406 { 407 struct sock *sk = (struct sock *) sock->data; 408 if(inet_autobind(sk)!=0) 409 return -EAGAIN; 410 /* We might as well re use these. */ 411 /* 412 * note that the backlog is "unsigned char", so truncate it 413 * somewhere. We might as well truncate it to what everybody 414 * else does.. 415 */ 416 if (backlog > 5) 417 backlog = 5; 418 sk->max_ack_backlog = backlog; 419 if (sk->state != TCP_LISTEN) 420 { 421 sk->ack_backlog = 0; 422 sk->state = TCP_LISTEN; 423 } 424 return(0); 425 } inet_listen 函数不言而喻是对应用程序调用 listen 函数的 L5 层处理。注意 inet_listen 并没有继续 调用更底层的函数,即 listen 函数实际上在 L5 层已完成处理。这些处理主要是对 sock 结构中 state 字段的设置。另外需要注意的是从 socket 结构获得其对应 sock 结构的方法:socket 结构的 data 字段用于指向其对应的 sock 结构。如此自然的在 L5,L6 层之间建立了联系。此处内核限 制最大连接数为 5。 426 /* 427 * Default callbacks for user INET sockets. These just wake up 428 * the user owning the socket. 429 */ 430 static void def_callback1(struct sock *sk) 431 { 432 if(!sk->dead) 433 wake_up_interruptible(sk->sleep); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 434 } 435 static void def_callback2(struct sock *sk,int len) 436 { 437 if(!sk->dead) 438 { 439 wake_up_interruptible(sk->sleep); 440 sock_wake_async(sk->socket, 1); 441 } 442 } 443 static void def_callback3(struct sock *sk) 444 { 445 if(!sk->dead) 446 { 447 wake_up_interruptible(sk->sleep); 448 sock_wake_async(sk->socket, 2); 449 } 450 } 以上是对三个回调函数的实现,目前实现的功能只是唤醒相关的进程,并未进行更复杂的响应。 这些函数将作为 sock 结构中 state_change,data_ready,write_space,error_report 字段的初始化 值。 451 /* 452 * Create an inet socket. 453 * 454 * FIXME: Gcc would generate much better code if we set the parameters 455 * up in in-memory structure order. Gcc68K even more so 456 */ 457 static int inet_create(struct socket *sock, int protocol) 458 { 459 struct sock *sk; 460 struct proto *prot; 461 int err; 462 sk = (struct sock *) kmalloc(sizeof(*sk), GFP_KERNEL); 463 if (sk == NULL) 464 return(-ENOBUFS); 465 sk->num = 0; 466 sk->reuse = 0; //根据套接字类型进行相关字段的赋值。 467 switch(sock->type) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 468 { //流式套接字,使用 TCP 协议操作函数集。 469 case SOCK_STREAM: 470 case SOCK_SEQPACKET: //在 socket 系统调用时,我们一般将 protocol 参数设置为 0。如果设置为非 0, //则对于不同的类型,必须赋予正确值,否则可能在此处处理时出现问题。 471 if (protocol && protocol != IPPROTO_TCP) 472 { 473 kfree_s((void *)sk, sizeof(*sk)); 474 return(-EPROTONOSUPPORT); 475 } 476 protocol = IPPROTO_TCP; //TCP_NO_CHECK定义为 1,表示对于 TCP 协议默认使用校验。 477 sk->no_check = TCP_NO_CHECK; //注意此处 prot 变量被初始化为 tcp_prot,稍后 sock 结构的 prot 字段将被初始 //化为 prot 变量值。 478 prot = &tcp_prot; 479 break; //报文套接字,即使用 UDP 协议操作函数集。 480 case SOCK_DGRAM: 481 if (protocol && protocol != IPPROTO_UDP) 482 { 483 kfree_s((void *)sk, sizeof(*sk)); 484 return(-EPROTONOSUPPORT); 485 } 486 protocol = IPPROTO_UDP; 487 sk->no_check = UDP_NO_CHECK; 488 prot=&udp_prot; 489 break; //原始套接字类型,注意此时 protocol 参数表示端口号。 //原始套接字使用 raw_prot 变量指定的函数集。 490 case SOCK_RAW: 491 if (!suser()) 492 { 493 kfree_s((void *)sk, sizeof(*sk)); 494 return(-EPERM); 495 } 496 if (!protocol) 497 { 498 kfree_s((void *)sk, sizeof(*sk)); 499 return(-EPROTONOSUPPORT); 500 } 501 prot = &raw_prot; 502 sk->reuse = 1; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 503 sk->no_check = 0; /* 504 * Doesn't matter no checksum is 505 * performed anyway. 506 */ 507 sk->num = protocol; 508 break; //包套接字,该套接字在网络层直接进行数据包的收发。此时使用由 packet_prot 变 //量指定的函数集。注意此时 protocol 参数表示套接字端口号。 509 case SOCK_PACKET: 510 if (!suser()) 511 { 512 kfree_s((void *)sk, sizeof(*sk)); 513 return(-EPERM); 514 } 515 if (!protocol) 516 { 517 kfree_s((void *)sk, sizeof(*sk)); 518 return(-EPROTONOSUPPORT); 519 } 520 prot = &packet_prot; 521 sk->reuse = 1; 522 sk->no_check = 0; /* Doesn't matter no checksum is 523 * performed anyway. 524 */ 525 sk->num = protocol; 526 break; //不符合以上任何类型,则出错返回。 527 default: 528 kfree_s((void *)sk, sizeof(*sk)); 529 return(-ESOCKTNOSUPPORT); 530 } inet_create 函数被上层 sock_socket 函数调用(通过 socket->ops->create 函数指针),用于创建一 个套接字对应的 sock 结构并对其进行初始化。该函数首先分配一个 sock 结构,然后根据套接 字类型对 sock 相关字段进行初始化。具体分析参见函数中注释。注意其中对 prot 变量的赋值非 常重要,该变量所指向的操作函数集将处理该套接字之后所有的实际请求。 531 sk->socket = sock; 建立与其对应的 socket 结构之间的关系,socket 结构先于 sock 结构建立。 如果定义了 Nagle 算法,则将 sock 结构的 nonagle 字段初始化为 0,否则为 1。 532 #ifdef CONFIG_TCP_NAGLE_OFF 533 sk->nonagle = 1; 534 #else 535 sk->nonagle = 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 536 #endif 537 sk->type = sock->type; //初始化 sock 结构 type 字段:套接字类型 538 sk->stamp.tv_sec=0; 539 sk->protocol = protocol; //传输层协议 540 sk->wmem_alloc = 0; 541 sk->rmem_alloc = 0; 542 sk->sndbuf = SK_WMEM_MAX; //最大发送缓冲区大小 543 sk->rcvbuf = SK_RMEM_MAX; //最大接收缓冲区大小 //以下是对 sock 结构字段的初始化,这些字段的意义参见前文中对 sock 结构的介绍。 544 sk->pair = NULL; 545 sk->opt = NULL; 546 sk->write_seq = 0; 547 sk->acked_seq = 0; 548 sk->copied_seq = 0; 549 sk->fin_seq = 0; 550 sk->urg_seq = 0; 551 sk->urg_data = 0; 552 sk->proc = 0; 553 sk->rtt = 0; /*TCP_WRITE_TIME << 3;*/ 554 sk->rto = TCP_TIMEOUT_INIT; /*TCP_WRITE_TIME*/ 555 sk->mdev = 0; 556 sk->backoff = 0; 557 sk->packets_out = 0; //cong_window 设置为 1,即 TCP 首先进入慢启动阶段。这是 TCP 协议处理拥塞的 //一种策略。 558 sk->cong_window = 1; /* start with only sending one packet at a time. */ 559 sk->cong_count = 0; 560 sk->ssthresh = 0; 561 sk->max_window = 0; 562 sk->urginline = 0; 563 sk->intr = 0; 564 sk->linger = 0; 565 sk->destroy = 0; 566 sk->priority = 1; 567 sk->shutdown = 0; 568 sk->keepopen = 0; 569 sk->zapped = 0; 570 sk->done = 0; 571 sk->ack_backlog = 0; 572 sk->window = 0; 573 sk->bytes_rcv = 0; 574 sk->state = TCP_CLOSE; //由于尚未进行连接,状态设置为 CLOSE。 575 sk->dead = 0; 576 sk->ack_timed = 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 577 sk->partial = NULL; 578 sk->user_mss = 0; 579 sk->debug = 0; //设置最大可暂缓应答的字节数。 580 /* this is how many unacked bytes we will accept for this socket. */ 581 sk->max_unacked = 2048; /* needs to be at most 2 full packets. */ 582 /* how many packets we should send before forcing an ack. 583 if this is set to zero it is the same as sk->delay_acks = 0 */ 584 sk->max_ack_backlog = 0; 585 sk->inuse = 0; 586 sk->delay_acks = 0; 587 skb_queue_head_init(&sk->write_queue); 588 skb_queue_head_init(&sk->receive_queue); 589 sk->mtu = 576; //MTU设置为保守的576字节,该大小在绝大多数连接中不会造成分片。 //注意对于 prot 字段的初始化,prot 变量根据套接字类型已在函数前部分初始化为正确//的 值,此处用于对 sock 结构的 prot 字段进行初始化。此后该套接字所有实质性的操作 //均有该变量所指向的操作函数集完成。 590 sk->prot = prot; 591 sk->sleep = sock->wait; 592 sk->daddr = 0; 593 sk->saddr = 0 /* ip_my_addr() */; 594 sk->err = 0; 595 sk->next = NULL; 596 sk->pair = NULL; 597 sk->send_tail = NULL; 598 sk->send_head = NULL; 599 sk->timeout = 0; 600 sk->broadcast = 0; 601 sk->localroute = 0; 602 init_timer(&sk->timer); 603 init_timer(&sk->retransmit_timer); 604 sk->timer.data = (unsigned long)sk; 605 sk->timer.function = &net_timer; 606 skb_queue_head_init(&sk->back_log); 607 sk->blog = 0; 608 sock->data =(void *) sk; //sock 结构之 dummy_th 字段是 tcphdr 结构,该结构与 TCP 首部各字段对应, //故对以下的代码理解可结合 TCP 首部格式。TCP 首部格式参见第一章的有关介绍。 609 sk->dummy_th.doff = sizeof(sk->dummy_th)/4; 610 sk->dummy_th.res1=0; 611 sk->dummy_th.res2=0; 612 sk->dummy_th.urg_ptr = 0; 613 sk->dummy_th.fin = 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 614 sk->dummy_th.syn = 0; 615 sk->dummy_th.rst = 0; 616 sk->dummy_th.psh = 0; 617 sk->dummy_th.ack = 0; 618 sk->dummy_th.urg = 0; 619 sk->dummy_th.dest = 0; 620 sk->ip_tos=0; 621 sk->ip_ttl=64; 622 #ifdef CONFIG_IP_MULTICAST 623 sk->ip_mc_loop=1; 624 sk->ip_mc_ttl=1; 625 *sk->ip_mc_name=0; 626 sk->ip_mc_list=NULL; 627 #endif //对 sock 结构中几个回调函数字段的初始化。 628 sk->state_change = def_callback1; 629 sk->data_ready = def_callback2; 630 sk->write_space = def_callback3; 631 sk->error_report = def_callback1; //如果该套接字已经分配本地端口号,则对 sock 结构中 dummy_th 结构字段进行赋值。 632 if (sk->num) 633 { 634 /* 635 * It assumes that any protocol which allows 636 * the user to assign a number at socket 637 * creation time automatically 638 * shares. 639 */ 640 put_sock(sk->num, sk); 641 sk->dummy_th.source = ntohs(sk->num); 642 } //对于不同的操作函数集,可能需要进行某些初始化操作,此处调用其对应的初始化函 //数完成相关操作,如对于 packet_prot 操作函数集,需要建立一个 packet_type 结构插 //入到内核变量 ptype_base 指向的队列中,以完成在网络层收发数据包的功能。这个 //packet_type 结构的建立即是在其初始化函数中完成。注意如果需要进行初始化,而初 //始化失败,则直接销毁该套接字,因为其它部分尚未准备好,即便建立了该套接字, //也将无法使用。 643 if (sk->prot->init) 644 { 645 err = sk->prot->init(sk); 646 if (err != 0) 647 { 648 destroy_sock(sk); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 649 return(err); 650 } 651 } 652 return(0); 653 } inet_create 函数较长,但实现的功能只有一个:建立套接字对应的 sock 结构并对其进行初始化, 由于 sock 结构较为庞大,造成初始化代码较长。结合上文中对相关代码的注视,应不难理解。 654 /* 655 * Duplicate a socket. 656 */ 657 static int inet_dup(struct socket *newsock, struct socket *oldsock) 658 { 659 return(inet_create(newsock,((struct sock *)(oldsock->data))->protocol)); 660 } inet_dup 函数被服务器端调用。服务器端用于实际通信的套接字与其监听套接字是不同的。当 应用程序在服务器端使用 accept 函数接收一个客户端请求时,会创建一个新的套接字用于与客 户端进行具体的数据通信,而监听套接字仍然只负责监听其它客户端的请求。在 sock_accept(accept 系统调用的 BSD 层处理函数)在分配一个新的 socket 结构后,调用 inet_dup 函数对此新创建的 socket 结构进行初始化,而初始化信息主要来自监听套接字,inet_dup 函数 的输入参数中 newsock 即表示新创建的 socket 结构,而 oldsock 则表示监听套接字 socket 结构。 从 inet_dup 实现来看,该函数调用 inet_create 函数完成具体的功能:分配新 socket 结构对应的 下层 sock 结构,此处只使用了监听 socket 结构的 protocol 字段。不过在 sock_accept 函数中这个 新创建的 socket 结构之 ops,type 字段已经被初始化为监听 socket 结构中相应字段值。而这对 于 inet_create 函数已经足够,因为注意到 inet_create 函数需要根据 type,protocol 字段值进行 sock 结构关键字段的赋值。读者可将 sock_accept, inet_create 函数结合来看。 661 /* 662 * Return 1 if we still have things to send in our buffers. 663 */ 664 static inline int closing(struct sock * sk) 665 { 666 switch (sk->state) { 667 case TCP_FIN_WAIT1: 668 case TCP_CLOSING: 669 case TCP_LAST_ACK: 670 return 1; 671 } 672 return 0; 673 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 closing 函数实际上是对套接字状态的检测。当 TCP 连接状态为 FIN_WAIT1,CLOSING, LAST_ACK 时,即表示本地套接字已处于关闭状态。实际上还可以加上 FIN_WAIT2 状态。有 关 TCP 协议状态的介绍,请参考第一章中内容。 674 /* 675 * The peer socket should always be NULL (or else). When we call this 676 * function we are destroying the object and from then on nobody 677 * should refer to it. 678 */ 679 static int inet_release(struct socket *sock, struct socket *peer) 680 { 681 struct sock *sk = (struct sock *) sock->data; 682 if (sk == NULL) 683 return(0); 684 sk->state_change(sk); 685 /* Start closing the connection. This may take a while. */ 686 #ifdef CONFIG_IP_MULTICAST 687 /* Applications forget to leave groups before exiting */ 688 ip_mc_drop_socket(sk); 689 #endif 690 /* 691 * If linger is set, we don't return until the close 692 * is complete. Other wise we return immediately. The 693 * actually closing is done the same either way. 694 * 695 * If the close is due to the process exiting, we never 696 * linger.. 697 */ 698 if (sk->linger == 0 || (current->flags & PF_EXITING)) 699 { 700 sk->prot->close(sk,0); 701 sk->dead = 1; 702 } 703 else 704 { 705 sk->prot->close(sk, 0); 706 cli(); 707 if (sk->lingertime) 708 current->timeout = jiffies + HZ*sk->lingertime; 709 while(closing(sk) && current->timeout>0) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 710 { 711 interruptible_sleep_on(sk->sleep); 712 if (current->signal & ~current->blocked) 713 { 714 break; 715 #if 0 716 /* not working now - closes can't be restarted */ 717 sti(); 718 current->timeout=0; 719 return(-ERESTARTSYS); 720 #endif 721 } 722 } 723 current->timeout=0; 724 sti(); 725 sk->dead = 1; 726 } 727 sk->inuse = 1; 728 /* This will destroy it. */ 729 release_sock(sk); 730 sock->data = NULL; 731 sk->socket = NULL; 732 return(0); 733 } inet_release 函数是 sock_release 调用的 INET 层函数。 该函数将进一步完成套接字的关闭操作。首先通过 sock 结构中 state_change 函数指针通知相关 进程套接字状态的变化。之后如果该套接字之前加入了一个多播组,则在关闭之前需要首先退 出该多播组。然后根据 sock 结构中 linger 标志位进行相应的具体关闭操作。linger 标志位表示 是否等待关闭操作的完成,如果 linger 标志位设置为 1,则执行关闭操作后,需要等待一段时间, 等待套接字状态变为 CLOSED;如果 linger 标志位为 0,则执行关闭操作后立刻返回,并不等 待一段时间。此处一再提到的执行关闭是指调用下层关闭函数(通过调用 sock->prot->close 函 数指针所指函数完成的,如 tcp_close)。注意在 linger 标志位设置为 1 时的等待是通过定时器完 成的。在设置定时器后,函数进入一个 while 循环,不断地检测套接字状态。其中 while 语句为 真的条件是套接字只是处于“正在关闭”状态(见上文中 closing 函数的实现),而非“关闭 (TCP_CLOSED)”状态,另外等待时间未超时。如果等待过程发生中断,则停止等待,返回 ERESTARTSYS,该返回值表示上层应用程序应该重新执行关闭函数。release_sock 函数完成的 功能是将 sock 结构中缓存的数据包送给应用程序读取,并在检测套接字已处于关闭且等待销毁 时,设置一定时器,在定时器到期时,进行 sock 结构的释放,具体参见下文中对 sock.c 文件分 析。inet_release:函数最后解除 sock 结构与 socket 结构之间的互相引用关系,因为二者即将被释 放,之前为实现函数调用而维持的上下层次关系已无必要。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 734 /* this needs to be changed to disallow 735 the rebinding of sockets. What error 736 should it return? */ 737 static int inet_bind(struct socket *sock, struct sockaddr *uaddr, 738 int addr_len) 739 { 740 struct sockaddr_in *addr=(struct sockaddr_in *)uaddr; 741 struct sock *sk=(struct sock *)sock->data, *sk2; 742 unsigned short snum = 0 /* Stoopid compiler.. this IS ok */; 743 int chk_addr_ret; 744 /* check this error. */ //在进行地址邦定时,该套接字应该处于关闭状态,否则错误返回。 //如果套接字处于其它任何状态,则表示该套接字已经被邦定,否则无法进入其它状态。 745 if (sk->state != TCP_CLOSE) 746 return(-EIO); //地址长度字段必须不小于 sockaddr_in 结构长度,否则表示邦定的地址有问题。 747 if(addr_lentype != SOCK_RAW) 750 { //对于非原始套接字类型,在邦定地址之前,其应该尚无断口号。 //而对于原始套接字,在 socket 系统调用时,protocol 参数作为端口号处理。 //具体参见前文中对 inet_create 函数的分析。 751 if (sk->num != 0) 752 return(-EINVAL); //获取邦定地址中端口号,如果端口号为 0,则系统自动分配一个,这在客户端 //编程时是经常发生的事。 snum = ntohs(addr->sin_port); 753 snum = ntohs(addr->sin_port); 754 /* 755 * We can't just leave the socket bound wherever it is, it might 756 * be bound to a privileged port. However, since there seems to 757 * be a bug here, we will leave it if the port is not privileged. 758 */ 759 if (snum == 0) 760 { 761 snum = get_new_socknum(sk->prot, 0); 762 } //非超级用户,不可使用 1024 以下的端口。对于自动分配端口号,对于非超级用 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 //户,系统不会分配到 1024 以下的端口的。所以以下 if 语句主要是对用户传入的 //端口号进行检查。 763 if (snum < PROT_SOCK && !suser()) 764 return(-EACCES); 765 } //ip_chk_addr 函数用于检查地址是否是一个本地接口地址。 766 chk_addr_ret = ip_chk_addr(addr->sin_addr.s_addr); //如果指定的地址不是本地地址,并且也不是一个多播地址,则错误返回:地址不可用! 767 if (addr->sin_addr.s_addr != 0 && chk_addr_ret != IS_MYADDR && chk_addr_ret != IS_MULTICAST) 768 return(-EADDRNOTAVAIL); /* Source address MUST be ours! */ //如果没有指定地址,则系统自动分配一个本地地址。 769 if (chk_addr_ret || addr->sin_addr.s_addr == 0) 770 sk->saddr = addr->sin_addr.s_addr; //此处又对原始套接字进行了区分,对于原始套接字,有专门的 sock 结构队列。 //对于非原始套接字,则需要将此分配了端口号的 sock 结构插入到对应协议的 sock 结 //构队列中。有关 sock 结构队列的解释在前文中介绍 sock 结构时有较为详细的说明。 771 if(sock->type != SOCK_RAW) 772 { 773 /* Make sure we are allowed to bind here. */ 774 cli(); //根据端口号寻找到对应的 sock 结构队列后,对队列中已有的 sock 结构进行检查。 775 for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)]; 776 sk2 != NULL; sk2 = sk2->next) 777 { 778 /* should be below! */ //如果此已有 sock 结构端口号不同此带插入 sock 结构,则表示无冲突,继续 //进行下一个 sock 结构的检查。 779 if (sk2->num != snum) 780 continue; //二者端口号相同,检查该新套接字是否设置了“地址复用”标志,如果没有 //设置,则返回地址复用错误。这就是通过应用程序编程时使用的 //REUSEADDR 选项。 781 if (!sk->reuse) 782 { 783 sti(); 784 return(-EADDRINUSE); 785 } 786 if (sk2->num != snum) 787 continue; /* more than one */ //以下的检查是针对地址的,与端口号的处理类似。 788 if (sk2->saddr != sk->saddr) 789 continue; /* socket per slot ! -FB */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 //如果 sock 结构的状态是 TCP_LISTEN,则表示该套接字是一个服务端,服务 //端不可使用地址复用选项。 790 if (!sk2->reuse || sk2->state==TCP_LISTEN) 791 { 792 sti(); 793 return(-EADDRINUSE); 794 } 795 } 796 sti(); //remove_sock 函数将 sock 结构从其之前队列中删除,由 put_sock 函数根据新分配 //的端口号插入到新的队列中。 797 remove_sock(sk); 798 put_sock(snum, sk); //初始化 dummy_th 字段,此时本地端口,本地地址子字段都可进行初始化。 799 sk->dummy_th.source = ntohs(sk->num); 800 sk->daddr = 0; 801 sk->dummy_th.dest = 0; 802 } 803 return(0); 804 } inet_bind 函数是 sock_bind 调用的下层函数,完成本地地址邦定。本地地址邦定包括 IP 地址和 端口号两个部分。注意如果没有具体指定地址和端口号,则系统在此会自动进行分配。函数实 现的其它方面涉及 sock 结构的重新定位。 805 /* 806 * Handle sk->err properly. The cli/sti matter. 807 */ 808 static int inet_error(struct sock *sk) 809 { 810 unsigned long flags; 811 int err; 812 save_flags(flags); 813 cli(); 814 err=sk->err; 815 sk->err=0; 816 restore_flags(flags); 817 return -err; 818 } inet_error 函数用于返回套接字通信过程中发生的问题。注意 sock 结构 err 字段是一个正数,返 回时是作为负数返回的。在 Linux 系统中,错误的返回都是以负数形式进行的。注意在返回错 误时会清除当前的错误值。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 819 /* 820 * Connect to a remote host. There is regrettably still a little 821 * TCP 'magic' in here. 822 */ 823 static int inet_connect(struct socket *sock, struct sockaddr * uaddr, 824 int addr_len, int flags) 825 { 826 struct sock *sk=(struct sock *)sock->data; 827 int err; 828 sock->conn = NULL; 829 if (sock->state == SS_CONNECTING && tcp_connected(sk->state)) 830 { 831 sock->state = SS_CONNECTED; 832 /* Connection completing after a connect/EINPROGRESS/select/connect */ 833 return 0; /* Rock and roll */ 834 } 835 if (sock->state == SS_CONNECTING && sk->protocol == IPPROTO_TCP && (flags & O_NONBLOCK)) 836 return -EALREADY; /* Connecting is currently in progress */ inet_connect 函数时 sock_connect 下层调用函数,完成套接字连接操作。 函数首先检查已有的连接状态,如果现在已经处于连接状态,则简单返回。由此可见,应用程 序在连接建立后多次调用 connect 函数是可以容忍的。如果套接字正在处于建立连接阶段,并且 套接字使用 TCP 协议,如果使用了非阻塞选项,则立刻返回连接正在进行错误。注意只有对 TCP 协议才有可能出现这种情况,因为 UDP 协议连接的建立不涉及网络数据的传输,所以虽然 UDP 也存在连接操作,但这种连接是软件上的,是立刻可以完成的。而 TCP 协议 需要网络数 据的传输。从此处的代码可以看出,对于 TCP 协议在连接正在进行时,不可连续调用 connect 函数。在对该函数继续分析之前,需要对 TCP 协议的三路握手建立连接的过程进行简单的介绍。 TCP 协议是一种面向连接的流式传输协议,其声称提供可靠性数据传输。使用 TCP 协议必须首 先与远端主机建立连接,这是 TCP 协议为保证可靠性数据传输要求的(附录一对此有介绍)。 另外为保证 TCP 协议所声称的可靠性数据传输,TCP 协议实现软件对使用该协议的套接字维护 其通信状态如 TCP_CLOSED, TCP_SYN_SENT, TCP_FIN_WAIT1 等等,这些状态的维护也是 TCP 协议 RFC 文档所要求的,所以使用 TCP 协议的套接字在通信过程的不同阶段是在这个状 态机上不断的转变的。一般的,一个使用 TCP 协议的套接字需要经过几个状态的转变,根据是 服务器端还是客户端,其中的转变过程有些不同(此处对于客户端和服务器的定义是:将主动 发起连接操作和主动发起关闭操作的一端称为客户端)。 /*include/linux/tcp.h*/ 52 enum { 53 TCP_ESTABLISHED = 1, 54 TCP_SYN_SENT, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 55 TCP_SYN_RECV, 56 TCP_FIN_WAIT1, 57 TCP_FIN_WAIT2, 58 TCP_TIME_WAIT, 59 TCP_CLOSE, 60 TCP_CLOSE_WAIT, 61 TCP_LAST_ACK, 62 TCP_LISTEN, 63 TCP_CLOSING /* now a valid state */ 64 }; 对于客户端而言,我们一般假设其主动发送连接请求,连接请求是通过发送一个 TCP 首部中 SYN 标志位设置为 1 的数据包完成的。客户端在发送该数据包后,将 sock 结构中 state 字段从 TCP_CLOSE 更新为 TCP_SYN_SENT,表示本地已发送 SYN 请求连接数据包。此时本地将等待 服务器应答数据包。服务器监听套接字在接收到远端客户端一个连接请求后,调用相关函数进 行处理,这些处理包括分配一个新的套接字用于与远端客户端的实际通信,将这个请求客户端 挂入服务器端请求队列等待 accept 函数的读取,之后发送应答数据包给远端,并将之前创建的 新的套接字对于 sock 结构中 state 字段值更新为 TCP_SYN_RECV.客户端在接收到服务器发送的 应答数据包后,将其状态进一步更新为 TCP_ESTABLISHED, 同时发送一个应答数据包给服务 器端,服务器端在接收到此应答数据包后将对应通信 sock 结构状态更新为 TCP_ESTABLISHED, 二者正式建立连接。这个连接建立过程如下图(图 2-3)所示。 图 2-3 TCP 协议三路握手连接建立过程 837 if (sock->state != SS_CONNECTING) 838 { 839 /* We may need to bind the socket. */ 840 if(inet_autobind(sk)!=0) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 841 return(-EAGAIN); 842 if (sk->prot->connect == NULL) 843 return(-EOPNOTSUPP); 844 err = sk->prot->connect(sk, (struct sockaddr_in *)uaddr, addr_len); 845 if (err < 0) 846 return(err); 847 sock->state = SS_CONNECTING; 848 } 这个 if 语句块判断如果套接字是初次调用 connect 函数则调用下层函数进行实质上的连接建立。 并将 socket 结构状态更新为 SS_CONNECTING。注意如果本地尚未邦定地址,则内核会在进行 实际连接之间进行地址的自动邦定。对于 TCP 协议而言,sk->prot->connect 实际上调用的是 tcp_connect 函数。该函数将发送 SYN 数据包进行三路握手连接建立过程(如图 2-3 所示)。注 意在 tcp_connect 函数中,会将 sock 结构状态更新为 TCP_SYN_SENT.这一更新方使得 inet_connect 函数之后的判断有意义。再看接下来的代码。 849 if (sk->state > TCP_FIN_WAIT2 && sock->state==SS_CONNECTING) 850 { 851 sock->state=SS_UNCONNECTED; 852 cli(); 853 err=sk->err; 854 sk->err=0; 855 sti(); 856 return -err; 857 } 如果 sock 结构状态大于 TCP_FIN_WAIT2,则表示 sock 状态可能为如下状态:TCP_TIME_WAIT, TCP_CLOSE, TCP_CLOSE_WAIT.因为在 tcp_connect 函数中 sock 结构更新为 TCP_SYN_SENT, 而现在变成了如上一些状态,表示在连接过程中出现了异常,而 socket 结构状态却表示连接正 在进行。对于初次连接这个 if 条件是不可能为真的,只有在多次调用 connect 函数的情况下, 此处条件才有可能为真。无论如何,在条件为真的情况下,都表示出现连接错误,此时需要更 新 socket 结构状态为 SS_UNCONNECTED, 并返回具体的错误。 在调用底层函数完成具体连接时,此时需要根据阻塞标志位进行等待或立刻返回的操作。所谓 等待则需要等待连接完全建立方才返回。而立刻返回的含义是指将连接任务交给下层后即可安 全返回。等待操作是在一个 while 循环中进行的,注意 while 循环的条件是 sock 状态为 TCP_SYN_SENT 或者 TCP_SYN_RECV。这两种状态是在连接过程中唯一合法的状态。对于客 户端而言,建立连接时常见状态为 TCP_SYN_SENT, 只有当远端同时发送连接建立操作时才可 能出现 TCP_SYN_RECV 状态。双方同时建立连接的过程如下图(图 2-4)所示。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 图 2-4 TCP 协议通信双方同时发起连接建立时的情况 858 if (sk->state != TCP_ESTABLISHED &&(flags & O_NONBLOCK)) 859 return(-EINPROGRESS); 860 cli(); /* avoid the race condition */ 861 while(sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) 862 { 863 interruptible_sleep_on(sk->sleep); 864 if (current->signal & ~current->blocked) 865 { 866 sti(); 867 return(-ERESTARTSYS); 868 } 869 /* This fixes a nasty in the tcp/ip code. There is a hideous hassle with 870 icmp error packets wanting to close a tcp or udp socket. */ 871 if(sk->err && sk->protocol == IPPROTO_TCP) 872 { 873 sti(); 874 sock->state = SS_UNCONNECTED; 875 err = -sk->err; 876 sk->err=0; 877 return err; /* set by tcp_err() */ 878 } 879 } 880 sti(); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 881 sock->state = SS_CONNECTED; 882 if (sk->state != TCP_ESTABLISHED && sk->err) 883 { 884 sock->state = SS_UNCONNECTED; 885 err=sk->err; 886 sk->err=0; 887 return(-err); 888 } 889 return(0); 890 } 注意如果在等待过程中出现错误,则简单返回错误并置状态为未连接。 如果正常退出 while 循环,则表示连接成功,不过在函数最后还是对 sock 结构状态及错误字段 进行了检查。一切正常时,sock 状态将被设置为 TCP_ESTABLISHED, 而 socket 结构状态将被 设置为 SS_CONNECTED. 注意 sock 结构和 socket 结构各自使用不同的状态集合。对于 inet_connect 函数的单一分析很难深刻理解前文中的意义,读者可暂时结合 tcp.c 文件中 tcp_connect 函数进行理解。使用 TCP 协议的套接字其状态的转化相对而言是比较麻烦的。关于 TCP 协议的详细说明将在下文中分析 tcp.c 文件是给出。读者也可直接阅读 RFC-793 文档。 891 static int inet_socketpair(struct socket *sock1, struct socket *sock2) 892 { 893 return(-EOPNOTSUPP); 894 } 对于 INET 域,不存在配对套接字。配对套接字仅对 UNIX 域有效,所以该函数简单返回 EOPNOTSUPP 错误标志(表示不支持该功能)。 895 /* 896 * Accept a pending connection. The TCP layer now gives BSD semantics. 897 */ 898 static int inet_accept(struct socket *sock, struct socket *newsock, int flags) 899 { 900 struct sock *sk1, *sk2; 901 int err; 902 sk1 = (struct sock *) sock->data; 903 /* 904 * We've been passed an extra socket. 905 * We need to free it up because the tcp module creates 906 * its own when it accepts one. 907 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 908 if (newsock->data) 909 { 910 struct sock *sk=(struct sock *)newsock->data; 911 newsock->data=NULL; 912 sk->dead = 1; 913 destroy_sock(sk); 914 } 915 if (sk1->prot->accept == NULL) 916 return(-EOPNOTSUPP); 917 /* Restore the state if we have been interrupted, and then returned. */ 918 if (sk1->pair != NULL ) 919 { 920 sk2 = sk1->pair; 921 sk1->pair = NULL; 922 } 923 else 924 { 925 sk2 = sk1->prot->accept(sk1,flags); 926 if (sk2 == NULL) 927 { 928 if (sk1->err <= 0) 929 printk("Warning sock.c:sk1->err <= 0. Returning non-error.\n"); 930 err=sk1->err; 931 sk1->err=0; 932 return(-err); 933 } 934 } 935 newsock->data = (void *)sk2; 936 sk2->sleep = newsock->wait; 937 sk2->socket = newsock; 938 newsock->conn = NULL; 939 if (flags & O_NONBLOCK) 940 return(0); 941 cli(); /* avoid the race. */ 942 while(sk2->state == TCP_SYN_RECV) 943 { 944 interruptible_sleep_on(sk2->sleep); 945 if (current->signal & ~current->blocked) 946 { 947 sti(); 948 sk1->pair = sk2; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 949 sk2->sleep = NULL; 950 sk2->socket=NULL; 951 newsock->data = NULL; 952 return(-ERESTARTSYS); 953 } 954 } 955 sti(); 956 if (sk2->state != TCP_ESTABLISHED && sk2->err > 0) 957 { 958 err = -sk2->err; 959 sk2->err=0; 960 sk2->dead=1; /* ANK */ 961 destroy_sock(sk2); 962 newsock->data = NULL; 963 return(err); 964 } 965 newsock->state = SS_CONNECTED; 966 return(0); 967 } inet_accept 用于接收一个套接字。该函数是 accept 函数的 INET 层相应函数,由服务器端调用。 服务器端在接收到一个远端客户端连接请求后(在传输层),调用相应函数进行处理(如 TCP 协议的 tcp_conn_request 函数)。诚如上文中所述,所谓处理指: 1>创建一个本地新套接字用于通信,原监听套接字仍然用于监听请求。 2>发送应答数据包 3>将新创建套接字对于的 sock 结构挂接到监听 sock 结构的接收队列中(receive_queue 指向的 队列)以供 accept 函数读取。 inet_accept 是由 sock_accept 函数调用的,调用形式如下: i=newsock->ops->accept(sock, newsock, file->f_flags); 其中 newsock 为 sock_accept 为新的通信套接字创建的对应 socket 结构。 sock_accept 函数在调用 inet_accept 函数之前已经使用 inet_dup 函数创建了其对应的 sock 结构, 即新创建 socket 结构的 data 字段指向其对应的新创建的 sock 结构。 所以在 inet_accept 函数中对于 socket->data 字段的检查是必要的,而且所得结果一般都不为 NULL,而是指向了对应的 sock 结构。所以以下代码一般不会执行到。 if (newsock->data) { struct sock *sk=(struct sock *)newsock->data; newsock->data=NULL; sk->dead = 1; destroy_sock(sk); } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 而如下的 if 语句用于检测是否存在下层处理函数,如果不存在,则错误返回。同前文所说,INET 层函数并不进行实质上的处理,而是将请求传递给下层,如果下层没有对应的处理函数,则必 须出错返回。 if (sk1->prot->accept == NULL) return(-EOPNOTSUPP); /* Restore the state if we have been interrupted, and then returned. */ if (sk1->pair != NULL ) { sk2 = sk1->pair; sk1->pair = NULL; } 对于这个 if 语句的必要性可以从函数的结尾处代码理解。如果套接字在等待连接的过程中被中 断,则监听套接字 sock 结构之 pair 字段将被初始化为指向该被中断的套接字对应的 sock 结构, 这样在下一次调用 accept 函数时,可以优先处理这个之前被中断的套接字。 否则将按正常的程序从监听套接字的接收队列中取下一个 sock 结构(该结构是由传输层创建的 用于与远端通信的 sock 结构,所以对于监听套接字而言,其对应 sock 结构中接收队列中均是 经过处理的请求连接数据包,“经过处理”是指对于这些请求本地都创建了新的 sock 结构)。 newsock->data = (void *)sk2; sk2->sleep = newsock->wait; sk2->socket = newsock; newsock->conn = NULL; if (flags & O_NONBLOCK) return(0); 此后代码初始化 socket,sock 结构各自对应的字段使二者相互联系起来。 之后代码检测是否已经完全建立起连接,这是在 while 循环中进行的,注意如果等待中被中断, 则将监听套接字 pair 字段赋值为被中断的套接字,以便下次可以优先处理。 如果 while 循环正常退出,则表示成功建立连接,此后即可进行数据的传送。如果异常退出, 则表示出现错误,此时函数的处理是销毁该出错套接字对应的 socket 和 sock 结构(sock 结构是 在本函数进行销毁的,而 socket 结构将在 sock_accept 函数被销毁)。否则一切正常,则更改 socket 状态,并正常返回。此时一个通道即成功建立,之后可以进行数据的正常传输。 968 /* 969 * This does both peername and sockname. 970 */ 971 static int inet_getname(struct socket *sock, struct sockaddr *uaddr, 972 int *uaddr_len, int peer) 973 { 974 struct sockaddr_in *sin=(struct sockaddr_in *)uaddr; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 975 struct sock *sk; 976 sin->sin_family = AF_INET; 977 sk = (struct sock *) sock->data; 978 if (peer) 979 { 980 if (!tcp_connected(sk->state)) 981 return(-ENOTCONN); 982 sin->sin_port = sk->dummy_th.dest; 983 sin->sin_addr.s_addr = sk->daddr; 984 } 985 else 986 { 987 sin->sin_port = sk->dummy_th.source; 988 if (sk->saddr == 0) 989 sin->sin_addr.s_addr = ip_my_addr(); 990 else 991 sin->sin_addr.s_addr = sk->saddr; 992 } 993 *uaddr_len = sizeof(*sin); 994 return(0); 995 } inet_getname 函数用于获取本地和远端地址。改函数是上层 BSD 层 sock_getsockname, sock_getpeername 的 INET 层相应函数。改函数中 peer 参数表示获取本地还是远端地址。对于 已经成功建立连接的 TCP 协议而言,远端地址封装在 sock 结构字段中。sock 结构中 daddr 字段 表示远端 IP 地址,而 dummy_th 字段用于向远端传送数据包时进行数据包中 TCP 首部的创建, 即该字段中含有远端通信端口号。如果是获取远端地址,则这些值即来自于 sock 结构的这些字 段,而如果获取本地地址值,原理基本相同。 ip_my_addr 函数定义在 devinet.c 文件中,该函数较短,故先列出如下: /*net/inet/devinet.c*/ 144 unsigned long ip_my_addr(void) 145 { 146 struct device *dev; 147 for (dev = dev_base; dev != NULL; dev = dev->next) 148 { 149 if (dev->flags & IFF_LOOPBACK) 150 return(dev->pa_addr); 151 } 152 return(0); 153 } ip_my_addr 的基本实现是通过遍历系统中所有设备,检查该设备标志位中是否包含 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 IFF_LOOPBACK,如果包含,则返回该设备所配置的 IP 地址。device 结构中 pa_addr 字段是一 个 unsigned long 类型,正好表示一个 IP 地址。 注意设置有 IFF_LOOPBACK 标志位的设备通常并不表示其是一个回环设备。通常我们将具有 地址值为 127.0.0.1 称为回环设备,不过这个设备通常只存在于软件概念上,即实际并无真正的 硬件与之对应。对于一个实际硬件网卡设备,有时我们也对其设置 IFF_LOOPBACK 标志位, 从而当从该设备发送数据包时,该设备会回送一份数据包拷贝给本机。 从 inet_getsockname 函数的实现来看,获取地址值对于底层实现而言十分简单的:只是从底层 数据结构中读取字段值。 996 /* 997 * The assorted BSD I/O operations 998 */ 999 static int inet_recvfrom(struct socket *sock, void *ubuf, int size, int noblock, 1000 unsigned flags, struct sockaddr *sin, int *addr_len ) 1001 { 1002 struct sock *sk = (struct sock *) sock->data; 1003 if (sk->prot->recvfrom == NULL) 1004 return(-EOPNOTSUPP); 1005 if(sk->err) 1006 return inet_error(sk); 1007 /* We may need to bind the socket. */ 1008 if(inet_autobind(sk)!=0) 1009 return(-EAGAIN); 1010 return(sk->prot->recvfrom(sk, (unsigned char *) ubuf, size, noblock, flags, 1011 (struct sockaddr_in*)sin, addr_len)); 1012 } inet_recvfrom 是 BSD 层 sock_recvfrom 函数的 INET 层实现函数,当用户调用 recvfrom 函数时, 该请求被传递给 sock_recvfrom 函数,而 sock_recvfrom 函数继续调用 inet_recvfrom 函数处理, 即该函数。从其实现来看,其继续调用下层(传输层)函数处理请求(对于 TCP 协议而言,即 tcp_recvfrom)。inet_recvfrom 函数在调用下层函数继续处理之前,进行了处理前检查,包括是 否定义了下层可调用函数,在这之前是否出现错误,以及该套接字是否绑定了本地地址(主要 是断口号)。 注意该函数参数:ubuf 表示读取数据的用户缓冲区,size 表示用户请求的数据长度,noblock 表 示在底层尚未读取到数据时是否进行等待,flags 是一些标志位的设置用于表示某种特定信息, 如 MSG_OOB 表示读取“带外”数据(一般底层将其解释为紧急数据),MSG_PEEK 标志位表 示只是预读取数据,数据被读取后底层仍然保存这些数据以便以后进行正式读取。 1013 static int inet_recv(struct socket *sock, void *ubuf, int size, int noblock, 1014 unsigned flags) 1015 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1016 /* BSD explicitly states these are the same - so we do it this way to be sure */ 1017 return inet_recvfrom(sock,ubuf,size,noblock,flags,NULL,NULL); 1018 } 该函数通过调用 inet_recvfrom 函数实现,本质与 inet_recvfrom,唯一不同之处在于该函数无需 返回数据包发送端的地址值,所以对应参数值设置为 NULL。 1019 static int inet_read(struct socket *sock, char *ubuf, int size, int noblock) 1020 { 1021 struct sock *sk = (struct sock *) sock->data; 1022 if(sk->err) 1023 return inet_error(sk); 1024 /* We may need to bind the socket. */ 1025 if(inet_autobind(sk)) 1026 return(-EAGAIN); 1027 return(sk->prot->read(sk, (unsigned char *) ubuf, size, noblock, 0)); 1028 } 能够调用该函数则一定是使用面向连接的协议,对于目前协议簇而言,一般情况下即指 TCP 协 议,所以在对 inet_autobind 函数的调用处理上稍微有些不同。inet_autobind 函数在检测到参数 对应的套接字尚未分配本地段口号时会自动分配一个端口号,对于 TCP 协议而言,由于需要一 个建立连接的过程,而在建立过程中一定会对本地地址进行绑定,所以对于使用 TCP 协议的套 接字此处对于 inet_autobind 的调用完全没有必要,而对于其它面向连接的协议类型,同样此处 对于 inet_autobind 函数的调用也无必要。对于 inet_read 函数的实现为代码更严密性考虑,倒是 应该添加如下一条语句: if (sk->prot->read==NULL) return (-EOPNOTSUPP); 以下三个函数是针对 INET 层数据发送而言的。基本实现方式是通过调用下层(传输层)函数 传递上层请求。在调用之前进行必要的检查。读者可参考以上对于数据读取函数的说明来对这 些数据发送函数进行分析。 1029 static int inet_send(struct socket *sock, void *ubuf, int size, int noblock, 1030 unsigned flags) 1031 { 1032 struct sock *sk = (struct sock *) sock->data; 1033 if (sk->shutdown & SEND_SHUTDOWN) 1034 { 1035 send_sig(SIGPIPE, current, 1); 1036 return(-EPIPE); 1037 } 1038 if(sk->err) 1039 return inet_error(sk); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1040 /* We may need to bind the socket. */ 1041 if(inet_autobind(sk)!=0) 1042 return(-EAGAIN); 1043 return(sk->prot->write(sk, (unsigned char *) ubuf, size, noblock, flags)); 1044 } 1045 static int inet_write(struct socket *sock, char *ubuf, int size, int noblock) 1046 { 1047 return inet_send(sock,ubuf,size,noblock,0); 1048 } 1049 static int inet_sendto(struct socket *sock, void *ubuf, int size, int noblock, 1050 unsigned flags, struct sockaddr *sin, int addr_len) 1051 { 1052 struct sock *sk = (struct sock *) sock->data; 1053 if (sk->shutdown & SEND_SHUTDOWN) 1054 { 1055 send_sig(SIGPIPE, current, 1); 1056 return(-EPIPE); 1057 } 1058 if (sk->prot->sendto == NULL) 1059 return(-EOPNOTSUPP); 1060 if(sk->err) 1061 return inet_error(sk); 1062 /* We may need to bind the socket. */ 1063 if(inet_autobind(sk)!=0) 1064 return -EAGAIN; 1065 return(sk->prot->sendto(sk, (unsigned char *) ubuf, size, noblock, flags, 1066 (struct sockaddr_in *)sin, addr_len)); 1067 } 1068 static int inet_shutdown(struct socket *sock, int how) 1069 { 1070 struct sock *sk=(struct sock*)sock->data; 1071 /* 1072 * This should really check to make sure 1073 * the socket is a TCP socket. (WHY AC...) 1074 */ 1075 how++; /* maps 0->1 has the advantage of making bit 1 rcvs and 1076 1->2 bit 2 snds. 1077 2->3 */ 1078 if ((how & ~SHUTDOWN_MASK) || how==0) /* MAXINT->0 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1079 return(-EINVAL); 1080 if (sock->state == SS_CONNECTING && sk->state == TCP_ESTABLISHED) 1081 sock->state = SS_CONNECTED; 1082 if (!tcp_connected(sk->state)) 1083 return(-ENOTCONN); 1084 sk->shutdown |= how; 1085 if (sk->prot->shutdown) 1086 sk->prot->shutdown(sk, how); 1087 return(0); 1088 } inet_shutdown 被上层 sock_shutdown 以及 sock_release 函数调用用于套接字的关闭。 参数 how 用于表示对套接字如何进行关闭操作:即进行半关闭还是全关闭。注意在进一步调用 下层函数进行处理之前,how 参数被加 1 处理;而且对套接字目前所处的状态进行验证,如果 套接字目前实际上并非处于“连接建立”状态(tcp_connected 函数返回 false),则错误返回, 因为不能对一个未连接套接字进行关闭操作。函数最后调用传输层函数(如 tcp_shutdown)进 行实际关闭操作。 1089 static int inet_select(struct socket *sock, int sel_type, select_table *wait ) 1090 { 1091 struct sock *sk=(struct sock *) sock->data; 1092 if (sk->prot->select == NULL) 1093 { 1094 return(0); 1095 } 1096 return(sk->prot->select(sk, sel_type, wait)); 1097 } inet_select 函数被上层 sock_select 函数调用,而 inet_select 进一步调用下层函数进行处理(如 tcp_select)。参数 sock 表示被进行检查的套接字,而 sel_type 表示检查的类型:SEL_IN(检查 是否有输入数据),SEL_OUT(检查是否有待发送数据),SEL_EX(检查是否出错以及是否有 紧急数据需要读取)。至于 select_table 结构类型的 wait 参数用于等待操作,该结构定义在 include/linux/wait.h 文件中,由于该结构不涉及到网络代码的本质部分,此处不做说明。 1098 /* 1099 * ioctl() calls you can issue on an INET socket. Most of these are 1100 * device configuration and stuff and very rarely used. Some ioctls 1101 * pass on to the socket itself. 1102 * 1103 * NOTE: I like the idea of a module for the config stuff. ie ifconfig 1104 * loads the devconfigure module does its configuring and unloads it. 1105 * There's a good 20K of config code hanging around the kernel. 1106 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1107 static int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg) 1108 { 1109 struct sock *sk=(struct sock *)sock->data; 1110 int err; 1111 switch(cmd) 1112 { 1113 case FIOSETOWN: 1114 case SIOCSPGRP: 1115 err=verify_area(VERIFY_READ,(int *)arg,sizeof(long)); 1116 if(err) 1117 return err; 1118 sk->proc = get_fs_long((int *) arg); 1119 return(0); 1120 case FIOGETOWN: 1121 case SIOCGPGRP: 1122 err=verify_area(VERIFY_WRITE,(void *) arg, sizeof(long)); 1123 if(err) 1124 return err; 1125 put_fs_long(sk->proc,(int *)arg); 1126 return(0); 1127 case SIOCGSTAMP: 1128 if(sk->stamp.tv_sec==0) 1129 return -ENOENT; 1130 err=verify_area(VERIFY_WRITE,(void *)arg,sizeof(struct timeval)); 1131 if(err) 1132 return err; 1133 memcpy_tofs((void *)arg,&sk->stamp,sizeof(struct timeval)); 1134 return 0; 1135 case SIOCADDRT: case SIOCADDRTOLD: 1136 case SIOCDELRT: case SIOCDELRTOLD: 1137 return(ip_rt_ioctl(cmd,(void *) arg)); 1138 case SIOCDARP: 1139 case SIOCGARP: 1140 case SIOCSARP: 1141 return(arp_ioctl(cmd,(void *) arg)); 1142 #ifdef CONFIG_INET_RARP 1143 case SIOCDRARP: 1144 case SIOCGRARP: 1145 case SIOCSRARP: 1146 return(rarp_ioctl(cmd,(void *) arg)); 1147 #endif 1148 case SIOCGIFCONF: 1149 case SIOCGIFFLAGS: 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1150 case SIOCSIFFLAGS: 1151 case SIOCGIFADDR: 1152 case SIOCSIFADDR: 1153 /* begin multicast support change */ 1154 case SIOCADDMULTI: 1155 case SIOCDELMULTI: 1156 /* end multicast support change */ 1157 case SIOCGIFDSTADDR: 1158 case SIOCSIFDSTADDR: 1159 case SIOCGIFBRDADDR: 1160 case SIOCSIFBRDADDR: 1161 case SIOCGIFNETMASK: 1162 case SIOCSIFNETMASK: 1163 case SIOCGIFMETRIC: 1164 case SIOCSIFMETRIC: 1165 case SIOCGIFMEM: 1166 case SIOCSIFMEM: 1167 case SIOCGIFMTU: 1168 case SIOCSIFMTU: 1169 case SIOCSIFLINK: 1170 case SIOCGIFHWADDR: 1171 case SIOCSIFHWADDR: 1172 case OLD_SIOCGIFHWADDR: 1173 case SIOCSIFMAP: 1174 case SIOCGIFMAP: 1175 case SIOCSIFSLAVE: 1176 case SIOCGIFSLAVE: 1177 return(dev_ioctl(cmd,(void *) arg)); 1178 default: 1179 if ((cmd >= SIOCDEVPRIVATE) && 1180 (cmd <= (SIOCDEVPRIVATE + 15))) 1181 return(dev_ioctl(cmd,(void *) arg)); 1182 if (sk->prot->ioctl==NULL) 1183 return(-EINVAL); 1184 return(sk->prot->ioctl(sk, cmd, arg)); 1185 } 1186 /*NOTREACHED*/ 1187 return(0); 1188 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 inet_ioctl 函数用于对套接字选项进行控制。该函数根据需要改变的不同选项下发任务到具体的 下层模块进行处理,如对于 ARP 协议中使用的地址映射项目进行添加和删除操作时,则调用 arp_ioctl 下层函数进行处理,而对设备进行控制时则调用 dev_ioctl 函数进行处理等等。 1189 /* 1190 * This routine must find a socket given a TCP or UDP header. 1191 * Everything is assumed to be in net order. 1192 * 1193 * We give priority to more closely bound ports: if some socket 1194 * is bound to a particular foreign address, it will get the packet 1195 * rather than somebody listening to any address.. 1196 */ 1197 struct sock *get_sock(struct proto *prot, unsigned short num, 1198 unsigned long raddr, 1199 unsigned short rnum, unsigned long laddr) 1200 { 1201 struct sock *s; 1202 struct sock *result = NULL; 1203 int badness = -1; 1204 unsigned short hnum; 1205 hnum = ntohs(num); 1206 /* 1207 * SOCK_ARRAY_SIZE must be a power of two. This will work better 1208 * than a prime unless 3 or more sockets end up using the same 1209 * array entry. This should not be a problem because most 1210 * well known sockets don't overlap that much, and for 1211 * the other ones, we can just be careful about picking our 1212 * socket number when we choose an arbitrary one. 1213 */ 1214 for(s = prot->sock_array[hnum & (SOCK_ARRAY_SIZE - 1)]; 1215 s != NULL; s = s->next) 1216 { 1217 int score = 0; 1218 if (s->num != hnum) 1219 continue; 1220 if(s->dead && (s->state == TCP_CLOSE)) 1221 continue; 1222 /* local address matches? */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1223 if (s->saddr) { 1224 if (s->saddr != laddr) 1225 continue; 1226 score++; 1227 } 1228 /* remote address matches? */ 1229 if (s->daddr) { 1230 if (s->daddr != raddr) 1231 continue; 1232 score++; 1233 } 1234 /* remote port matches? */ 1235 if (s->dummy_th.dest) { 1236 if (s->dummy_th.dest != rnum) 1237 continue; 1238 score++; 1239 } 1240 /* perfect match? */ 1241 if (score == 3) 1242 return s; 1243 /* no, check if this is the best so far.. */ 1244 if (score <= badness) 1245 continue; 1246 result = s; 1247 badness = score; 1248 } 1249 return result; 1250 } get_sock 函数虽然看起来较长,原理上非常简单,即根据本地地址和远端地址查找本地套接字 对应的 sock 结构。从前文中可知,对于适用同一种协议的所有套接字都被插入到该协议对应 proto(如 tcp_prot)结构中 sock_array 数组中。所以该函数即直接以本地地址和远端地址为关键 字对对应协议中 sock_array 数组进行查找。注意 sock_array 是一个数组,其数组中每个元素均 对应一个 sock 结构队列,数组元素用本地地址端口号进行索引。即 get_sock 函数中本地端口号 用于索引 sock_array 元素(即寻找 sock 结构队列),其它参数:本地 IP 地址,远端端口号和 IP 地址用于队列中对每个 sock 结构进行匹配。 1251 /* 1252 * Deliver a datagram to raw sockets. 1253 */ 1254 struct sock *get_sock_raw(struct sock *sk, 1255 unsigned short num, 1256 unsigned long raddr, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1257 unsigned long laddr) 1258 { 1259 struct sock *s; 1260 s=sk; 1261 for(; s != NULL; s = s->next) 1262 { 1263 if (s->num != num) 1264 continue; 1265 if(s->dead && (s->state == TCP_CLOSE)) 1266 continue; 1267 if(s->daddr && s->daddr!=raddr) 1268 continue; 1269 if(s->saddr && s->saddr!=laddr) 1270 continue; 1271 return(s); 1272 } 1273 return(NULL); 1274 } 对于 raw 套接字专门有一个 proto 结构变量 raw_prot 进行收集:即所有使用 raw 类型的套接字 均插入到 raw_prot 对应 proto 结构中 sock_array 数组中。从 ip.c 文件中 ip_rcv 函数中摘取代码片 断可得知 get_sock_raw 函数被调用的环境。 /*以下代码片断摘自 ip.c 文件 ip_rcv 函数*/ //iph 表示所接收数据包中 IP 首部。 //对于 raw 类型套接字而言,IP 首部中 protocol 字段用于索引对应的 sock_array 数组中元素。 hash = iph->protocol & (SOCK_ARRAY_SIZE-1); if((raw_sk=raw_prot.sock_array[hash])!=NULL) { struct sock *sknext=NULL; struct sk_buff *skb1; raw_sk=get_sock_raw(raw_sk, hash, iph->saddr, iph->daddr); if(raw_sk) /* Any raw sockets */ { …… 在寻找到对应的 sock 结构队列后,之后的操作即进行具体匹配。 1275 #ifdef CONFIG_IP_MULTICAST 1276 /* 1277 * Deliver a datagram to broadcast/multicast sockets. 1278 */ 1279 struct sock *get_sock_mcast(struct sock *sk, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1280 unsigned short num, 1281 unsigned long raddr, 1282 unsigned short rnum, unsigned long laddr) 1283 { 1284 struct sock *s; 1285 unsigned short hnum; 1286 hnum = ntohs(num); 1287 /* 1288 * SOCK_ARRAY_SIZE must be a power of two. This will work better 1289 * than a prime unless 3 or more sockets end up using the same 1290 * array entry. This should not be a problem because most 1291 * well known sockets don't overlap that much, and for 1292 * the other ones, we can just be careful about picking our 1293 * socket number when we choose an arbitrary one. 1294 */ 1295 s=sk; 1296 for(; s != NULL; s = s->next) 1297 { 1298 if (s->num != hnum) 1299 continue; 1300 if(s->dead && (s->state == TCP_CLOSE)) 1301 continue; 1302 if(s->daddr && s->daddr!=raddr) 1303 continue; 1304 if (s->dummy_th.dest != rnum && s->dummy_th.dest != 0) 1305 continue; 1306 if(s->saddr && s->saddr!=laddr) 1307 continue; 1308 return(s); 1309 } 1310 return(NULL); 1311 } 1312 #endif get_sock_mcast 函数实现本上并不复杂,读者对此应该没有问题。该函数在 udp_rcv 函数中被调 用用于处理多播数据包,以下代码片断摘自 udp_rcv 函数: /*net/inet/udp.c-udp_rcv*/ #ifdef CONFIG_IP_MULTICAST if (addr_type!=IS_MYADDR) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 { /* * Multicasts and broadcasts go to each listener. */ struct sock *sknext=NULL; sk=get_sock_mcast(udp_prot.sock_array[ntohs(uh->dest)&(SOCK_ARRAY_SIZE-1)], uh->dest, saddr, uh->source, daddr); if(sk) { do { struct sk_buff *skb1; sknext=get_sock_mcast(sk->next, uh->dest, saddr, uh->source, daddr); if(sknext) skb1=skb_clone(skb,GFP_ATOMIC); else skb1=skb; if(skb1) udp_deliver(sk, uh, skb1, dev,saddr,daddr,len); sk=sknext; } while(sknext!=NULL); } else kfree_skb(skb, FREE_READ); return 0; } #endif 其中 uh 表示所接收数据包 UDP 首部指针,所以 uh->dest 表示本地通信端口号,该数字被作为 索引寻址对应的 sock 队列。注意如果队列中有不止一个 sock 结构满足条件(即有多个套接字 加入了同一个多播组),则将复制数据包,以便给每个套接字传送一份该数据包数据。 以下定义 INET 域操作函数集。这是由一个 proto_ops 结构表示的。对于 INET 域而言,该操作 函数集由 inet_proto_ops 变量表示,对于 UNIX 域,则对应变量为 unix_proto_ops。读者可以比 较前文中对 socket.c 文件的分析,从中可以看出 socket.c 文件中定义的大量函数在此都有对应的 函数,如 sock_bind 对应 inet_bind,等等,即 af_inet.c 文件中定义的函数将作为 INET 层处理自 1313 static struct proto_ops inet_proto_ops = { 1314 AF_INET, 1315 inet_create, 1316 inet_dup, 1317 inet_release, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1318 inet_bind, 1319 inet_connect, 1320 inet_socketpair, 1321 inet_accept, 1322 inet_getname, 1323 inet_read, 1324 inet_write, 1325 inet_select, 1326 inet_ioctl, 1327 inet_listen, 1328 inet_send, 1329 inet_recv, 1330 inet_sendto, 1331 inet_recvfrom, 1332 inet_shutdown, 1333 inet_setsockopt, 1334 inet_getsockopt, 1335 inet_fcntl, 1336 }; af_inet.c 文件定义的最后一个函数作为 INET 层的初始化函数。其初始化对上,下层的接口和变 量。 seq_offset 全局变量为本地套接字创建初始序列号。为了区分不同套接字的数据包,在新建一个 套接字时,分配给其的初始序列号是由该全局变量进行统一同步的。如此可以尽量避免使用重 叠的序列号。 1337 extern unsigned long seq_offset; 1338 /* 1339 * Called by socket.c on kernel startup. 1340 */ 1341 void inet_proto_init(struct net_proto *pro) 1342 { 1343 struct inet_protocol *p; 1344 int i; 1345 printk("Swansea University Computer Society TCP/IP for NET3.019\n"); 1346 /* 1347 * Tell SOCKET that we are alive... 1348 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 //注册 INET 域操作函数集,域值为 AF_INET. //inet_proto_ops 将被插入到 pops 全局数组中。具体情况请参见前文中对 sock_register 函数 //的分析。 1349 (void) sock_register(inet_proto_ops.family, &inet_proto_ops); //初始化 seq_offset,CURRENT_TIME 表示当前时间值。 1350 seq_offset = CURRENT_TIME*250; 1351 /* 1352 * Add all the protocols. 1353 */ 1354 for(i = 0; i < SOCK_ARRAY_SIZE; i++) 1355 { 1356 tcp_prot.sock_array[i] = NULL; 1357 udp_prot.sock_array[i] = NULL; 1358 raw_prot.sock_array[i] = NULL; 1359 } 1360 tcp_prot.inuse = 0; 1361 tcp_prot.highestinuse = 0; 1362 udp_prot.inuse = 0; 1363 udp_prot.highestinuse = 0; 1364 raw_prot.inuse = 0; 1365 raw_prot.highestinuse = 0; 1366 printk("IP Protocols: "); //将静态定义的传输层协议加入 inet_protos 数组中。 //静态定义的传输层协议是由 inet_protocol_base 变量指向的一个 inet_protocol 结构类型 //的队列。具体情况参考下文中说明。 1367 for(p = inet_protocol_base; p != NULL;) 1368 { 1369 struct inet_protocol *tmp = (struct inet_protocol *) p->next; 1370 inet_add_protocol(p); 1371 printk("%s%s",p->name,tmp?", ":"\n"); 1372 p = tmp; 1373 } 1374 /* 1375 * Set the ARP module up 1376 */ 1377 arp_init(); 1378 /* 1379 * Set the IP module up 1380 */ 1381 ip_init(); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1382 } 函数最后调用 arp_init,ip_init 函数对地址解析层和 IP 层进行初始化。在下文中分析到对应文件 中将对这两个函数进行说明。 以下代码片断摘自 protocol.c 文件。tcp_protocl 变量定义了 TCP 协议的正常以及错误数据包接 收函数。其它类似。网络层函数在处理完本层需要的工作后,将根据数据包使用的具体传输层 协议查询 inet_protos 数组,从而将数据包传送给对应的接收函数进行上层的处理。 /*摘自 net/inet/protocol.c 文件*/ 44 static struct inet_protocol tcp_protocol = { 45 tcp_rcv, /* TCP handler */ 46 NULL, /* No fragment handler (and won't be for a long time) */ 47 tcp_err, /* TCP error control */ 48 NULL, /* next */ 49 IPPROTO_TCP, /* protocol ID */ 50 0, /* copy */ 51 NULL, /* data */ 52 "TCP" /* name */ 53 }; 54 static struct inet_protocol udp_protocol = { 55 udp_rcv, /* UDP handler */ 56 NULL, /* Will be UDP fraglist handler */ 57 udp_err, /* UDP error control */ 58 &tcp_protocol, /* next */ 59 IPPROTO_UDP, /* protocol ID */ 60 0, /* copy */ 61 NULL, /* data */ 62 "UDP" /* name */ 63 }; 64 static struct inet_protocol icmp_protocol = { 65 icmp_rcv, /* ICMP handler */ 66 NULL, /* ICMP never fragments anyway */ 67 NULL, /* ICMP error control */ 68 &udp_protocol, /* next */ 69 IPPROTO_ICMP, /* protocol ID */ 70 0, /* copy */ 71 NULL, /* data */ 72 "ICMP" /* name */ 73 }; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 74 #ifndef CONFIG_IP_MULTICAST 75 struct inet_protocol *inet_protocol_base = &icmp_protocol; 76 #else 77 static struct inet_protocol igmp_protocol = { 78 igmp_rcv, /* IGMP handler */ 79 NULL, /* IGMP never fragments anyway */ 80 NULL, /* IGMP error control */ 81 &icmp_protocol, /* next */ 82 IPPROTO_IGMP, /* protocol ID */ 83 0, /* copy */ 84 NULL, /* data */ 85 "IGMP" /* name */ 86 }; 87 struct inet_protocol *inet_protocol_base = &igmp_protocol; 88 #endif 89 struct inet_protocol *inet_protos[MAX_INET_PROTOS] = { 90 NULL 91 }; af_inet.c 文件小结 af_inet.c 文件作为 INET 层处理函数定义文件,处理来自 BSD 层的请求,并在完成本层相应的 检查工作后继续将请求发送给下层传输层函数进行具体的处理。这种分层实现方式以及实现中 函数指针的使用使得程序具有极大的伸缩性和可扩展性。程序功能的模块化和层次化是网络代 码的基本特点。由于 INET 层还只是作为请求的过渡层,并不进行请求的实际处理,所以该层 实现函数相对较为简单,在下文中我们将逐渐深入网络代码的实质部分。为了保持层次的连贯 性,接下来即以 TCP 协议实现文件 tcp.c 为分析对象分析传输层函数的实现。 2.4 net/inet/tcp.c 文件 tcp.c 文件应该是网络栈实现中代码最长的文件,达 5 千多行,这也从一方面验证了 TCP 协议的 相对复杂性。TCP 协议提供一种面向连接的可靠性数据传输。所谓面向连接只是可靠性的一个 方面。为了保证数据传输的可靠性,使用 TCP 协议的套接字在正式传送数据前必须首先建立与 远端的连接通道。当然由于数据传输是经由传输线(如双绞线,同轴电缆等),所以这种连接事 实上是一种虚拟通道,所谓的建立连接更准确的说应该是通信双方需要在传输数据之前交换某 种重要信息。而这种信息的预先交换则是保证可靠性的基本条件。如 UDP 协议无需可靠新保证, 其也就无须在传送数据报文之前建立连接。从 TCP 协议保证可靠性数据传输的实现来看,超时 重传,数据应答,序列号这三者是实现可靠性的基本保证。而连接建立过程中一个最为重要信 息的交换即是双方序列号的交换。序列号被用于 TCP 通信中的整个过程,双方传送的每个数据 都有一个序列号对应。而为解决传输线路的不稳定性所造成的数据包丢失问题,TCP 协议使用 发送方超时重传和接收方数据应答的策略。具体情况请参考附录 A-TCP 协议可靠性数据传输底 层实现方法分析。建议读者在继续下文之前,先预读附录 A,这样将有助于下文中对于代码的 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 理解。此处需要提及的几点是: 1> 数据应答是累积的,不可进行跨越式应答。 所谓“不可进行跨越式应答”是指,如果接收到的数据存在“空洞”,则发送应答序列号应 当是对这些“空洞”中数据的请求,而不能跨越这个“空洞”,对空洞之后的数据进行应答, 这样会造成数据的真正丢失。由数据“空洞”衍生的其它议题如快速重传/恢复机制。 2> 流量控制是 TCP 协议一个重要方面。 流量控制即控制传输线路中传输的数据包总数。有时由于数据包的暂时延迟造成发送方超 时重传和接收方启动快速重传机制(启动是自动进行的,通过不断的发送具有相同序列号 的应答数据包完成),从而造成线路中充斥着大量数据包,而这些大量进入的数据包又进一 步减低线路性能,而接收方和发送方又不断地发送数据包到线路上,从而陷入恶性循环。 流量控制是避免此类问题而设计的。在实现上,一方面通过使用“窗口”,另一方面通过使 用一些算法来完成。在下文中分析到相应函数时将进行具体的阐述。 使用 TCP 协议进行通信的套接字在不同阶段根据接收数据包内容将处于不同的状态,形成了 TCP 协议中特有的状态机。故在具体分析 tcp.c 文件中定义函数之前,首先简单介绍一下使用 TCP 协议的套接字所经过的各种不同状态(虽然前文中已有相关阐述)。 以下各种状态的中文解释为直译,一般我们在讨论 TCP 状态时,以英文单词为准以避免误解。 TCP_CLOSED 关闭状态:一个新建的 TCP 套接字起始将处于该状态。 TCP_LISTEN 监听状态:一般服务器端套接字在调用 listen 系统调用后即处于该状态。 TCP_SYN_SENT 同步信号已发送状态:该状态指客户端发送 SYN(建立连接的同步)数据包后所处的状态。 在接收到远端服务器端应答后,即从该状态进入 TCP_ESTABLISHED 状态。 TCP_SYN_RECEIVED 同步信号已接收状态:服务器端在接收到远端客户端 SYN 数据包后,进行相应的处理(创建通 信套接字等)后,发送应答数据包,并将新创建的通信套接字状态设置为 TCP_SYN_RECEIVED, 在接收到客户端的应答后,即进入 TCP_ESTABLISHED 状态。 TCP_ESTABLISHED 连接建立状态,这是双方进行正常的数据传送所处的状态。 TCP_FIN_WAIT_1 本地发送 FIN(用于结束连接的)数据包后即进入该状态,等待对方的应答。一般一端发送完 其所要发送的数据后,即可发送 FIN 数据包,此时发送通道被关闭,但仍可以继续接收远端发 送的数据包。在接收到远端发送的对于 FIN 数据包的应答后,将进入 TCP_FIN_WAIT_2 状态。 TCP_FIN_WAIT_2 进入该状态表示本地已接收到远端发送的对于本地之前发送的 FIN 数据包的应答。进入该状态 后,本地仍然可以继续接收远端发送给本地的数据包。在接收到远端发送的 FIN 数据包后(表 示远端也已经发送完数据),本地将发送一个应答数据包,并进入 TCP_TIME_WAIT 状态。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 TCP_TIME_WAIT 状态存在的时间被称为 2MSL 时间,这一方面是为避免本地发送的应答数据 包丢失,另一方面避免一个新创建的套接字接收到旧套接字中遗留的数据包。 TCP_TIME_WAIT 该状态被称为 2MSL 等待状态。如果在此期间接收到远端发送的 FIN 数据包,则表示之前在 TCP_FIN_WAIT_2 状态发送的 ACK 应答数据包在传输中丢失或者长时间被延迟,从而造成远 端重新发送了 FIN 数据包,此时重发 ACK 应答数据包。一旦 2MSL 时间到期,则将进入 TCP_CLOSED 状态,即完成关闭操作。 TCP_CLOSE_WAIT 该状态存在于后关闭的一端。当接收到远端发送的 FIN 数据包后,本地发送一个 ACK 应答数 据包,并将套接字状态从 TCP_ESTABLISHED 设置为 TCP_CLOSE_WAIT。本地可继续向远端 发送数据包,在发送完所有数据后,本地将发送一个 FIN 数据包关闭本地发送通道,并将状态 设置为 TCP_LAST_ACK 状态,等待远端对 FIN 数据包的应答数据包。 TCP_CLOSING 如果通信双方同时发送 FIN 数据包,即同时进行关闭操作,则双方将同时进入 TCP_CLOSING 状态。具体的,本地发送一个 FIN 数据包以结束本地数据包发送,如果在等待应答期间,接收 到远端发送的 FIN 数据包,则本地将状态设置为 TCP_CLOSING 状态。在接收到应答后,再继 续转入到 TCP_CLOSE_WAIT 状态。 TCP_LAST_ACK 作为后关闭的一方,在发送 FIN 数据包后,即进入 TCP_LAST_ACK 状态。此时等待远端发送 应答数据包,在接收到应答数据包后,即完成关闭操作,进入 TCP_CLOSED 状态。 以下进入 tcp.c 文件分析。 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * Implementation of the Transmission Control Protocol(TCP). 7 * 8 * Version: @(#)tcp.c 1.0.16 05/25/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * Mark Evans, 13 * Corey Minyard 14 * Florian La Roche, 15 * Charles Hedrick, 16 * Linus Torvalds, 17 * Alan Cox, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 18 * Matthew Dillon, 19 * Arnt Gulbrandsen, 20 * 21 * Fixes: 22 * Alan Cox : Numerous verify_area() calls 23 * Alan Cox : Set the ACK bit on a reset 24 * Alan Cox : Stopped it crashing if it closed while sk->inuse=1 25 * and was trying to connect (tcp_err()). 26 * Alan Cox : All icmp error handling was broken 27 * pointers passed where wrong and the 28 * socket was looked up backwards. Nobody 29 * tested any icmp error code obviously. 30 * Alan Cox : tcp_err() now handled properly. It wakes people 31 * on errors. select behaves and the icmp error race 32 * has gone by moving it into sock.c 33 * Alan Cox : tcp_reset() fixed to work for everything not just 34 * packets for unknown sockets. 35 * Alan Cox : tcp option processing. 36 * Alan Cox : Reset tweaked (still not 100%) [Had syn rule wrong] 37 * Herp Rosmanith : More reset fixes 38 * Alan Cox : No longer acks invalid rst frames. Acking 39 * any kind of RST is right out. 40 * Alan Cox : Sets an ignore me flag on an rst receive 41 * otherwise odd bits of prattle escape still 42 * Alan Cox : Fixed another acking RST frame bug. Should stop 43 * LAN workplace lockups. 44 * Alan Cox : Some tidyups using the new skb list facilities 45 * Alan Cox : sk->keepopen now seems to work 46 * Alan Cox : Pulls options out correctly on accepts 47 * Alan Cox : Fixed assorted sk->rqueue->next errors 48 * Alan Cox : PSH doesn't end a TCP read. Switched a bit to skb ops. 49 * Alan Cox : Tidied tcp_data to avoid a potential nasty. 50 * Alan Cox : Added some better commenting, as the tcp is hard to follow 51 * Alan Cox : Removed incorrect check for 20 * psh 52 * Michael O'Reilly : ack < copied bug fix. 53 * Johannes Stille : Misc tcp fixes (not all in yet). 54 * Alan Cox : FIN with no memory -> CRASH 55 * Alan Cox : Added socket option proto entries. Also added awareness of them to accept. 56 * Alan Cox : Added TCP options (SOL_TCP) 57 * Alan Cox : Switched wakeup calls to callbacks, so the kernel can layer network sockets. 58 * Alan Cox : Use ip_tos/ip_ttl settings. 59 * Alan Cox : Handle FIN (more) properly (we hope). 60 * Alan Cox : RST frames sent on unsynchronised state ack error/ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 61 * Alan Cox : Put in missing check for SYN bit. 62 * Alan Cox : Added tcp_select_window() aka NET2E 63 * window non shrink trick. 64 * Alan Cox : Added a couple of small NET2E timer fixes 65 * Charles Hedrick : TCP fixes 66 * Toomas Tamm : TCP window fixes 67 * Alan Cox : Small URG fix to rlogin ^C ack fight 68 * Charles Hedrick : Rewrote most of it to actually work 69 * Linus : Rewrote tcp_read() and URG handling 70 * completely 71 * Gerhard Koerting: Fixed some missing timer handling 72 * Matthew Dillon : Reworked TCP machine states as per RFC 73 * Gerhard Koerting: PC/TCP workarounds 74 * Adam Caldwell : Assorted timer/timing errors 75 * Matthew Dillon : Fixed another RST bug 76 * Alan Cox : Move to kernel side addressing changes. 77 * Alan Cox : Beginning work on TCP fastpathing (not yet usable) 78 * Arnt Gulbrandsen: Turbocharged tcp_check() routine. 79 * Alan Cox : TCP fast path debugging 80 * Alan Cox : Window clamping 81 * Michael Riepe : Bug in tcp_check() 82 * Matt Dillon : More TCP improvements and RST bug fixes 83 * Matt Dillon : Yet more small nasties remove from the TCP code 84 * (Be very nice to this man if tcp finally works 100%) 8) 85 * Alan Cox : BSD accept semantics. 86 * Alan Cox : Reset on closedown bug. 87 * Peter De Schrijver : ENOTCONN check missing in tcp_sendto(). 88 * Michael Pall : Handle select() after URG properly in all cases. 89 * Michael Pall : Undo the last fix in tcp_read_urg() (multi URG PUSH broke rlogin). 90 * Michael Pall : Fix the multi URG PUSH problem in tcp_readable(), select() after URG works now. 91 * Michael Pall : recv(...,MSG_OOB) never blocks in the BSD api. 92 * Alan Cox : Changed the semantics of sk->socket to 93 * fix a race and a signal problem with 94 * accept() and async I/O. 95 * Alan Cox : Relaxed the rules on tcp_sendto(). 96 * Yury Shevchuk : Really fixed accept() blocking problem. 97 * Craig I. Hagan : Allow for BSD compatible TIME_WAIT for 98 * clients/servers which listen in on 99 * fixed ports. 100 * Alan Cox : Cleaned the above up and shrank it to 101 * a sensible code size. 102 * Alan Cox : Self connect lockup fix. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 103 * Alan Cox : No connect to multicast. 104 * Ross Biro : Close unaccepted children on master 105 * socket close. 106 * Alan Cox : Reset tracing code. 107 * Alan Cox : Spurious resets on shutdown. 108 * Alan Cox : Giant 15 minute/60 second timer error 109 * Alan Cox : Small whoops in selecting before an accept. 110 * Alan Cox : Kept the state trace facility since it's 111 * handy for debugging. 112 * Alan Cox : More reset handler fixes. 113 * Alan Cox : Started rewriting the code based on the RFC's 114 * for other useful protocol references see: 115 * Comer, KA9Q NOS, and for a reference on the 116 * difference between specifications and how BSD 117 * works see the 4.4lite source. 118 * A.N.Kuznetsov : Don't time wait on completion of tidy 119 * close. 120 * Linus Torvalds : Fin/Shutdown & copied_seq changes. 121 * Linus Torvalds : Fixed BSD port reuse to work first syn 122 * Alan Cox : Reimplemented timers as per the RFC and using multiple 123 * timers for sanity. 124 * Alan Cox : Small bug fixes, and a lot of new 125 * comments. 126 * Alan Cox : Fixed dual reader crash by locking 127 * the buffers (much like datagram.c) 128 * Alan Cox : Fixed stuck sockets in probe. A probe 129 * now gets fed up of retrying without 130 * (even a no space) answer. 131 * Alan Cox : Extracted closing code better 132 * Alan Cox : Fixed the closing state machine to 133 * resemble the RFC. 134 * Alan Cox : More 'per spec' fixes. 135 * Alan Cox : tcp_data() doesn't ack illegal PSH 136 * only frames. At least one pc tcp stack 137 * generates them. 138 * 139 * 140 * To Fix: 141 * Fast path the code. Two things here - fix the window calculation 142 * so it doesn't iterate over the queue, also spot packets with no funny 143 * options arriving in order and process directly. 144 * 145 * Implement RFC 1191 [Path MTU discovery] 146 * Look at the effect of implementing RFC 1337 suggestions and their impact. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 147 * Rewrite output state machine to use a single queue and do low window 148 * situations as per the spec (RFC 1122) 149 * Speed up input assembly algorithm. 150 * RFC1323 - PAWS and window scaling. PAWS is required for IPv6 so we 151 * could do with it working on IPv4 152 * User settable/learned rtt/max window/mtu 153 * Cope with MTU/device switches when retransmitting in tcp. 154 * Fix the window handling to use PR's new code. 155 * 156 * Change the fundamental structure to a single send queue maintained 157 * by TCP (removing the bogus ip stuff [thus fixing mtu drops on 158 * active routes too]). Cut the queue off in tcp_retransmit/ 159 * tcp_transmit. 160 * Change the receive queue to assemble as it goes. This lets us 161 * dispose of most of tcp_sequence, half of tcp_ack and chunks of 162 * tcp_data/tcp_read as well as the window shrink crud. 163 * Separate out duplicated code - tcp_alloc_skb, tcp_build_ack 164 * tcp_queue_skb seem obvious routines to extract. 165 * 166 * This program is free software; you can redistribute it and/or 167 * modify it under the terms of the GNU General Public License 168 * as published by the Free Software Foundation; either version 169 * 2 of the License, or(at your option) any later version. 170 * 171 * Description of States: 172 * 173 * TCP_SYN_SENT sent a connection request, waiting for ack 174 * 175 * TCP_SYN_RECV received a connection request, sent ack, 176 * waiting for final ack in three-way handshake. 177 * 178 * TCP_ESTABLISHED connection established 179 * 180 * TCP_FIN_WAIT1 our side has shutdown, waiting to complete 181 * transmission of remaining buffered data 182 * 183 * TCP_FIN_WAIT2 all buffered data sent, waiting for remote 184 * to shutdown 185 * 186 * TCP_CLOSING both sides have shutdown but we still have 187 * data we have to finish sending 188 * 189 * TCP_TIME_WAIT timeout to catch resent junk before entering 190 * closed, can only be entered from FIN_WAIT2 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 191 * or CLOSING. Required because the other end 192 * may not have gotten our last ACK causing it 193 * to retransmit the data packet (which we ignore) 194 * 195 * TCP_CLOSE_WAIT remote side has shutdown and is waiting for 196 * us to finish writing our data and to shutdown 197 * (we have to close() to move on to LAST_ACK) 198 * 199 * TCP_LAST_ACK out side has shutdown after remote has 200 * shutdown. There may still be data in our 201 * buffer that we have to finish sending 202 * 203 * TCP_CLOSE socket is finished 204 */ 205 #include 206 #include 207 #include 208 #include 209 #include 210 #include 211 #include 212 #include 213 #include 214 #include 215 #include 216 #include 217 #include 218 #include "snmp.h" 219 #include "ip.h" 220 #include "protocol.h" 221 #include "icmp.h" 222 #include "tcp.h" 223 #include "arp.h" 224 #include 225 #include "sock.h" 226 #include "route.h" 227 #include 228 #include 229 #include 230 #include 231 #include 232 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 233 * The MSL timer is the 'normal' timer. 234 */ 235 #define reset_msl_timer(x,y,z) reset_timer(x,y,z) 236 #define SEQ_TICK 3 237 unsigned long seq_offset; 238 struct tcp_mib tcp_statistics; 239 static void tcp_close(struct sock *sk, int timeout); 注意到此处的 seq_offset 变量,在 af_inet.c 文件有对此变量的引用,并在 inet_proto_init 函数中 对该变量进行了初始化。tcp_statistics 是一个 tcp_mib 结构类型,是对 RFC2233 的信息统计实现。 另外是对 tcp_close 和 reset_msl_timer 的声明或定义。reset_timer 函数用一个新的值重新设置定 时器。 240 /* 241 * The less said about this the better, but it works and will do for 1.2 242 */ 243 static struct wait_queue *master_select_wakeup; 等待队列定义。等待队列用于进程在等待某个条件时暂时休眠之地。wait_queue 结构定义如下: // linux/wait.h 7 struct wait_queue { 8 struct task_struct * task; 9 struct wait_queue * next; 10 }; 244 static __inline__ int min(unsigned int a, unsigned int b) 245 { 246 if (a < b) 247 return(a); 248 return(b); 249 } 250 #undef STATE_TRACE 251 #ifdef STATE_TRACE 252 static char *statename[]={ 253 "Unused","Established","Syn Sent","Syn Recv", 254 "Fin Wait 1","Fin Wait 2","Time Wait", "Close", 255 "Close Wait","Last ACK","Listen","Closing" 256 }; 257 #endif 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 258 static __inline__ void tcp_set_state(struct sock *sk, int state) 259 { 260 if(sk->state==TCP_ESTABLISHED) 261 tcp_statistics.TcpCurrEstab--; 262 #ifdef STATE_TRACE 263 if(sk->debug) 264 printk("TCP sk=%p, State %s -> %s\n",sk, statename[sk->state],statename[state]); 265 #endif 266 /* This is a hack but it doesn't occur often and it's going to 267 be a real to fix nicely */ 268 if(state==TCP_ESTABLISHED && sk->state==TCP_SYN_RECV) 269 { 270 wake_up_interruptible(&master_select_wakeup); 271 } 272 sk->state=state; 273 if(state==TCP_ESTABLISHED) 274 tcp_statistics.TcpCurrEstab++; 275 } tcp_set_state 函数用于设置套接字状态,参数 state 为新的套接字状态。注意函数实现的开头和 结尾形成的对称,从而保证统计信息的准确性。如果新的状态为 TCP_ESTABLISHED 并且原来 状态为 TCP_SYN_RECV,表示套接字完成连接。从代码的实现来看,此时将睡眠队列 master_select_wakeup 等待的任务唤醒。由于之前的状态为 TCP_SYN_RECV,所以这种行为一 般发生在服务器端,如果从服务器的角度来分析则不难理解。对于服务器端套接字,只有在调 用 accept 函数后,才进行真正的通信。如果在调用 accept 函数之前,没有客户端请求连接,则 服务器端进行 accept 函数调用的任务将等待,即被挂入 master_select_wakeup 队列中。此处在一 个套接字完成建立后,accept 函数即可返回,此时就需要唤醒 master_select_wakeup 队列中的任 务。 276 /* 277 * This routine picks a TCP windows for a socket based on 278 * the following constraints 279 * 280 * 1. The window can never be shrunk once it is offered (RFC 793) 281 * 2. We limit memory per socket 282 * 283 * For now we use NET2E3's heuristic of offering half the memory 284 * we have handy. All is not as bad as this seems however because 285 * of two things. Firstly we will bin packets even within the window 286 * in order to get the data we are waiting for into the memory limit. 287 * Secondly we bin common duplicate forms at receive time 288 * Better heuristics welcome 289 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 290 int tcp_select_window(struct sock *sk) 291 { 292 int new_window = sk->prot->rspace(sk); 293 if(sk->window_clamp) 294 new_window=min(sk->window_clamp,new_window); 295 /* 296 * Two things are going on here. First, we don't ever offer a 297 * window less than min(sk->mss, MAX_WINDOW/2). This is the 298 * receiver side of SWS as specified in RFC1122. 299 * Second, we always give them at least the window they 300 * had before, in order to avoid retracting window. This 301 * is technically allowed, but RFC1122 advises against it and 302 * in practice it causes trouble. 303 * 304 * Fixme: This doesn't correctly handle the case where 305 * new_window > sk->window but not by enough to allow for the 306 * shift in sequence space. 307 */ 308 if (new_window < min(sk->mss, MAX_WINDOW/2) || new_window < sk->window) 309 return(sk->window); 310 return(new_window); 311 } tcp_select_window 窗口选择函数。所谓窗口即对所接收数据包数量的一种限制。在本地发送的 每个数据包中 TCP 首部都会包含一个本地声明的窗口大小,远端应节制其数据包发送不可超过 本地通报的窗口大小。窗口大小以字节数为单位。窗口大小的设置需要考虑到以下两个因素: 1> RFC793 文档(TCP 协议文档)强烈推荐不可降低窗口大小值。 2> 窗口大小的设置应考虑到本地接收缓冲区的大小。 函数实现首先调用 sk->prot->rspace 指向的函数(sock_rspace)查看接收缓冲区空闲区域的大小。 之后检查 sock 结构 window_clamp 字段是否为 0,如不为 0,则表示本地进行窗口节制,此时取 节制值与空闲缓冲区大小中之较小值。然后进一步检查这个较小值是否小于最大报文长度值(一 般 MAX_WINDOW/2>MSS),如是,则返回原窗口值。这是优化数据包传送的一种方式。MSS 表示最大报文长度值,如果本地通报给远端的窗口值小于 MSS 值,则会造成小数据包的传送, 大量小数据包充斥传输线路,对于充分利用传送线路不利,所以如果新的窗口值小于 MSS 值, 则继续通报旧的窗口值。另外 RFC793 要求避免使用降低窗口大小策略,所以如果新的窗口值 小于原有的窗口值,则仍然使用原窗口值进行通报。当这些条件都不成立时(即新窗口值大于 MSS 值,且大于原窗口值),方才通报新的窗口值给远端。 312 /* 313 * Find someone to 'accept'. Must be called with 314 * sk->inuse=1 or cli() 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 315 */ /* 对于监听 socket 而言,其接收队列中的数据包是建立连接数据包,即 SYN 数据包, * 不含数据数据包,数据的处理是另外的 socket 负责。 */ 316 static struct sk_buff *tcp_find_established(struct sock *s) 317 { 318 struct sk_buff *p=skb_peek(&s->receive_queue); 319 if(p==NULL) 320 return NULL; 321 do 322 { 323 if(p->sk->state == TCP_ESTABLISHED || p->sk->state >= TCP_FIN_WAIT1) 324 return p; 325 p=p->next; 326 } 327 while(p!=(struct sk_buff *)&s->receive_queue); 328 return NULL; 329 } tcp_find_established 函数的作用是从监听套接字缓冲队列中检查是否存在已经完成连接的远端 发送的数据包,该数据包的作用即完成连接。本地监听套接字在处理完该连接,设置相关状态 后将该数据包缓存在其队列中。诚如函数前注释所述,对于监听套接字而言,其缓冲队列中缓 存的均是连接请求数据包(或者是已经完成连接请求的数据包,由 tcp_conn_request 函数经过处 理后缓存到该队列中),对于 accept 系统调用,底层将最终调用此处的 tcp_find_established 函数 查看该队列中是否有已经完成连接的数据包,如有,则返回该数据包,由调用者 (tcp_dequeue_established,该函数又被 tcp_accept 函数调用)继续处理。读者需要结合下文中 对 tcp_conn_request, tcp_dequeue_established, tcp_accept 函数介绍理解该函数作用。 330 /* 331 * Remove a completed connection and return it. This is used by 332 * tcp_accept() to get connections from the queue. 333 */ 334 static struct sk_buff *tcp_dequeue_established(struct sock *s) 335 { 336 struct sk_buff *skb; 337 unsigned long flags; 338 save_flags(flags); 339 cli(); 340 skb=tcp_find_established(s); 341 if(skb!=NULL) 342 skb_unlink(skb); /* Take it off the queue */ 343 restore_flags(flags); 344 return skb; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 345 } tcp_dequeue_established 函数被 tcp_accept 函数调用,而该函数则调用 tcp_find_established 函数 查看监听套接字接收队列中是否有已经建立连接的数据包,如果则返回该数据包,有 tcp_accept 函数进行进一步的处理后,返回给客户端本地用于数据传输的套接字。下面给出 tcp_accept 函 数实现,展示 accept 系统调用底层处理的流程。 用户调用 accept 函数,由 BSD 层 sock_accept 函数处理,该函数调用 INET 层 inet_accept 函数 进行处理,对于 TCP 协议而言,inet_accept 函数则调用 tcp_accept 函数完成具体的任务。众所 周知,accept 函数将返回用于通信的另一个套接字(不是监听套接字)描述符。tcp_accept 函数 将返回表示该通信套接字的 sock 结构,inet_accept 函数返回值表示下层(及其自身)处理是否 成功,返回 0 表示成功,此时 sock_accept 将分配一个未使用的文件描述符返回给用户应用程序。 3638 /* 3639 * This will accept the next outstanding connection. 3640 */ 3641 static struct sock *tcp_accept(struct sock *sk, int flags) 3642 { 3643 struct sock *newsk; 3644 struct sk_buff *skb; 3645 /* 3646 * We need to make sure that this socket is listening, 3647 * and that it has something pending. 3648 */ 3649 if (sk->state != TCP_LISTEN) 3650 { 3651 sk->err = EINVAL; 3652 return(NULL); 3653 } 3654 /* Avoid the race. */ 3655 cli(); 3656 sk->inuse = 1; 3657 while((skb = tcp_dequeue_established(sk)) == NULL) 3658 { 3659 if (flags & O_NONBLOCK) 3660 { 3661 sti(); 3662 release_sock(sk); 3663 sk->err = EAGAIN; 3664 return(NULL); 3665 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3666 release_sock(sk); 3667 interruptible_sleep_on(sk->sleep); 3668 if (current->signal & ~current->blocked) 3669 { 3670 sti(); 3671 sk->err = ERESTARTSYS; 3672 return(NULL); 3673 } 3674 sk->inuse = 1; 3675 } 3676 sti(); 3677 /* 3678 * Now all we need to do is return skb->sk. 3679 */ 3680 newsk = skb->sk; 3681 kfree_skb(skb, FREE_READ); 3682 sk->ack_backlog--; 3683 release_sock(sk); 3684 return(newsk); 3685 } tcp_accept 函数完成的工作很直接,对于调用 accept 的套接字而言,其首先必须是一个监听套接 字,否则错误返回。其后调用 tcp_dequeue_established 函数检查监听套接字接收队列,看是否存 在已经完成连接的数据包,如有,则返回用于此连接通信的本地套接字,该通信套接字在 tcp_conn_request 函数中处理连接请求时被建立,并与该请求连接数据包相联系起来,从而此处 只需返回该套接字即可: newsk=skb->sk; …… return (newsk); 如果对于查看的监听套接字尚无完成建立的数据包存在(tcp_dequeue_established 函数返回 NULL),则如果此监听套接字之前设置了 NON_BLOCK 选项,则直接返回 NULL;否则调用 interruptible_sleep_on 函数等待睡眠(即用户应用程序即表现为阻塞于 accept 系统调用)。 346 /* 347 * This routine closes sockets which have been at least partially 348 * opened, but not yet accepted. Currently it is only called by 349 * tcp_close, and timeout mirrors the value there. 350 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 /* 关闭一个监听 socket,对于半连接状态的 socket(即对方发送了 SYN 数据包,己方也进 * 行了处理,但己方尚未调用 accept 建立完全连接)逐一进行关闭操作。 */ 351 static void tcp_close_pending (struct sock *sk) 352 { 353 struct sk_buff *skb; 354 while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) 355 { 356 skb->sk->dead=1; 357 tcp_close(skb->sk, 0); 358 kfree_skb(skb, FREE_READ); 359 } 360 return; 361 } tcp_close_pending 函数关闭监听套接字接收队列中所有请求连接的通信端。关闭操作处理发送 FIN 数据包给远端对应通信端外,还需要释放之前创建的用于本地通信的套接字。这通过将该 套接字状态设置为 dead,并调用 tcp_close 函数完成,最后释放缓存的请求数据包: kfree_skb(skb, FREE_READ); 362 /* 363 * Enter the time wait state. 364 */ /* 设置本地 socket 进入 TIME_WAIT 状态,并设置定时期 2MSL 等待时间。*/ 365 static void tcp_time_wait(struct sock *sk) 366 { 367 tcp_set_state(sk,TCP_TIME_WAIT); 368 sk->shutdown = SHUTDOWN_MASK; 369 if (!sk->dead) 370 sk->state_change(sk); 371 reset_msl_timer(sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 372 } tcp_time_wait 函数设置套接字为 2MSL 等待状态,并启动相应定时器,用于超时处理。有关 TCP 协议中 TIME_WAIT 状态的作用请参阅前文对 TCP 协议的介绍。 373 /* 374 * A socket has timed out on its send queue and wants to do a 375 * little retransmitting. Currently this means TCP. 376 */ 377 void tcp_do_retransmit(struct sock *sk, int all) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 378 { 379 struct sk_buff * skb; 380 struct proto *prot; 381 struct device *dev; 382 int ct=0; 383 prot = sk->prot; 384 skb = sk->send_head; 385 while (skb != NULL) 386 { 387 struct tcphdr *th; 388 struct iphdr *iph; 389 int size; 390 dev = skb->dev; 391 IS_SKB(skb); 392 skb->when = jiffies; 393 /* 394 * In general it's OK just to use the old packet. However we 395 * need to use the current ack and window fields. Urg and 396 * urg_ptr could possibly stand to be updated as well, but we 397 * don't keep the necessary data. That shouldn't be a problem, 398 * if the other end is doing the right thing. Since we're 399 * changing the packet, we have to issue a new IP identifier. 400 */ 401 iph = (struct iphdr *)(skb->data + dev->hard_header_len); 402 th = (struct tcphdr *)(((char *)iph) + (iph->ihl << 2)); 403 size = skb->len - (((unsigned char *) th) - skb->data); 404 /* 405 * Note: We ought to check for window limits here but 406 * currently this is done (less efficiently) elsewhere. 407 * We do need to check for a route change but can't handle 408 * that until we have the new 1.3.x buffers in. 409 * 410 */ /* 重发时,并非是重发原始的数据包,而是对数据包进行一些字段的调整后, * 将该数据包发送出去,注意,如果数据包在中间路由器被分片,而远端只是由 * 于某个分片未接收到, * 本地重发时还是重发整个数据包,而非某个分片,因为无法确知哪个分片丢失。 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 411 iph->id = htons(ip_id_count++); 412 ip_send_check(iph); 413 /* 414 * This is not the right way to handle this. We have to 415 * issue an up to date window and ack report with this 416 * retransmit to keep the odd buggy tcp that relies on 417 * the fact BSD does this happy. 418 * We don't however need to recalculate the entire 419 * checksum, so someone wanting a small problem to play 420 * with might like to implement RFC1141/RFC1624 and speed 421 * this up by avoiding a full checksum. 422 */ 423 th->ack_seq = ntohl(sk->acked_seq); 424 th->window = ntohs(tcp_select_window(sk)); 425 tcp_send_check(th, sk->saddr, sk->daddr, size, sk); 426 /* 427 * If the interface is (still) up and running, kick it. 428 */ 429 if (dev->flags & IFF_UP) 430 { 431 /* 432 * If the packet is still being sent by the device/protocol 433 * below then don't retransmit. This is both needed, and good - 434 * especially with connected mode AX.25 where it stops resends 435 * occurring of an as yet unsent anyway frame! 436 * We still add up the counts as the round trip time wants 437 * adjusting. 438 */ 439 if (sk && !skb_device_locked(skb)) 440 { 441 /* Remove it from any existing driver queue first! */ 442 skb_unlink(skb); 443 /* Now queue it */ 444 ip_statistics.IpOutRequests++; 445 dev_queue_xmit(skb, dev, sk->priority); 446 } 447 } 448 /* 449 * Count retransmissions 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 450 */ 451 ct++; 452 sk->prot->retransmits ++; 453 /* 454 * Only one retransmit requested. 455 */ 456 if (!all) 457 break; 458 /* 459 * This should cut it off before we send too many packets. 460 */ 461 if (ct >= sk->cong_window) 462 break; 463 skb = skb->link3; 464 } 465 } tcp_do_retransmit 函数用于 TCP 协议数据包重传,该函数遍历相应套接字重传队列,依照要求 重传该队列中数据包。数据包重传策略是 TCP 协议保证可靠性数据传输的必要手段,有关内容 请参考附录 A。每个套接字对于其发送的每个数据包都会缓存其在重发队列中,该重发队列由 sock 结构中 send_head, send_tail 两个字段以及 sk_buff 结构中 link3 字段完成建立:send_head 指 向队列首,send_tail 指向队列尾,而 link3 则用于队列中数据包之间相互连接。 需要注意的一点是,对于重发的数据包,其 IP 首部中用于标识数据包的 id 字段将被赋予新值, 以区别于之前发送的数据包。只要数据包中 TCP 首部中本地序列号不发生改变,其表示的数据 依然是原先发送的数据,不会在本地发生人为的数据包乱序问题。另外一个值得注意的地方是: 该函数对于重发的数据包个数维护一个计数器,以便重发的数据包个数不超过对应套接字设定 的节制窗口值(sk->cong_window),而如果参数 all 为 0,则表示只发送一个数据包后即可退出。 466 /* 467 * Reset the retransmission timer 468 */ 469 static void reset_xmit_timer(struct sock *sk, int why, unsigned long when) 470 { 471 del_timer(&sk->retransmit_timer); 472 sk->ip_xmit_timeout = why; 473 if((int)when < 0) 474 { 475 when=3; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 476 printk("Error: Negative timer in xmit_timer\n"); 477 } 478 sk->retransmit_timer.expires=when; 479 add_timer(&sk->retransmit_timer); 480 } reset_xmit_timer 函数用于复位重传定时器,该定时器用于数据包定时重传操作,具体定时含义 由 sock 结构ip_xmit_timeout 字段指示,而到期执行函数为 retransmit_timer,这在tcp_conn_request 函数中被初始化: /*newsk 为新创建的用于通信的本地 sock 结构*/ init_timer(&newsk->retransmit_timer); newsk->retransmit_timer.data = (unsigned long)newsk; newsk->retransmit_timer.function=&retransmit_timer; 所谓复位定时器,是指首先将原有定时器从系统定时器队列中删除,更新定时时间间隔后,重 新插入队列。sock 结构中 retransmit_timer 定时器用于普通 TCP 数据包超时重传,窗口探测 (PROBE),保活(Keep Alive)数据包发送。 481 /* 482 * This is the normal code called for timeouts. It does the retransmission 483 * and then does backoff. tcp_do_retransmit is separated out because 484 * tcp_ack needs to send stuff from the retransmit queue without 485 * initiating a backoff. 486 */ 487 void tcp_retransmit_time(struct sock *sk, int all) 488 { 489 tcp_do_retransmit(sk, all); 490 /* 491 * Increase the timeout each time we retransmit. Note that 492 * we do not increase the rtt estimate. rto is initialized 493 * from rtt, but increases here. Jacobson (SIGCOMM 88) suggests 494 * that doubling rto each time is the least we can get away with. 495 * In KA9Q, Karn uses this for the first few times, and then 496 * goes to quadratic. netBSD doubles, but only goes up to *64, 497 * and clamps at 1 to 64 sec afterwards. Note that 120 sec is 498 * defined in the protocol as the maximum possible RTT. I guess 499 * we'll have to use something other than TCP to talk to the 500 * University of Mars. 501 * 502 * PAWS allows us longer timeouts and large windows, so once 503 * implemented ftp to mars will work nicely. We will have to fix 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 504 * the 120 second clamps though! 505 */ 506 sk->retransmits++; 507 sk->backoff++; 508 sk->rto = min(sk->rto << 1, 120*HZ); 509 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 510 } tcp_retransmit_time 函数一方面调用 tcp_do_retransmit 函数进行 TCP 数据包超时重传,另一方面 调用 reset_xmit_timer 复位重传定时器,所依据的原理即 TCP 协议规范中表达的指数退避重传 时间计算方式,即下一次数据包重传时间间隔为前一次的两倍,不过不可超过 2 分钟上限,当 然这个上限值大多是实现相关的。 511 /* 512 * A timer event has trigger a tcp retransmit timeout. The 513 * socket xmit queue is ready and set up to send. Because 514 * the ack receive code keeps the queue straight we do 515 * nothing clever here. 516 */ 517 static void tcp_retransmit(struct sock *sk, int all) 518 { 519 if (all) 520 { 521 tcp_retransmit_time(sk, all); 522 return; 523 } /* 发送端猜测拥塞发生的唯一方法是它必须重发一个报文段,重传发生在 * 1>RTO 定时期超时 * 2>收到三个 ACK 数据报 * TCP 对拥塞发生时的处理: * 1>如超时重传 * A. 慢启动门限值设置为当前窗口的一半 * B. 将 CWND 设置为一个报文段,启动慢启动阶段 */ 524 sk->ssthresh = sk->cong_window >> 1; /* remember window where we lost */ 525 /* sk->ssthresh in theory can be zero. I guess that's OK */ 526 sk->cong_count = 0; 527 sk->cong_window = 1; 528 /* Do the actual retransmit. */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 529 tcp_retransmit_time(sk, all); 530 } tcp_retransmit 函数是最上层的重传函数,重传定时器到期时,如果当前超时原因是由于 TCP 数 据包在规定时间内未得到应答,则需要重传相关数据包,此时到期执行函数 retransmit_timer 将 调用 tcp_retransmit 函数重传相关 TCP 数据包(即 sock 结构中由 send_head 指向的队列),而 tcp_retrasmit 函数则进一步调用 tcp_retransmit_time 函数处理重传工作,tcp_retransmit_time 则调 用 tcp_do_retransmit 函数具体完成重传数据包的工作。如此逐层调用主要是分层进行各种信息 的处理更新。如 tcp_do_retransmit 函数只负责从 send_head 队列中取数据包进行重传,而 tcp_retransmit_time 在调用 tcp_do_retransmit 的基础上,对重传时间间隔进行更新;最上层的 tcp_retransmit 函数在调用 tcp_retransmit_time 函数的基础上,判断造成此次重传的深层原因,如 重传是由于连续收到三个应答数据包造成的,则 tcp_retransmit 需要进行拥塞处理。总之,以上 这种层层调用的关系是为了分工更加细致,从而使层次更加分明。读者如继续研究内核代码实 现,自然而然会发现,后期代码除了在功能上更全面之外,一个显著的特点是原来一个函数完 成的工作被分为几个函数完成,如此可以独立出一种层次关系,使得代码更易于阅读和编写。 531 /* 532 * A write timeout has occurred. Process the after effects. 533 */ 534 static int tcp_write_timeout(struct sock *sk) 535 { 536 /* 537 * Look for a 'soft' timeout. 538 */ 539 if ((sk->state == TCP_ESTABLISHED && sk->retransmits && !(sk->retransmits & 7)) 540 || (sk->state != TCP_ESTABLISHED && sk->retransmits > TCP_RETR1)) 541 { 542 /* 543 * Attempt to recover if arp has changed (unlikely!) or 544 * a route has shifted (not supported prior to 1.3). 545 */ 546 arp_destroy (sk->daddr, 0); 547 ip_route_check (sk->daddr); 548 } 549 /* 550 * Has it gone just too far ? 551 */ 552 if (sk->retransmits > TCP_RETR2) 553 { 554 sk->err = ETIMEDOUT; 555 sk->error_report(sk); 556 del_timer(&sk->retransmit_timer); 557 /* 558 * Time wait the socket 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 559 */ 560 if (sk->state == TCP_FIN_WAIT1 || sk->state == TCP_FIN_WAIT2 || sk->state == TCP_CLOSING ) 561 { 562 tcp_set_state(sk,TCP_TIME_WAIT); 563 reset_msl_timer (sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 564 } 565 else 566 { 567 /* 568 * Clean up time. 569 */ 570 tcp_set_state(sk, TCP_CLOSE); 571 return 0; 572 } 573 } 574 return 1; 575 } tcp_write_timeout 函数在重传定时器发生超时时被调用,诚如上文所述,有三种情况发生重传定 时器超时:普通 TCP 数据包超时重传,远端窗口探测,保活探测。 tcp_write_timeout 函数将根据套接字当前状态以及超时次数对该套接字进行一些处理: 1) 如果套接字处于正常连接状态,并且超时次数是 8 的倍数,则重新核对通信远端 MAC 地址。 核对工作是通过 ip_route_check 函数完成,当然在这之前需要首先将原先的 MAC 地址删除。 2) 如果套接字处于非连接状态,则如果超时次数在 7(TCP_RETR1)次以上,同上将重新校对 远端 MAC 地址。 3) 如果检查到超时次数达到 15(TCP_RETR2)以上,则表示很可能发生某种错误:中间路由 器坏后者远端主机突然掉电灯等。此时: A. 如果套接字状态为 TCP_FIN_WAIT1,TCP_FIN_WAIT2,TCP_CLOSING,则设置套 接字状态为 TCP_CLOSE,并设置 2MSL 等待时间。 B. 否则直接将套接字状态设置为 TCP_CLOSE,无须等待 2MSL 时间。 C. 有关 TCP 协议各种状态说明在本书前文介绍 TCP 协议时有详细解释,此处不再论述。 576 /* 577 * The TCP retransmit timer. This lacks a few small details. 578 * 579 * 1. An initial rtt timeout on the probe0 should cause what we can 580 * of the first write queue buffer to be split and sent. 581 * 2. On a 'major timeout' as defined by RFC1122 we shouldn't report 582 * ETIMEDOUT if we know an additional 'soft' error caused this. 583 * tcp_err should save a 'soft error' for us. 584 */ 585 static void retransmit_timer(unsigned long data) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 586 { 587 struct sock *sk = (struct sock*)data; 588 int why = sk->ip_xmit_timeout; 589 /* 590 * only process if socket is not in use 591 */ 592 cli(); 593 if (sk->inuse || in_bh) 594 { 595 /* Try again in 1 second */ 596 sk->retransmit_timer.expires = HZ; 597 add_timer(&sk->retransmit_timer); 598 sti(); 599 return; 600 } 601 sk->inuse = 1; 602 sti(); 603 /* Always see if we need to send an ack. */ 604 if (sk->ack_backlog && !sk->zapped) 605 { 606 sk->prot->read_wakeup (sk); 607 if (! sk->dead) 608 sk->data_ready(sk,0); 609 } 610 /* Now we need to figure out why the socket was on the timer. */ 611 switch (why) 612 { 613 /* Window probing */ 614 case TIME_PROBE0: 615 tcp_send_probe0(sk); 616 tcp_write_timeout(sk); 617 break; 618 /* Retransmitting */ 619 case TIME_WRITE: 620 /* It could be we got here because we needed to send an ack. 621 * So we need to check for that. 622 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 623 { 624 struct sk_buff *skb; 625 unsigned long flags; 626 save_flags(flags); 627 cli(); 628 skb = sk->send_head; 629 if (!skb) 630 { 631 restore_flags(flags); 632 } 633 else 634 { 635 /* 636 * Kicked by a delayed ack. Reset timer 637 * correctly now 638 */ 639 if (jiffies < skb->when + sk->rto) 640 { 641 reset_xmit_timer (sk, TIME_WRITE, skb->when + sk->rto - jiffies); 642 restore_flags(flags); 643 break; 644 } 645 restore_flags(flags); 646 /* 647 * Retransmission 648 */ 649 sk->prot->retransmit (sk, 0); 650 tcp_write_timeout(sk); 651 } 652 break; 653 } 654 /* Sending Keepalives */ 655 case TIME_KEEPOPEN: 656 /* 657 * this reset_timer() call is a hack, this is not 658 * how KEEPOPEN is supposed to work. 659 */ 660 reset_xmit_timer (sk, TIME_KEEPOPEN, TCP_TIMEOUT_LEN); 661 /* Send something to keep the connection open. */ 662 if (sk->prot->write_wakeup) 663 sk->prot->write_wakeup (sk); 664 sk->retransmits++; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 665 tcp_write_timeout(sk); 666 break; 667 default: 668 printk ("rexmit_timer: timer expired - reason unknown\n"); 669 break; 670 } 671 release_sock(sk); 672 } retransmit_timer 函数是重传定时器到期执行函数,其根据重传定时器具体超时原因(由 sock 结 构 ip_xmit_timeout 表示)进行相应处理。 1) 如果表示相应超时套接字的 sock 结构正被使用,则继续延迟 1s 后进行处理。 2) 如果本地存在尚未应答的数据包且连接并未被复位,则顺带发送应答数据包给远端。 注意 sock 结构 zapped 字段只在 tcp_std_reset 函数中被赋值为 1,即只在本地接收到远端发 送的复位数据包后设置。 3) 此后根据此次超时原因进行具体处理: A. 窗口探测超时:窗口探测数据包是通信一端发送给另一端的探测窗口大小的数据包。 对于 TCP 协议,其首部中包含一个窗口字段,用于表示该端目前接收缓冲区大小,另 一端将根据该值节制其数据包的发送,以防止发送过快,使得对方接收缓冲区溢出, 这是 TCP 协议提供的一种数据流控制方式。当一方检测到接收缓冲区大小不足以接收 一个完整数据包时(一般为 MTU 大小),其不会通知给对方一个小的窗口以造成传输 线路上小数据包泛滥,而是直接通报一个 0 窗口,对方在接收到 0 窗口通知后,会暂 时停止向另一端发送数据包,直到得到一个非 0 大小的窗口通知,由于该通知可能丢 失,此时将会造成一端在等待一个非 0 窗口通知,而另一端认为其发送了非 0 窗口通 知(实际上丢失了)则等待对方发送普通数据包,从而双方各自等待,造成死锁。窗 口探测数据包的作用即为了阻止这种由于非 0 窗口通知数据包丢失而造成的死锁问题。 一般非 0 窗口的通知是由原先声称窗口为 0 的一方主动发送的,为了防止该非 0 窗口 通知数据包丢失,另一端在接收到 0 窗口数据包后,会启动一个窗口探测数据包,(注 意即便此时远端窗口为 0,仍然可以接收窗口探测数据包,这是 TCP 协议规范规定的), 这样即便由于对方非 0 窗口通知数据包丢失,在接收到窗口探测数据包后,对方可以 重新发送一个非 0 窗口通知数据包,从而解除上文中所叙述的可能的死锁问题。 在了解到窗口探测数据包的由来后,我们也清楚了何时应该启动窗口探测定时器:即 当对方通知 0 窗口时。如此问题即集中在如何判定对方通知 0 窗口大小。具体启动是 在 tcp_send_skb 函数和 tcp_ack 函数中进行的。 tcp_send_skb 函数在发送一个数据包时被调用,该函数启动窗口探测定时器的依据是: 所有之前发送出去的数据包都已得到对方应答,而对方发送的所有数据包本地也已进 行了应答,且当前发送的数据包长度超出远端通知的窗口范围,则启动窗口探测定时 器。此时由于双方数据包都已得到对方应答,而本地当前将要发送的数据包由于窗口 大小所限发送不出去,则必须得到对方更新窗口大小的数据包,而此时必须发送一个 窗口探测数据包方能让对方发送一个更新窗口大小的数据包。原则上我们可以等待对 方发送一个普通数据包从而更新窗口大小,但这种消极等待并不能解决问题,如果对 方从不主动发送数据包,则本地接下来的所有数据包将永远发送不出去。当然,在实 际网络中很少会出现双方都对对方发送的数据包进行了应答的情况下,一端窗口大小 出现问题的现象,但对于代码实现必须考虑到各种危险情况,以免造成内核死锁。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 tcp_ack 函数用于对对方发送一个应答数据包进行处理。该函数启动窗口探测定时器的 机理与 tcp_send_skb 函数相同,实现代码基本一致,此处不再叙述。 如果 retransmit_timer 函数判断出此次超时是窗口探测定时器,则调用 tcp_send_probe0 函数发送窗口探测数据包,并调用 tcp_write_timeout 函数进行后续超时处理,该函数在 前文中已有介绍。对于 tcp_send_probe0 函数,其一方面发送窗口探测数据包,另一方 面采用指数退避算法更新超时间隔时间后,重新设置窗口探测定时器,以便在对方未 响应的情况下,继续进行窗口探测。 B. TCP 数据包传输超时:这是最为普遍的重传定时器超时情况,当使用 TCP 协议的套接 字发送一个数据包后,会立刻设置传输超时定时器,如果定时器到期时,尚未得到应 答(实际上如果得到应答的话,则该定时器会被删除,不存在定时器到期的问题),则 将重新发送该数据包。注意对于使用 TCP 协议的套接字而言,其发送的每一个数据包 都暂时被缓存到 sock 结构 send_head 字段指向的队列中,直到接收到对端对该数据包 的应答后,方才被真正释放。 C. 保活定时器超时:保活定时器是为了防止由于远端通信端在不通知的情况下关闭的现 象出现,此时由于本地得不到通知,仍然保存大量资源,如果这样的情况出现较多(如 远端采用此种方式进行攻击),则本地将由于资源耗尽,将无法对其它需要的远端客户 提供服务。保活数据包就是为了解决这一问题而设置的。保活数据包的作用就是探测 远端通信端是否还“在线上”。一般在通常双方交互中,由于各自需要传输数据,无须 进行保活数据包的发送(普通数据包即起到保活数据包的作用),但如果双方长时间未 进行数据的交换,则本地需要专门发送保活数据包对远端主机的状态进行探测。保活 数据包与普通数据包无异,只是其作用并非是发送数据,而是希望发送一个数据包后, 得到对方应答,已确知对方还在,所以保活数据包中使用老序列号(即之前正常数据 使用过的序列号)。保活数据包的发送即当双方长时间未进行通信的情况下,所以保活 定时器就要在双方将要进入长时间“休眠”时进行设置,“休眠”的确定在代码实现上 是这样判断的:如果本地发送队列和重发队列中都无数据包,即本地既无需要重发的 数据包,也无新的数据包需要发送。可以预见在此后一段时间内,本地将不会发送数 据包,即可能将进入一个“休眠”时期,此时处理上将启动保活定时器。当然,此处 的判断是完全从本地的角度出发的,在本地无数据包可发送的情况下,远端或许会发 送数据包,不过由于对于远端的情况无从把握,所以只好武断的设置保活定时器,这 不会造成冲突,因为如果在设置保活定时器后接收到远端发送的数据包,则保活定时 器将被删除。保活定时器具体的是在 tcp_send_ack, tcp_ack 函数中被设置的,设置条件 如上所述。 如果 retransmit_timer 函数判断是保活定时器超时,其一方面重新设置保活定时器,另 一方面调用 tcp_write_wakeup 向远端发送保活数据包。注意对于保活定时器,并不如前 定时器一样,对于定时间隔使用指数退避算法,而是维持一个固定的间隔时间,原因 很简单,保活的目的即在于探测远端通信端的活性,越快探测出越好,所以就没有必 要每次探测后进行进一步的延迟。由于其功能上的不同,有别于上文中所述的窗口探 测定时器和数据包重传定时器。 673 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 674 * This routine is called by the ICMP module when it gets some 675 * sort of error condition. If err < 0 then the socket should 676 * be closed and the error returned to the user. If err > 0 677 * it's just the icmp type << 8 | icmp code. After adjustment 678 * header points to the first 8 bytes of the tcp header. We need 679 * to find the appropriate port. 680 */ 681 void tcp_err(int err, unsigned char *header, unsigned long daddr, 682 unsigned long saddr, struct inet_protocol *protocol) 683 { 684 struct tcphdr *th; 685 struct sock *sk; 686 struct iphdr *iph=(struct iphdr *)header; 687 header+=4*iph->ihl; 688 th =(struct tcphdr *)header; 689 sk = get_sock(&tcp_prot, th->source, daddr, th->dest, saddr); 690 if (sk == NULL) 691 return; 692 if(err<0) 693 { 694 sk->err = -err; 695 sk->error_report(sk); 696 return; 697 } 698 if ((err & 0xff00) == (ICMP_SOURCE_QUENCH << 8)) 699 { 700 /* 701 * FIXME: 702 * For now we will just trigger a linear backoff. 703 * The slow start code should cause a real backoff here. 704 */ 705 if (sk->cong_window > 4) 706 sk->cong_window--; 707 return; 708 } 709 /* sk->err = icmp_err_convert[err & 0xff].errno; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 -- moved as TCP should hide non fatals internally (and does) */ 710 /* 711 * If we've already connected we will keep trying 712 * until we time out, or the user gives up. 713 */ 714 if (icmp_err_convert[err & 0xff].fatal || sk->state == TCP_SYN_SENT) 715 { 716 if (sk->state == TCP_SYN_SENT) 717 { 718 tcp_statistics.TcpAttemptFails++; 719 tcp_set_state(sk,TCP_CLOSE); 720 sk->error_report(sk); /* Wake people up to see the error (see connect in sock.c) */ 721 } 722 sk->err = icmp_err_convert[err & 0xff].errno; 723 } 724 return; 725 } tcp_err 函数在系统接收到一个表示某种错误的数据包后,被 ICMP 模块(icmp_rcv)调用,用 于对使用 TCP 协议的某个套接字进行处理。 1) 如果错误号小于 0,则表示这是一个较为严重的错误,此时一方面将错误号存储到 sock 结 构 err 字段以便用户使用 ioctl 函数进行读取,另一方调用相关函数将错误通知给用户。 2) 如果是源端节制 ICMP 通知,则减小拥塞窗口即可。拥塞窗口是本地自身进行数据包发送 节制的一种方式,这与远端窗口相对应而言,远端窗口是远端对本地数据包发送进行节制 的一种方式。 3) 如果错误是致命的,则将错误存储起来,但并不立刻通知用户,用户在此后的操作中,会 被通知到。 4) 如果错误发生在本地试图与远端建立连接的过程中,则取消连接,并及时通知用户,并将 错误存储起来(存储在 sock 结构 err 字段中)。 726 /* 727 * Walk down the receive queue counting readable data until we hit the end or we find a gap 728 * in the received data queue (ie a frame missing that needs sending to us). Not 729 * sorting using two queues as data arrives makes life so much harder. 730 */ 731 static int tcp_readable(struct sock *sk) 732 { 733 unsigned long counted; 734 unsigned long amount; 735 struct sk_buff *skb; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 736 int sum; 737 unsigned long flags; 738 if(sk && sk->debug) 739 printk("tcp_readable: %p - ",sk); 740 save_flags(flags); 741 cli(); 742 if (sk == NULL || (skb = skb_peek(&sk->receive_queue)) == NULL) 743 { 744 restore_flags(flags); 745 if(sk && sk->debug) 746 printk("empty\n"); 747 return(0); 748 } 749 counted = sk->copied_seq; /* Where we are at the moment */ 750 amount = 0; 751 /* 752 * Do until a push or until we are out of data. 753 */ 754 do 755 { 756 if (before(counted, skb->h.th->seq)) /* Found a hole so stops here */ 757 break; 758 sum = skb->len -(counted - skb->h.th->seq); /* Length - header but start from where we are up to (avoid overlaps) */ 759 if (skb->h.th->syn) 760 sum++; 761 if (sum > 0) 762 { /* Add it up, move on */ 763 amount += sum; 764 if (skb->h.th->syn) 765 amount--; 766 counted += sum; 767 } 768 /* 769 * Don't count urg data ... but do it in the right place! 770 * Consider: "old_data (ptr is here) URG PUSH data" 771 * The old code would stop at the first push because 772 * it counted the urg (amount==1) and then does amount-- 773 * *after* the loop. This means tcp_readable() always 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 774 * returned zero if any URG PUSH was in the queue, even 775 * though there was normal data available. If we subtract 776 * the urg data right here, we even get it to work for more 777 * than one URG PUSH skb without normal data. 778 * This means that select() finally works now with urg data 779 * in the queue. Note that rlogin was never affected 780 * because it doesn't use select(); it uses two processes 781 * and a blocking read(). And the queue scan in tcp_read() 782 * was correct. Mike 783 */ 784 if (skb->h.th->urg) 785 amount--; /* don't count urg data */ 786 if (amount && skb->h.th->psh) break; 787 skb = skb->next; 788 } 789 while(skb != (struct sk_buff *)&sk->receive_queue); 790 restore_flags(flags); 791 if(sk->debug) 792 printk("got %lu bytes.\n",amount); 793 return(amount); 794 } tcp_readable 函数用于查看目前可读数据的长度。套接字软件表示 sock 结构 copied_seq 字段表 示到目前为止读取的数据序列号,例如 copied_seq=100,则表示序列号在 100 之前的数据均已被 用户读取,序列号为 100 以后(包括 100)的数据尚未被用户读取。 又从上层而言,其读取的数据包均被缓冲在 sock 结构 receive_queue 字段指向的队列中, tcp_readable 函数即从该队列中取数据包,查看目前可读取的数据量。 判断的标准是:直到遇到紧急数据或者数据序列号出现断裂。此处紧急数据的含义即发送该数 据的远端设置在 TCP 首部中设置了 PUSH 标志位,意为该数据需要立刻上传给用户应用程序; 而序列号出现断裂即表示数据包乱序到达,由于数据需要按序送交给用户应用程序,故遇到序 列号断裂后,即计算出目前可读取的数据量。在数据量的计算中,需要注意,因为 SYN 标志位 占据一个序列号,故在使用序列号计算数据量时,如果数据包中设置了 SYN 标志位,则必须从 数据量计算值减去 1。另外该函数实现也不将标志为 urgent 的数据计算在内,因为标志为 urgent 的数据是与普通数据区分处理的(从应用程序角度看也是如此)。 795 /* 796 * LISTEN is a special case for select.. 797 */ 798 static int tcp_listen_select(struct sock *sk, int sel_type, select_table *wait) 799 { 800 if (sel_type == SEL_IN) { 801 int retval; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 802 sk->inuse = 1; 803 retval = (tcp_find_established(sk) != NULL); 804 release_sock(sk); 805 if (!retval) 806 select_wait(&master_select_wakeup,wait); 807 return retval; 808 } 809 return 0; 810 } tcp_listen_select 函数用于判断监听套接字接收队列中是否有已经与远端通信端完成建立的套接 字存在。注意接收队列中缓冲的是数据包,套接字状态的检测可以通过查看 tcp_find_established 函数实现来看。 312 /* 313 * Find someone to 'accept'. Must be called with 314 * sk->inuse=1 or cli() 315 */ 316 static struct sk_buff *tcp_find_established(struct sock *s) 317 { 318 struct sk_buff *p=skb_peek(&s->receive_queue); 319 if(p==NULL) 320 return NULL; 321 do 322 { 323 if(p->sk->state == TCP_ESTABLISHED || p->sk->state >= TCP_FIN_WAIT1) 324 return p; 325 p=p->next; 326 } 327 while(p!=(struct sk_buff *)&s->receive_queue); 328 return NULL; 329 } 注意 do{}while 语句块中的判断语句。 如果 tcp_find_establised 函数返回 NULL,则表示尚无已经完成连接建立的套接字存在,此时调 用 select_wait 函数将允许进程封装在 wait_queue 结构中加入到由 master_select_wakeup 变量指 向的 wait_queue 结构链表中,并返回 NULL,上层处理函数可根据 NULL 返回值将当前进程置 于休眠状态,当然也可以仅仅将结构通知给用户应用程序而不做休眠操作。 master_select_wakeup 变量所指向 wait_queue 队列中休眠进程的唤醒是在 tcp_set_state 函数中进 行的, tcp_set_state 函数前文已经介绍,此处再次列出其实现代码,进行分析。 258 static __inline__ void tcp_set_state(struct sock *sk, int state) 259 { 260 if(sk->state==TCP_ESTABLISHED) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 261 tcp_statistics.TcpCurrEstab--; 262 #ifdef STATE_TRACE 263 if(sk->debug) 264 printk("TCP sk=%p, State %s -> %s\n",sk, statename[sk->state],statename[state]); 265 #endif 266 /* This is a hack but it doesn't occur often and it's going to 267 be a real to fix nicely */ 268 if(state==TCP_ESTABLISHED && sk->state==TCP_SYN_RECV) 269 { 270 wake_up_interruptible(&master_select_wakeup); 271 } 272 sk->state=state; 273 if(state==TCP_ESTABLISHED) 274 tcp_statistics.TcpCurrEstab++; 275 } tcp_set_state 函数在一个套接字状态发生变化时(或者说改变一个套接字状态时)被调用。从实 现来看,如果套接字先前的状态为 TCP_SYN_RECV, 而新状态为 TCP_ESTABLISHED,则表示 一个套接字完成连接的建立,此时唤醒 master_select_wakeup 所指向队列中各休眠进程,从而使 得各进程继续运行。因为之前进程之所以被缓存到该队列,正是由于其处理的监听套接字接收 队列中无完成连接建立的通信套接字。当前既然有一个通信套接字完成连接,则可以使得某个 进程继续其处理了。 811 /* 812 * Wait for a TCP event. 813 * 814 * Note that we don't need to set "sk->inuse", as the upper select layers 815 * take care of normal races (between the test and the event) and we don't 816 * go look at any of the socket buffers directly. 817 */ 818 static int tcp_select(struct sock *sk, int sel_type, select_table *wait) 819 { 820 if (sk->state == TCP_LISTEN) 821 return tcp_listen_select(sk, sel_type, wait); 822 switch(sel_type) { 823 case SEL_IN: 824 if (sk->err) 825 return 1; 826 if (sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) 827 break; 828 if (sk->shutdown & RCV_SHUTDOWN) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 829 return 1; 830 if (sk->acked_seq == sk->copied_seq) 831 break; 832 if (sk->urg_seq != sk->copied_seq || 833 sk->acked_seq != sk->copied_seq+1 || 834 sk->urginline || !sk->urg_data) 835 return 1; 836 break; 837 case SEL_OUT: 838 if (sk->shutdown & SEND_SHUTDOWN) 839 return 0; 840 if (sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) 841 break; 842 /* 843 * This is now right thanks to a small fix 844 * by Matt Dillon. 845 */ 846 if (sk->prot->wspace(sk) < sk->mtu+128+sk->prot->max_header) 847 break; 848 return 1; 849 case SEL_EX: 850 if (sk->err || sk->urg_data) 851 return 1; 852 break; 853 } 854 select_wait(sk->sleep, wait); 855 return 0; 856 } tcp_select 函数用于各种情况的测试,使系统调用 select 函数的针对网络栈 TCP 协议的底层实现。 1) 如果所探测的套接字当前状态为 TCP_LISTEN(即这是一个监听套接字),则调用 tcp_listen_select 函数查看是否有已经完成连接建立的套接字存在,具体情况参考上文。 2) 如果所探测的类型为 SEL_IN,即探测是否有数据可读取,则通过检查 sock 结构几个字段值 进行判断。此处需要注意的一点是 acked_seq 值在逻辑上(当不发生值回绕时,在数值上) 一定大于 copied_seq 字段值。这一点从各字段表示的意义上也可看出:acked_seq 字段表示 本地希望接收到的下一个数据序列号,copied_seq 字段表示本地已被读取的最后一个数据的 序列号加 1。而被读取的数据一定是已经接收到的,copied_seq 字段值是已经接收的最后一 个数据的序列号加 1。 3) 如果探测类型为 SEL_OUT,即表示探测对应套接字发送缓冲区的空闲空间大小。注意此处的 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 实现是:如果该空闲空间大小不能缓存一个 MTU 长度的数据包,仍然返回无效值。 4) 如果探测类型为 SEL_EX,即表示探测之前操作是否有错误产生。 5) 如果在以上的判断中,该函数没有返回,则最后调用 select_wait 函数将当前执行进程插入 到 sock 结构 sleep 队列中。上层函数将根据该函数的返回值判定是否将当前进程置于休眠状 态。(应用程序对 select 系统调用一般为阻塞的,所以底层实现上在将当前进程插入 sleep 队列后,会置当前进程为休眠状态。) 857 int tcp_ioctl(struct sock *sk, int cmd, unsigned long arg) 858 { 859 int err; 860 switch(cmd) 861 { 862 case TIOCINQ: 863 #ifdef FIXME /* FIXME: */ 864 case FIONREAD: 865 #endif 866 { 867 unsigned long amount; 868 if (sk->state == TCP_LISTEN) 869 return(-EINVAL); 870 sk->inuse = 1; 871 amount = tcp_readable(sk); 872 release_sock(sk); 873 err=verify_area(VERIFY_WRITE,(void *)arg, 874 sizeof(unsigned long)); 875 if(err) 876 return err; 877 put_fs_long(amount,(unsigned long *)arg); 878 return(0); 879 } 880 case SIOCATMARK: 881 { 882 int answ = sk->urg_data && sk->urg_seq == sk->copied_seq; 883 err = verify_area(VERIFY_WRITE,(void *) arg, 884 sizeof(unsigned long)); 885 if (err) 886 return err; 887 put_fs_long(answ,(int *) arg); 888 return(0); 889 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 890 case TIOCOUTQ: 891 { 892 unsigned long amount; 893 if (sk->state == TCP_LISTEN) return(-EINVAL); 894 amount = sk->prot->wspace(sk); 895 err=verify_area(VERIFY_WRITE,(void *)arg, 896 sizeof(unsigned long)); 897 if(err) 898 return err; 899 put_fs_long(amount,(unsigned long *)arg); 900 return(0); 901 } 902 default: 903 return(-EINVAL); 904 } 905 } tcp_ioctl 函数用于查询相关信息,此处对于可读取数据量的大小是通过调用 tcp_readable 函数完 成;而发送缓冲区空闲空间大小则通过调用 sock_wspace 函数完成(sk->prot->wspace 指向的函 数)。函数实现中的SIOCATMARK标志字段用于判断下一个将要读取得数据是否为urgent数据。 tcp_ioctl 函数实现较为直观简单,此处不进一步说明。 906 /* 907 * This routine computes a TCP checksum. 908 */ 909 unsigned short tcp_check(struct tcphdr *th, int len, 910 unsigned long saddr, unsigned long daddr) 911 { 912 unsigned long sum; 913 if (saddr == 0) saddr = ip_my_addr(); 914 /* 915 * stupid, gcc complains when I use just one __asm__ block, 916 * something about too many reloads, but this is just two 917 * instructions longer than what I want 918 */ 919 __asm__(" 920 addl %%ecx, %%ebx 921 adcl %%edx, %%ebx 922 adcl $0, %%ebx 923 " 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 924 : "=b"(sum) 925 : "0"(daddr), "c"(saddr), "d"((ntohs(len) << 16) + IPPROTO_TCP*256) 926 : "bx", "cx", "dx" ); 927 __asm__(" 928 movl %%ecx, %%edx 929 cld 930 cmpl $32, %%ecx 931 jb 2f 932 shrl $5, %%ecx 933 clc 934 1: lodsl 935 adcl %%eax, %%ebx 936 lodsl 937 adcl %%eax, %%ebx 938 lodsl 939 adcl %%eax, %%ebx 940 lodsl 941 adcl %%eax, %%ebx 942 lodsl 943 adcl %%eax, %%ebx 944 lodsl 945 adcl %%eax, %%ebx 946 lodsl 947 adcl %%eax, %%ebx 948 lodsl 949 adcl %%eax, %%ebx 950 loop 1b 951 adcl $0, %%ebx 952 movl %%edx, %%ecx 953 2: andl $28, %%ecx 954 je 4f 955 shrl $2, %%ecx 956 clc 957 3: lodsl 958 adcl %%eax, %%ebx 959 loop 3b 960 adcl $0, %%ebx 961 4: movl $0, %%eax 962 testw $2, %%dx 963 je 5f 964 lodsw 965 addl %%eax, %%ebx 966 adcl $0, %%ebx 967 movw $0, %%ax 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 968 5: test $1, %%edx 969 je 6f 970 lodsb 971 addl %%eax, %%ebx 972 adcl $0, %%ebx 973 6: movl %%ebx, %%eax 974 shrl $16, %%eax 975 addw %%ax, %%bx 976 adcw $0, %%bx 977 " 978 : "=b"(sum) 979 : "0"(sum), "c"(len), "S"(th) 980 : "ax", "bx", "cx", "dx", "si" ); 981 /* We only want the bottom 16 bits, but we never cleared the top 16. */ 982 return((~sum) & 0xffff); 983 } tcp_check 函数用于计算 TCP 校验和。TCP 校验和包含 TCP 数据及其负载。读者可借此了解一 下内联汇编的格式。GNU 下内联汇编编程在很多资料上都有介绍,为便于读者查阅,附录 B 中 介绍了 GNU 内联汇编的一些知识。在实现上需要注意的是,虽然在计算中使用 32 位,不过在 最后处理上是将高 16 位与低 16 相加,将此相加得到的结果取反(NOT)后返回低 16 位值。 984 void tcp_send_check(struct tcphdr *th, unsigned long saddr, 985 unsigned long daddr, int len, struct sock *sk) 986 { 987 th->check = 0; 988 th->check = tcp_check(th, len, saddr, daddr); 989 return; 990 } tcp_send_check 函数用于计算 TCP 首部中校验和字段,注意在计算之前需要首先将该字段值清 零。该函数只用在数据包发送中,对于接收的数据包,检查其 TCP 校验和是否正确的方式是: 只需将校验和字段计算在内,检查计算出的结果是否为 0 即可,如为 0,则表示校验和正确, 否则表示校验和错误,传输过程中出现了问题。 TCP 协议数据包发送到网络之前可能经过如下队列: 1) partial 指向的队列,该队列只缓存一个数据包,当该数据包长度大于 MSS 值时,将被发送 出去或者缓存到其它队列上。 2) write_queue 指向的队列,该队列缓存所有由于窗口限制或者由于本地节制而缓存下来的数 据包,该队列中可以无限制的缓存被节制下来的数据包。 以上队列中数据包的缓存是在传输层进行的。 3) send_head 指向的队列(send_tail 指向该队列的尾部),该队列缓存已发送出去但尚未得到对 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 方应答的数据包。 send_head 指向的队列中的数据包是在网络层中被缓存的。 以上 partial,write_queue, send_head, send_tail 均是 sock 结构中的字段。 partial 队列中缓存的单个数据包源于 TCP 协议的流式传输,对于 TCP 协议,为了避免在网络中 传输小数据包,充分利用网络效率,底层网络栈实现对于用户应用程序发送的少量数据进行收 集缓存,当积累到一定数量后(MSS),方才作为整个包发送出去。partial 队列中数据包的意义 即在于此,对于少量数据,如果数据并非是 OOB 数据(即无需立刻发送给远端),则暂时分配 一个大容量的数据包,将数据拷贝到该大数据包中,之后将该数据包缓存到 partial 队列中,当 下次用户继续发送数据时,内核首先检查 partial 队列中是否有之前未填充满的数据包,则这些 数据可以继续填充到该数据包,直到填满才将其发送出去。当然为了尽量减少对应用程序效率 的影响,这个等待填满的时间是一定的,在实现上,内核设置一个定时器,当定时器超时时, 如果 partial 队列中缓存有未填满的数据包,仍然将其发送出去,超时发送函数为 tcp_send_partial. 此外在其它条件下,当需要发送 partial 中数据包时,内核也直接调用 tcp_send_partial 函数进行 发送。 write_queue 队列中缓存的是由于窗口限制或者本地数据包节制而缓存下来的数据包。该队列中 数据包尚未真正发送出去,而是暂时被缓存下来,等待窗口限制解除或者未应答数据包减少从 而跳出节制范围时被发送出去。该队列中数据包将由 tcp_write_xmit 函数负责发送出去, tcp_write_xmit 函数被 tcp_ack 调用,而 tcp_ack 函数用于处理接收到的应答数据包。 数据封装后可以不经过 partial 和 write_queue 指向的队列,直接被发送出去,对于不经过 partial 队列的原因如下: 1) 数据为 OOB 数据(亦称为带外数据),需要立刻发送给远端。 2) 数据长度够长,达到 MSS,内核无需进行等待合并。 3) 之前发送的所有数据包都得到应答,且亦无缓存的数据包待发送。 如下情况下,数据包将被暂时缓存到 write_queue 队列中,除此之外,数据包将不经过 write_queue 队列而直接被发送出去。 1) 数据长度受窗口大小限制,即数据最后一个字节的序列号已在远端窗口所声称的最大序列 号之外。 2) 内核正在试图进行其它数据的重传。 3) 待应答的数据包个数超出了系统设定值。 对于 TCP 协议而言,其发送的每个数据包都将必然被缓存到 send_head 队列中以保证其所声称 的可靠性数据传输。该队列中的数据包可能在传送超时时被重新发送或者在得到应答后从该队 列中删除。 当然如果继续向下层分析,其实在表示网卡设备的结构中(device 结构)还存在一个缓存队列, 该队列用于缓存由于网卡处于正忙状态而暂时未能发送的数据包。一旦数据包下次被发送出去, 数据包即可从队列中删除,网卡设备不负责数据包在传输过程中可能丢失的问题。 由于 tcp_send_skb 函数较长,我们分段进行分析。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 991 /* 992 * This is the main buffer sending routine. We queue the buffer 993 * having checked it is sane seeming. 994 */ 995 static void tcp_send_skb(struct sock *sk, struct sk_buff *skb) 996 { 997 int size; 998 struct tcphdr * th = skb->h.th; 999 /* 1000 * length of packet (not counting length of pre-tcp headers) 1001 */ 1002 size = skb->len - ((unsigned char *) th - skb->data); 1003 /* 1004 * Sanity check it.. 1005 */ 1006 if (size < sizeof(struct tcphdr) || size > skb->len) 1007 { 1008 printk("tcp_send_skb: bad skb (skb = %p, data = %p, th = %p, len = %lu)\n", 1009 skb, skb->data, th, skb->len); 1010 kfree_skb(skb, FREE_WRITE); 1011 return; 1012 } 1013 /* 1014 * If we have queued a header size packet.. (these crash a few 1015 * tcp stacks if ack is not set) 1016 */ 1017 if (size == sizeof(struct tcphdr)) 1018 { 1019 /* If it's got a syn or fin it's notionally included in the size..*/ 1020 if(!th->syn && !th->fin) 1021 { 1022 printk("tcp_send_skb: attempt to queue a bogon.\n"); 1023 kfree_skb(skb,FREE_WRITE); 1024 return; 1025 } 1026 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1027 /* 1028 * Actual processing. 1029 */ 1030 tcp_statistics.TcpOutSegs++; 1031 skb->h.seq = ntohl(th->seq) + size - 4*th->doff; 以上代码着重检查数据包的有效性。size 参数值表示的是 TCP 首部及其可能的负载的长度。如 果该值小于 TCP 首部长度或者大于整个数据包的长度,则表示长度存在问题,此时将直接释放 数据包并返回。如果长度值等于 TCP 首部长度,则表示 TCP 负载部分长度为 0,对于 SYN 和 FIN 数据包是有效的,所以在此条件下,代码即检查 TCP 首部中 SYN,FIN 字段的设置情况。 如果检查无误,则更新统计信息,并且为了此后对应答的处理,更新 skb->h.seq 字段。该字段 的意义表示对该数据包的应答数据包所应具有的最小序列号,tcp_ack 函数将由此判断是否将缓 存在 send_head 队列中的该数据包删除。注意该字段的计算方式为初始序列号(该数据包 TCP 首部中 seq 字段值)与纯数据长度之和。 1032 /* 1033 * We must queue if 1034 * 1035 * a) The right edge of this frame exceeds the window 1036 * b) We are retransmitting (Nagle's rule) 1037 * c) We have too many packets 'in flight' 1038 */ 1039 if (after(skb->h.seq, sk->window_seq) || 1040 (sk->retransmits && sk->ip_xmit_timeout == TIME_WRITE) || 1041 sk->packets_out >= sk->cong_window) 1042 { 1043 /* checksum will be supplied by tcp_write_xmit. So 1044 * we shouldn't need to set it at all. I'm being paranoid */ 1045 th->check = 0; 1046 if (skb->next != NULL) 1047 { 1048 printk("tcp_send_partial: next != NULL\n"); 1049 skb_unlink(skb); 1050 } 1051 skb_queue_tail(&sk->write_queue, skb); 1052 /* 1053 * If we don't fit we have to start the zero window 1054 * probes. This is broken - we really need to do a partial 1055 * send _first_ (This is what causes the Cisco and PC/TCP 1056 * grief). 1057 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1058 if (before(sk->window_seq, sk->write_queue.next->h.seq) && 1059 sk->send_head == NULL && sk->ack_backlog == 0) 1060 reset_xmit_timer(sk, TIME_PROBE0, sk->rto); 1061 } 1062 else 1063 { 1064 /* 1065 * This is going straight out 1066 */ 1067 th->ack_seq = ntohl(sk->acked_seq); 1068 th->window = ntohs(tcp_select_window(sk)); 1069 tcp_send_check(th, sk->saddr, sk->daddr, size, sk); 1070 sk->sent_seq = sk->write_seq; 1071 /* 1072 * This is mad. The tcp retransmit queue is put together 1073 * by the ip layer. This causes half the problems with 1074 * unroutable FIN's and other things. 1075 */ 1076 sk->prot->queue_xmit(sk, skb->dev, skb, 0); 1077 /* 1078 * Set for next retransmit based on expected ACK time. 1079 * FIXME: We set this every time which means our 1080 * retransmits are really about a window behind. 1081 */ 1082 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 1083 } 1084 } 之后根据当前条件决定是将这个数据包缓存到 write_queue 队列中,还是直接将其发送给下层。 如果以下三个条件满足,数据包将被暂时缓存到 write_queue 队列中,否则直接发送往下层(通 过调用 ip_queue_xmit 函数)。 1) 数据包长度超出远端窗口界限。 2) 正在进行其它数据包的重传。 3) 带应答数据包的个数超出系统规定值。 如果仅仅是由于窗口问题而缓存数据包,则此时还必须启动窗口探测定时器,进行窗口探测, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 以免造成双方等待死锁,有关窗口探测定时器的内容请参考前文中介绍。 如果数据包直接被发送往下层,则最后还须启动传输超时定时器。对于使用 TCP 协议的套接字 发送的每个数据包,系统在发送出一个数据包后,都会重新启动一个传输超时定时器,从而保 证数据可能丢失时进行重传,从而实现 TCP 协议所声称的可靠性数据传输。 1085 /* 1086 * Locking problems lead us to a messy situation where we can have 1087 * multiple partially complete buffers queued up. This is really bad 1088 * as we don't want to be sending partial buffers. Fix this with 1089 * a semaphore or similar to lock tcp_write per socket. 1090 * 1091 * These routines are pretty self descriptive. 1092 */ 1093 struct sk_buff * tcp_dequeue_partial(struct sock * sk) 1094 { 1095 struct sk_buff * skb; 1096 unsigned long flags; 1097 save_flags(flags); 1098 cli(); 1099 skb = sk->partial; 1100 if (skb) { 1101 sk->partial = NULL; 1102 del_timer(&sk->partial_timer); 1103 } 1104 restore_flags(flags); 1105 return skb; 1106 } tcp_dequeue_partial 函数完成的功能如其名,从 partial 字段指向的队列中取数据包,如果之前设 置了等待超时定时器(partial_timer),则同时删除该定时器。 1107 /* 1108 * Empty the partial queue 1109 */ 1110 static void tcp_send_partial(struct sock *sk) 1111 { 1112 struct sk_buff *skb; 1113 if (sk == NULL) 1114 return; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1115 while ((skb = tcp_dequeue_partial(sk)) != NULL) 1116 tcp_send_skb(sk, skb); 1117 } tcp_send_partial 函数发送 partial 队列中缓存的数据包,该函数连续调用 tcp_dequeue_partial 和 tcp_send_skb 函数完成发送任务。 1118 /* 1119 * Queue a partial frame 1120 */ 1121 void tcp_enqueue_partial(struct sk_buff * skb, struct sock * sk) 1122 { 1123 struct sk_buff * tmp; 1124 unsigned long flags; 1125 save_flags(flags); 1126 cli(); 1127 tmp = sk->partial; 1128 if (tmp) 1129 del_timer(&sk->partial_timer); 1130 sk->partial = skb; 1131 init_timer(&sk->partial_timer); 1132 /* 1133 * Wait up to 1 second for the buffer to fill. 1134 */ 1135 sk->partial_timer.expires = HZ; 1136 sk->partial_timer.function = (void (*)(unsigned long)) tcp_send_partial; 1137 sk->partial_timer.data = (unsigned long) sk; 1138 add_timer(&sk->partial_timer); 1139 restore_flags(flags); 1140 if (tmp) 1141 tcp_send_skb(sk, tmp); 1142 } tcp_enqueue_partial 函数将一个新的数据包缓存到 partial 队列中,如前文所述,partial 队列中只 可缓存一个数据包,所以如果 partial 队列中已存在数据包,则调用 tcp_send_skb 函数将该数据 包发送出去。由于缓存一个新的数据包,所以之前为旧数据包设置的定时器(partial)_timer) 将重新被设置。注意此处定时器到期执行函数为 tcp_send_partial,定时时间为 HZ,即 1 秒,也 即此版本 TCP 协议实现等待合并小数据包的时间延迟为 1 秒。 1143 /* 1144 * This routine sends an ack and also updates the window. 1145 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1146 static void tcp_send_ack(unsigned long sequence, unsigned long ack, 1147 struct sock *sk, 1148 struct tcphdr *th, unsigned long daddr) 1149 { 1150 struct sk_buff *buff; 1151 struct tcphdr *t1; 1152 struct device *dev = NULL; 1153 int tmp; 1154 if(sk->zapped) 1155 return; /* We have been reset, we may not send again */ 1156 /* 1157 * We need to grab some memory, and put together an ack, 1158 * and then put it into the queue to be sent. 1159 */ 1160 buff = sk->prot->wmalloc(sk, MAX_ACK_SIZE, 1, GFP_ATOMIC); 1161 if (buff == NULL) 1162 { 1163 /* 1164 * Force it to send an ack. We don't have to do this 1165 * (ACK is unreliable) but it's much better use of 1166 * bandwidth on slow links to send a spare ack than 1167 * resend packets. 1168 */ 1169 sk->ack_backlog++; 1170 if (sk->ip_xmit_timeout != TIME_WRITE && tcp_connected(sk->state)) 1171 { 1172 reset_xmit_timer(sk, TIME_WRITE, HZ); 1173 } 1174 return; 1175 } tcp_send_ack 函数用于向远端发送应答数据包,由于该函数较长,将分成几段对其进行介绍。 1) 参数介绍 A. sequence:该参数指定 TCP 首部中 seq 字段,即本地数据目前序列号。 B. ack:该参数指定 TCP 首部中 ack_seq 字段,即应答序列号,或者说希望远端发送的下一个 字节数据的序列号。 C. daddr:远端 IP 地址。 2) sock 结构 zapped 字段用于标识本地是否接收到远端发送的 RESET 复位数据包,zapped=1 表示对方发送了复位数据包进行了连接复位,所以在发送任何数据包(包括应答数据包) 之前必须重新进行连接,换句话说,如果 zapped=1,此处不必发送应答数据包。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3) 如果数据包内核缓冲区分配失败,则增加应答计数器计数,从而在稍后进行应答,这个“稍 后发送“的完成是通过设置重传定时器完成的。通过设置 sock 结构 ip_xmit_timeout 字段为 TIME_WAIT,并调用 reset_xmit_timer 即完成重传定时器的设置,注意此处设置的时间间隔 为 HZ(即 1 秒)。 1176 /* 1177 * Assemble a suitable TCP frame 1178 */ 1179 buff->len = sizeof(struct tcphdr); 1180 buff->sk = sk; 1181 buff->localroute = sk->localroute; 1182 t1 =(struct tcphdr *) buff->data; 1183 /* 1184 * Put in the IP header and routing stuff. 1185 */ 1186 tmp = sk->prot->build_header(buff, sk->saddr, daddr, &dev, 1187 IPPROTO_TCP, sk->opt, MAX_ACK_SIZE,sk->ip_tos,sk->ip_ttl); 1188 if (tmp < 0) 1189 { 1190 buff->free = 1; 1191 sk->prot->wfree(sk, buff->mem_addr, buff->mem_len); 1192 return; 1193 } 1194 buff->len += tmp; 1195 t1 =(struct tcphdr *)((char *)t1 +tmp); 1196 memcpy(t1, th, sizeof(*t1)); 1197 /* 1198 * Swap the send and the receive. 1199 */ 1200 t1->dest = th->source; 1201 t1->source = th->dest; 1202 t1->seq = ntohl(sequence); 1203 t1->ack = 1; 1204 sk->window = tcp_select_window(sk); 1205 t1->window = ntohs(sk->window); 1206 t1->res1 = 0; 1207 t1->res2 = 0; 1208 t1->rst = 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1209 t1->urg = 0; 1210 t1->syn = 0; 1211 t1->psh = 0; 1212 t1->fin = 0; 以上代码即完成 MAC,IP,TCP 首部的建立,通过调用 ip_build_header(由 sk->prot->build_header 指向的函数)完成 MAC,IP 首部的建立,该函数返回 MAC,IP 首部的总长度。之后 tcp_send_ack 函数直接进行 TCP 首部的建立。 1213 /* 1214 * If we have nothing queued for transmit and the transmit timer 1215 * is on we are just doing an ACK timeout and need to switch 1216 * to a keepalive. 1217 */ 1218 if (ack == sk->acked_seq) 1219 { 1220 sk->ack_backlog = 0; 1221 sk->bytes_rcv = 0; 1222 sk->ack_timed = 0; 1223 if (sk->send_head == NULL && skb_peek(&sk->write_queue) == NULL 1224 && sk->ip_xmit_timeout == TIME_WRITE) 1225 { 1226 if(sk->keepopen) { 1227 reset_xmit_timer(sk,TIME_KEEPOPEN,TCP_TIMEOUT_LEN); 1228 } else { 1229 delete_timer(sk); 1230 } 1231 } 1232 } 接着判断应答序列号参数与 sock 结构 acked_seq 字段值是否相等,sock 结构 acked_seq 字段表 示该套接字希望从远端接收的下一个字节的序列号,如果二者相等,则表示这个应答数据包是 最新的,所以此时可对待应答数据包计数器清零。如果套接字 send_head 重传队列和 write_queue 写队列都已清空,则无需设置传输超时定时器,此时由于双方可能进入长时间休眠(二者都不 发送数据包),所以如果该套接字要求进行保活操作,则启动保活定时器,否则直接删除定时器 (retransmit_timer)。 1233 /* 1234 * Fill in the packet and send it 1235 */ 1236 t1->ack_seq = ntohl(ack); 1237 t1->doff = sizeof(*t1)/4; 1238 tcp_send_check(t1, sk->saddr, daddr, sizeof(*t1), sk); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1239 if (sk->debug) 1240 printk("\rtcp_ack: seq %lx ack %lx\n", sequence, ack); 1241 tcp_statistics.TcpOutSegs++; 1242 sk->prot->queue_xmit(sk, dev, buff, 1); 1243 } 函数最后设置 TCP 首部 ack_seq 应答序列号字段,计算校验和后,调用 ip_queue_xmit 函数将数 据包发往网络层处理。 1244 /* 1245 * This routine builds a generic TCP header. 1246 */ 1247 extern __inline int tcp_build_header(struct tcphdr *th, struct sock *sk, int push) 1248 { 1249 memcpy(th,(void *) &(sk->dummy_th), sizeof(*th)); /* 注意此处的序列号使用的是 write_seq, 而非 sent_seq。 * 该函数目前仅被 tcp_write 调用构建 TCP 首部。 */ 1250 th->seq = htonl(sk->write_seq); 1251 th->psh =(push == 0) ? 1 : 0; 1252 th->doff = sizeof(*th)/4; 1253 th->ack = 1; 1254 th->fin = 0; 1255 sk->ack_backlog = 0; 1256 sk->bytes_rcv = 0; 1257 sk->ack_timed = 0; 1258 th->ack_seq = htonl(sk->acked_seq); 1259 sk->window = tcp_select_window(sk); 1260 th->window = htons(sk->window); 1261 return(sizeof(*th)); 1262 } tcp_build_header 函数专门用于创建 TCP 首部,该函数首先复制 sock 结构 dummy_th 字段,然 后对一些通用字段进行赋值,其它调用该函数的例程完成其它 TCP 首部中其它字段的赋值并且 可能根据具体情况修改本函数初始化的值。 1263 /* 1264 * This routine copies from a user buffer into a socket, 1265 * and starts the transmit system. 1266 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1267 static int tcp_write(struct sock *sk, unsigned char *from, 1268 int len, int nonblock, unsigned flags) 1269 { 1270 int copied = 0; 1271 int copy; 1272 int tmp; 1273 struct sk_buff *skb; 1274 struct sk_buff *send_tmp; 1275 unsigned char *buff; 1276 struct proto *prot; 1277 struct device *dev = NULL; 1278 sk->inuse=1; 1279 prot = sk->prot; tcp_write 函数被上层调用用于发送数据,这是对系统调用 write 函数的传输层处理函数。该函数 也是其它很多此类功能函数的最后”归属“,对于 TCP 协议而言,如 send,sendto 系统调用最 后都将调用 tcp_write 函数进行处理。此处函数参数中 from 字段表示待发送数据所在的用户缓 冲区,len 表示待发送数据长度,nonblock 表示在暂时发送失败时(如内核缓冲区分配失败)是 否进行等待,flags 表示数据的属性,如数据是否为 OOB 数据。 tcp_write 函数本身由一个 while 循环构成,循环条件为 len>0,即直到所有数据都进行了处理后方 才退出。 首先判断之前套接字操作是否出现错误,如果出现错误,则返回错误。 其次在发送数据之前我们必须确定套接字状态为可发送数据状态,如果状态条件不满足,则根 据 nonblock 参数决定是否等待: A.对于发送通道被关闭的情况,则需返回管道错误并立刻返回,因为该错误是不可恢复的。 B.对于套接字连接状态不满足的情况,可以根据 nonblock 参数决定是否等待状态改变到可发 送状态。 当套接字状态允许发送数据时,则进行内核数据封装结构的创建,复制用户缓冲区数据到内 核结构缓冲区中,在成功完成帧首部创建后,将封装后的数据发往下层进行进一步的处理。 所以该函数虽然较长,但结构较为简单,下面我们进入该函数主体部分进行分析。 1280 while(len > 0) 1281 { 1282 if (sk->err) 1283 { /* Stop on an error */ 1284 release_sock(sk); 1285 if (copied) 1286 return(copied); 1287 tmp = -sk->err; 1288 sk->err = 0; 1289 return(tmp); 1290 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1291 /* 1292 * First thing we do is make sure that we are established. 1293 */ 1294 if (sk->shutdown & SEND_SHUTDOWN) 1295 { 1296 release_sock(sk); 1297 sk->err = EPIPE; 1298 if (copied) 1299 return(copied); 1300 sk->err = 0; 1301 return(-EPIPE); 1302 } 此段代码完成对套接字之前操作错误检测,和通道状态检测。如果之前套接字操作出现错误, 则直接返回错误号退出;如果发送管道被关闭,则返回管道错误退出。注意如果在发送过程中 发送管道被关闭,则返回已经发送的数据字节数,而非管道错误号(负值)。 函数接下来将对套接字状态进行检测,这一个内嵌 while 循环。当套接字状态为 TCP_ESTABLISHED 或者为 TCP_CLOSE_WAIT 时退出循环。 1303 /* 1304 * Wait for a connection to finish. 1305 */ 1306 while(sk->state != TCP_ESTABLISHED && sk->state != TCP_CLOSE_WAIT) 1307 { 1308 if (sk->err) 1309 { 1310 release_sock(sk); 1311 if (copied) 1312 return(copied); 1313 tmp = -sk->err; 1314 sk->err = 0; 1315 return(tmp); 1316 } 如果在检测状态的过程中,套接字操作出现错误,则: 1) 如果此时已经发送部分数据,则返回已发送的数据字节数。 2) 否则返回错误号。 1317 if (sk->state != TCP_SYN_SENT && sk->state != TCP_SYN_RECV) 1318 { 1319 release_sock(sk); 1320 if (copied) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1321 return(copied); 1322 if (sk->err) 1323 { 1324 tmp = -sk->err; 1325 sk->err = 0; 1326 return(tmp); 1327 } 1328 if (sk->keepopen) 1329 { 1330 send_sig(SIGPIPE, current, 0); 1331 } 1332 return(-EPIPE); 1333 } 一般用户在调用 connect 函数后立刻调用 write 函数进行数据的发送,但由于建立连接需要时间, 所以可能在调用 write 函数时,尚未完成建立连接的过程,此时套接字状态可能为 TCP_SYN_SENT 或者 TCP_SYN_RECV,这表示套接字正在建立连接,如果此时需要发送数据的 话,则将根据用户设置的 nonblock 参数决定是否等待。 如果套接字并非是在进行连接,则表示出现某种错误,如果此时已经发送部分数据,则返回发 送的数据字节数,如果 sock 结构中缓存有出现的错误问题,则直接将此错误返回给用户进行处 理,否则返回管道错误给用户表示发送管道出现某种异常,无法进行发送。 release_sock 函数主要完成接收数据包的处理,以及在套接字状态已经设置为 TCP_CLOSE 后, 设置 2MSL 等待时间。 1334 if (nonblock || copied) 1335 { 1336 release_sock(sk); 1337 if (copied) 1338 return(copied); 1339 return(-EAGAIN); 1340 } 如果套接字正在进行连接建立(或者是一开始的连接建立或者是复位后的连接建立),此时间根 据 nonblock 参数值决定是否进行等待。如果已经发送了一部分数据,则返回的已经发送的数据 字节数。 1341 release_sock(sk); 1342 cli(); 1343 if (sk->state != TCP_ESTABLISHED && 1344 sk->state != TCP_CLOSE_WAIT && sk->err == 0) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1345 { 1346 interruptible_sleep_on(sk->sleep); /*如果被唤醒的原因是中断造成的,则: *1>如果已复制部分数据,则返回复制的数据字节数。 *2>否则返回 ERESTARTSYS 错误标志,该标志用于指示用户程序重新 * 进行写操作。 */ 1347 if (current->signal & ~current->blocked) 1348 { 1349 sti(); 1350 if (copied) 1351 return(copied); 1352 return(-ERESTARTSYS); 1353 } 1354 } 1355 sk->inuse = 1; 1356 sti(); 1357 } 当套接字非如下状态时,则无法进行数据的发送: 1) TCP_ESTABLSIHED 连接状态,通信双方在通常情况下只在该状态进行数据的交换。 2) TCP_CLOSE_WAIT 当对方关闭其发送通道后,本地处于该状态,此时本地仍然可以继续发送数据。 以上代码即根据此事实进行判断,当然要求在套接字没有出现错误的情况下进行。当套接字状 态不满足发送数据时,则调用 interruptible_sleep_on 函数进入睡眠等待。当被唤醒时,程序首先 检查是否由于中断被唤醒,如是,则根据是否已经复制部分数据返回对应值。如果是因为状态 转移到可发送数据状态,则设置 sock 结构 inuse 字段为 1,表示本进程在接下来的一段时间内 将独占套接字资源进行相关处理,最后开启中断进入下一步操作。程序运行到此,表示套接字 可以发送用户数据,我们继续分析函数余下代码。 1358 /* 1359 * The following code can result in copy <= if sk->mss is ever 1360 * decreased. It shouldn't be. sk->mss is min(sk->mtu, sk->max_window). 1361 * sk->mtu is constant once SYN processing is finished. I.e. we 1362 * had better not get here until we've seen his SYN and at least one 1363 * valid ack. (The SYN sets sk->mtu and the ack sets sk->max_window.) 1364 * But ESTABLISHED should guarantee that. sk->max_window is by definition 1365 * non-decreasing. Note that any ioctl to set user_mss must be done 1366 * before the exchange of SYN's. If the initial ack from the other 1367 * end has a window of 0, max_window and thus mss will both be 0. 1368 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1369 /* 1370 * Now we need to check if we have a half built packet. 1371 */ 1372 if ((skb = tcp_dequeue_partial(sk)) != NULL) 1373 { 1374 int hdrlen; 1375 /* IP header + TCP header */ 1376 hdrlen = ((unsigned long)skb->h.th - (unsigned long)skb->data) 1377 + sizeof(struct tcphdr); 1378 /* Add more stuff to the end of skb->len */ 1379 if (!(flags & MSG_OOB)) 1380 { /* skb->len-hdrlen 表示现有用户数据的长度, * sk->mss 表示最大用户数据长度,二者之差表示 * 可增加用户数据长度。 */ 1381 copy = min(sk->mss - (skb->len - hdrlen), len); 1382 /* FIXME: this is really a bug. */ 1383 if (copy <= 0) 1384 { 1385 printk("TCP: **bug**: \"copy\" <= 0!!\n"); 1386 copy = 0; 1387 } 1388 memcpy_fromfs(skb->data + skb->len, from, copy); 1389 skb->len += copy; 1390 from += copy; 1391 copied += copy; 1392 len -= copy; 1393 sk->write_seq += copy; 1394 } 1395 if ((skb->len - hdrlen) >= sk->mss || 1396 (flags & MSG_OOB) || !sk->packets_out) 1397 tcp_send_skb(sk, skb); //这种情况下,表示当前需发送的数据长度较小,不足以达到发送长度, //故继续缓存该数据包以接收更多数据。 1398 else 1399 tcp_enqueue_partial(skb, sk); 1400 continue; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1401 } 诚如前文所述,TCP 协议对于小数据会进行合并发送,sock 结构中 partial 字段指向的数据包即 用于此目的。如果 partial 队列不为空,则从 partial 队列中取下数据包,用现在的数据填充这个 数据包空闲缓冲区。如果当前用户发送数据仍然较少,不足以达到发送数据包的目的,则调用 tcp_enqueue_partial 函数继续将该数据包缓存在 partial 队列中,否则如果数据包长度已足够发送 或者当前数据需要立刻发送给远端(设置了 OOB 标志),则调用 tcp_send_skb 立刻将数据包发 送出去。对 partial 队列中数据包进行了处理后,程序回到最外的 while 循环继续对数据进行处 理。如果用户数据长度较长,则在填充 partial 队列中可能有的数据包后,仍然需要重新分配一 个新数据包进行数据的封装,这就是接下来代码所作的工作。 1402 /* 1403 * We also need to worry about the window. 1404 * If window < 1/2 the maximum window we've seen from this 1405 * host, don't use it. This is sender side 1406 * silly window prevention, as specified in RFC1122. 1407 * (Note that this is different than earlier versions of 1408 * SWS prevention, e.g. RFC813.). What we actually do is 1409 * use the whole MSS. Since the results in the right 1410 * edge of the packet being outside the window, it will 1411 * be queued for later rather than sent. 1412 */ 1413 copy = sk->window_seq - sk->write_seq;/*当前窗口可容纳数据量*/ /*如果窗口容限小于最大窗口值的一半,或者大于 MSS 值,都设置最终值为 MSS*/ 1414 if (copy <= 0 || copy < (sk->max_window >> 1) || copy > sk->mss) 1415 copy = sk->mss; /*如果容量大于用户实际要发送的数据量,暂时将容量设置为用户数据长度, *注意这个设置只是暂时的,下面代码将根据数据的紧迫性重新对容量值进行更新 */ 1416 if (copy > len) 1417 copy = len; 首先计算本次可分配的数据包的容量,注意此处的计算值并非最终值,下面还将对其进行处理。 1418 /* 1419 * We should really check the window here also. 1420 */ 1421 send_tmp = NULL; /*如果数据非 OOB 数据,则可以进行等待以进行数据合并发送,所以更新容量 *为 MSS 值。注意以下分配的容量为 MTU 加上首部可占用的最大长度,实际上分 *配的空间绰绰有余。 */ 1422 if (copy < sk->mss && !(flags & MSG_OOB)) 1423 { 1424 /* 1425 * We will release the socket in case we sleep here. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1426 */ 1427 release_sock(sk); 1428 /* 1429 * NB: following must be mtu, because mss can be increased. 1430 * mss is always <= mtu 1431 */ 1432 skb = prot->wmalloc(sk, sk->mtu + 128 + prot->max_header, 0, GFP_KERNEL); 1433 sk->inuse = 1; /*注意此处对于临时变量 send_tmp 的设置,下面代码将根据 send_tmp 是否为 *进行不同的处理。此处对 send_tmp 进行赋值表示 send_tmp 不为 NULL! */ 1434 send_tmp = skb; 1435 } /*如果数据为 OOB 数据或容量值已经设置为 MSS,则直接分配以上计算的容量*/ 1436 else 1437 { 1438 /* 1439 * We will release the socket in case we sleep here. 1440 */ 1441 release_sock(sk); 1442 skb = prot->wmalloc(sk, copy + prot->max_header , 0, GFP_KERNEL); 1443 sk->inuse = 1; 1444 } 接下来代码检查以上封装数据包的分配是否成功,如果 skb==NULL,则表示内核数据包结构分 配失败,此时设置套接字标志位 SO_NOSPACE,如果 nonblock 参数为 1,则立刻返回。否则将 进入睡眠等待。 1445 /* 1446 * If we didn't get any memory, we need to sleep. 1447 */ 1448 if (skb == NULL) 1449 { 1450 sk->socket->flags |= SO_NOSPACE; 1451 if (nonblock) 1452 { 1453 release_sock(sk); 1454 if (copied) 1455 return(copied); 1456 return(-EAGAIN); 1457 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1458 /* 1459 * FIXME: here is another race condition. 1460 */ /*对原先的写缓冲区空闲空间大小进行保存,下面代码将不断检查 sock 结构 *wmem_alloc 字段与此处保存的旧值进行比较,从而判断写空闲缓冲区是否 *增大,从而可以进行封装数据包的分配. */ 1461 tmp = sk->wmem_alloc; /*此处调用 release_sock,间接的发送数据包从而腾出写缓冲区空间*/ 1462 release_sock(sk); 1463 cli(); 1464 /* 1465 * Again we will try to avoid it. 1466 */ /*此处的判断条件一方面要检测写缓冲区增大,另一方面要保证套接字可进行 *数据发送,另外在此期间部可出现错误。 */ 1467 if (tmp <= sk->wmem_alloc && 1468 (sk->state == TCP_ESTABLISHED|| sk->state == TCP_CLOSE_WAIT) 1469 && sk->err == 0) 1470 { 1471 sk->socket->flags &= ~SO_NOSPACE; /*睡眠等待写缓冲区空闲空间增大*/ 1472 interruptible_sleep_on(sk->sleep); /*如果唤醒是由中断造成的,则返回相应值*/ 1473 if (current->signal & ~current->blocked) 1474 { 1475 sti(); 1476 if (copied) 1477 return(copied); 1478 return(-ERESTARTSYS); 1479 } 1480 } /*程序运行到此处,表示有足够写空闲缓冲区供分配,此时设置 inuse 标志, *表示本进程在使用该套接字。调用 continue 语句使程序回到前面进行封装 *数据包的分配。 */ 1481 sk->inuse = 1; 1482 sti(); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1483 continue; 1484 } 承接上文,如果 skb!=NULL,则表示封装数据包分配成功,此时将拷贝用户数据到给数据包 中,并进行帧首部的建立,为发送数据包做准备。 1485 skb->len = 0; 1486 skb->sk = sk; 1487 skb->free = 0; 1488 skb->localroute = sk->localroute|(flags&MSG_DONTROUTE); 以上代码暂时初始化数据封装结构 sk_buff 结构变量 skb 中的一些字段,len 字段值将在稍后被 更新。 1489 buff = skb->data; buff 字段指向缓冲区首部,该缓冲区中将包含一个完整数据帧。以下代码将完成这个数据帧的 创建:包括 MAC,IP,TCP 首部及最后用户数据。 1490 /* 1491 * FIXME: we need to optimize this. 1492 * Perhaps some hints here would be good. 1493 */ 1494 tmp = prot->build_header(skb, sk->saddr, sk->daddr, &dev, 1495 IPPROTO_TCP, sk->opt, skb->mem_len,sk->ip_tos,sk->ip_ttl); 1496 if (tmp < 0 ) 1497 { 1498 prot->wfree(sk, skb->mem_addr, skb->mem_len); 1499 release_sock(sk); 1500 if (copied) 1501 return(copied); 1502 return(tmp); 1503 } 调用 ip_build_header 函数创建 MAC,IP 首部,如果创建失败,释放封装数据包,返回相应值。 1504 skb->len += tmp; 1505 skb->dev = dev; 1506 buff += tmp; 1507 skb->h.th =(struct tcphdr *) buff; 更新字段值,注意此时 buff 变量指向将要创建的 TCP 首部的第一个字节位置处。 1508 tmp = tcp_build_header((struct tcphdr *)buff, sk, len-copy); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1509 if (tmp < 0) 1510 { 1511 prot->wfree(sk, skb->mem_addr, skb->mem_len); 1512 release_sock(sk); 1513 if (copied) 1514 return(copied); 1515 return(tmp); 1516 } 创建 TCP 首部,如果创建失败,返回相应值。如果 TCP 首部创建成功,则表示数据帧所有首 部均创建成功,接下来要进行用户数据的复制填充。 1517 if (flags & MSG_OOB) 1518 { 1519 ((struct tcphdr *)buff)->urg = 1; /* 注意 TCP 首部中 urgent 字段的赋值,该字段被赋值为用户数据的长度。 * TCP 首部中 urgent 指针的大小为 16-bit,故最大值为 65535, * 从而用其表示数据长度是合理的。 * 而不是位置的绝对地址(因为大多数时候序列号会超出该字段最大值)。 */ 1520 ((struct tcphdr *)buff)->urg_ptr = ntohs(copy); 1521 } 以上针对 OOB 数据设置 TCP 首部中 urg 标志位,该标志位设置为 1,接收端在接收到该数据包 后将立刻将数据上交给用户应用程序,所以该标志位作为紧急数据的一种暗示。 1522 skb->len += tmp; //完成用户数据的复制 1523 memcpy_fromfs(buff+tmp, from, copy); 1524 from += copy; 1525 copied += copy; 1526 len -= copy; 1527 skb->len += copy; 1528 skb->free = 0; 1529 sk->write_seq += copy; 更新各变量值,注意 sock 结构之 write_seq 字段值的更新,该字段表示本地所发送数据的最后 序列号,用于对数据进行编号,创建 TCP 首部时,其中 seq 字段值即来自于 write_seq。 1530 if (send_tmp != NULL && sk->packets_out) 1531 { 1532 tcp_enqueue_partial(send_tmp, sk); 1533 continue; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1534 } 注意在前文分析中特别交待,如果 send_tmp!=NULL,则表示可以等待合并后需数据,而且分 配的数据包是按最大容量计算的以便合并后续用户数据,所以此处调用 tcp_euqueue_partial 函数 将数据包缓存到 partial 队列中。 1535 tcp_send_skb(sk, skb); 如果无需缓存,则直接调用 tcp_send_skb 函数将该数据包发往下层进行处理。如果剩余数据长 度仍然大于 0,则回到 while 循环继续其它剩余数据的处理。 1536 }//when(len>0) 跳出 while 循环,表示此次用户所以数据均已成功进行了处理,清 err 字段表示操作正常。 如果此时 partial 队列中缓存有数据包(等待后续数据),并且之前发送所有数据包得到应答(即 无数据包在网络中,packets_out=0),或者未采用 Nagle 算法(每次最多只能有一个未应答数据 包在外发送)且数据包长度在窗口容限之内,则不再进行后续数据等待,而是调用 tcp_send_partial 将此数据包发送出去,这是为了利用这一段空闲时刻进行数据包的发送。 1537 sk->err = 0; 1538 /* 1539 * Nagle's rule. Turn Nagle off with TCP_NODELAY for highly 1540 * interactive fast network servers. It's meant to be on and 1541 * it really improves the throughput though not the echo time 1542 * on my slow slip link - Alan 1543 */ 1544 /* 1545 * Avoid possible race on send_tmp - c/o Johannes Stille 1546 */ 1547 if(sk->partial && ((!sk->packets_out) 1548 /* If not nagling we can send on the before case too.. */ 1549 || (sk->nonagle && before(sk->write_seq , sk->window_seq)) 1550 )) 1551 tcp_send_partial(sk); 1552 release_sock(sk); 1553 return(copied); 1554 } 到此,tcp_write 函数分析完毕。虽然较长,但实现功能简单。可以简单总结如下: 1) 检测发送通道是否正常,否则等待。 2) 是否需要进行前后数据合并,否则分配新封装数据包,是则填充 partial 队列中老数据包。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3) 是否成功分配封装数据包,否则等待。 4) 是否成功创建数据帧,否则退出,是则发送。 5) 所有操作中是否出现错误,是则返回错误退出。 1555 /* 1556 * This is just a wrapper. 1557 */ 1558 static int tcp_sendto(struct sock *sk, unsigned char *from, 1559 int len, int nonblock, unsigned flags, 1560 struct sockaddr_in *addr, int addr_len) 1561 { 1562 if (flags & ~(MSG_OOB|MSG_DONTROUTE)) 1563 return -EINVAL; 1564 if (sk->state == TCP_CLOSE) 1565 return -ENOTCONN; 1566 if (addr_len < sizeof(*addr)) 1567 return -EINVAL; 1568 if (addr->sin_family && addr->sin_family != AF_INET) 1569 return -EINVAL; 1570 if (addr->sin_port != sk->dummy_th.dest) 1571 return -EISCONN; 1572 if (addr->sin_addr.s_addr != sk->daddr) 1573 return -EISCONN; 1574 return tcp_write(sk, from, len, nonblock, flags); 1575 } 由于使用 TCP 协议的套接字在整个数据传输期间是一对一的,在通信过程中通信双方是固定的。 所以一旦建立连接后,发送的所有数据包都是到达同一个目的端应用程序。虽然对于 TCP 协议 定义了 sendto 函数,在发送时可以指定远端地址,但这个地址必须与起初建立连接时所指定的 远端地址一致,不可指定其它地址以期望将该数据包发送出去。此处 sendto 函数传输层实现函 数 tcp_sendto 在调用 tcp_write 函数发送数据进行的检查即为此:查看参数指定的远端地址是否 为建立连接时指定的远端地址,如果不是,则直接错误返回。 1576 /* 1577 * Send an ack if one is backlogged at this point. Ought to merge 1578 * this with tcp_send_ack(). 1579 */ 1580 static void tcp_read_wakeup(struct sock *sk) 1581 { 1582 int tmp; 1583 struct device *dev = NULL; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1584 struct tcphdr *t1; 1585 struct sk_buff *buff; 1586 if (!sk->ack_backlog) 1587 return; 1588 /* 1589 * FIXME: we need to put code here to prevent this routine from 1590 * being called. Being called once in a while is ok, so only check 1591 * if this is the second time in a row. 1592 */ 1593 /* 1594 * We need to grab some memory, and put together an ack, 1595 * and then put it into the queue to be sent. 1596 */ 1597 buff = sk->prot->wmalloc(sk,MAX_ACK_SIZE,1, GFP_ATOMIC); 1598 if (buff == NULL) 1599 { 1600 /* Try again real soon. */ 1601 reset_xmit_timer(sk, TIME_WRITE, HZ); 1602 return; 1603 } 1604 buff->len = sizeof(struct tcphdr); 1605 buff->sk = sk; 1606 buff->localroute = sk->localroute; 1607 /* 1608 * Put in the IP header and routing stuff. 1609 */ 1610 tmp = sk->prot->build_header(buff, sk->saddr, sk->daddr, &dev, 1611 IPPROTO_TCP, sk->opt, MAX_ACK_SIZE,sk->ip_tos,sk->ip_ttl); 1612 if (tmp < 0) 1613 { 1614 buff->free = 1; 1615 sk->prot->wfree(sk, buff->mem_addr, buff->mem_len); 1616 return; 1617 } 1618 buff->len += tmp; 1619 t1 =(struct tcphdr *)(buff->data +tmp); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1620 memcpy(t1,(void *) &sk->dummy_th, sizeof(*t1)); /* 不同于 tcp_buid_header 函数中该字段的赋值,此处使用 sent-seq,而非 write-seq。 * 原因是此处构建的数据包会立刻发送出去。而调用 tcp_build_header 函数 * 构建 TCP 首部的对应数据包 * 可能需要缓存到写队列中,即可能不会立刻发送出去。 */ 1621 t1->seq = htonl(sk->sent_seq); 1622 t1->ack = 1; 1623 t1->res1 = 0; 1624 t1->res2 = 0; 1625 t1->rst = 0; 1626 t1->urg = 0; 1627 t1->syn = 0; 1628 t1->psh = 0; 1629 sk->ack_backlog = 0; 1630 sk->bytes_rcv = 0; 1631 sk->window = tcp_select_window(sk); 1632 t1->window = ntohs(sk->window); 1633 t1->ack_seq = ntohl(sk->acked_seq); 1634 t1->doff = sizeof(*t1)/4; 1635 tcp_send_check(t1, sk->saddr, sk->daddr, sizeof(*t1), sk); 1636 sk->prot->queue_xmit(sk, dev, buff, 1); 1637 tcp_statistics.TcpOutSegs++; 1638 } tcp_read_wakeup 该函数的名称令人费解,该函数实际上完成的功能即发送应答数据包。sock 结 构中 ack_backlog 字段用于计算目前累计的应发送而未发送的应答数据包的个数。该函数被 cleanup_rbuf 函数调用,从该函数被调用的目的来看,主要是通知远端发送数据包。当然远端接 收到这个应答数据包后,确实会对其发送队列进行处理(通过调用 tcp_write_xmit 函数),从这 个意义上看,函数应该改为 tcp_write_wakeup 更为合适!实际上,TCP 协议确实定义了一个名 为 tcp_write_wakeup 的函数,其与 tcp_read_wakeup 函数实现的区别在于 TCP 首部中对本地序 列号的设置,在 tcp_read_wakeup 函数中设置如下: t1->seq = htonl(sk->sent_seq); 而在 tcp_write_wakeup 函数中设置如下: t1->seq = htonl(sk->sent_seq-1); 其中 t1 均表示 TCP 首部结构类型变量。seq 表示该数据包用户数据中第一个字节的序列号。 二者虽然相差 1,但实际上从远端的角度来看,所起的作用基本相同。从本地的角度看, tcp_read_wakeup 函数可以看作是负责发送未应答数据包的专用函数,而 tcp_write_wakeup 函数 的作用如其名,是暗示远端向本地发送数据包。此处给出 tcp_write_wakeup 函数实现如下: 4236 /* 4237 * This routine sends a packet with an out of date sequence 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4238 * number. It assumes the other end will try to ack it. 4239 */ 4240 static void tcp_write_wakeup(struct sock *sk) 4241 { 4242 struct sk_buff *buff; 4243 struct tcphdr *t1; 4244 struct device *dev=NULL; 4245 int tmp; /*如果 sk->zapped=1,则表示该套接字已被远端复位,要发送数据包,必须重新建立 *连接。所以此处没有必要再发送数据包,直接退出。 */ 4246 if (sk->zapped) 4247 return; /* After a valid reset we can send no more */ 4248 /* 4249 * Write data can still be transmitted/retransmitted in the 4250 * following states. If any other state is encountered, return. 4251 * [listen/close will never occur here anyway] 4252 */ 4253 if (sk->state != TCP_ESTABLISHED && 4254 sk->state != TCP_CLOSE_WAIT && 4255 sk->state != TCP_FIN_WAIT1 && 4256 sk->state != TCP_LAST_ACK && 4257 sk->state != TCP_CLOSING 4258 ) 4259 { 4260 return; 4261 } 4262 buff = sk->prot->wmalloc(sk,MAX_ACK_SIZE,1, GFP_ATOMIC); 4263 if (buff == NULL) 4264 return; 4265 buff->len = sizeof(struct tcphdr); 4266 buff->free = 1; 4267 buff->sk = sk; 4268 buff->localroute = sk->localroute; 4269 t1 = (struct tcphdr *) buff->data; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4270 /* Put in the IP header and routing stuff. */ 4271 tmp = sk->prot->build_header(buff, sk->saddr, sk->daddr, &dev, 4272 IPPROTO_TCP, sk->opt, MAX_ACK_SIZE,sk->ip_tos,sk->ip_ttl); 4273 if (tmp < 0) 4274 { 4275 sk->prot->wfree(sk, buff->mem_addr, buff->mem_len); 4276 return; 4277 } 4278 buff->len += tmp; 4279 t1 = (struct tcphdr *)((char *)t1 +tmp); 4280 memcpy(t1,(void *) &sk->dummy_th, sizeof(*t1)); 4281 /* 4282 * Use a previous sequence. 4283 * This should cause the other end to send an ack. 4284 */ 4285 t1->seq = htonl(sk->sent_seq-1); 4286 t1->ack = 1; 4287 t1->res1= 0; 4288 t1->res2= 0; 4289 t1->rst = 0; 4290 t1->urg = 0; 4291 t1->psh = 0; 4292 t1->fin = 0; /* We are sending a 'previous' sequence, and 0 bytes of data - thus no FIN bit */ 4293 t1->syn = 0; 4294 t1->ack_seq = ntohl(sk->acked_seq); 4295 t1->window = ntohs(tcp_select_window(sk)); 4296 t1->doff = sizeof(*t1)/4; 4297 tcp_send_check(t1, sk->saddr, sk->daddr, sizeof(*t1), sk); 4298 /* 4299 * Send it and free it. 4300 * This will prevent the timer from automatically being restarted. 4301 */ 4302 sk->prot->queue_xmit(sk, dev, buff, 1); 4303 tcp_statistics.TcpOutSegs++; 4304 } 需要说明的一点是函数对当前套接字状态的判断,判断的原则是本地状态允许主动发送数据包。 tcp_write_wakeup 函数只在 tcp_send_probe0 函数中被调用,tcp_send_probe0 函数用于发送窗口 探测数据包,每当窗口探测定时器超时,该函数即被调用。窗口探测定时器是 TCP 协议四大定 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 时器之一(超时重传定时器,保活定时器,窗口探测定时器,2MSL 定时器),用于探测远端主 机窗口,防止远端主机窗口通报数据包丢失从而造成死锁,具体情况参考 TCP 协议规范 (RFC793)。从远端主机角度而言,在接收到一个 ACK 包后,其调用 tcp_ack 函数进行处理, 该函数实现中检查是否有可以发送的数据包,如有即刻发送,由此而言,tcp_write_wakeup 确实 完成了其所声称的作用。 1639 /* 1640 * FIXME: 1641 * This routine frees used buffers. 1642 * It should consider sending an ACK to let the 1643 * other end know we now have a bigger window. 1644 */ /* 本函数清除已被用户程序读取完的数据包,并通知远端本地新的窗口大小。*/ 1645 static void cleanup_rbuf(struct sock *sk) 1646 { 1647 unsigned long flags; 1648 unsigned long left; 1649 struct sk_buff *skb; 1650 unsigned long rspace; 1651 if(sk->debug) 1652 printk("cleaning rbuf for sk=%p\n", sk); 1653 save_flags(flags); 1654 cli(); 1655 left = sk->prot->rspace(sk); 1656 /* 1657 * We have to loop through all the buffer headers, 1658 * and try to free up all the space we can. 1659 */ 1660 while((skb=skb_peek(&sk->receive_queue)) != NULL) 1661 { 1662 if (!skb->used || skb->users) 1663 break; 1664 skb_unlink(skb); 1665 skb->sk = sk; 1666 kfree_skb(skb, FREE_READ); 1667 } 1668 restore_flags(flags); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1669 /* 1670 * FIXME: 1671 * At this point we should send an ack if the difference 1672 * in the window, and the amount of space is bigger than 1673 * TCP_WINDOW_DIFF. 1674 */ 1675 if(sk->debug) 1676 printk("sk->rspace = %lu, was %lu\n", sk->prot->rspace(sk), 1677 left); 1678 if ((rspace=sk->prot->rspace(sk)) != left) 1679 { 1680 /* 1681 * This area has caused the most trouble. The current strategy 1682 * is to simply do nothing if the other end has room to send at 1683 * least 3 full packets, because the ack from those will auto- 1684 * matically update the window. If the other end doesn't think 1685 * we have much space left, but we have room for at least 1 more 1686 * complete packet than it thinks we do, we will send an ack 1687 * immediately. Otherwise we will wait up to .5 seconds in case 1688 * the user reads some more. 1689 */ 1690 sk->ack_backlog++; 1691 /* 1692 * It's unclear whether to use sk->mtu or sk->mss here. They differ only 1693 * if the other end is offering a window smaller than the agreed on MSS 1694 * (called sk->mtu here). In theory there's no connection between send 1695 * and receive, and so no reason to think that they're going to send 1696 * small packets. For the moment I'm using the hack of reducing the mss 1697 * only on the send side, so I'm putting mtu here. 1698 */ 1699 if (rspace > (sk->window - sk->bytes_rcv + sk->mtu)) 1700 { 1701 /* Send an ack right now. */ 1702 tcp_read_wakeup(sk); 1703 } 1704 else 1705 { 1706 /* Force it to send an ack soon. */ 1707 int was_active = del_timer(&sk->retransmit_timer); 1708 if (!was_active || TCP_ACK_TIME < sk->timer.expires) 1709 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1710 reset_xmit_timer(sk, TIME_WRITE, TCP_ACK_TIME); 1711 } 1712 else 1713 add_timer(&sk->retransmit_timer); 1714 } 1715 } 1716 } cleanup_rbuf 函数主要负责清除接收队列(receive_queue)中数据包,清除的依据是数据包中数 据已被上层应用程序读取完,即 skb->used 设置为 1。上层应用程序在使用完一个数据包后,会 相应的将该数据包的 used 字段设置为 1。cleanup_rbuf 函数即以此为据进行清除,从而释放接收 缓冲区,如果释放后接收缓冲区增加到可以容纳 MTU 大小的数据包,则发送窗口通报数据包 (通过 tcp_read_wakeup 函数发送)。否则简单重置之前设定的定时器。 1717 /* 1718 * Handle reading urgent data. BSD has very simple semantics for 1719 * this, no blocking and very strange errors 8) 1720 */ 1721 static int tcp_read_urg(struct sock * sk, int nonblock, 1722 unsigned char *to, int len, unsigned flags) 1723 { 1724 /* 1725 * No URG data to read 1726 */ 1727 if (sk->urginline || !sk->urg_data || sk->urg_data == URG_READ) 1728 return -EINVAL; /* Yes this is right ! */ 1729 if (sk->err) 1730 { 1731 int tmp = -sk->err; 1732 sk->err = 0; 1733 return tmp; 1734 } 1735 if (sk->state == TCP_CLOSE || sk->done) 1736 { 1737 if (!sk->done) { 1738 sk->done = 1; 1739 return 0; 1740 } 1741 return -ENOTCONN; 1742 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1743 if (sk->shutdown & RCV_SHUTDOWN) 1744 { 1745 sk->done = 1; 1746 return 0; 1747 } 1748 sk->inuse = 1; 1749 if (sk->urg_data & URG_VALID) 1750 { 1751 char c = sk->urg_data; 1752 if (!(flags & MSG_PEEK)) 1753 sk->urg_data = URG_READ; 1754 put_fs_byte(c, to); 1755 release_sock(sk); 1756 return 1; 1757 } 1758 release_sock(sk); 1759 /* 1760 * Fixed the recv(..., MSG_OOB) behaviour. BSD docs and 1761 * the available implementations agree in this case: 1762 * this call should never block, independent of the 1763 * blocking state of the socket. 1764 * Mike 1765 */ 1766 return -EAGAIN; 1767 } tcp_read_urg 函数用于读取紧急(urgent)数据,TCP 首部中 URG,urg_pointer 字段用于处理紧 急数据。当接收到一个 URG 字段设置为 1 的数据包后,表示该数据包中包含紧急数据,此时调 用 tcp_urg 函数进行处理,该函数将 skb->urg_data 设置为紧急指针指向的字节: sk->urg_data = URG_VALID | *(ptr + (unsigned char *) th); 同时设置 URG_VALID 标志。注意此处的 ptr 已经过换算,表示紧急数据相对于 TCP 首部第 1 个字节的偏移,而非是紧急指针,TCP 首部中紧急指针偏移量是相对于 TCP 首部中的序列号而 言的,即相对于第一个纯数据对应的序列号而言的。 如此,tcp_read_urg 函数实现就较为容易理解,其在检查紧急数据有效后(检查 URG_VALID 标 志位),即将此紧急数据拷贝到用户缓冲区中。注意此处的紧急数据被处理成一个字节,这与 TCP 协议规范中对于紧急指针的定位有些不一致。当一个数据包 TCP 首部中 URG 字段被设置 时,我们称数据传送进入紧急模式,从其含义来看,该数据包中所包含的第一个字节直到紧急 指针所指向的字节都应该属于紧急数据范围。对于实现中将紧急数据处理成一个字节,这种处 理是实现相关的,因为本身对于紧急数据的处理方式 TCP 规范上并无明确说明,各具体实现可 以自行对如何处理紧急数据进行各自的解释,而且 TCP 协议规范虽然定义了紧急模式并且指出 紧急指针指向紧急数据的最后一个字节(或者有的地方将紧急指针解释为指向紧急数据过后的 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 第一个非紧急数据字节),但并没有说明紧急数据从何处开始,只是说当 TCP 首部中 URG 字段 被设置时,就进入紧急数据传输模式,所以在实现上,各自实现均可有不同的解释。此版本内 核中,紧急数据只被解释为紧急指针指向的那一个单个字节! 有一点需要指出的是,紧急数据与带外(OOB:Out Of Band)数据是不同的,不过现在这种不 同被简单的混淆了,很多地方将此二者混为一谈,将他们等同起来。实际上紧急数据与带外数 据是不同的概念,紧急数据是指插入到普通数据中一些字节,而待外数据一般需要新创建一个 数据通道进行独自传输,如果将 OOB 翻译为待外通道应该更确切一些。另外这种混淆一大部分 原因来源于 socket 编程接口直接将紧急数据映射为 OOB 数据。 对于接收数据包,内核维护如下几个队列: 1>全局 backlog 队列,驱动程序调用 netif_rx 将接收到的数据包缓存于该队列中。 2>sock 结构中 back_log 队列,网络层在 tcp_rcv 函数中将接收到的数据包缓存于该队列中,如 果该数据包对应的套接字正在忙于处理其它任务,无暇处理该数据包时。 3>sock 结构中 receive_queue 队列,进入该队列的数据包方可由应用层程序读取。 将全局 backlog 队列中数据包向 sock->back_log 队列移动的函数为 net_bh 函数,该函数作为下 半部分执行。而将 sock->back_log 中数据包向 sock->receive_queue 队列移动的函数为 release_sock。注意以上所指的移动并非直接的从一个队列中删除,并直接的加入的另一个队列 中。此处着重强调的是数据包的转移途径和来源,实际上此处所指的移动要复杂的多,而且数 据包从一个队列中删除,并非一定会加入的另一个队列中。 以下所讨论的 tcp_read 函数是系统调用 read 的网络层实现函数,其操作的就是 sock 结构 receive_queue 队列,只有进入该队列中数据包方可将其中的数据递交给应用程序处理。而 tcp_read 函数所实现的主要功能即是从 receive_queue 队列中取数据包,检查其中数据的合法性, 并根据用户所要求读取的数据量,尽量拷贝该数量的数据到用户缓冲区中。所谓尽量拷贝是指 有时将无法满足用户所要求的数据量,具体情况见下面对 tcp_read 函数的分析。该函数较长, 我们分段进行分析。 1768 /* 1769 * This routine copies from a sock struct into the user buffer. 1770 */ 1771 static int tcp_read(struct sock *sk, unsigned char *to, 1772 int len, int nonblock, unsigned flags) 1773 { 1774 struct wait_queue wait = { current, NULL }; 1775 int copied = 0; 1776 unsigned long peek_seq; 1777 volatile unsigned long *seq; /* So gcc doesn't overoptimise */ 1778 unsigned long used; 1779 /* 1780 * This error should be checked. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1781 */ //对于侦听套接字而言,其不负责数据的传送,其 receive_queue 队列中缓存的均是] //连接请求数据包(SYN 数据包),如果要读取的套接字状态为侦听状态,则表示应用 //层请求出现问题。 1782 if (sk->state == TCP_LISTEN) 1783 return -ENOTCONN; 1784 /* 1785 * Urgent data needs to be handled specially. 1786 */ //诚如上文对紧急指针的简单分析,虽然 OOB 数据本质上并非紧急数据,在此处直接 //将二者等同起来,如果上层请求 OOB 数据,则返回了紧急数据! //tcp_read_urg 函数上文已有介绍,注意该函数返回一个字节的紧急数据,具体情况请 //参考上文分析。 1787 if (flags & MSG_OOB) 1788 return tcp_read_urg(sk, nonblock, to, len, flags); 1789 /* 1790 * Copying sequence to update. This is volatile to handle 1791 * the multi-reader case neatly (memcpy_to/fromfs might be 1792 * inline and thus not flush cached variables otherwise). 1793 */ //分配要更新的变量,如果仅仅是 PEEK,则不对内核变量进行更新。 1794 peek_seq = sk->copied_seq; 1795 seq = &sk->copied_seq; 1796 if (flags & MSG_PEEK) 1797 seq = &peek_seq; //将函数开始处初始化的 wait 变量加入到 sock 结构 sleep 睡眠队列中,下文代码 //随时可以进入睡眠等待状态。 1798 add_wait_queue(sk->sleep, &wait); //将 sock 结构 inuse 字段设置为 1,表示套接字正忙。这种设置可以减少竞争。如 //tcp_rcv 函数处理数据包,如果检查到 inuse 字段为 1,则将数据包缓存到 sock 结构 //back_log 队列中退出,并不立刻对数据包进行处理。 1799 sk->inuse = 1; 以上片断代码只是做外围简单工作,对套接字状态的检查,以及对 OOB 数据的特殊处理,注意 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 此处代码将对 OOB 数据的请求直接映射到了紧急数据。另外上层读取数据时,需要更新 sock 结构 copied_seq 字段,从而跟踪上层应用程序读取的进度。如果仅仅是数据预先读取和检查 (MSG_PEEK),则不更新 copied_seq 字段,而是使用一个临时变量进行本函数的跟踪。最后将 sock 结构 inuse 字段设置为 1,表示套接字正忙,该字段实现了某种程度上的互斥机制。 下面我们进入 tcp_read 函数的主体部分,该部分实现为一个 while 循环,退出条件是上层所要 求的数据量已得到满足。注意 while 循环中有几处将直接返回,即并非一定要读取上层要求的 数据量方可返回,如上层使用了 NON_BLOCK 选项,即便没有读取到任何数据,也将直接返回。 1800 while (len > 0) 1801 { 1802 struct sk_buff * skb; 1803 unsigned long offset; 1804 /* 1805 * Are we at urgent data? Stop if we have read anything. 1806 */ //此处跳出循环的依据是紧急数据不可与普通数据混为一谈,如果读取过程中 //碰到紧急数据,则停止读取,返回已读取的数据量。 1807 if (copied && sk->urg_data && sk->urg_seq == *seq) 1808 break; 紧急数据是嵌入到普通数据流中的一段数据,在处理上或者其所要求的关注度上不同于普通数 据,所以在读取时紧急数据和普通数据是分开的,如果在读取普通数据的过程中碰到紧急数据, 则停止读取普通数据,不可将紧急数据作为普通数据的一部分返回给上层,上面代码的所做的 处理即是如此。 1809 /* 1810 * Next get a buffer. 1811 */ 1812 current->state = TASK_INTERRUPTIBLE; 1813 skb = skb_peek(&sk->receive_queue); 1814 do 1815 { 此处将当前进程状态设置为可中断状态,在相关条件不满足时,随时进入睡眠等待。正常情况 下,之前进程状态为 TASK_RUNNING。之后从 sock 结构 receive_queue 队列中取数据包,进入 do-while 内部循环。 1814 do 1815 { 1816 if (!skb) 1817 break; 1818 if (before(*seq, skb->h.th->seq)) 1819 break; 1820 offset = *seq - skb->h.th->seq; 1821 if (skb->h.th->syn) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1822 offset--; 1823 if (offset < skb->len) 1824 goto found_ok_skb; 1825 if (skb->h.th->fin) 1826 goto found_fin_ok; 1827 if (!(flags & MSG_PEEK)) 1828 skb->used = 1; 1829 skb = skb->next; 1830 } 1831 while (skb != (struct sk_buff *)&sk->receive_queue); 上面的 do-while 循环所做的工作如同一个检查和任务派遣机关:检查相关条件,在条件满足后, 进入相关模块进行处理(使用 goto 跳转语句实现,而非函数调用)。首先检查 receive_queue 是 否为空,如接收队列中暂时没有可读取的数据,则跳出此处的 while 循环,进入下面的处理, 具体情况参见下文的分析。此后检查数据流是否出现的断裂,seq 变量指向的是当前应用程序的 读取位置(读取的最后一个数据的序列号),如果小于接收队列中第一个数据包中数据的序列号, 则表示出现了断裂,此时无法进行读取,也跳出此处的 while 循环。如果没有出现断裂,则将 offset 字段初始化为之前已读取最后一个字节在当前要读取数据包中的偏移位置,例如如果 seq 字段为 10,skb->h.th->seq 字段为 6,则表示 skb 所表示的数据包中 6,7,8,9 四个字节为重 复字节,此时需要跳过,offset=10-6=4,即只需从偏移 4 处开始读取(偏移从 0 计数,0-3 偏移 量对应 6-9 四个字节)。另外我们知道对于 SYN,FIN 标志位,其本身都使用一个序列号,如果 该数据包中 SYN 标志位置 1,则将 offset 字段减去 1,由此来看,对于一个包含数据的 SYN 数 据包而言,SYN 标志位使用了最开始的一个序列号,数据部分使用其后的序列号。以上例为例, 则序列号 6 现在表示 SYN 标志位,数据部分从序列号 7 开始,而 seq 字段(即 copied_seq)为 10,则真正应该跳过 7,8,9 三个字节,所以此时的偏移量应该为 3,这正是将 offset 字段减 1 的原因所在。 如果 offset 小于 skb->len 则表示 skb 所表示的数据包中有可用数据,直接跳转到 found_ok_skb 处执行;否则表示该数据包中所有数据均属于重复数据,如果 MSG_PEEK 字段没有设置的话, 则将 skb->used 字段设置为 1,表示该数据包已作处理(即数据已被读取完,可以释放),如果 设置了 MSG_PEEK 标志,由于只是“偷看”,当然不能留下痕迹,所以将不作任何处理。另一 个需要特殊处理的情况是数据包中 FIN 字段设置为 1,这表示远端发送的文件结束标志(FIN 字段设置为 1,表示远端已发送完其数据而且已请求关闭发送通道),此时无论上层应用程序要 求读取多少数量,都需要返回(不可按通产情况进行等待,所以需要另行处理),这正是如下代 码的作用,对于 FIN 标志位被设置的数据包,专门跳转到 found_fin_ok 标志符所表示的模块处 进行处理。 1825 if (skb->h.th->fin) 1826 goto found_fin_ok; 对该段 do-while 作用总结如下: 1>检查接收队列中是否存在可读的数据包,如无,跳出该 do-while 模块。 2>检查数据流是否出现断裂,如果出现断裂,跳出该 do-while 模块。 3>检查数据包中是否有可用数据,如有,跳转到 found_ok_skb 标志符表示的模块处进行处理。 4>检查数据包 FIN 字段设置情况,如果 FIN 字段被设置,则表示该数据包除了可能携带的数据 以外,还是一个请求关闭发送通道数据包,此时需要特别处理,跳转到 found_fin_ok 标志符所 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 表示的模块处进行处理。 5>如果数据包中包含的都是重复数据,则在正常情况下(MSG_PEEK 标志位没有设置),设置 skb->used 字段设置为 1,表示该数据包可以被释放(cleanup_rbuf 函数负责释放),并处理下一 个可能的数据包。 注意以上五种情况并非相关排斥,如数据包可以既有可读数据,且其 FIN 标志位也设置为 1, 对应以上 3>,4>两种情况都满足,此时处理上对应的相关两个模块都会执行,但分析时我们每次 只关注其中一个方面。 根据上面的总结,我们可以将如下分析分为三个方面: A>暂时没有可读数据,对应以上总结中之 1>,2>两种情况。 B>有可读数据,对应以上总结中之 3>. C>FIN 标志位被设置,对应以上总结中之 4>. 对于暂时无可读数据的情况,将直接跳出 do-while 循环,执行跟随该 do-while 循环之后的代码 段,如下。 1832 if (copied) 1833 break; 1834 if (sk->err) 1835 { 1836 copied = -sk->err; 1837 sk->err = 0; 1838 break; 1839 } 1840 if (sk->state == TCP_CLOSE) 1841 { 1842 if (!sk->done) 1843 { 1844 sk->done = 1; 1845 break; 1846 } 1847 copied = -ENOTCONN; 1848 break; 1849 } 1850 if (sk->shutdown & RCV_SHUTDOWN) 1851 { 1852 sk->done = 1; 1853 break; 1854 } 1855 if (nonblock) 1856 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1857 copied = -EAGAIN; 1858 break; 1859 } 1860 cleanup_rbuf(sk); 1861 release_sock(sk); 1862 sk->socket->flags |= SO_WAITDATA; 1863 schedule(); 1864 sk->socket->flags &= ~SO_WAITDATA; 1865 sk->inuse = 1; 1866 if (current->signal & ~current->blocked) 1867 { 1868 copied = -ERESTARTSYS; 1869 break; 1870 } 1871 continue; 如果了解应用程序编程,我们知道对 TCP 协议使用 read 之类的读取函数时,应该检查其返回值, 我们不能期望一定会读取到我们所要求的数据量,由于 TCP 协议是基于流的,所以未读取的数 据,我们下次读取也不会出现问题,如果应用程序一定要读取一定数量的数据后,方可进行下 一步的处理,则可以在应用程序中实现为一个简单的循环,直到读取到所要求的数据量后才退 出,只要在循环条件中作适当的检查即可。 从以上代码的第一个语句即可看出其中的原因,对于 TCP 协议,如果已经读取了部分数据,暂 且又无数据可读时,其直接返回已读取的数据量,而无视上层所要求的数据量到底是多少。当 然如果一个字节都未读取,其将根据 NON_BLOCK 标志位是否设置来决定到底是立刻返回还是 睡眠等待。还有一个例外就是,如果在读取的过程中发生了错误,则即便连一个字节都未读取, 也将返回(此时使用的是 break 语句,因为跳出最外部的 while 循环后,还需要作一些处理)。 如果既没有读取到数据,期间也没有发生错误,则再次检查以下套接字的状态,如果状态变为 TCP_CLOSE,则表示如果当前没有数据可读取,以后已不会有了,此时必须立刻返回,已经没 有其他准备工作需要进行了(即不必使用 break 语句返回,而是直接使用 return 语句)。从 TCP 协议整个上下文来看,sock 结构 done 字段设置为 1,则表示对应套接字接收通道已关闭(或完 全关闭:TCP_CLOSE;或者半关闭:RCV_SHUTDOWN,且接收队列(注意是接收队列,其他 队列可能有尚未处理的数据包)中所有数据已被上层读取。以下代码实现的含义即如此。 1840 if (sk->state == TCP_CLOSE) 1841 { 1842 if (!sk->done) 1843 { 1844 sk->done = 1; 1845 break; 1846 } 1847 copied = -ENOTCONN; 1848 break; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1849 } 1850 if (sk->shutdown & RCV_SHUTDOWN) 1851 { 1852 sk->done = 1; 1853 break; 1854 } 其下即是对 NON_BLOCK 标志位的检查,如果该标志位被设置,则应上层需要,可以简单返回, 即便连一个字节都未读取到。 如果执行到此处,则表示尚未读取一个字节,套接字状态正常(可以进行数据读取),没有设置 NON_BLOCK 标志,则下面将要进行的工作无非是清除 receive_queue 队列中数据已被读取完的 数据包(注意,没有读取到数据,并非表示 receive_queue 队列为空,receive_queue 队列中有数 据包,但数据包中包含的都是重复数据也是可能情况之一),然后从其他队列中移动数据包到 receive_queue 队列中(注意,诚如前文对系统三个队列的讨论,应用程序数据只可以从 receive_queue 队列数据包中读取,如果 receive_queue 队列中无可用数据包,则需要调用相关函 数从其他队列中将数据包移动到 receive_queue 队列中。release_sock 函数的作用即是将 sock 结 构 back_log 队列中缓存的数据包尽可能的转移到 receive_queue 队列中。这儿需要注意的是对 schedule 进程调度函数的调用。由于之前当前执行进程状态已经被设置为 TASK_INTERRUPTIBLE,所以一旦调用 schedule 将本进程调度出去,则只有中断操作将本进程 状态设置为 TASK_RUNNING 方可被再次调度执行。对于中断,也分信号中断和内部中断。信 号中断是硬中断,此时的处理是退出 tcp_read 函数,即便没有读取一个字节,注意此时的返回 值为 ERESTARTSYS,该返回值提示上层程序可以重新发送 read 系统调用进行读取。对于内部 中断,则是调用能够诸如 wake_up_interruptible 函数进行的,该函数将相关进程状态重新设置为 TASK_RUNNING,使得进程具备调度的资格。 对于内部中断的情况,关键是中断调用点,我们可以想见,一旦 receive_queue 队列中加入了新 的数据包,则可以使用内部中断将这个进程唤醒,继续进行处理,tcp_data 函数(被 tcp_rcv 函 数调用,而 tcp_rcv 函数又被 release_sock 函数调用)完成数据包向 receive_queue 队列的加入, 该函数在向 receive_queue 队列加入一个新的数据包后,调用 sock 结构 data_ready 函数指针指向 的回调函数,data_ready 在 inet_create(af_inet.c)函数中创建一个新的 sock 结构时(socket 系统调 用底层响应函数)被初始化为 def_callback2 函数,def_callback2 函数实现为: 435 static void def_callback2(struct sock *sk,int len) 436 { 437 if(!sk->dead) 438 { 439 wake_up_interruptible(sk->sleep); 440 sock_wake_async(sk->socket, 1); 441 } 442 } 当前进程被重新调度后,检查被唤醒的原因,如果是硬中断,则返回 ERESTARTSYS,跳出外 部 while 循环,从而退出 tcp_read 函数,否则再一次调转到(continue 语句)外部 while 循环起 始处,从 receive_queue 队列中取数据包,进入 do-while 模块进行处理判断。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 对于代码中对 schedule 函数的调用,主要的目的是与内核其他进程共享 CPU,以 便 CPU 在网络 栈代码中处理时间过长,另外移动数据包的进程也需要得到执行。 分析完第一种情况,下面分析第二种情景:找到一个可读取数据的数据包,此时将跳转到 found_ok_skb 标志符所表示的模块处执行,该模块代码如下。该模块完成的主要工作将是拷贝 可读数据到用户缓冲区中,并更新内核相关跟踪变量。 1872 found_ok_skb: 1873 /* 1874 * Lock the buffer. We can be fairly relaxed as 1875 * an interrupt will never steal a buffer we are 1876 * using unless I've missed something serious in 1877 * tcp_data. 1878 */ 1879 skb->users++; 1880 /* 1881 * Ok so how much can we use ? 1882 */ 1883 used = skb->len - offset; 1884 if (len < used) 1885 used = len; 1886 /* 1887 * Do we have urgent data here? 1888 */ 这段代码首先将 skb 结构 users 字段加 1,防止在使用该数据包时,内核其它地方被释放。实际 上,在内核代码中很多结构都包含这样一个字段,用于保护该结构。之后代码计算该数据包中 可读取的数据量。offset 字段在前面部分被初始化为可读取数据的起始偏移量,skb->len 表示的 是数据包的长度,也可以将其认为是最后一个字节的偏移量,二者相减得到的即可读取的字节 数。如果上层要求读取的字节数小于此处提供的字节数,则只读取要求的字节数。used 变量最 后初始化为本次需要读取的字节数。 1889 if (sk->urg_data) 1890 { 当数据包中包含紧急数据时,对应 sock 结构的 urg_data 字段被赋值为紧急数据字节(注意本版 本将紧急数据处理为一个字节),即 sk->urg_data 不为 0。 /* The applellation 'urg_offset' is not appropriate, may be 'urg_data_len' would be more suitable.*/ 1891 unsigned long urg_offset = sk->urg_seq - *seq; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 注意 urg_offset 变量被初始化为以 copied_seq 为起点的偏移量,此时和 used 变量具有相同的起 点,可以直接进行比较,如 urg_offseturginline) 1897 { 1898 ++*seq; 1899 offset++; 1900 used--; 1901 } 1902 } urg_offset 等于 0 表示紧急数据是第一个可读的字节,此时根据 sock 结构 urginline 标志位进行 不同的处理,如果 sk->urginline 为 0,则简单跳过该字节,读取其之后其它普通数据。注意这种 简单跳过不同造成该紧急数据字节的丢失,因为网络栈代码在监测到 TCP 首部中 URG 标志位 被设置时,在 sk->urg_data 字段中已经保存了该紧急数据字节(tcp_urg 函数完成),此处跳过的 影响紧紧是紧急数据会被延迟提交给上层,紧急数据变得不再“紧急”而已。如果 urginline 设 置为 1,则当作普通数据处理。 1903 else 1904 used = urg_offset; 1905 } 1906 } 此处表达的意思是如果 urg_offset 处于要读取的普通数据中间,则只读取到该紧急数据为止,不 可跨越读取。结合 while 循环开始处的如下代码,可以理解为紧急数据需要另行进行处理。 1807 if (copied && sk->urg_data && sk->urg_seq == *seq) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1808 break; 1907 /* 1908 * Copy it - We _MUST_ update *seq first so that we 1909 * don't ever double read when we have dual readers 1910 */ 1911 *seq += used; 更新读取序列号,如果非 PEEK 方式,则更新的就是 copied_seq 变量,该变量表示当前读取的 最后一个字节的序列号,用于跟踪上层读取进度。 1912 /* 1913 * This memcpy_tofs can sleep. If it sleeps and we 1914 * do a second read it relies on the skb->users to avoid 1915 * a crash when cleanup_rbuf() gets called. 1916 */ 1917 memcpy_tofs(to,((unsigned char *)skb->h.th) + 1918 skb->h.th->doff*4 + offset, used); 1919 copied += used; 1920 len -= used; 1921 to += used; 1922 /* 1923 * We now will not sleep again until we are finished 1924 * with skb. Sorry if you are doing the SMP port 1925 * but you'll just have to fix it neatly ;) 1926 */ 1927 skb->users --; 1928 if (after(sk->copied_seq,sk->urg_seq)) 1929 sk->urg_data = 0; 这个语句的含义为紧急数据已经被处理,此时对 sk->urg_data 字段进行清除。在有紧急数据时, 该字段表示的是紧急数据本身。 1930 if (used + offset < skb->len) 1931 continue; 如果该数据包中数据都被处理完,则继续下面的处理,否则跳过下面代码进行再一次循环。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1932 /* 1933 * Process the FIN. 1934 */ 1935 if (skb->h.th->fin) 1936 goto found_fin_ok; 如果数据包中数据此次被读取完,则检查该数据包是否同时是一个 FIN 数据包,如是,则跳转 到 found_fin_ok 标志符处的模块进行处理。注意在前文分析内部 do-while 循环模块时也有跳转 到 found_fin_ok 处模块进行执行的情况,不过与此处不同,此处是刚处理完一个有数据的数据 包后,而前文是对重复数据包进行的处理。 1937 if (flags & MSG_PEEK) 1938 continue; 1939 skb->used = 1; 1940 continue; 将 skb->used 字段设置为 1,表示该数据包中所有数据均已经得到处理(已被读取),可以进行 释放。 下面进入第三部分分析,即如果数据包是一个 FIN 数据包,此时将进入 found_fin_ok 处模块进 行执行。 1941 found_fin_ok: 1942 ++*seq; 加 1 的含义是对 FIN 标志位所占用的一个序列号进行计数。 1943 if (flags & MSG_PEEK) 1944 break; 如果紧急是 PEEK,则目的是预读数据,无需对 FIN 数据包进行处理,此时直接跳出 while 循环 即可,等到真正进行数据读取时,会进行相应处理的。 1945 /* 1946 * All is done 1947 */ 1948 skb->used = 1; 1949 sk->shutdown |= RCV_SHUTDOWN; 1950 break; 从上文分析来看,有两种情况会跳转到 found_fin_ok 模块处进行执行,其一即这是一个重复数 据包;其二即数据包中所有数据已被读取。无论哪种情况,数据包都可以进行释放。此处将 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 skb->used 字段再次赋值为 1,有些多余,实际上以上两种情况下,该字段已经被初始化为 1。 另外既然是接收到了 FIN 数据包,则表示远端已经关闭了其发送通道,相对本地而言,即本地 接收通道已被关闭,此处相应的设置 sock 结构 shutdown 字段,最后跳出 while 循环。即主要处 理到 FIN 数据包,无论读取了多少字节,都跳出 while 循环,因为已经没有数据可供读取了, FIN 数据包标志了接收数据的结束。说到此处,有一点需要提请注意,receive_queue 中数据包 是按序列号进行排列的,所以 FIN 数据包一定是排在 receive_queue 队列的最后面,不存在 FIN 数据包后还有其它未读取数据包的情况。 1951 }//end of while 1952 remove_wait_queue(sk->sleep, &wait); 1953 current->state = TASK_RUNNING; 1954 /* Clean up data we have read: This will do ACK frames */ 1955 cleanup_rbuf(sk); 1956 release_sock(sk); 1957 return copied; 1958 }//end of tcp-read routine 函数结尾处的处理,即将本进程从可能的睡眠队列中删除,重新设置进程状态为 TASK_RUNNING.调用 cleanup_rbuf 处理 receive_queue 中已被处理的数据包(skb->used=1),最后 调用 release_sock 函数向 receive_queue 队列中加入新的数据包。 1959 /* 1960 * State processing on a close. This implements the state shift for 1961 * sending our FIN frame. Note that we only send a FIN for some 1962 * states. A shutdown() may have already sent the FIN, or we may be 1963 * closed. 1964 */ /* 执行关闭操作时(用户程序调用 close 函数),系统调用该函数根据目前连接所处的状态进行 * 相应处理,如是否需要发送 FIN。 */ 1965 static int tcp_close_state(struct sock *sk, int dead) 1966 { 1967 int ns=TCP_CLOSE; 1968 int send_fin=0; 1969 switch(sk->state) 1970 { 1971 case TCP_SYN_SENT: /* No SYN back, no FIN needed */ 1972 break; 1973 case TCP_SYN_RECV: 1974 case TCP_ESTABLISHED: /* Closedown begin */ 1975 ns=TCP_FIN_WAIT1; 1976 send_fin=1; 1977 break; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1978 case TCP_FIN_WAIT1: /* Already closing, or FIN sent: no change */ 1979 case TCP_FIN_WAIT2: 1980 case TCP_CLOSING: 1981 ns=sk->state; 1982 break; 1983 case TCP_CLOSE: 1984 case TCP_LISTEN: 1985 break; 1986 case TCP_CLOSE_WAIT: /* They have FIN'd us. We send our FIN and 1987 wait only for the ACK */ 1988 ns=TCP_LAST_ACK; 1989 send_fin=1; 1990 } 1991 tcp_set_state(sk,ns); 1992 /* 1993 * This is a (useful) BSD violating of the RFC. There is a 1994 * problem with TCP as specified in that the other end could 1995 * keep a socket open forever with no application left this end. 1996 * We use a 3 minute timeout (about the same as BSD) then kill 1997 * our end. If they send after that then tough - BUT: long enough 1998 * that we won't make the old 4*rto = almost no time - whoops 1999 * reset mistake. 2000 */ 2001 if(dead && ns==TCP_FIN_WAIT2) 2002 { 2003 int timer_active=del_timer(&sk->timer); 2004 if(timer_active) 2005 add_timer(&sk->timer); 2006 else 2007 reset_msl_timer(sk, TIME_CLOSE, TCP_FIN_TIMEOUT); 2008 } 2009 return send_fin; 2010 } tcp_close_state 函数在本地套接字(半)关闭时被调用(tcp_shutdown, tcp_close),该函数实现逻辑 也较为简单,即根据套接字的当前状态决定是否发送 FIN 数据包,然后对状态进行更新;所依 据的基本思想是,FIN 数据包的发送是为了断开已有的连接(关闭相应的发送通道),如果之前 通道尚未建立,则无需发送 FIN 数据包;如果之前成功建立的通道,则在关闭时需要发送一个 FIN 数据包进行关闭操作。 TCP 协议有以下两个状态需要明确发送 FIN 数据包:TCP_ESTABLISHED, TCP_CLOSE_WAIT. 对于实际上 TCP_SYN_RECV 状态也需要发送。TCP_SYN_RECV 状态的进入表示本地接收到 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 远端一个 SYN 数据包,而且本地对此已经进行了确认,但尚未接收到远端的确认数据包。从这 个角度而言,此时远端很可能已经接收到本地发送的 ACK 数据包,并将其状态更新为 TCP_ESTABLISHED,所以需要本地发送一个 FIN 数据包进行关闭操作。对于 TCP 协议其它状 态均无需发送 FIN 数据包,只需要根据要求更新套接字状态即可。 函数结尾处理的是远端主机长时间不进行关闭操作,从而使得本地套接字永远滞留在 TCP_FIN_WAIT2 状态的情况。TCP_FIN_WAIT2 状态表示本地已经成功进行了发送通道关闭操 作,正在等待远端发送 FIN 数据包进行关闭操作。一般而言,除某些特殊应用外,在一方进行 关闭操作后,另一方紧接着也应进行关闭操作,个别情况下会有半关闭,但处于半关闭的时间 不应(也不会)太长,否则极有可能远端主机发生问题,如果不对这种情况进行处理,则本地 将有可能永远处于半关闭状态,但又无法与远端取得联系(因为本地已经关闭发送通道:注意 半关闭只能是关闭发送通道),将造成系统资源浪费。所以一旦本地进入 TCP_FIN_WAIT2 状态 后,即启动一个定时器,如果定时器超时后,仍然处于半关闭状态,则认为与对方完全断开, 即强行设置进入 TCP_CLOSE 状态,并释放相关系统资源。函数结尾处表达的含义即是如此: 如果已经启动了定时器,则直接退出,否则就启动这样一个定时器。 2011 /* 2012 * Send a fin. 2013 */ 2014 static void tcp_send_fin(struct sock *sk) 2015 { 2016 struct proto *prot =(struct proto *)sk->prot; 2017 struct tcphdr *th =(struct tcphdr *)&sk->dummy_th; 2018 struct tcphdr *t1; 2019 struct sk_buff *buff; 2020 struct device *dev=NULL; 2021 int tmp; 2022 release_sock(sk); /* in case the malloc sleeps. */ 2023 buff = prot->wmalloc(sk, MAX_RESET_SIZE,1 , GFP_KERNEL); 2024 sk->inuse = 1; 2025 if (buff == NULL) 2026 { 2027 /* This is a disaster if it occurs */ 2028 printk("tcp_send_fin: Impossible malloc failure"); 2029 return; 2030 } 2031 /* 2032 * Administrivia 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2033 */ 2034 buff->sk = sk; 2035 buff->len = sizeof(*t1); 2036 buff->localroute = sk->localroute; 2037 t1 =(struct tcphdr *) buff->data; 2038 /* 2039 * Put in the IP header and routing stuff. 2040 */ 2041 tmp = prot->build_header(buff,sk->saddr, sk->daddr, &dev, 2042 IPPROTO_TCP, sk->opt, 2043 sizeof(struct tcphdr),sk->ip_tos,sk->ip_ttl); 2044 if (tmp < 0) 2045 { 2046 int t; 2047 /* 2048 * Finish anyway, treat this as a send that got lost. 2049 * (Not good). 2050 */ 2051 buff->free = 1; 2052 prot->wfree(sk,buff->mem_addr, buff->mem_len); 2053 sk->write_seq++; 2054 t=del_timer(&sk->timer); 2055 if(t) 2056 add_timer(&sk->timer); 2057 else 2058 reset_msl_timer(sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 2059 return; 2060 } 2061 /* 2062 * We ought to check if the end of the queue is a buffer and 2063 * if so simply add the fin to that buffer, not send it ahead. 2064 */ 2065 t1 =(struct tcphdr *)((char *)t1 +tmp); 2066 buff->len += tmp; 2067 buff->dev = dev; 2068 memcpy(t1, th, sizeof(*t1)); /* 注意使用的是 write-seq,因为即便是 FIN,也有可能被缓存, * 如果有未发送出去的数据包。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 * 则 FIN 的发送需要缓存到这些数据包之后。 */ 2069 t1->seq = ntohl(sk->write_seq); 2070 sk->write_seq++; 2071 buff->h.seq = sk->write_seq; 2072 t1->ack = 1; 2073 t1->ack_seq = ntohl(sk->acked_seq); 2074 t1->window = ntohs(sk->window=tcp_select_window(sk)); 2075 t1->fin = 1; 2076 t1->rst = 0; 2077 t1->doff = sizeof(*t1)/4; 2078 tcp_send_check(t1, sk->saddr, sk->daddr, sizeof(*t1), sk); 2079 /* 2080 * If there is data in the write queue, the fin must be appended to 2081 * the write queue. 2082 */ 2083 if (skb_peek(&sk->write_queue) != NULL) 2084 { 2085 buff->free = 0; 2086 if (buff->next != NULL) 2087 { 2088 printk("tcp_send_fin: next != NULL\n"); 2089 skb_unlink(buff); 2090 } 2091 skb_queue_tail(&sk->write_queue, buff); 2092 } 2093 else 2094 { /* 每次真正发送数据包时方才更新 sent-seq。*/ 2095 sk->sent_seq = sk->write_seq; 2096 sk->prot->queue_xmit(sk, dev, buff, 0); 2097 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 2098 } 2099 } tcp_send_fin 函数完成的功能单一,即创建一个 FIN 数据包,发送到远端进行关闭操作。函数中 大部分代码都不难理解,此处不再叙述。需要注意的一点是如果该套接字尚存在之前数据未发 送出去,则将此 FIN 数据包插入到队列最后,只当这些之前写入的数据都发送到远端后,方才 发送该 FIN 数据包进行关闭操作。 另外一点是对 sock 结构中各表示序列号变量的更新,此处需要着重说明一下,在涉及到数据包 写入和发送时都会发现这样的序列号变量的更新而且易于混淆。我们可以从内核维护的几个发 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 送队列为基础理解这几个涉及数据包发送的序列号变量(均定义在 sock 结构中)。 write_seq 变量对应 write_queue 写入队列;sent_seq 变量对应 send_head, send_tail 表示的发送队 列。内核将上层(网络层之上-即应用层)写入的数据和网络层向下层发送的数据区分对待。通 常情况下,上层写入的数据会被网络层直接发往下层(网络层之下-即传输层))进行处理,换 句话说,应用层写入的数据经过封装后将直接进入 send_head,send_tail 表示的发送队列(不经过 write_queue 写入队列),此时 write_seq 和 sent_seq 两个变量将始终相同,但是如果应用层写入 的速度大于网络层(以及下层和网络传输介质)可以处理的速度,则数据需要先在 write_queue 进行缓存,此时 write_seq 就大于 sent_seq。write_seq 队列中数据包表示应用层写入的,但尚未 发送出去的数据包;send_head,send_tail 表示的队列表示已经发送出去(此处发送出去并非一定 是指已经发送到传输介质上,有可能数据包还缓存在硬件缓冲队列中,但一旦交给硬件,我们 即认为已经发送出去)并等待 ACK 的数据包,所以 send_head,send_tail 表示的队列又称为重发 队列。TCP 协议发生超时重发时,即从该队列中取数据包重新发送。 从以上分析,对本地要发送的一个普通数据包创建 TCP 首部时,我们可以看到使用的始终将是 write_seq, 只当对一个数据包真正进行发送时,才更新 sent_seq。个别特殊情况下(如窗口探测 数据包)使用 sent_seq 进行 TCP 首部的创建。 2100 /* 2101 * Shutdown the sending side of a connection. Much like close except 2102 * that we don't receive shut down or set sk->dead=1. 2103 */ 2104 void tcp_shutdown(struct sock *sk, int how) 2105 { 2106 /* 2107 * We need to grab some memory, and put together a FIN, 2108 * and then put it into the queue to be sent. 2109 * Tim MacKenzie(tym@dibbler.cs.monash.edu.au) 4 Dec '92. 2110 */ 2111 if (!(how & SEND_SHUTDOWN)) 2112 return; 2113 /* 2114 * If we've already sent a FIN, or it's a closed state 2115 */ //对应如下这些状态,均无需作进一步处理,直接返回即可。 2116 if (sk->state == TCP_FIN_WAIT1 || 2117 sk->state == TCP_FIN_WAIT2 || 2118 sk->state == TCP_CLOSING || 2119 sk->state == TCP_LAST_ACK || 2120 sk->state == TCP_TIME_WAIT || 2121 sk->state == TCP_CLOSE || 2122 sk->state == TCP_LISTEN 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2123 ) 2124 { 2125 return; 2126 } 2127 sk->inuse = 1; 2128 /* 2129 * flag that the sender has shutdown 2130 */ 2131 sk->shutdown |= SEND_SHUTDOWN; 2132 /* 2133 * Clear out any half completed packets. 2134 */ 2135 if (sk->partial) 2136 tcp_send_partial(sk); 2137 /* 2138 * FIN if needed 2139 */ 2140 if(tcp_close_state(sk,0)) 2141 tcp_send_fin(sk); 2142 release_sock(sk); 2143 } tcp_shutdown 函数实现为进行半关闭操作。借助于第一章中对 TCP 协议各种状态的介绍,读者 不难理解本函数实现代码。注意函数尾部调用 tcp_close_state 函数,根据其返回值决定是否发送 FIN 数据包。另外如果 sock 结构 partial 字段还缓存有合并数据包,则将其立刻发送出去。sock 结构 partial 字段是一个 sk_buff 结构指针,其并非是一个数据包队列,仅仅指向一个数据包,这 个指针字段的作用是对小量数据流进行合并。因为 TCP 协议是面向流的,即数据之间不存在界 限,所以可以对应用层前后发送的分段小量数据进行合并,从而减少发送到介质上的数据包数 量,增加传输介质使用率和传输效率。实现上,如果一次应用层写入的数据量较小时,而且应 用层并非急于将数据发送给对方,则网络层处理上将分配一个最大容量(MTU)的数据包,从 而收集本次及其后写入的小量数据,最后一次性发送出去。如果在进行关闭操作时,partial 指 针指向这样的一个数据包,由于应用层已经进行关闭操作,表示已经没有数据可以积累,则可 以现在将其发送出去了,函数中完成的工作即是如此。 2144 static int 2145 tcp_recvfrom(struct sock *sk, unsigned char *to, 2146 int to_len, int nonblock, unsigned flags, 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2147 struct sockaddr_in *addr, int *addr_len) 2148 { 2149 int result; 2150 /* 2151 * Have to check these first unlike the old code. If 2152 * we check them after we lose data on an error 2153 * which is wrong 2154 */ 2155 if(addr_len) 2156 *addr_len = sizeof(*addr); 2157 result=tcp_read(sk, to, to_len, nonblock, flags); 2158 if (result < 0) 2159 return(result); 2160 if(addr) 2161 { 2162 addr->sin_family = AF_INET; 2163 addr->sin_port = sk->dummy_th.dest; 2164 addr->sin_addr.s_addr = sk->daddr; 2165 } 2166 return(result); 2167 } tcp_recvfrom 函数调用 tcp_read 函数完成数据的读取,由于其本身需要检查是否需要返回远端地 址,在读取相应数据后,还需要进行地址的复制工作,地址来源非常直接,由于 TCP 协议是面 向连接的,底层结构在连接建立之时就维护远端地址,所以此处的工作直接从 sock 结构相关字 段复制即可。 2168 /* 2169 * This routine will send an RST to the other tcp. 2170 */ 2171 static void tcp_reset(unsigned long saddr, unsigned long daddr, struct tcphdr *th, 2172 struct proto *prot, struct options *opt, struct device *dev, int tos, int ttl) 2173 { 2174 struct sk_buff *buff; 2175 struct tcphdr *t1; 2176 int tmp; 2177 struct device *ndev=NULL; 2178 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2179 * Cannot reset a reset (Think about it). 2180 */ 2181 if(th->rst) 2182 return; 2183 /* 2184 * We need to grab some memory, and put together an RST, 2185 * and then put it into the queue to be sent. 2186 */ 2187 buff = prot->wmalloc(NULL, MAX_RESET_SIZE, 1, GFP_ATOMIC); 2188 if (buff == NULL) 2189 return; 2190 buff->len = sizeof(*t1); 2191 buff->sk = NULL; 2192 buff->dev = dev; 2193 buff->localroute = 0; 2194 t1 =(struct tcphdr *) buff->data; 2195 /* 2196 * Put in the IP header and routing stuff. 2197 */ 2198 tmp = prot->build_header(buff, saddr, daddr, &ndev, IPPROTO_TCP, opt, 2199 sizeof(struct tcphdr),tos,ttl); 2200 if (tmp < 0) 2201 { 2202 buff->free = 1; 2203 prot->wfree(NULL, buff->mem_addr, buff->mem_len); 2204 return; 2205 } 2206 t1 =(struct tcphdr *)((char *)t1 +tmp); 2207 buff->len += tmp; 2208 memcpy(t1, th, sizeof(*t1)); 2209 /* 2210 * Swap the send and the receive. 2211 */ 2212 t1->dest = th->source; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2213 t1->source = th->dest; 2214 t1->rst = 1; 2215 t1->window = 0; /* 当需要发送一个 RST 数据包给对方时, * 此时根据引起该 RST 的数据包的 ACK 标志位是否设置情况进行对应的处理: * 如果对方 ACK=1, 则将 seq 设置为对方 TCP 首部中 ack-seq 字段值, * 否则设置 seq=0,ack-seq=th->seq+data_len. */ 2216 if(th->ack) 2217 { 2218 t1->ack = 0; 2219 t1->seq = th->ack_seq; 2220 t1->ack_seq = 0; 2221 } 2222 else 2223 { 2224 t1->ack = 1; 2225 if(!th->syn) 2226 t1->ack_seq=htonl(th->seq); 2227 else 2228 t1->ack_seq=htonl(th->seq+1); 2229 t1->seq=0; 2230 } 2231 t1->syn = 0; 2232 t1->urg = 0; 2233 t1->fin = 0; 2234 t1->psh = 0; 2235 t1->doff = sizeof(*t1)/4; 2236 tcp_send_check(t1, saddr, daddr, sizeof(*t1), NULL); 2237 prot->queue_xmit(NULL, ndev, buff, 1); 2238 tcp_statistics.TcpOutSegs++; 2239 } tcp_reset 函数功能单一,即向对方发送一个 RST 复位数据包。复位数据包的效果是促使对方断 开与本地的连接,如果需要的话,对方可以重新发起连接请求;或者是如果对方就是在请求连 接,则表示本地无对方请求的服务,对方在接收到该 RST 数据包,应该做出相应处理,不可“坚 持不懈”的对本地进行请求。 本函数唯一需要说明的地方即对 RST 数据包 TCP 首部中 ack 标志位,seq 和 ack_seq 字段的设 置。如果对方在请求本地未提供的服务(即此 RST 数据包是对对方 SYN 数据包的应答),则将 ack_seq 字段设置为对方想要的序列号,至于本地序列号则简单设置为 0,反正以后也犯不着与 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 对方”啰嗦“了,所以也就无关紧要。当然实际上对于 ack_seq 应答序列号的设置也可以随意, 但由于对方缓存了这个 SYN 数据包,需要本地 ACK 一下,所以正确设置这个 ack_seq 字段还 是必要的。当然如果不是请求连接的情况,那么通常情况下引起发送 RST 的数据包本身是一个 ACK 数据包,此时就需要正确的设置本地 seq 字段,而对于 ack_seq 字段则无关紧要,因为既 然本地发送一个 RST 数据包给对方,摆明了不想再从对方接收到任何数据。注意对于 TCP 协 议,一旦连接建立后,直到最后发送的一个数据包,期间相互交往的所有的数据包中 TCP 首部 ACK 标志位都被设置为 1。所以上文说,如果不是一个 SYN 请求连接数据包,则可以认定为一 个 ACK 数据包。 下一个介绍的函数是 tcp_options,该函数专门用于处理 TCP 选项,所以在介绍该函数之前,先 讨论一下 TCP 协议定义的如下几个选项。这样将大大有助于对于 tcp_options 函数的理解。 如同IP协议一样,在正常的首部之后,TCP协议也可以附带选项。最初的TCP协议规范(RFC793) 只定义三种选项:列表结束选项,无操作选项,MSS 选项。新的协议规范(RFC1323)定义了 额外的选项,如窗口变化因子选项,时间戳选项,这些选项只在某些新的网络栈代码中才有实 现,当然对于本书分析的网络栈代码只实现了最初的三个选项,所以下面这对这三个选项进行 说明,读者可察看 RFC1323 了解更多的 TCP 选项内容。 1〉 列表结束选项(End of Option List),该选项标志着所有 TCP 选项的结束,选项类 型值为 0。 2〉 无操作选项(No Operation),该选项的主要作用用于填充,从而使得选项长度始 终保持为 4 字节的倍数,或者对其下一选项在 4 字节边界上。该选项对应的类型 值为 1。 3〉 最大 TCP 报文长度选项,该选项用于在建立连接时向对方通报本地可接受的最大 TCP 报文长度,用于节制对方发送的最大报文大小。该选项只使用在连接建立的 过程中。 如下显示了以上三种选项具体格式。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2240 /* 2241 * Look for tcp options. Parses everything but only knows about MSS. 2242 * This routine is always called with the packet containing the SYN. 2243 * However it may also be called with the ack to the SYN. So you 2244 * can't assume this is always the SYN. It's always called after 2245 * we have set up sk->mtu to our own MTU. 2246 * 2247 * We need at minimum to add PAWS support here. Possibly large windows 2248 * as Linux gets deployed on 100Mb/sec networks. 2249 */ 2250 static void tcp_options(struct sock *sk, struct tcphdr *th) 2251 { 2252 unsigned char *ptr; 2253 int length=(th->doff*4)-sizeof(struct tcphdr); 2254 int mss_seen = 0; 2255 ptr = (unsigned char *)(th + 1); 2256 while(length>0) 2257 { 2258 int opcode=*ptr++; 2259 int opsize=*ptr++; 2260 switch(opcode) 2261 { 2262 case TCPOPT_EOL: 2263 return; 2264 case TCPOPT_NOP: /* Ref: RFC 793 section 3.1 */ 2265 length--; 2266 ptr--; /* the opsize=*ptr++ above was a mistake */ 2267 continue; 2268 default: 2269 if(opsize<=2) /* Avoid silly options looping forever */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2270 return; 2271 switch(opcode) 2272 { 2273 case TCPOPT_MSS: 2274 if(opsize==4 && th->syn) 2275 { 2276 sk->mtu=min(sk->mtu,ntohs(*(unsigned short *)ptr)); 2277 mss_seen = 1; 2278 } 2279 break; 2280 /* Add other options here as people feel the urge to implement stuff like large windows */ 2281 } 2282 ptr+=opsize-2; 2283 length-=opsize; 2284 } 2285 } 2286 if (th->syn) 2287 { 2288 if (! mss_seen) 2289 sk->mtu=min(sk->mtu, 536); /* default MSS if none sent */ 2290 } 2291 #ifdef CONFIG_INET_PCTCP 2292 sk->mss = min(sk->max_window >> 1, sk->mtu); 2293 #else 2294 sk->mss = min(sk->max_window, sk->mtu); 2295 #endif 2296 } 读者对照函数之前对于 TCP 选项(及其格式)的介绍,将不难理解 tcp_options 函数中代码,此 处不再叙述。需要注意的是,在建立连接时,MSS 选项是必须的,而且该选项也只能使用在连 接建立的过程中。MSS 选项用于远端对本地发送数据包大小进行限制,所以在发送一个数据包 时,除了检查本地 MTU 外,还需要对远端声明的 MSS 值进行考虑。 另外函数最后将 sock 结 构 mss 字段初始化为 mtu 字段值(一般 mtu 小于 max_window),有些令人费解,而且函数中在处 理 MSS 选项时,使用 MSS 选项值对 MTU 字段进行初始化也是不妥。这要从这两个字段所表 示的含义出发进行理解。MSS 表示最大段长度,是远端加于本地的最大段大小。MTU 表示最 大传输单元,是本地网络加于本地的数据包大小限制。最后发送的数据包对二者都要进行考虑。 实际上 MSS 值表示的 TCP 数据负载的长度,而 MTU 表示的是 IP 首部及其负载长度,即 MSS=MTU-sizeof(ip header)-sizeof(tcp header)。二者虽然都是对数据包大小的限制,但表示的范 围并不相同,函数中将二者混为一谈,实在不该。实际上,对于 1.2.13 内核版本对应的网络栈 代码在所有地方都将此二者等同起来了,这是实现上的一个错误。 2297 static inline unsigned long default_mask(unsigned long dst) 2298 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2299 dst = ntohl(dst); 2300 if (IN_CLASSA(dst)) 2301 return htonl(IN_CLASSA_NET); 2302 if (IN_CLASSB(dst)) 2303 return htonl(IN_CLASSB_NET); 2304 return htonl(IN_CLASSC_NET); 2305 } default_mask 函数用于返回对应地址的默认网络掩码。网络地址从总体上被分为 5 类,分别称为 A,B,C,D,E。具体如下所示。 函数中 IN_CLASSA 等宏均定义在 include/linux/in.h 头文件中,读者可参考第一章相关内容。 2306 /* 2307 * Default sequence number picking algorithm. 2308 * As close as possible to RFC 793, which 2309 * suggests using a 250kHz clock. 2310 * Further reading shows this assumes 2MB/s networks. 2311 * For 10MB/s ethernet, a 1MHz clock is appropriate. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2312 * That's funny, Linux has one built in! Use it! 2313 */ 2314 extern inline unsigned long tcp_init_seq(void) 2315 { 2316 struct timeval tv; 2317 do_gettimeofday(&tv); 2318 return tv.tv_usec+tv.tv_sec*1000000; 2319 } tcp_init_seq 函数用于在 TCP 协议连接建立时创建本地初始序列号。从实现来看,初始序列号根 据当前系统时间计算而得。 2320 /* 2321 * This routine handles a connection request. 2322 * It should make sure we haven't already responded. 2323 * Because of the way BSD works, we have to send a syn/ack now. 2324 * This also means it will be harder to close a socket which is 2325 * listening. 2326 */ //参数中 daddr,saddr 的理解应从远端角度出发,即实际上 daddr 表示的是本地地址, //saddr 表示的是远端地址。opt 表示接收到的 IP 选项(如有)。 //seq 是函数调用 tcp_init_seq()的返回值,表示本地初始序列号。 //dev 表示接收该数据包(skb 参数表示)的接口设备。 2327 static void tcp_conn_request(struct sock *sk, struct sk_buff *skb, 2328 unsigned long daddr, unsigned long saddr, 2329 struct options *opt, struct device *dev, unsigned long seq) 2330 { 当 tcp_rcv 函数接收一个 SYN 连接请求数据包后,其调用 tcp_conn_request 函数进行具体处理。 tcp_conn_request 函数实现的功能如同函数名,专门用于处理连接请求。该函数虽然较长,但逻 辑上非常简单:其首先创建一个新的 sock 结构(这就是我们通常所说的,侦听套接字在接收到 一个连接请求时,会创建一个新的套接字用于通信,其本身继续侦听其他客户端的请求)并对 该 sock 结构进行初始化(该函数较长即源于初始化 sock 结构字段的代码较长);此后发送一个 应答数据包,并将新创建的 sock 结构状态设置为 TCP_SYN_RECV(实际上在初始化 sock 结构 时已经进行了设置,注意侦听套接字状态仍然为 TCP_LISTEN),函数最后将该新创建的 sock 结 构与请求连接数据包绑定并挂接到侦听套接字的 receive_queue 队列中。本书在前文中一再强调, 侦听套接字接收队列 receive_queue 中缓存的均是请求连接数据包,不包含普通数据包。accept 系统调用即从侦听套接字 receive_queue 中取数据包,获得该数据包对应的 sock 结构,检查其状 态,如果状态为 TCP_ESTABLISHED,则 accept 系统调用成功返回,否则等待该 sock 结构状态 进入 TCP_ESTABLISHED.注意 tcp_conn_request 函数发送应答时,相应 sock 结构状态设置为 TCP_SYN_RECV,该 sock 结构状态转为 TCP_ESTABLISHED 是由 tcp_ack 函数完成的,tcp_ack 函数专门负责对方发送的 ACK 数据包,当监测到某个 ACK 数据包是三路握手连接过程中的完 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 成连接建立的应答数据包时,tcp_ack 函数会将对应该连接的本地 sock 结构状态设置为 TCP_ESTABLISHED.具体请参考下文中对于 tcp_ack 函数的相关分析。 对于 tcp_conn_request 函数中的大部分代码,读者可对照 sock 结构(net/inet/sock.h)定义理解。 2331 struct sk_buff *buff; 2332 struct tcphdr *t1; 2333 unsigned char *ptr; 2334 struct sock *newsk; 2335 struct tcphdr *th; 2336 struct device *ndev=NULL; 2337 int tmp; 2338 struct rtable *rt; 2339 th = skb->h.th; 此处将 th 变量设置为所接收数据包的 TCP 首部第一个字节地址,便于下面的处理。 2340 /* If the socket is dead, don't accept the connection. */ 2341 if (!sk->dead) 2342 { 2343 sk->data_ready(sk,0); 2344 } 注意 sk 变量表示的是侦听套接字 sock 结构,如果该侦听套接字状态正常,则回调 data_ready 函数指针指向的函数(def_callback2,af_inet.c),这个回调函数会唤醒睡眠在该侦听套接字睡眠 队列中的进程。一般而言,对于侦听套接字,进程在对其使用 accept 系统调用时,如果暂时无 法获得连接请求套接字,则会等待于侦听套接字的睡眠队列中。本函数是处理连接请求的,或 者更确切的说是处理对方发送的第一个 SYN 数据包,所以在这个函数中无法完成连接建立的三 路握手全过程,连接的最终完成是在 tcp_ack 中完成的,在 tcp_ack 函数中,其在检查到一个连 接完全建立后,会调用侦听套接字 sock 结构中 state_change 函数指针指向的回调函数 (def_callback1, af_inet.c),该回调函数只完成一个功能即唤醒侦听套接字睡眠队列中的进程,此 时的唤醒将使得进程可以重新检查是否有可用的已经完成连接建立的套接字请求,从而从 accept 函数中返回。对于本函数此处对 data_ready 的调用有些显得多余。 2345 else 2346 { 2347 if(sk->debug) 2348 printk("Reset on %p: Connect on dead socket.\n",sk); 2349 tcp_reset(daddr, saddr, th, sk->prot, opt, dev, sk->ip_tos,sk->ip_ttl); 2350 tcp_statistics.TcpAttemptFails++; 2351 kfree_skb(skb, FREE_READ); 2352 return; 2353 } 如果侦听套接字状态已经被设置为 dead,则表示该侦听套接字不能在继续接受远端请求,即不 再提供之前的服务,所以如果此时接收到一个服务请求,则回复 RST 数据包,断开连接或者称 为复位连接,中断与远端的进一步交互。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2354 /* 2355 * Make sure we can accept more. This will prevent a 2356 * flurry of syns from eating up all our memory. 2357 */ 2358 if (sk->ack_backlog >= sk->max_ack_backlog) 2359 { 2360 tcp_statistics.TcpAttemptFails++; 2361 kfree_skb(skb, FREE_READ); 2362 return; 2363 } sock 结构中 max_ack_backlog 变量表示的是最大接收的连接请求数据包个数,从实现的结果来 看,表达的是 sock 结构 receive_queue 队列中可缓存的最大数据包个数,这个数据包个数包括已 经完成连接请求和尚未完成连接请求的数据包。max_ack_backlog 变量是在 listen 系统调用中进 行初始化的,这一点在介绍 inet_listen(af_inet.c)函数将会清楚,另外 inet_listen 函数将 max_ack_backlog 最大值限制为 5。如果连接请求个数超过已经达到最大可处理的请求数,则简 单丢弃该数据包。注意这种丢弃将造成远端延迟重发请求,这是合理的,不同于发送一个 RST 数据包。简单丢弃表示请求暂时无法得到满足,在对方的“一再坚持”下,稍候可能获得服务, 而发送一个 RST 数据包,则表示本地根本不提供此种服务,“再坚持”也没用,所以发送一个 RST 数据包明确的告知对方本地无此服务项,不要再发送此类服务请求,而远端接收到一个 RST 数据包后,也会停止发送服务请求数据包,并返回一个错误给上层应用程序,至于上层应用程 序如何处理,则由应用程序员负责了。 接下来的这段代码较长,但完成的功能较为简单,即完成新套接字 sock 结构的创建和初始化, 初始化信息主要来自于侦听套接字中已有信息,另一方面由该新创建套接字的作用决定。 以下将对其中部分值得注意的地方进行注释,其他部分代码容易理解,此处不再阐述。 2364 /* 2365 * We need to build a new sock struct. 2366 * It is sort of bad to have a socket without an inode attached 2367 * to it, but the wake_up's will just wake up the listening socket, 2368 * and if the listening socket is destroyed before this is taken 2369 * off of the queue, this will take care of it. 2370 */ 2371 newsk = (struct sock *) kmalloc(sizeof(struct sock), GFP_ATOMIC); 2372 if (newsk == NULL) 2373 { 2374 /* just ignore the syn. It will get retransmitted. */ 2375 tcp_statistics.TcpAttemptFails++; 2376 kfree_skb(skb, FREE_READ); 2377 return; 2378 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 分配新套接字,如果分配失败,则简单丢弃连接请求数据包。 2379 memcpy(newsk, sk, sizeof(*newsk)); 注意此处的复制,下面只需要对新套接字 sock 结构中需要进行修改的地方进行操作即可。而且 这也表示了新创建套接字中主要信息来自于侦听套接字中的已有信息。 2380 skb_queue_head_init(&newsk->write_queue); 2381 skb_queue_head_init(&newsk->receive_queue); 2382 newsk->send_head = NULL; 2383 newsk->send_tail = NULL; 2384 skb_queue_head_init(&newsk->back_log); 2385 newsk->rtt = 0; /*TCP_CONNECT_TIME<<3*/ 2386 newsk->rto = TCP_TIMEOUT_INIT; 由于是连接建立期间,所以将 RTO 值设置为 TCP_TIMEOUT_INIT. 在连接建立后,RTO 值将 根据相关算法有 RTT 值计算而得,这在下文中介绍 tcp_ack 函数时会有分析。 2387 newsk->mdev = 0; sock 结构中 mdev 字段用于计算 RTO 值。 2388 newsk->max_window = 0; 2389 newsk->cong_window = 1; 2390 newsk->cong_count = 0; 2391 newsk->ssthresh = 0; 2392 newsk->backoff = 0; 2393 newsk->blog = 0; 2394 newsk->intr = 0; 2395 newsk->proc = 0; 2396 newsk->done = 0; 2397 newsk->partial = NULL; 2398 newsk->pair = NULL; 2399 newsk->wmem_alloc = 0; 2400 newsk->rmem_alloc = 0; 2401 newsk->localroute = sk->localroute; 2402 newsk->max_unacked = MAX_WINDOW - TCP_WINDOW_DIFF; 2403 newsk->err = 0; 2404 newsk->shutdown = 0; 2405 newsk->ack_backlog = 0; 2406 newsk->acked_seq = skb->h.th->seq+1; 2407 newsk->copied_seq = skb->h.th->seq+1; 2408 newsk->fin_seq = skb->h.th->seq; 此处对新创建 sock 结构中这三个序列号字段的设置非常重要。所接收数据包中 TCP 首部中 seq 字段表示其初始序列号,由于 SYN 标志位本身占据一个序列号,所以本地将从 seq+1 开始期望, acked_seq 表示本地希望从远端接收的下一个字节的序列号,copied_seq 表示本地已经送达给上 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 层应用的最后一个字节序列号,至于 fin_seq 表示的则是本地最后发送的 FIN 标志位所对应的序 列号。 2409 newsk->state = TCP_SYN_RECV; //it is very important to set this new-created sock to TCP_SYN_RECV state. //注意此处新创建的套接字状态被设置为 TCP_SYN_RECV,熟悉 TCP 协议的读者知道该状//态 是三路握手连接建立过程中服务器端在接收到一个 SYN 数据包,服务器回送一个 //SYN+ACK 数据包后进入的状态。只不过单从协议介绍中,只是简单的从表面上进行了说 //明,实际上 TCP_SYN_RECV 状态的设置是针对新创建的套接字的,侦听套接字从其起初 //创建直到其最后关闭,状态将一直是 TCP_LISTEN。 2410 newsk->timeout = 0; 2411 newsk->ip_xmit_timeout = 0; 2412 newsk->write_seq = seq; 2413 newsk->window_seq = newsk->write_seq; 2414 newsk->rcv_ack_seq = newsk->write_seq; 2415 newsk->urg_data = 0; 2416 newsk->retransmits = 0; 2417 newsk->linger=0; 2418 newsk->destroy = 0; 2419 init_timer(&newsk->timer); 2420 newsk->timer.data = (unsigned long)newsk; 2421 newsk->timer.function = &net_timer; 2422 init_timer(&newsk->retransmit_timer); 2423 newsk->retransmit_timer.data = (unsigned long)newsk; 2424 newsk->retransmit_timer.function=&retransmit_timer; 2425 newsk->dummy_th.source = skb->h.th->dest; 2426 newsk->dummy_th.dest = skb->h.th->source; 2427 /* 2428 * Swap these two, they are from our point of view. 2429 */ 2430 newsk->daddr = saddr; 2431 newsk->saddr = daddr; 从此处赋值,读者对照前文中对 tcp_conn_request 函数参数的分析。 /* Note that the new-created data socket uses the same local port at listener socket.*/ 2432 put_sock(newsk->num,newsk); 2433 newsk->dummy_th.res1 = 0; 2434 newsk->dummy_th.doff = 6; 2435 newsk->dummy_th.fin = 0; 2436 newsk->dummy_th.syn = 0; 2437 newsk->dummy_th.rst = 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2438 newsk->dummy_th.psh = 0; 2439 newsk->dummy_th.ack = 0; 2440 newsk->dummy_th.urg = 0; 2441 newsk->dummy_th.res2 = 0; 2442 newsk->acked_seq = skb->h.th->seq + 1; 2443 newsk->copied_seq = skb->h.th->seq + 1; 2444 newsk->socket = NULL; 2445 /* 2446 * Grab the ttl and tos values and use them 2447 */ 2448 newsk->ip_ttl=sk->ip_ttl; 2449 newsk->ip_tos=skb->ip_hdr->tos; 至此,这个新创建的套接字完成了主要初始化工作。 2450 /* 2451 * Use 512 or whatever user asked for 2452 */ 2453 /* 2454 * Note use of sk->user_mss, since user has no direct access to newsk 2455 */ 2456 rt=ip_rt_route(saddr, NULL,NULL); 注意 saddr 表示的远端地址,此处以此地址为目的地址在 IP 路由表查找表项,返回一个可能的 路有表项。 2457 if(rt!=NULL && (rt->rt_flags&RTF_WINDOW)) 2458 newsk->window_clamp = rt->rt_window; 2459 else 2460 newsk->window_clamp = 0; 如果返回一个可用的路有表项,则 newsk 对应 sock 结构中 window_clamp 字段的赋值即来自于 该表项(rtable 结构)中 rt_window 字段。否则直接初始化为 0。sock 结构中 window_clamp 字 段表示的是本地窗口的最大限制,该字段的具体使用在介绍相关函数时会进行说明。 对于本版本网络内核代码实现中,对于 MTU,MSS 值的处理有些随意,所以与现在这两个值 的含义冲突,所以读者在看有关 MTU,MSS 值的初始化时,不可太计较即可。 2461 if (sk->user_mss) 2462 newsk->mtu = sk->user_mss; sock 结构中 user_mss 字段是由用户明确指定的 MSS 值。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 /* 不可思议!MTUrt_flags&RTF_MSS)) 2464 newsk->mtu = rt->rt_mss - HEADER_SIZE; 2465 else 2466 { 2467 #ifdef CONFIG_INET_SNARL /* Sub Nets Are Local */ 2468 if ((saddr ^ daddr) & default_mask(saddr)) 2469 #else 2470 if ((saddr ^ daddr) & dev->pa_mask) 2471 #endif 2472 newsk->mtu = 576 - HEADER_SIZE; 2473 else 2474 newsk->mtu = MAX_WINDOW; 2475 } 2476 /* 2477 * But not bigger than device MTU 2478 */ 2479 newsk->mtu = min(newsk->mtu, dev->mtu - HEADER_SIZE); 2480 /* 2481 * This will min with what arrived in the packet 2482 */ 2483 tcp_options(newsk,skb->h.th); tcp_options 函数用于处理远端发送的 TCP 选项,对于远端发送的 SYN 数据包,其中必须包含 MSS 选项,用于向本地通报远端最大报文长度。所以 tcp_options 函数主要是从 MSS 选项中抽 出 MSS 值,并对 newsk 对应的 mss 字段进行初始化。在发送数据包给远端时,本地不可发送报 文长度大于此处远端声明的 MSS 值。 服务器端在进行新的套接字创建后,接下来的任务即回送一个 SYN+ACK 数据包,下面的代码 即完成该 SYN+ACK 数据包的创建并调用下层模块函数发送出去。对于这段代码,将不作详细 介绍,读者应该可以理解。 2484 buff = newsk->prot->wmalloc(newsk, MAX_SYN_SIZE, 1, GFP_ATOMIC); 2485 if (buff == NULL) 2486 { 2487 sk->err = ENOMEM; 2488 newsk->dead = 1; 2489 newsk->state = TCP_CLOSE; 2490 /* And this will destroy it */ 2491 release_sock(newsk); 2492 kfree_skb(skb, FREE_READ); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2493 tcp_statistics.TcpAttemptFails++; 2494 return; 2495 } 如果空间分配失败,则将其处理为简单丢弃的情况,将新创建的套接字状态设置为 TCP_CLOSE, 且将 dead 字段设置为 1,表示该套接字已不可使用。 2496 buff->len = sizeof(struct tcphdr)+4; 2497 buff->sk = newsk; 2498 buff->localroute = newsk->localroute; 2499 t1 =(struct tcphdr *) buff->data; 2500 /* 2501 * Put in the IP header and routing stuff. 2502 */ 2503 tmp = sk->prot->build_header(buff, newsk->saddr, newsk->daddr, &ndev, 2504 IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl); sock 结构 prot 字段初始化为 ip_build_header 函数,ip_build_header 函数完成 MAC,IP 首部的创 建,返回值为 MAC,IP 首部的总长度,如果创建过程中出现错误,则返回的长度值取反(即 返回一个负数)。 2505 /* 2506 * Something went wrong. 2507 */ 2508 if (tmp < 0) 2509 { 2510 sk->err = tmp; 2511 buff->free = 1; 2512 kfree_skb(buff,FREE_WRITE); 2513 newsk->dead = 1; 2514 newsk->state = TCP_CLOSE; 2515 release_sock(newsk); 2516 skb->sk = sk; 2517 kfree_skb(skb, FREE_READ); 2518 tcp_statistics.TcpAttemptFails++; 2519 return; 2520 } 如果 MAC,IP 首部创建失败,则也检查处理为丢弃数据包。 2521 buff->len += tmp; 2522 t1 =(struct tcphdr *)((char *)t1 +tmp); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2523 memcpy(t1, skb->h.th, sizeof(*t1)); /* 这个语句应该放在 t1->seq = ntohl(newsk->write_seq++)语句之后!*/ 2524 buff->h.seq = newsk->write_seq; 2525 /* 2526 * Swap the send and the receive. 2527 */ 2528 t1->dest = skb->h.th->source; 2529 t1->source = newsk->dummy_th.source; 2530 t1->seq = ntohl(newsk->write_seq++); 2531 t1->ack = 1; 2532 newsk->window = tcp_select_window(newsk); 2533 newsk->sent_seq = newsk->write_seq; 2534 t1->window = ntohs(newsk->window); 2535 t1->res1 = 0; 2536 t1->res2 = 0; 2537 t1->rst = 0; 2538 t1->urg = 0; 2539 t1->psh = 0; 2540 t1->syn = 1; 2541 t1->ack_seq = ntohl(skb->h.th->seq+1); 2542 t1->doff = sizeof(*t1)/4+1; 2543 ptr =(unsigned char *)(t1+1); 2544 ptr[0] = 2; 2545 ptr[1] = 4; 2546 ptr[2] = ((newsk->mtu) >> 8) & 0xff; 2547 ptr[3] =(newsk->mtu) & 0xff; 如上代码创建 TCP 首部,注意此处必须包含一个 MSS 选项。 2548 tcp_send_check(t1, daddr, saddr, sizeof(*t1)+4, newsk); 2549 newsk->prot->queue_xmit(newsk, ndev, buff, 0); 2550 reset_xmit_timer(newsk, TIME_WRITE , TCP_TIMEOUT_INIT); 在计算校验值后,调用 ip_queue_xmit(prot->queue_xmit 指针指向的函数)发送出去,并启动一个 超时重发定时器。 2551 skb->sk = newsk; 这个语句意义关键:将这个连接请求数据包属主更改为新创建的套接字,这与上文中的相关讨 论以及下文中将要进行的有关讨论思想一致。 2552 /* 2553 * Charge the sock_buff to newsk. 2554 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2555 sk->rmem_alloc -= skb->mem_len; 2556 newsk->rmem_alloc += skb->mem_len; 2557 skb_queue_tail(&sk->receive_queue,skb); 2558 sk->ack_backlog++; 2559 release_sock(newsk); 2560 tcp_statistics.TcpOutSegs++; 2561 } 函数结尾处的处理很‘明智’:侦听套接字只负责侦听服务请求,一旦远端发送了一个请求,侦 听套接字会创建一个实际负责提供服务的新套接字,其本身继续等待其他远端的请求。既然把 实际提供服务的任务交给了这个新的套接字,那么花费的代价就应该由该新套接字承担,所以 将正在处理的这个连接请求数据包转交给这个新创建的套接字,自然该数据包所占据的空间也 应该由新套接字承担。最后将表示连接请求的这个数据包挂接到侦听套接字 receive_queue 队列 中,注意表示这个数据包的 sk_buff 结构中的 sk 指针指向的是新创建的 sock 结构,表示真正负 责该数据包的套接字,侦听套接字只不过是其暂居之所,一旦连接完全建立,就与侦听套接字 无任何关系!另外注意,侦听套接字 sock 结构的 receive_queue 队列中缓存的数据包均是连接请 求数据包,系统调用 accept 函数传输层对应函数 tcp_accept 即从该队列中取数据包,检查取下 的数据包 sk_buff 结构中 sk 指针指向的套接字状态,如果状态已经变为连接建立完成 (TCP_ESTABLISHED),则返回sk指针指向的sock结构,否则等待于侦听套接字的睡眠队列中(注 意是侦听套接字),tcp_ack 函数在完成三路握手连接后,会通过侦听套接字设置的回调函数唤 醒侦听套接字睡眠队列中正在睡眠的进程,从而继续 accept(tcp_accept)函数的处理。这一点 在前文中已有所阐述。tcp_conn_request 函数最后更新 ack_backlog 变量值,该变量表示侦听套 接字 receive_queue 队列中最大可缓存的连接请求数据包个数。注意此处的数据包个数包括已完 成连接建立和尚未完成连接建立的数据包。 在下文中介绍 tcp_accept 函数中读者将会体会到这一点。 函数对 release_sock(newsk)的调用显得多余。对于一个新创建的 sock 结构,其不可能进行数据 包的接收,所以对其调用 release_sock 函数毫无意义。 2562 static void tcp_close(struct sock *sk, int timeout) 2563 { 2564 /* 2565 * We need to grab some memory, and put together a FIN, 2566 * and then put it into the queue to be sent. 2567 */ 2568 sk->inuse = 1; //套接字是侦听套接字的情况 2569 if(sk->state == TCP_LISTEN) 2570 { 2571 /* Special case */ 2572 tcp_set_state(sk, TCP_CLOSE); 2573 tcp_close_pending(sk); 2574 release_sock(sk); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2575 return; 2576 } //普通数据交换套接字(如客户端套接字) 2577 sk->keepopen = 1; 2578 sk->shutdown = SHUTDOWN_MASK; 2579 if (!sk->dead) 2580 sk->state_change(sk); 因为要进行关闭操作,所以将这种状态的改变通过回调函数通知到对此套接字进行操作的进程。 2581 if (timeout == 0) 2582 { timeout 参数表示等待关闭的时间,如果时间设置为 0,则表示立刻进行关闭,此时将对对应 sock 结构中 receive_queue 接收队列中数据包进行释放操作,因为进行应用程序要求立刻关闭,对于 receive_queue 队列中尚未读取的数据可以丢弃。不必等待这些数据都被读取完后才进行关闭操 作。 2583 struct sk_buff *skb; 2584 /* 2585 * We need to flush the recv. buffs. We do this only on the 2586 * descriptor close, not protocol-sourced closes, because the 2587 * reader process may not have drained the data yet! 2588 */ 2589 while((skb=skb_dequeue(&sk->receive_queue))!=NULL) 2590 kfree_skb(skb, FREE_READ); 2591 /* 2592 * Get rid off any half-completed packets. 2593 */ 2594 if (sk->partial) 2595 tcp_send_partial(sk); 2596 } 注意,在进行关闭操作时,如果 sock 结构中 partial 指针非空,则表示有一个收集数据的数据包 正待发送,由于现在要关闭该套接字(关闭其发送通道),所以现在就把该数据包直接发送出去。 2597 /* 2598 * Timeout is not the same thing - however the code likes 2599 * to send both the same way (sigh). 2600 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2601 if(timeout) 2602 { 2603 tcp_set_state(sk, TCP_CLOSE); /* Dead */ 2604 } 如果 timeout 参数非 0,则简单设置状态为 TCP_CLOSE,但并不立刻发送一个 FIN 数据包。注意 状态设置为 TCP_CLSOE 后,此后本地不可再发送其他数据包。 2605 else 2606 { 2607 if(tcp_close_state(sk,1)==1) 2608 { 2609 tcp_send_fin(sk); 2610 } 2611 } 否则根据当前套接字的状态决定是否需要发送一个 FIN 数据包给远端,具体情况参考 tcp_close_state 函数实现。 2612 release_sock(sk); release_sock 函数处理该套接字接收的数据包,注意本地进行关闭操作后,实现上只是关闭了本 地发送通道,但仍可以进行数据的接收。 2613 }//end of tcp_close tcp_close 函数是系统调用 close 函数的传输层实现。该函数首先检查进行关闭操作的套接字是否 为侦听套接字,如果是,则首先将其状态设置为 TCP_CLOSE,之后调用 tcp_close_pending 函数 对侦听套接字 receive_queue 队列中连接请求数据包进行处理。注意侦听套接字并不进行数据交 换,所以对于其进行关闭操作,主要是对其 receive_queue 队列中连接请求数据包进行处理即可, 状态的转换较为简单,此处简单设置为 TCP_CLOSE 即可。对于非侦听套接字的处理如上文分 析。 2614 /* 2615 * This routine takes stuff off of the write queue, 2616 * and puts it in the xmit queue. This happens as incoming acks 2617 * open up the remote window for us. 2618 */ 2619 static void tcp_write_xmit(struct sock *sk) 2620 { 2621 struct sk_buff *skb; tcp_write_xmit 函数处理 write_queue 队列,将队列中缓存的之前由于应用层发送数据较快而越 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 出发送窗口的数据包发送出去。该函数被 tcp_ack 函数调用,当 tcp_ack 函数接收一个应答数据 包后,其根据新的发送窗口处理 write_queue 队列,从而将其中满足发送要求的数据包及时发送 出去。 2622 /* 2623 * The bytes will have to remain here. In time closedown will 2624 * empty the write queue and all will be happy 2625 */ 2626 if(sk->zapped) 2627 return; 当接收到远端发送的 RST 复位信号后,本地 sock 结构 zapped 字段将被设置为 1,这表示通信 通道已被关闭。在传送数据之前,需要重新建立连接方可。 2628 /* 2629 * Anything on the transmit queue that fits the window can 2630 * be added providing we are not 2631 * 2632 * a) retransmitting (Nagle's rule) 2633 * b) exceeding our congestion window. 2634 */ 2635 while((skb = skb_peek(&sk->write_queue)) != NULL && 2636 before(skb->h.seq, sk->window_seq + 1) && 2637 (sk->retransmits == 0 || 2638 sk->ip_xmit_timeout != TIME_WRITE || 2639 before(skb->h.seq, sk->rcv_ack_seq + 1)) 2640 && sk->packets_out < sk->cong_window) 2641 { 2635 行这个 while 语句即表示了 write_queue 中缓存的数据包得以发送需要满足的条件,共有四 个,我们分别列出如下: 1> write_queue 队列中首先必须有需要发送的数据包,如果该队列为空,则无需进行任何操作。 2> 数据包中包含的所有数据必须在发送窗口之内。TCP 协议发送窗口的本质是远端接收缓冲 区中可用空间大小。 3> 之前发送的数据包可以得到应答,即现在没有处于超时重发状态;另外当前从 write_queue 队列中取下的数据包必须是一个新的数据包,即数据包中数据序列号应该在对方已应答序 列号之外。 4> 现在已发送出去而尚未得到应答的数据包的个数(sk->packets_out)必须小于拥塞窗口所允 许的值(sk->cong_window)。拥塞窗口值表示当前网络的拥塞程度,是通过拥塞算法计算 而得的一个估计值(计算主要在 tcp_ack 函数中进行)。 当满足以上四个条件时,就表示可以继续处理该数据包,将其发往下层进行处理。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2642 IS_SKB(skb); 2643 skb_unlink(skb); 2644 /* 2645 * See if we really need to send the packet. 2646 */ 2647 if (before(skb->h.seq, sk->rcv_ack_seq +1)) 2648 { 2649 /* 2650 * This is acked data. We can discard it. This 2651 * cannot currently occur. 2652 */ 2653 sk->retransmits = 0; 2654 kfree_skb(skb, FREE_WRITE); 2655 if (!sk->dead) 2656 sk->write_space(sk); 2657 } 2643 将数据包从 write_queue 队列中正式取下(注意 while 语句中使用了 skb_peek 函数)进行 处理。2647 行是对数据包发送必要性的检查,虽然在 2635 行 while 语句中对此作过检查,但从 C 语言“||”操作符的执行方式而言,如果第一,二两个条件其中之一满足,则并不会执行到 before(skb->h.seq, sk->rcv_ack_seq +1)语句,所以 2647 行对此进行重新检查,如果数据包中数 据的序列号在已应答序列号之中,则表示之前已经发送了相同的数据,那么该数据包就无发送 的必要,作直接丢弃处理。2656 行是通过应用层又有可用的空闲写缓冲区(因为 2654 行刚刚 释放了一个数据包占用的空间)。如果 2647 行 if 语句不满足,那么就表示可以将该数据包真正 发往下层进行处理,并最终将其发送到网络介质上。这种情况对应如下的 else 语句块。 2658 else 2659 { 2660 struct tcphdr *th; 2661 struct iphdr *iph; 2662 int size; 2663 /* 2664 * put in the ack seq and window at this point rather than earlier, 2665 * in order to keep them monotonic. We really want to avoid taking 2666 * back window allocations. That's legal, but RFC1122 says it's frowned on. 2667 * Ack and window will in general have changed since this packet was put 2668 * on the write queue. 2669 */ 2670 iph = (struct iphdr *)(skb->data + 2671 skb->dev->hard_header_len); 2672 th = (struct tcphdr *)(((char *)iph) +(iph->ihl << 2)); 2673 size = skb->len - (((unsigned char *) th) - skb->data); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2674 th->ack_seq = ntohl(sk->acked_seq); 2675 th->window = ntohs(tcp_select_window(sk)); 2676 tcp_send_check(th, sk->saddr, sk->daddr, size, sk); //TCP 校验和计算 2677 sk->sent_seq = skb->h.seq; 2678 /* 2679 * IP manages our queue for some crazy reason 2680 */ 2681 sk->prot->queue_xmit(sk, skb->dev, skb, skb->free); 2682 /* 2683 * Again we slide the timer wrongly 2684 */ 2685 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 2686 }//end of else 2687 }//end of while 2688 }//end of fucntion 2658-2686 行代码初始化数据包中必要字段,并最终调用能够 ip_queue_xmit(2681 行)函数将数 据包送往下层进行处理。2674 行对 TCP 首部中应答序列号(ack_seq)字段进行赋值,该字段 表示本地从远端已接收到的最后数据字节的序列号加 1(加 1 并非特意而为之,只是序列号从 0 算起,所以当加上数据的长度后得到的序列号自然比最后一个字节的序列号多出 1 个单位)。 2675 行初始化窗口值,这个窗口值作为远端的发送窗口,表示的是本地接收缓冲区空闲区域大 小,用以 TCP 协议的流量控制功能实现。2677 行赋值很重要,每个连接都有通信双方各自的 sock 结构表示,而 sock 结构中 sent_seq 字段表示当前本端已发送数据的最后一个字节的序列号 (加 1,因为序列号从 0 算起,以下类此,不再特别指出),这个字段在关闭连接时变得很重要, 因为可以据此检查对方是否已接收到己方发送的所有数据,只要比较对方发送的 FIN 序列号和 本地维护的 sent_seq 字段值即可。另外需要注意的是 sock 结构中还维护一个 write_seq 字段, 这两个字段表示不同的含义:write_seq 表示当前应用层发给网络栈的所有数据的最后一个字节 的序列号,而 sent_seq 表示网络栈已经发送出去(其含义表示至少数据已经交给硬件负责,已 经脱离网络栈负责范围)的所有数据的最后一个字节的序列号。 下面要介绍的 tcp_ack 函数较长,在分析该函数之前,我们先从理论上分析该函数应该完成的任 务,然后再看具体代码,可以帮助我们理解 tcp_ack 函数的具体实现。 首先 tcp_ack 函数用于处理本地接收到的 ACK 数据包,在本书其它地方已经提及,使用 TCP 协议的套接字,在连接建立完成后,此后发送的每个数据包中 ACK 标志位都被设置为 1,所以 一个 ACK 数据包本身也将包含数据,不过数据的处理专门有其他函数(tcp_data, tcp_urg)负责, tcp_ack 函数将只对 ACK 标志位及其相关联字段进行处理,这一点需要注意。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 1. 首先既然是一个 ACK 数据包,则表示本地发送的数据已经成功被远端接收,此时可以对重 发队列中已得到 ACK 的数据包进行释放。 2. 只要是从远端接收的数据包(包括 ACK 数据包),该数据包中包含远端的当前窗口大小, 本地将对此窗口进行检查,从而决定是否将写队列中的相关数据包发送出去(对应窗口增 加)。或者是将重发队列中部分数据包重新缓存到写队列中(对应窗口减小的情况)。 3. 如果数据包的交换着重于状态的更新(如连接建立,连接关闭),则根据套接字的当前状态 进行相应的更新(如将状态从 TCP_SYN_RECV 更新为 TCP_ESTABLISHED)。 4. 如果该 ACK 数据包对本地当前连接而言是一个非法 ACK 数据包,则也需要进行相关的处 理。 以上涉及到发送过程中的两个队列: 1〉 写队列,对应 sock 结构中 write_queue 字段指向的队列,这是一个双向队列。该队列接收应 用层发送的数据包(传输层将数据封装为数据包,将其挂接到 write_queue 队列中)。该队 列中数据包尚未发送出去。 2〉 重发队列,对应 sock 结构中 send_head, send_tail 字段指向的队列,这是一个单向队列, send_head 指向队列头部,send_tail 指向队列尾部。传输层(实际上是网络层)将数据包发 送出去以后,将数据包缓存到该队列中,以防止发送的数据包可能丢失后的重发工作。 下面我们进入 tcp_ack 函数实现代码的具体分析,在代码相关部分我们会对应到上面提到的需要 实现功能的 4 个方面。 2689 /* 2690 * This routine deals with incoming acks, but not outgoing ones. 2691 */ 2692 extern __inline__ int tcp_ack(struct sock *sk, struct tcphdr *th, unsigned long saddr, int len) 2693 { 2694 unsigned long ack; 2695 int flag = 0; 2696 /* 2697 * 1 - there was data in packet as well as ack or new data is sent or 2698 * in shutdown state 2699 * 2 - data from retransmit queue was acked and removed 2700 * 4 - window shrunk or data from retransmit queue was acked and removed 2701 */ 2702 if(sk->zapped) 2703 return(1); /* Dead, cant ack any more so why bother */ sock 结构 zapped 字段设置为 1,表示该套接字之前接收到远端发送的一个 RST 数据包,所以任 何从远端接收到的数据包都简单丢弃,不用处理,ACK 数据包也不例外。 2704 /* 2705 * Have we discovered a larger window 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2706 */ 2707 ack = ntohl(th->ack_seq); 将 ack 字段设置为远端期望从本地接收的下一个字节的序列号。此处对 ack_seq 的含义需要进行 说明。ack_seq 一方面表示应答序列号,另一方面也表示请求序列号。如果本地发送一个数据包, 被远端接收后,从远端的角度看,该数据包 TCP 首部中 ack_seq 字段的含义为本地期望从远端 接收的下一个字节的序列号,换句话说,在 ack_seq 字段值表示的序列号之前的数据都已经成 功被本地接收(注意不包含 ack_seq 本身所代表的序列号)。 2708 if (ntohs(th->window) > sk->max_window) 2709 { 2710 sk->max_window = ntohs(th->window); 2711 #ifdef CONFIG_INET_PCTCP 2712 /* Hack because we don't send partial packets to non SWS 2713 handling hosts */ 2714 sk->mss = min(sk->max_window>>1, sk->mtu); 2715 #else 2716 sk->mss = min(sk->max_window, sk->mtu); 2717 #endif 2718 } 以上这段代码对该数据包中所声称的窗口进行处理,并相应的更新 MSS 值。 2719 /* 2720 * We have dropped back to keepalive timeouts. Thus we have 2721 * no retransmits pending. 2722 */ 2723 if (sk->retransmits && sk->ip_xmit_timeout == TIME_KEEPOPEN) 2724 sk->retransmits = 0; 保活(Keepalive)定时器用于在双方长时间内暂无数据交换时,进行连接保持,以防止一方崩 溃后,另一方始终占用资源的情况发生。如果对应 sock 结构定时器当前被设置为保活定时器, 则表示当前应无重发情况,如果 sock 结构 retransmits 字段非 0,则对其进行清零操作。注意对 该字段的清零操作较为重要,该字段被作为其他相关处理的判断条件,在介绍到相关内容时会 做出说明。 2725 /* 2726 * If the ack is newer than sent or older than previous acks 2727 * then we can probably ignore it. 2728 */ 2729 if (after(ack, sk->sent_seq) || before(ack, sk->rcv_ack_seq)) 2730 { 2731 if(sk->debug) 2732 printk("Ack ignored %lu %lu\n",ack,sk->sent_seq); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2733 /* 2734 * Keepalive processing. 2735 */ 2736 if (after(ack, sk->sent_seq)) 2737 { 2738 return(0); 2739 } 2740 /* 2741 * Restart the keepalive timer. 2742 */ 2743 if (sk->keepopen) 2744 { 2745 if(sk->ip_xmit_timeout==TIME_KEEPOPEN) 2746 reset_xmit_timer(sk, TIME_KEEPOPEN, TCP_TIMEOUT_LEN); 2747 } 2748 return(1); 2749 } 这段代码对应答序列号进行检查,ack 变量在前文中被初始化为所接收 ACK 数据包中 TCP 首部 中 ack_seq 字段。sock 结构中 sent_seq 字段表示本地已发送的最后一个字节的序列号,注意此 处已发送并非是指这些数据已得到对方应答,而只是本地通过调用网卡驱动发送到网络介质上, 这些数据包可能仍然缓存于本地重发队列中(send_head, send_tail 指向的队列),尚未得到应答。 rcv_ack_seq 字段表示本地当前为止从远端接收到的最后一个 ACK 数据包中所包含的应答序列 号。如果当前接收的 ACK 数据包中应答序列号在 sent_seq 之后,则表示这是一个无效的应答序 列号,处理上可以直接丢弃该应答数据包。事实上,上面这段代码也是如此处理的,但是返回 值设置为 0,对于 tcp_ack 函数而言,返回 0 表示出现错误,从这一点来看,实际实现上,对于 应答序列号超前的情况是作为错误处理的。如果当前接收的应答序列号在 rcv_ack_seq 之前,表 示这是一个过期的应答序列号,对于过期的应答可能是该数据包在网络某个节点(路由器)上 被延迟的原因,所以对于这种情况,作简单丢弃处理,返回值设置为 1,表示一切正常,但无 需进行下面代码的进一步执行。注意对于以上这两种情况,均表示远端与本地的通信通道正常 (当然双方主机也正常),如果 TCP 当前处于保活阶段,则可以将保活定时器重置。 代码往下执行,表示应答序列号正常,此时需要进行进一步的处理。 2750 /* 2751 * If there is data set flag 1 2752 */ 2753 if (len != th->doff*4) 2754 flag |= 1; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 len 参数表示 IP 数据负载的长度:包括 TCP 首部和 TCP 数据负载。如果这个长度大于 TCP 首 部长度,则表示该 ACK 数据包同时包含有正常数据。此时将 flag 标志位进行相应的设置(最 低有效位设置为 1)。 2755 /* 2756 * See if our window has been shrunk. 2757 */ 2758 if (after(sk->window_seq, ack+ntohs(th->window))) 2759 { 2760 /* 2761 * We may need to move packets from the send queue 2762 * to the write queue, if the window has been shrunk on us. 2763 * The RFC says you are not allowed to shrink your window 2764 * like this, but if the other end does, you must be able 2765 * to deal with it. 2766 */ 2767 struct sk_buff *skb; 2768 struct sk_buff *skb2; 2769 struct sk_buff *wskb = NULL; 2770 skb2 = sk->send_head; 2771 sk->send_head = NULL; 2772 sk->send_tail = NULL; 2773 /* 2774 * This is an artifact of a flawed concept. We want one 2775 * queue and a smarter send routine when we send all. 2776 */ //窗口大小发生变化(被缩减),设置相应的标志位。 2777 flag |= 4; /* Window changed */ //注意本地 sock 结构 window_seq 字段的赋值方式,它的值设置为应答序列号加上 TCP 首部 //中的窗口大小,所以 window_seq 字段表示的是绝对序列号数值,而非仅仅是一个大小。 //这一点值得注意。从此处赋值语句的方式,对于前面的 if 条件判断语句应不难理解:判断 //本地窗口序列号是否大于这个 ACK 数据包所声明的窗口序列号,如果大于,则表示窗口 //大小被缩减了,此时需要对重发队列中缓存的部分数据包进行回送(到写队列中)处理。 2778 sk->window_seq = ack + ntohs(th->window); 2779 cli(); 2780 while (skb2 != NULL) 2781 { 2782 skb = skb2; 2783 skb2 = skb->link3; 2784 skb->link3 = NULL; 2785 if (after(skb->h.seq, sk->window_seq)) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2786 { 2787 if (sk->packets_out > 0) 2788 sk->packets_out--; 2789 /* We may need to remove this from the dev send list. */ 2790 if (skb->next != NULL) 2791 { 2792 skb_unlink(skb); 2793 } 2794 /* Now add it to the write_queue. */ 2795 if (wskb == NULL) 2796 skb_queue_head(&sk->write_queue,skb); 2797 else 2798 skb_append(wskb,skb); 2799 wskb = skb; 2800 } 2801 else 2802 { 2803 if (sk->send_head == NULL) 2804 { 2805 sk->send_head = skb; 2806 sk->send_tail = skb; 2807 } 2808 else 2809 { 2810 sk->send_tail->link3 = skb; 2811 sk->send_tail = skb; 2812 } 2813 skb->link3 = NULL; 2814 } 2815 } 2816 sti(); 2817 } TCP 协议极不推荐窗口大小缩减的行为,但要求 TCP 协议实现必须能够处理这种缩减行为。如 上这段代码即是处理窗口大小缩减的情况。本地 sock 结构中 window_seq 字段表示的远端窗口 大小,这是一个以远端发送的应答的序列号(这个也随时在改变)为基点的绝对数值(虽然被 称为窗口大小)。从上文中对该字段的赋值方式可以看出这点(参见上文中相关注释)。本质上, 远端窗口大小表示的是当前远端接收缓冲区中可用空间大小,表示了本地当前最多可以发送的 数据量。TCP 首部中 window 字段表示的即是实际的窗口值,但本地在表示上加上了 TCP 首部 中的应答序列号字段值。这样表成为了一个绝对数值,这种表示方式方便本地在发送数据包时 进行序列号检查。无论何种表示方式本地上都是一致的。如果窗口大小被缩减了,则在处理上 必须将重发队列中序列号超出窗口之外的数据包回送到写队列中。这段代码完成的工作即是如 此。其中重发队列由 send_head,send_tail 所指向的单向队列表示,写队列由 write_queue 指向的 双向队列表示。这段代码遍历重发队列,对队列中每个数据包进行序列号检查,如果该数据包 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 中数据序列号超出当前窗口之外,则将该数据包从重发队列中删除,插入到写队列中。处理完 后,本地 sock 结构中 sent_seq 字段将处于不一致状态。不过这种不一致状态不会对网络栈造成 实际工作上的影响。因为该字段将很快得到更新。 2818 /* 2819 * Pipe has emptied 2820 */ 2821 if (sk->send_tail == NULL || sk->send_head == NULL) 2822 { 2823 sk->send_head = NULL; 2824 sk->send_tail = NULL; 2825 sk->packets_out= 0; 2826 } 2827 /* 2828 * Update the right hand window edge of the host 2829 */ 2830 sk->window_seq = ack + ntohs(th->window); 在处理完可能的窗口大小被缩减的情况后,重新检查重发队列是否为空,如为空,则将 sock 结 构中 send_head, send_tail, packets_out 字段设置为正确值。另外更新 window_seq 字段为远端当 前声明的窗口值(前面只是用于 if 条件判断,并非进行更新,所以此处进行窗口的正式更新)。 2831 /* 2832 * We don't want too many packets out there. 2833 */ 2834 if (sk->ip_xmit_timeout == TIME_WRITE && 2835 sk->cong_window < 2048 && after(ack, sk->rcv_ack_seq)) 2836 { 2837 /* 2838 * This is Jacobson's slow start and congestion avoidance. 2839 * SIGCOMM '88, p. 328. Because we keep cong_window in integral 2840 * mss's, we can't do cwnd += 1 / cwnd. Instead, maintain a 2841 * counter and increment it once every cwnd times. It's possible 2842 * that this should be done only if sk->retransmits == 0. I'm 2843 * interpreting "new data is acked" as including data that has 2844 * been retransmitted but is just now being acked. 2845 */ 2846 if (sk->cong_window < sk->ssthresh) 2847 /* 2848 * In "safe" area, increase 2849 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2850 sk->cong_window++; 2851 else 2852 { 2853 /* 2854 * In dangerous area, increase slowly. In theory this is 2855 * sk->cong_window += 1 / sk->cong_window 2856 */ 2857 if (sk->cong_count >= sk->cong_window) 2858 { 2859 sk->cong_window++; 2860 sk->cong_count = 0; 2861 } 2862 else 2863 sk->cong_count++; 2864 } 2865 } 网络栈代码每当新发送一个数据包后,就将 sock 结构 ip_xmit_timeout 字段设置为 TCP_WRITE, 并同时启动超时重传定时器,此时接收到的这个 ACK 数据包,其无论是对之前发送的数据包的 应答,还是刚刚发送的这个数据包的应答,都表示通信通道正常。cong_window 字段含义为拥 塞窗口大小,表示的是本地最大可同时发送但未得到应答的数据包个数,注意这时本地加入的 限制,远端通过 TCP 首部中窗口大小对本地数据包发送加以限制。cong_window 字段主要用于 处理拥塞(与慢启动配合使用),如果发送的数据包得到应答,则相应的增加该窗口大小,直到 达到某个最大值(sock 结构中 ssthresh 字段表示这个最大值)。cong_count 字段在此版本网络代 码中用途不明,此处对 cong_count 进行了赋值,但并非在其他地方进行使用(只在 tcp_retransmit 函数中进行满启动过程时将该字段重新设置为 0),在发送数据包时,使用的是 cong_window 字 段和远端窗口大小进行检查。 上面的 if 条件语句判断接收到的应答序列号是否是一个好的序列号,此处硬编码的 2048 常数有 些诡异,正确的比较应该是与 sock 结构中 ssthresh 字段进行。在判断出一个好的序列号后,对 cong_window 字段进行更新(在达到最大值之前进行加 1 操作)。 2866 /* 2867 * Remember the highest ack received. 2868 */ 2869 sk->rcv_ack_seq = ack; 更新 sock 结构中 rcv_ack_seq 字段值,这个字段的含义从此处赋值语句可以看出:表示最近一 次接收的应答序列号。(大部分时候,我们通过对一个变量的赋值方式来理解该变量的意义,这 一点在分析内核代码时是一个重要和有效的方法。) 2870 /* 2871 * If this ack opens up a zero window, clear backoff. It was 2872 * being used to time the probes, and is probably far higher than 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2873 * it needs to be for normal retransmission. 2874 */ 2875 if (sk->ip_xmit_timeout == TIME_PROBE0) 2876 { 2877 sk->retransmits = 0; /* Our probe was answered */ 2878 /* 2879 * Was it a usable window open ? 2880 */ 2881 if (skb_peek(&sk->write_queue) != NULL && /* should always be non-null */ 2882 ! before (sk->window_seq, sk->write_queue.next->h.seq)) 2883 { 2884 sk->backoff = 0; 2885 /* 2886 * Recompute rto from rtt. this eliminates any backoff. 2887 */ 2888 sk->rto = ((sk->rtt >> 2) + sk->mdev) >> 1; 2889 if (sk->rto > 120*HZ) 2890 sk->rto = 120*HZ; 2891 if (sk->rto < 20) /* Was 1*HZ, then 1 - turns out we must allow about 2892 .2 of a second because of BSD delayed acks - on a 100Mb/sec link 2893 .2 of a second is going to need huge windows (SIGH) */ 2894 sk->rto = 20; 2895 } 2896 } 这段代码判断该 ACK 数据包是否是一个窗口通报数据包,如果发送端发送数据包的速度大于接 收端处理的速度,则一段时间后,接收端接收缓冲区将耗尽,此时在接收端回复的 ACK 数据包 中窗口大小将被设置为 0(注意窗口大小本质上是接收缓冲区可用空间大小),此时发送端将停 止发送数据包,直到接收端通报一个非 0 窗口大小。接收端在经过一段时间的处理后,闲置出 部分接收缓冲区,其容量可以容纳一个数据包长度后(该长度一般为一个最大报文长度),就发 送一个窗口通报数据包给发送端,从而解除发送端的发送禁令。但这种方式存在的一个可能局 面是双方限于死锁状态:接收端发送的非 0 窗口大小通报数据包不幸丢失,此时发送端“傻乎 乎”的等待这个非 0 窗口大小通报数据包,在没有等来该数据包之前,就一直禁止发送数据包; 而接收端认为其已经发送了非 0 窗口大小通报数据包,其“乐乎乎”的等待发送端发送其他数 据包,这样双方都在盲目的等待,“直到永远”。而造成这种局面的原因仅仅是丢失了该非 0 窗 口大小通报数据包。为了解决这个问题,TCP 协议规范定义了一个窗口探测定时器,发送端一 旦接收到一个 0 窗口大小的数据包,就启动该窗口探测定时器,定时间隔也采用指数退避算法, 每隔一个间隔就发送这样一个窗口探测数据包(注意虽然接收端已经声明了 0 窗口,但其必须 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 对窗口探测数据包进行接收和处理以及回复),这样即便接收端主动发送非 0 窗口大小数据包发 生丢失的情况,在接收端窗口探测数据包后,其可以再回复一个非 0 窗口数据包,可以解决之 前的死锁问题。 以上这段代码就是判断这个数据包是否是一个非 0 窗口通报数据包,如果当前定时器设置为窗 口探测定时器,则接收到的这个 ACK 数据包就是一个非 0 窗口通报数据包(无论是远端主动发 送的,还是响应本地发送的窗口探测数据包而发送的,都不影响问题本质),此时一方面更新窗 口大小值(这在前面对窗口大小进行处理时已经得到更新),另一方面对相关其他变量进行更新, 这包括清除重发标志(将 sock 结构中 retransmits 字段设置为 0),清除指数退避算法(将 sock 结构中 backoff 字段设置为 0,并重新计算 RTO 值,注意重新计算后自动清除指数退避算法引 起的加倍操作),并对计算后结果进行调整(RTO 值不可太大,也不可太小,这个大小的掌握 由具体实现决定,此处最大不可大于 2 分钟,最小不可小于 20 个系统 tick 数,一个 tick 表示一 次系统时钟中断)。 注意此处处理完窗口探测情况后并不是直接退出该函数的执行,而是进行执行下面的代码,因 为窗口探测发生于一个 0 窗口通告,而这时发生在数据正在传输过程中,而非 0 窗口通告数据 包其本身并非具有特别的格式,或者说非 0 窗口通告只是作为 TCP 首部中的一个窗口字段而存 在,这个数据包本身还可能包含有发送到另一端的数据,并且同时也是一个 ACK 数据包,所以 从无所谓的角度而言,对于窗口通告数据包的处理只是涉及到其关联的几个字段而已,而且也 不是关键字段,所以处理完这些字段后,还需要对其他重要方面进行处理,这些重要方面中其 中之一就是对 ACK 标志位进行处理,这正是下文代码所处理的任务。 对 ACK 标志位的处理体现在对重发队列中数据包的处理,所以以下这段代码主要是遍历重发队 列,对每个数据包进行序列号检查,查看接收的应答序列号是否对该数据包中数据进行了应答, 由于重发队列中数据包是按序列号排序的,所以一旦碰到一个数据序列号在应答序列号之外的 数据包即可停止遍历过程。此处一个隐含的思想是一个应答数据包可以对多个数据包进行应答, 所以发送的数据包和接收到的应答数据包不是一一对应的,应答数据包个数通常小于发送的数 据包(如果使用了 Nagle 算法,则这种关系将是一一对应的,因为 Nagle 算法的要求即是在为 接收到上一个发送的数据包应答之前,不可继续发送下一个数据包,不过 Nagle 算法主要用于 处理小量数据传输的情况,用于节约网络上数据包的个数和增加网络使用效率,绝大多数应用 包括有些小数据量传输的应用(为了保证良好的交互性)都会禁用 Nagle 算法)。 2897 /* 2898 * See if we can take anything off of the retransmit queue. 2899 */ 2900 while(sk->send_head != NULL) 2901 { 2902 /* Check for a bug. */ 2903 if (sk->send_head->link3 && 2904 after(sk->send_head->h.seq, sk->send_head->link3->h.seq)) 2905 printk("INET: tcp.c: *** bug send_list out of order.\n"); 重发队列中数据包是按序列号进行排序的,如果发现乱序的情况,打印错误信息。注意此处没 有对乱序进行处理,实际上是期望通过本 ACK 序列号和将来的 ACK 序列号进行自动修复,修 复的原理读者可自行仔细体会。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2906 /* 2907 * If our packet is before the ack sequence we can 2908 * discard it as it's confirmed to have arrived the other end. 2909 */ 对于发送的数据包 sk_buff 结构中 h 字段(union 类型)中 seq 子字段,其表示的含义该数据包 中最后一个字节的序列号(读者可参看前文中 tcp_send_skb 函数的相关代码)。在比较一个 ACK 数据包是否是对一个本地发送的数据包的应答时,当然应该使用本地发送数据包中最后一个数 据字节的序列号是否在应答序列号包含范围之内。下面的这个 if 语句即作此判断。如果在应答 范围之内,则可以对该数据包进行释放,以释放本地发送缓冲区空间。 2910 if (before(sk->send_head->h.seq, ack+1)) 2911 { 2912 struct sk_buff *oskb; 2913 if (sk->retransmits) 2914 { 2915 /* 2916 * We were retransmitting. don't count this in RTT est 2917 */ 2918 flag |= 2; 如果 sock 结构 retransmits 字段非 0,则表示已经进行过数据包重新发送操作,此时将相应设置 flag 标志位。这个标志位将决定下面是否使用该 ACK 数据包进行 RTO 计算。如果发生重发情 况,则应为无法知道该 ACK 数据包是对哪一次重发进行的应答,根据 Karn 算法,将不使用该 ACK 数据包对 RTO 值进行重新计算。 2919 /* 2920 * even though we've gotten an ack, we're still 2921 * retransmitting as long as we're sending from 2922 * the retransmit queue. Keeping retransmits non-zero 2923 * prevents us from getting new data interspersed with 2924 * retransmissions. 2925 */ 2926 if (sk->send_head->link3) /* Any more queued retransmits? */ 2927 sk->retransmits = 1; 2928 else 2929 sk->retransmits = 0; 如果重发队列中只有一个数据包,则由于收到一个 ACK 数据包,此时即结束了重发过程,则相 应的将 retransmits 字段设置为 0;否则由于暂且无法知道这个 ACK 数据包是否对重发队列中所 有数据包进行了应答,则不能将 retransmits 字段设置为 0,此处的处理是将其设置为 1(取 1 的 原因有些随意,并非非要设置为 1)。 2930 } 2931 /* 2932 * Note that we only reset backoff and rto in the 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2933 * rtt recomputation code. And that doesn't happen 2934 * if there were retransmissions in effect. So the 2935 * first new packet after the retransmissions is 2936 * sent with the backoff still in effect. Not until 2937 * we get an ack from a non-retransmitted packet do 2938 * we reset the backoff and rto. This allows us to deal 2939 * with a situation where the network delay has increased 2940 * suddenly. I.e. Karn's algorithm. (SIGCOMM '87, p5.) 2941 */ 2942 /* 2943 * We have one less packet out there. 2944 */ 2945 if (sk->packets_out > 0) 2946 sk->packets_out --; sock 结构 packets_out 字段表示当前已被发送出去但尚未接收到应答的数据包个数。由于发现一 个被应答的数据包,所以此处对应的将此字段减 1。注意相关这些变量的更新是必要的,因为 在网络代码的其他地方将根据这些变量的数值决定行为。 2947 /* 2948 * Wake up the process, it can probably write more. 2949 */ 2950 if (!sk->dead) 2951 sk->write_space(sk); 更新 packets_out 字段后,就可以对等待发送数据包的进程进行唤醒操作。注意在数据包发送函 数中 packets_out 字段将作为判断条件之一来决定是否将数据包发送出去,即系统对发送出去但 未得到应答的数据包个数有限制。 2952 oskb = sk->send_head; oskb 指向一个待释放的数据包。 2953 if (!(flag&2)) /* Not retransmitting */ 2954 { 如果 flag 变量对应标志位没有被设置,则表示可以使用该 ACK 数据包进行 RTO 的更新。由于 flag 变量对应标志位的设置参见上文分析。另外如下代码对 RTO 的更新算法请参考 TCP 协议规 范,此处也不做解释。 2955 long m; 2956 /* 2957 * The following amusing code comes from Jacobson's 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2958 * article in SIGCOMM '88. Note that rtt and mdev 2959 * are scaled versions of rtt and mean deviation. 2960 * This is designed to be as fast as possible 2961 * m stands for "measurement". 2962 */ 2963 m = jiffies - oskb->when; /* RTT */ 2964 if(m<=0) 2965 m=1; /* IS THIS RIGHT FOR <0 ??? */ 2966 m -= (sk->rtt >> 3); /* m is now error in rtt est */ 2967 sk->rtt += m; /* rtt = 7/8 rtt + 1/8 new */ 2968 if (m < 0) 2969 m = -m; /* m is now abs(error) */ 2970 m -= (sk->mdev >> 2); /* similar update on mdev */ 2971 sk->mdev += m; /* mdev = 3/4 mdev + 1/4 new */ 2972 /* 2973 * Now update timeout. Note that this removes any backoff. 2974 */ 2975 sk->rto = ((sk->rtt >> 2) + sk->mdev) >> 1; 2976 if (sk->rto > 120*HZ) 2977 sk->rto = 120*HZ; 2978 if (sk->rto < 20) /* Was 1*HZ - keep .2 as minimum cos of the BSD delayed acks */ 2979 sk->rto = 20; 注意在重新计算 RTO 值后,就需要清除指数退避算法,如下将 backoff 字段设置为 0 即是此目 的。 2980 sk->backoff = 0; 2981 } 此处重新更新 flag 变量值,对于 flag|=2 的情况可以理解,因为无论是不允许重新计算 RTO 值 还是允许计算 RTO 值,代码执行到此处,可以等同于一个结论:不允许计算 RTO 值(对于允 许的情况,刚才已经计算过了,所以此处设置该标志位,已经无关紧要)。对于 flag|=4 的含义 需要在下文代码使用到该标志位的地方才可推测出该标志位的意义,此处暂且不论。 2982 flag |= (2|4); /* 2 is really more like 'don't adjust the rtt 2983 In this case as we just set it up */ 2984 cli(); 2985 oskb = sk->send_head; 2986 IS_SKB(oskb); 2987 sk->send_head = oskb->link3; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2988 if (sk->send_head == NULL) 2989 { 2990 sk->send_tail = NULL; 2991 } 这段代码对数据包进行释放,并更新相关字段。 2992 /* 2993 * We may need to remove this from the dev send list. 2994 */ 2995 if (oskb->next) 2996 skb_unlink(oskb); sk_buff 结构 next 指针用于硬件队列中,既然该数据包已经得到应答,如果该数据包仍然缓存于 硬件队列中,则此时可以将其从中删除了。注意重发队列中数据包每重新发送一次,都要首先 缓存到硬件队列中,如果在这个 ACK 数据包到达之前,刚刚又进行了重发操作,则有可能这个 数据包还被挂接在硬件队列中。 2997 sti(); 2998 kfree_skb(oskb, FREE_WRITE); /* write. */ 解除了该数据包与其它部分的关联后,现在可以安全的调用 kfree_skb 进行释放了。 2999 if (!sk->dead) 3000 sk->write_space(sk); 每释放一个数据包,就表示发送缓冲区又有可用空间了,如果有进程等待于发送缓冲区,则需 要对这些进程进行唤醒,如上代码的目的即是如此。 3001 } 3002 else 3003 { 这个 else 语句对应重发队列中数据包最后一个字节的序列号不在应答序列号范围之内,此时可 以跳出 while 循环了,因为应答序列号所应答的数据包已经处理完毕(注意基于的前提是重发 队列中数据包是按序列号排序的,实际上也是这样操作的)。对于出现乱序的问题,只不过一个 应该被释放的数据包仍然挂接在重发队列中,后续的 ACK 数据包会对此进行处理的。所以这种 乱序不会造成不能工作的结局,而且乱序的情况也不会轻易出现。 3004 break; 3005 } 3006 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 对于 ACK 标志位的处理,诚如上文所述,即遍历重发队列,对每个数据包进行检查,具体分析 请参考代码中注释。一旦序列号超出应答序列号之外,则进入最后的 else 语句,通过 break 跳 出 while 循环,结束对 ACK 标志位的处理。 tcp_ack 函数执行到此处,通过以上对窗口的处理,以及对重发队列中有关数据包的释放,主要 有如下变量得到更新: 1〉 远端窗口大小(远端接收缓冲区可用空间大小) 2〉 本地发送缓冲区可用空间大小 3〉 已发送出去但尚未得到应答的数据包个数可能减小 条件 1〉,3〉允许我们调整 write_queue 写队列中的数据包,将其尽可能发送出去(转移到 send_head 重发队列中);条件 2〉允许上层应用程序向 write_queue 写队列中写入更多数据包。 对于向 write_queue 的写入是通过唤醒上层应用进程完成的,这个写入内核网络栈无法自行解 决,但对于 write_queue 中已有的数据包,在条件允许时,可以将其发送出去,下面的一段代码 完成的工作即是如此。 3007 /* 3008 * XXX someone ought to look at this too.. at the moment, if skb_peek() 3009 * returns non-NULL, we complete ignore the timer stuff in the else 3010 * clause. We ought to organize the code so that else clause can 3011 * (should) be executed regardless, possibly moving the PROBE timer 3012 * reset over. The skb_peek() thing should only move stuff to the 3013 * write queue, NOT also manage the timer functions. 3014 */ 3015 /* 3016 * Maybe we can take some stuff off of the write queue, 3017 * and put it onto the xmit queue. 3018 */ 3019 if (skb_peek(&sk->write_queue) != NULL) 3020 { 3021 if (after (sk->window_seq+1, sk->write_queue.next->h.seq) && 3022 (sk->retransmits == 0 || 3023 sk->ip_xmit_timeout != TIME_WRITE || 3024 before(sk->write_queue.next->h.seq, sk->rcv_ack_seq + 1)) 3025 && sk->packets_out < sk->cong_window) 3026 { 3027 /* 3028 * Add more data to the send queue. 3029 */ 3030 flag |= 1; 3031 tcp_write_xmit(sk); 3032 } 3033 else if (before(sk->window_seq, sk->write_queue.next->h.seq) && 3034 sk->send_head == NULL && 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3035 sk->ack_backlog == 0 && 3036 sk->state != TCP_TIME_WAIT) 3037 { 3038 /* 3039 * Data to queue but no room. 3040 */ 3041 reset_xmit_timer(sk, TIME_PROBE0, sk->rto); 3042 } 3043 } 注意首先使用的是 skb_peek 函数查看 write_queue 中是否缓存有数据包,对于没有缓存的情况, 我们下文分析,如果有数据包缓存,那么,现在可以对其进行处理了。当然首先要检查数据包 序列号是否在远端窗口序列号之内,并且检查这些数据包是否是需要发送的新的数据,以及发 送出去但尚未得到应答的数据包个数是否在系统允许范围之内,另外判断条件中加入的一项是 检查本地是否已经处于超时重发过程,如果还是处于超时重发,则不会处理 write_queue 中数据 包,原因很简单,对于超时的情况,有可能网络当前很忙,或者传输线路暂时出现某种问题(或 中间某个路由器挂了,或远端主机死了等等),此时不可继续进行发送从而使得问题更加严重。 对于以上检查数据包中数据是否有必要发送这个条件需要说明一下,在前面代码分析中,我们 刚刚分析有远端窗口大小缩减时,重发队列中数据包会被重新送回到写队列中,由于重发队列 中数据包已经发送出去,只是等待应答,如果对这些数据包的应答在传输中被延迟,而远端后 发送的窗口大小缩减数据包由于选择了另一条线路先行到达,则此时有可能这些被回送到写队 列中的数据已经得到应答(即远端已经成功接收这些数据),通过对 sock 结构 rcv_ack_seq 字段 检查,我们可以决定是否有必要发送这些数据(注意 rcv_ack_seq 字段的含义,参见前文对其的 赋值方式和相关分析)。 如果以上条件均满足发送,则调用 tcp_write_xmit 函数处理写队列,此函数在前文中已有分析, 此处不再论述。 如果条件不满足发送,则检查是否需要启动窗口探测定时器,注意启动的条件,重发队列为空, 本地也无需要发送的 ACK 数据包,通信通道还没有被关闭,但窗口大小不能允许本地发送数据 包(注意本地维护的窗口大小是一个绝对值,所以不可直接与 0 进行比较,而是应该与需要发 送的第一个数据包的序列号进行比较,加上其它辅助条件检查,可以推断出窗口大小是否为 0), 如果判断出窗口大小为 0,则启动窗口探测定时器。 以上 tcp_ack 函数的所有代码都没有涉及到状态转换的处理,对于三路握手连接建立过程和连接 关闭过程(此时需要 4 个数据包),都需要根据 ACK 数据包进行状态的更新。函数中对状态的 更新放在函数结尾处,此时将对状态的处理分成两个模块: 1〉 在对其他方面(主要是数据包的发送)进行了处理后进行。该模块是在一个 else 语句中, 对应的 if 语句用于判断 write_queue 中是否有待发送的数据包,如果有,则直接进入 if 语句 块,不会进行这些状态的更新,所表达的意思是如果双方都有数据包等待发送,则表示双 方正在进行数据交互,对于这种情况,对应的某些状态无需进行更新或者处理。这些状态 有:TCP_TIME_WAIT,TCP_CLOSE。 2〉 无论是否有其他数据包需要处理,对于每个接收到的 ACK 数据包都必须进行处理的状态, 这些状态的检查不与任何其他条件形成互斥,即只有代码可以执行到此处,则一定对这些 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 状态进行更新。这些状态有:TCP_LAST_ACK,TCP_FIN_WAIT1,TCP_CLOSING, TCP_SYN_RECV。 先看第一个模块中状态的处理,诚如刚才的分析,该模块放在一个 else 语句块中。 3044 else 3045 { 3046 /* 3047 * from TIME_WAIT we stay in TIME_WAIT as long as we rx packets 3048 * from TCP_CLOSE we don't do anything 3049 * 3050 * from anything else, if there is write data (or fin) pending, 3051 * we use a TIME_WRITE timeout, else if keepalive we reset to 3052 * a KEEPALIVE timeout, else we delete the timer. 3053 * 3054 * We do not set flag for nominal write data, otherwise we may 3055 * force a state where we start to write itsy bitsy tidbits 3056 * of data. 3057 */ 3058 switch(sk->state) { 3059 case TCP_TIME_WAIT: 3060 /* 3061 * keep us in TIME_WAIT until we stop getting packets, 3062 * reset the timeout. 3063 */ 3064 reset_msl_timer(sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 3065 break; 3066 case TCP_CLOSE: 3067 /* 3068 * don't touch the timer. 3069 */ 3070 break; 3071 default: 3072 /* 3073 * Must check send_head, write_queue, and ack_backlog 3074 * to determine which timeout to use. 3075 */ 3076 if (sk->send_head || skb_peek(&sk->write_queue) != NULL || sk->ack_backlog) { 3077 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 3078 } else if (sk->keepopen) { 3079 reset_xmit_timer(sk, TIME_KEEPOPEN, TCP_TIMEOUT_LEN); 3080 } else { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3081 del_timer(&sk->retransmit_timer); 3082 sk->ip_xmit_timeout = 0; 3083 } 3084 break; 3085 } 3086 } 对于 TCP_TIME_WAIT 状态的处理很简单,只要从远端接收到(FIN)数据包,则重置 2MSL 定时器。TCP_TIME_WAIT 状态表示本地已经接收到远端发送的 FIN 数据包,并且已经发送了 ACK 数据包,如果此时还继续从远端能够接收到数据包,则很有可能是本地发送的 ACK 数据 包丢失,造成远端正在重发 FIN 数据包,对于这种情况,则通过重置 2MSL 定时器对这些重发 的 FIN 数据包进行接收和处理。对于 TCP_CLOSE 状态,则无需进行任何处理,有关的处理在 其他函数关联部分会进行,对于 tcp_ack 函数就不用做什么事了。缺省状态的处理主要是对几个 定时器进行检查更新。结合该模块执行的条件加上此处本身的判断条件,这些语句应不难理解。 下面将进入到几个关键状态的更新处理。不过首先判断是否需要将用于小量数据收集的数据包 发送出去。允许发送的条件是,本地所有其他数据包都已经得到处理(即已发送被得到应答)。 为何在 tcp_ack 函数中插入该语句,原因是只有在 tcp_ack 函数中才会同时对写队列和重发队列 进行清理。我们知道网络代码最核心的理念是完成数据的传送和接收,所以一有机会,就将数 据包发送出去,在这一点网络代码是相当的“贪婪”和“得寸进尺”的。这也是一个网络栈实 现应有的理念。 3087 /* 3088 * We have nothing queued but space to send. Send any partial 3089 * packets immediately (end of Nagle rule application). 3090 */ 3091 if (sk->packets_out == 0 && sk->partial != NULL && 3092 skb_peek(&sk->write_queue) == NULL && sk->send_head == NULL) 3093 { 3094 flag |= 1; 3095 tcp_send_partial(sk); 3096 } 好了,既然该发送的都发送了,现在终于可以处理状态的更新了。首先是 TCP_LAST_ACK 状 态,代码如下: 3097 /* 3098 * In the LAST_ACK case, the other end FIN'd us. We then FIN'd them, and 3099 * we are now waiting for an acknowledge to our FIN. The other end is 3100 * already in TIME_WAIT. 3101 * 3102 * Move to TCP_CLOSE on success. 3103 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3104 if (sk->state == TCP_LAST_ACK) 3105 { 3106 if (!sk->dead) 3107 sk->state_change(sk); 3108 if(sk->debug) 3109 printk("rcv_ack_seq: %lX==%lX, acked_seq: %lX==%lX\n", 3110 sk->rcv_ack_seq,sk->write_seq,sk->acked_seq,sk->fin_seq); 3111 if (sk->rcv_ack_seq == sk->write_seq /*&& sk->acked_seq == sk->fin_seq*/) 3112 { 3113 flag |= 1; 3114 tcp_set_state(sk,TCP_CLOSE); 3115 sk->shutdown = SHUTDOWN_MASK; 3116 } 3117 } 处于 TCP_LAST_ACK 状态表示远端已经完成关闭操作,本地已经发送 FIN 数据包,双方连接 的完全关闭现在寄希望于本地接收一个 ACK 数据包,处于 TCP_LAST_ACK 状态就是在等待这 么一个 ACK 数据包。现在来了一个 ACK 数据包,如果序列号满足,那么终于可以“寿终正寝” 了。此处 rcv_ack_seq 字段在前面代码中已经被更新为应答序列号值,write_seq 表示本地写入的 最后一个字节的序列号(FIN 标志位当然是最一个序列号),如果二者相等,那么表示这个 ACK 正是对之前发送的 FIN 的应答,可以完全关闭连接了。 3118 /* 3119 * Incoming ACK to a FIN we sent in the case of our initiating the close. 3120 * 3121 * Move to FIN_WAIT2 to await a FIN from the other end. Set 3122 * SEND_SHUTDOWN but not RCV_SHUTDOWN as data can still be coming in. 3123 */ 3124 if (sk->state == TCP_FIN_WAIT1) 3125 { 3126 if (!sk->dead) 3127 sk->state_change(sk); 3128 if (sk->rcv_ack_seq == sk->write_seq) 3129 { 3130 flag |= 1; 3131 sk->shutdown |= SEND_SHUTDOWN; 3132 tcp_set_state(sk, TCP_FIN_WAIT2); 3133 } 3134 } 处于 TCP_FIN_WAIT1 表示本地属于首先关闭的一方,并且已经发送了 FIN 数据包,正在等待 ACK 数据包,从而进入到 TCP_FIN_WAIT2 状态,此处的比较同上,状态设置为 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 TCP_FIN_WAIT2。进入 TCP_FIN_WAIT2 状态后,仍然可以进行数据的接收,但不可再进行数 据的发送,这也是为何连接关闭需要四个数据包的原因,一端发送 FIN 数据包只表示其关闭了 发送通道,接收通道需要远端进行关闭,对于远端而言,此时只表示接收通道被关闭,发送通 道需要其发送一个 FIN 数据包完成。读者不要混淆了,说道这儿,有一点需要提及,一般我们 在分析代码时尤其是网络栈代码这种处理双方交互的情况,不能老是从本地的角度考虑问题, 对于一个问题的理解,有时还需要从远端进行理解,换句话说,相同的网络栈实现是运行在所 有主机上的,这个代码是同时作为本地和远端而存在的。 3135 /* 3136 * Incoming ACK to a FIN we sent in the case of a simultaneous close. 3137 * 3138 * Move to TIME_WAIT 3139 */ 3140 if (sk->state == TCP_CLOSING) 3141 { 3142 if (!sk->dead) 3143 sk->state_change(sk); 3144 if (sk->rcv_ack_seq == sk->write_seq) 3145 { 3146 flag |= 1; 3147 tcp_time_wait(sk); 3148 } 3149 } 进入到 TCP_CLOSING 状态表示发生了双方同时关闭的操作,即本地发送 FIN 数据包后,在接 收到对应的 ACK 数据包前,接收到远端发送的 FIN 数据包,在发送对此 FIN 数据包的应答后, 此时将进入 TCP_CLOSING 状态,一旦接收到 ACK 数据包,则直接进入 TCP_TIME_WAIT 状 态,进行 2MSL 等待。tcp_time_wait 函数设置进入 TCP_TIME_WAIT 状态并启动 2MSL 定时器。 3150 /* 3151 * Final ack of a three way shake 3152 */ 3153 if(sk->state==TCP_SYN_RECV) 3154 { 3155 tcp_set_state(sk, TCP_ESTABLISHED); 3156 tcp_options(sk,th); 3157 sk->dummy_th.dest=th->source; 3158 sk->copied_seq = sk->acked_seq; 3159 if(!sk->dead) 3160 sk->state_change(sk); 3161 if(sk->max_window==0) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3162 { 3163 sk->max_window=32; /* Sanity check */ 3164 sk->mss=min(sk->max_window,sk->mtu); 3165 } 3166 } 对于 TCP_SYN_RECV 状态的处理比较重要,在此处主要完成三路握手连接过程。进入 TCP_SYN_RECV 状态表示处于被动打开的一方接收到了远端发送的 SYN 数据包,并且发送了 对应的 ACK 数据包,现在等待远端回复一个 ACK 数据包,即宣告连接建立的完成。此时接收 到一个有效的 ACK 数据包(注意在检查出当前状态是 TCP_SYN_RECV 后,并无进行进一步的 检查,这一点很容易理解,因为在连接建立之前没有其他数据包,没有乱序到达,重复到达等 等问题,前面代码或者其他函数代码已经判断出这是一个有效序列号的应答,那么对于连接建 立过程而言,这些判断已经足以,只要是 TCP_SYN_RECV 状态就可以直接进行处理,无需进 一步的检查,此处也没什么好检查的),对于 TCP_SYN_RECV 的处理首先是设置状态为 TCP_ESTABLISHED,对 TCP 选项进行处理(获取 MSS 值),更新 sock 结构中相关字段,通知 上层进程(阻塞于 accept 调用的进程)有可用的套接字连接了。 3167 /* 3168 * I make no guarantees about the first clause in the following 3169 * test, i.e. "(!flag) || (flag&4)". I'm not entirely sure under 3170 * what conditions "!flag" would be true. However I think the rest 3171 * of the conditions would prevent that from causing any 3172 * unnecessary retransmission. 3173 * Clearly if the first packet has expired it should be 3174 * retransmitted. The other alternative, "flag&2 && retransmits", is 3175 * harder to explain: You have to look carefully at how and when the 3176 * timer is set and with what timeout. The most recent transmission always 3177 * sets the timer. So in general if the most recent thing has timed 3178 * out, everything before it has as well. So we want to go ahead and 3179 * retransmit some more. If we didn't explicitly test for this 3180 * condition with "flag&2 && retransmits", chances are "when + rto < jiffies" 3181 * would not be true. If you look at the pattern of timing, you can 3182 * show that rto is increased fast enough that the next packet would 3183 * almost never be retransmitted immediately. Then you'd end up 3184 * waiting for a timeout to send each packet on the retransmission 3185 * queue. With my implementation of the Karn sampling algorithm, 3186 * the timeout would double each time. The net result is that it would 3187 * take a hideous amount of time to recover from a single dropped packet. 3188 * It's possible that there should also be a test for TIME_WRITE, but 3189 * I think as long as "send_head != NULL" and "retransmit" is on, we've 3190 * got to be in real retransmission mode. 3191 * Note that tcp_do_retransmit is called with all==1. Setting cong_window 3192 * back to 1 at the timeout will cause us to send 1, then 2, etc. packets. 3193 * As long as no further losses occur, this seems reasonable. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3194 */ 3195 if (((!flag) || (flag&4)) && sk->send_head != NULL && 3196 (((flag&2) && sk->retransmits) || 3197 (sk->send_head->when + sk->rto < jiffies))) 3198 { 3199 if(sk->send_head->when + sk->rto < jiffies) 3200 tcp_retransmit(sk,0); 3201 else 3202 { 3203 tcp_do_retransmit(sk, 1); 3204 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 3205 } 3206 } 3207 return(1); 3208 } 最后一段处理超时重发,其中的 if 语句判断条件有些难于理解,从主体上分为三个 AND 语句, 以上代码中分为三行进行显示。第一个判断语句:(!flag) || (flag&4),要为真的话,flag=0 或者 flag|4 等于 1,对 于 flag=0 的情况表示这个一个纯粹的 ACK 数据包,其中只包括 IP,TCP 首部, 无任何数据负载。对于 flag 变量中 bit2 的设置在窗口大小被缩减以及重新计算 RTO 值后才进了 设置。此处的判断诚如其源代码中注释所述,意义不明。第二个判断条件是重发队列不为空, 这个条件需要和其他条件结合理解方有意义。第三个条件是系统已经进入重发过程或者当前进 行重发的定时器已经超时,此时需要进行数据包的重发(如果重发队列非空)。对于系统进入重 发过程和当前定时器超时的情况,在进入 if 语句块后进行了划分,重发定时器超时的情况除了 重新发送 send_head 队列中的数据包外,还需要加倍 RTO 值以及进行拥塞处理(tcp_retransmit 函数);而对于系统进入重发过程,此时只是重新发送 send_head 队列中的数据包,并不进行拥 塞处理和 RTO 值加倍,其中的思想是既然现在接收到一个有效的 ACK 应答,则表示线路或者 远端可以响应了,之前发送的数据包可能由于之前的堵塞丢失了,与其等待远端明确的索要, 不如本地主动重新发送一遍。当然在某些情况下,如果线路还是比较拥挤(之前的那个 ACK 数 据包是跌跌撞撞跑到这边来的),这种重发会加剧线路的拥挤度,但我们总是喜欢想象好的方面。 函数最后返回 1,表示处理正常。 tcp_ack 函数代码较长,处理的方面也较多,在分析该函数之前,我们试图从理论上解析该函数 应该实现的功能,从具体代码分析上,我们也是首先从功能上进行入手,然后才进入的代码解 析,由于关联的部分较多,在分析上不免有些漏洞(加上本人理解有限),所以读者对于某些尚 不明白的地方结合 TCP 协议相关介绍仔细捉摸,相信能够进行理解。tcp_ack 函数是 TCP 协议 实现上几个关键函数之一,对于该函数的理解将大大加深对 TCP 协议本身的理解,希望读者进 行多次分析,应尽量理解的更多更深。 3209 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3210 * Process the FIN bit. This now behaves as it is supposed to work 3211 * and the FIN takes effect when it is validly part of sequence 3212 * space. Not before when we get holes. 3213 * 3214 * If we are ESTABLISHED, a received fin moves us to CLOSE-WAIT 3215 * (and thence onto LAST-ACK and finally, CLOSE, we never enter 3216 * TIME-WAIT) 3217 * 3218 * If we are in FINWAIT-1, a received FIN indicates simultaneous 3219 * close and we go into CLOSING (and later onto TIME-WAIT) 3220 * 3221 * If we are in FINWAIT-2, a received FIN moves us to TIME-WAIT. 3222 * 3223 */ 3224 static int tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th) 3225 { 3226 sk->fin_seq = th->seq + skb->len + th->syn + th->fin; 将本地 sock 结构中 fin_seq 字段初始化为远端发送的 FIN 标志位序列号,这种多个字段相加的 赋值方式原因是 FIN 数据包很有可能同时包含数据,所以此处根据接收到的该 FIN 数据包中初 始序列号加上数据长度加上 FIN 标志位本身占用的一个序列号。注意此处也加上了对 SYN 标志 位的处理,一般而言这种情况很难出现,即在建立连接的过程中同时进行关闭操作,在此处同 时对 SYN 标志位进行检测应是为了保险起见。 3227 if (!sk->dead) 3228 { 3229 sk->state_change(sk); 3230 sock_wake_async(sk->socket, 1); 3231 } 在接收到 FIN 数据包后,相应的需要通知上层应用程序。state_change 函数指针指向函数完成的 工作仅仅是唤醒睡眠在 sock 结构 sleep 队列中的进程。具体的,state_change 变量在 af_inet.c 文 件中被初始化为 def_callback1,该函数实现如下: //net/inet/af_inet.c 430 static void def_callback1(struct sock *sk) 431 { 432 if(!sk->dead) 433 wake_up_interruptible(sk->sleep); 434 } sock_wake_async 函数定义在 socket.c 文件中,其作用下面将逐渐分析,不过首先需要指出 socket 结构中的两个字段(socket 结构的具体定义参见第一章中内容),如下所示。 struct wait_queue **wait; /* ptr to place to wait on */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 struct fasync_struct *fasync_list; /* Asynchronous wake up list */ 相关结构定义如下: // linux/wait.h 7 struct wait_queue { 8 struct task_struct * task; 9 struct wait_queue * next; 10 }; //linux/fs.h 246 struct fasync_struct { 247 int magic; 248 struct fasync_struct *fa_next; /* singly linked list */ 249 struct file *fa_file; 250 }; wait 字段是一个真正的进程睡眠队列,当由于用户作出的要求暂时无法得到满足时(如读数据 时底层无数据可读时),用户进程就将被置于该队列中睡眠。fasync_list 字段称为文件异步操作 队列中,表示依赖于该套接字的当前文件,一般而言,一个套接字只对应一个文件描述符(即 一个文件,这是由于不允许建立两个同样套接字),介绍了这两个结构定义,问题依然不明朗, 我们需要看 sock_wake_faync 函数实现,该函数定义在 socket.c 文件中,如下: //net/socket.c 414 int sock_wake_async(struct socket *sock, int how) 415 { 416 if (!sock || !sock->fasync_list) 417 return -1; 418 switch (how) 419 { 420 case 0: 421 kill_fasync(sock->fasync_list, SIGIO); 422 break; 423 case 1: 424 if (!(sock->flags & SO_WAITDATA)) 425 kill_fasync(sock->fasync_list, SIGIO); 426 break; 427 case 2: 428 if (sock->flags & SO_NOSPACE) 429 { 430 kill_fasync(sock->fasync_list, SIGIO); 431 sock->flags &= ~SO_NOSPACE; 432 } 433 break; 434 } 435 return 0; 436 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 sock_wake_faync 函数继续调用 kill_fasync 函数,不过从其传入给 kill_fasync 函数的参数我们也 可大致推断出 kill_fasync 函数功能,第一个参数是 socket 结构中 fasync_list 指向的 fasync_struct 结构队列,第二个参数是一个中断信号标志,而 fasync_struct 结构中唯一有意义的参数时 file 结构(对应 fa_file 字段),file 结构中有包含有使用该 file 的用户进程号,好了,由于用户进程 号,又给了需要的信号标志,则很明显 kill_fasync 函数将遍历 fasync_list 指向的队列,给其中 每个 fasync_struct 结构对应的用户进程发送一个指定的信号量(此处为 SIGIO)。下面给出 kill_fasync 实际实现代码。 //fs/fcntl.c 174 void kill_fasync(struct fasync_struct *fa, int sig) 175 { 176 while (fa) { 177 if (fa->magic != FASYNC_MAGIC) { 178 printk("kill_fasync: bad magic number in " 179 "fasync_struct!\n"); 180 return; 181 } 182 if (fa->fa_file->f_owner > 0) 183 kill_proc(fa->fa_file->f_owner, sig, 1); 184 else 185 kill_pg(-fa->fa_file->f_owner, sig, 1); 186 fa = fa->fa_next; 187 } 188 } 对一个进程发送信号的结果是该进程被设置为 TASK_RUNNING 状态(即唤醒进程),并对表示 进程的结构(task_struct)中设置相关标志位,一旦进程被调度运行,由于大部分进程是在处理 内核部分代码时被设置为睡眠状态,此时被唤醒后,将继续执行内核代码,通过对被唤醒原因 的检查,采取不同的方式,我们可以通过借助于 tcp_read 函数中如下代码片断进行说明。 /*tcp_read 函数代码片断*/ 1862 sk->socket->flags |= SO_WAITDATA; 1863 schedule(); 1864 sk->socket->flags &= ~SO_WAITDATA; 1865 sk->inuse = 1; 1866 if (current->signal & ~current->blocked) 1867 { 1868 copied = -ERESTARTSYS; 1869 break; 1870 } 1871 continue; 注意 tcp_read 函数中此段代码之前已经将当前进程状态设置为 TASK_INTERRUPTIBLE,表示 可被信号唤醒的睡眠状态。 1812 current->state = TASK_INTERRUPTIBLE; 1863 行代码通过 schedule 函数调用将该进程调度出去,由于其状态已经被设置为 TASK_INTERRUPTIBLE,而 且 tcp_read 函数在函数开始处已经将该进程加入到 sock 结构 sleep 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 睡眠队列中,所以此处的 schedule 函数调用相当于将当前进程设置为睡眠挂起了。在被唤醒后, 通过此处被唤醒的具体原因(是 wake_up_interruptible 函数调用方式还是信号中断方式),对于 wake_up_interruptible 函数调用方式则继续进行执行,否退出处理信号中断。设置这两种不同的 方式来唤醒进程是因为内核必须对两种不同的情况进行区分:一是由于其他工作的完成,进程 所请求的条件可能会得到满足,此时唤醒进程进行检查(对应 wake_up_interruptible 函数调用方 式);二是由于某些行为的方式,进程所要求的条件将永远无法得到满足,此时必须唤醒进程进 行处理(对应信号中断方式)。对应于前文中接收到一个 FIN 数据包的情况,如果该 FIN 数据 包不包含任何数据,则 tcp_read 函数将永远无法读取到数据,此时仅仅通过 wake_up_interruptible 调用将可能无法退出 while 循环(注意 1871 行的 continue 调用),那么这个进程就成为了一个 不可被停止的进程。tcp_fin 函数中 3227-3231 行代码的作用就是防止这样的问题。另外 kill_proc,kill_pg 函数均定义在 kernel/exit.c 文件中,较为容易理解,读者可自行查看。 tcp_fin 函数接下来要完成的工作是检查当前套接字状态从而决定该如何响应远端发送的这个 FIN 数据包。 3232 switch(sk->state) 3233 { 3234 case TCP_SYN_RECV: 3235 case TCP_SYN_SENT: 3236 case TCP_ESTABLISHED: 3237 /* 3238 * move to CLOSE_WAIT, tcp_data() already handled 3239 * sending the ack. 3240 */ 3241 tcp_set_state(sk,TCP_CLOSE_WAIT); 3242 if (th->rst) 3243 sk->shutdown = SHUTDOWN_MASK; 3244 break; 对应于以上这三种状态,本地套接字需要发送一个 ACK 数据包,并同时更新本地套接字状态, 代码将状态设置为 TCP_CLOSE_WAIT, 对于后关闭一方的套接字而言,根据 TCP 协议规范, 接收到远端发送的 FIN 数据包后,在回复一个 ACK 数据包后(由 tcp_data 函数完成),将进入 TCP_CLOSE_WAIT 状态。另外代码还对 RST 标志位进行了检查,如果 TCP 首部中 RST 标志 位被设置,则表示本地还需要同时关闭发送通道,对于一个 FIN 数据包而言,一般只是表示本 地接收通道被关闭。RST 标志位的含义本书多个地方涉及到,此处不再说明。 3245 case TCP_CLOSE_WAIT: 3246 case TCP_CLOSING: 3247 /* 3248 * received a retransmission of the FIN, do 3249 * nothing. 3250 */ 3251 break; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 如果是这两种状态,则表示之前已经接收到 FIN 数据包,那么此时接收到的 FIN 数据包就是一 个副本,无需重复处理,直接退出即可。 3252 case TCP_TIME_WAIT: 3253 /* 3254 * received a retransmission of the FIN, 3255 * restart the TIME_WAIT timer. 3256 */ 3257 reset_msl_timer(sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 3258 return(0); 进入 TCP_TIME_WAIT 状态表示本地已经关闭发送通道(即本地已经发送了 FIN 数据包,也得 到了对方的应答),而且也接收到远端发送的 FIN 数据包,并回复了 ACK 数据包,正处于 2MSL 等待时间,此时又接收到一个 FIN 数据包,则很有可能本地发送的 ACK 数据包丢失或者长时 间延迟在网络中,造成远端的超时重传,又发送了一个 FIN 数据包,对于这种情况,应当再次 回复一个 ACK 数据包,并且重新设置 2MSL 定时器。以上代码只是重新设置了 2MSL 定时器, 并没有回复一个 ACK 数据包,从整体实现来看,这不会造成什么影响,因为双方都设置了定时 器,在定时器超期后,都会设置各自的套接字为 TCP_CLOSE 状态。注意 2MSL 等待时间的提 出一方面是为了防止 ACK 数据包的丢失,另一方面是为了接收远端之前发送的在网络中受到延 迟的其他一切数据包,所以每当从远端接收到一个这样的数据包,都会对 2MSL 定时器进行重 置。 3259 case TCP_FIN_WAIT1: 3260 /* 3261 * This case occurs when a simultaneous close 3262 * happens, we must ack the received FIN and 3263 * enter the CLOSING state. 3264 * 3265 * This causes a WRITE timeout, which will either 3266 * move on to TIME_WAIT when we timeout, or resend 3267 * the FIN properly (maybe we get rid of that annoying 3268 * FIN lost hang). The TIME_WRITE code is already correct 3269 * for handling this timeout. 3270 */ 3271 if(sk->ip_xmit_timeout != TIME_WRITE) 3272 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 3273 tcp_set_state(sk,TCP_CLOSING); 3274 break; 处于 TCP_FIN_WAIT1 状态表示本地已经发送了 FIN 数据包但尚未得到对方的应答,或者说处 于该状态的本地套接字正在等待对方应答数据包,而此时却收到一个 FIN 数据包,这就发生了 双方同时关闭的情况,对于这种情况,需要对接收到的 FIN 数据包回复一个应答数据包,然后 将进入 TCP_CLOSING 状态,这样在接收到对方发送的应答数据包后,直接进入 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 TCP_TIME_WAIT 状态,而不是 TCP_FIN_WAIT2 状态。对于应答数据包的发送,在 tcp_data 函数可能已经得到处理,此处所做的工作只是重置定时器为超时重传定时器。注意虽然 sock 结 构中定一个四个字段分别对应 TCP 协议中声明的四种定时器:超时重传定时器,保活(keepalive) 定时器,窗口探测(probe)定时器,2MSL 定时器,但实现上前三个定时器使用的是同一个定 时器(超时重传定时器),由 ip_xmit_timeout 字段表示定时器当前定时的目的。这一点可以从 reset_xmit_timer 函数的实现看出,reset_xmit_timer 函数在前文中已经介绍,此处只是给出其代 码方便读者查看。 466 /* 467 * Reset the retransmission timer 468 */ 469 static void reset_xmit_timer(struct sock *sk, int why, unsigned long when) 470 { 471 del_timer(&sk->retransmit_timer); 472 sk->ip_xmit_timeout = why; 473 if((int)when < 0) 474 { 475 when=3; 476 printk("Error: Negative timer in xmit_timer\n"); 477 } 478 sk->retransmit_timer.expires=when; 479 add_timer(&sk->retransmit_timer); 480 } TCP_FIN_WAIT2 状态表示本地已经关闭发送通道,正在等待对方关闭发送通道从而完全关闭 连接。在接收到对方发送的 FIN 数据包后,即可会送 ACK 数据包(由 tcp_data 函数完成),并 进入 TCP_TIME_WAIT 状态,一旦进入该状态就需要相应的设置 2MSL 定时器进入 2MSL 等待 时间。 3275 case TCP_FIN_WAIT2: 3276 /* 3277 * received a FIN -- send ACK and enter TIME_WAIT 3278 */ 3279 reset_msl_timer(sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 3280 sk->shutdown|=SHUTDOWN_MASK; 3281 tcp_set_state(sk,TCP_TIME_WAIT); 3282 break; 3283 case TCP_CLOSE: 3284 /* 3285 * already in CLOSE 3286 */ 3287 break; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 对已处于 TCP_CLOSE 状态的套接字无需进行任何操作。 3288 default: 3289 tcp_set_state(sk,TCP_LAST_ACK); 3290 /* Start the timers. */ 3291 reset_msl_timer(sk, TIME_CLOSE, TCP_TIMEWAIT_LEN); 3292 return(0); 其他状态(TCP_LISTEN, TCP_LAST_ACK),重置 2MSL 定时器(因为我们接收到对方发送的 一个数据包)。对于 TCP_LISTEN 状态的套接字是不会接收到对方发送的 FIN 数据包,因为其 只负责连接建立,之后所有的工作由新创建的套接字负责,所以要关闭一个 TCP_LISTEN 状态 的套接字,只能是本地进行关闭。所以此处的 default 不含 TCP_LISTEN 状态,那么就只剩下 TCP_LAST_ACK 状态,处于该状态的套接字表示该套接字已经发送了 FIN 数据包,现在整个 连接只差对方发送一个 ACK 数据包了,现在却收到一个 FIN 数据包,可能是远端出现了问题, 这个本地管不了,本地所做的工作只是仍然等待,但等待不能无限期,所以设置一个定时器, 一旦超时,无论这个最后的 ACK 数据包是否到来,都设置进入 TCP_CLOSE 状态,完全关闭连 接。 3293 } 3294 return(0); 3295 } tcp_fin 函数处理远端发送的 FIN 数据包,所采取的基本策略是本地在满足条件的情况下,发送 一个 ACK 数据包,并相应的更新状态。对该函数有关具体说明请参考代码中分析。 3296 /* 3297 * This routine handles the data. If there is room in the buffer, 3298 * it will be have already been moved into it. If there is no 3299 * room, then we will just have to discard the packet. 3300 */ 3301 extern __inline__ int tcp_data(struct sk_buff *skb, struct sock *sk, 3302 unsigned long saddr, unsigned short len) 3303 { tcp_data 函数处理被接收数据包中可能包含的数据,将数据挂接到 receive_queue 所指向的接收 队列中。参数中 saddr 表示的是远端地址。参数 len 表示 TCP 首部及其负载的总长度。skb 即为 接收到的数据包,sk 表示对应的本地套接字 sock 结构。 3304 struct sk_buff *skb1, *skb2; 3305 struct tcphdr *th; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3306 int dup_dumped=0; 3307 unsigned long new_seq; 3308 unsigned long shut_seq; 3309 th = skb->h.th; 3310 skb->len = len -(th->doff*4); 3311 /* 3312 * The bytes in the receive read/assembly queue has increased. Needed for the 3313 * low memory discard algorithm 3314 */ 3315 sk->bytes_rcv += skb->len; 3316 if (skb->len == 0 && !th->fin) 3317 { 3318 /* 3319 * Don't want to keep passing ack's back and forth. 3320 * (someone sent us dataless, boring frame) 3321 */ 3322 if (!th->ack) 3323 tcp_send_ack(sk->sent_seq, sk->acked_seq,sk, th, saddr); 3324 kfree_skb(skb, FREE_READ); 3325 return(0); 3326 } 代码实现计算负载数据的长度,将其保存在 skb->len 字段中,并更新 sock 结构 bytes_rcv 字段, 该字段的含义顾名思义,表示套接字从创建之时起到目前为止接收到的总字节数。代码之后对 负载数据长度和相关标志位进行了检查。如果负载数据长度为 0,且 FIN 标志位没有设置,此 时无需对该数据包进一步进行处理,简单丢弃即可。3322 行还对数据包是否是一个 ACK 数据 包进行了检查,实际上,对于 TCP 协议而言,除了三路握手连接中第一个数据包没有设置 ACK 标志位外,此后来往的所有数据包中 ACK 标志位均被设置为 1。所以如果将 3316 行与 3322 行 的代码综合起来看,那就是这是一个没有负载用户数据,FIN,ACK 标志位都没有设置的数据 包,那么该数据包就一定设置了 SYN 标志位(即是一个 SYN 数据包,对于 SYN 数据包,是不 会执行到 tcp_data 函数的),否则对于本版本网络实现而言,这就是一个无效的数据包。所以 3322 行代码更多的是一种心理安慰(有了总比没有好)。 3327 /* 3328 * We no longer have anyone receiving data on this connection. 3329 */ 3330 #ifndef TCP_DONT_RST_SHUTDOWN 3331 if(sk->shutdown & RCV_SHUTDOWN) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3332 { 这个条件语句判断接收通道是否已经被关闭,对于接收通道被关闭的情况,如果此时接收到一 个负载数据长度非 0 的数据包,则表示很可能出现问题,此时需要对可能的问题进行检查。 3333 /* 3334 * FIXME: BSD has some magic to avoid sending resets to 3335 * broken 4.2 BSD keepalives. Much to my surprise a few non 3336 * BSD stacks still have broken keepalives so we want to 3337 * cope with it. 3338 */ 3339 if(skb->len) /* We don't care if it's just an ack or 3340 a keepalive/window probe */ 3341 { 3342 new_seq= th->seq + skb->len + th->syn; /* edge of data part of frame */ 对于一个接收通道被关闭的套接字,如果又接收到一个负载数据长度非 0 的数据包,则检查该 数据包是否是一个被延迟的数据包,即查看其序列号是否是过期的。如果不是过期的(即序列 号比 FIN 序列号还要大),则根据当前套接字是否正处于释放状态进行相应的处理。对于处于释 放状态的套接字表示用户应用程序已经关闭该套接字,所以对于接收到的数据直接丢弃即可, 不需要进行缓存了。我们姑且不管远端在发送了 FIN 数据包后,是如何又冒出了一个序号号在 FIN 序列号之后的数据包,从本地处理的角度看,只要是从远端接收到的数据,如果应用程序 仍然在使用该套接字的话,就将数据缓存在接收队列中,等候用户进行处理。这种处理方式让 我们的思维方式跳出了框框,一般我们将 FIN 数据包看作是权威的,只要接收了 FIN 数据包, 就表示本地“不可”再从接收通道中接收数据(或者更本质的说,远端一定不会再发送数据了), 实际上 FIN 数据包说到底只是一种约定,如果有一方不遵从约定,那就变成了什么也不是(就 像远端明明已经关闭了其发送通道,现在又在发送数据包,简直是“儿戏”),既然如此,只要 你发,我就收,至于怎么办,让用户操心去吧,我网络代码只能这么办了。3353-3370 行代码就 是这个意思。 3343 /* Do this the way 4.4BSD treats it. Not what I'd 3344 regard as the meaning of the spec but it's what BSD 3345 does and clearly they know everything 8) */ 3346 /* 3347 * This is valid because of two things 3348 * 3349 * a) The way tcp_data behaves at the bottom. 3350 * b) A fin takes effect when read not when received. 3351 */ 3352 shut_seq=sk->acked_seq+1; /* Last byte */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3353 if(after(new_seq,shut_seq)) 3354 { 3355 if(sk->debug) 3356 printk("Data arrived on %p after close [Data right edge %lX, Socket shut on %lX] %d\n", 3357 sk, new_seq, shut_seq, sk->blog); 3358 if(sk->dead) 3359 { 3360 sk->acked_seq = new_seq + th->fin; 3361 tcp_reset(sk->saddr, sk->daddr, skb->h.th, 3362 sk->prot, NULL, skb->dev, sk->ip_tos, sk->ip_ttl); 3363 tcp_statistics.TcpEstabResets++; 3364 tcp_set_state(sk,TCP_CLOSE); 3365 sk->err = EPIPE; 3366 sk->shutdown = SHUTDOWN_MASK; 3367 kfree_skb(skb, FREE_READ); 3368 return 0; 3369 } 3370 } 3371 } 3372 } 3373 #endif 无论怎么折腾,既然可以执行到此处,就表示需要将这个数据包缓存到用户接收队列中去。下 面代码就是完成这个工作,代码较长的原因是接收队列中缓存的数据包需要按序列号进行排序, 由于数据包的乱序到达,所以将数据包插入到接收队列中的哪个位置还需要遍历队列中的现有 数据包进行序列号比较。注意在进行序列号比较时,使用的是数据包中第一个数据字节的序列 号,以及数据的长度,这样做的目的是可以检测到数据包重复(一个数据包完全覆盖另一个数 据包)的情况,从而进行替换;对于重叠(一个数据包部分覆盖另一个数据包)的情况不进行 替换,都插入到队列中,重叠的情况在 tcp_read 函数中会得到处理(至于怎么处理请参考前文 中 tcp_read 函数的讨论)。 3374 /* 3375 * Now we have to walk the chain, and figure out where this one 3376 * goes into it. This is set up so that the last packet we received 3377 * will be the first one we look at, that way if everything comes 3378 * in order, there will be no performance loss, and if they come 3379 * out of order we will be able to fit things in nicely. 3380 * 3381 * [AC: This is wrong. We should assume in order first and then walk 3382 * forwards from the first hole based upon real traffic patterns.] 3383 * 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3384 */ 3385 if (skb_peek(&sk->receive_queue) == NULL) /* Empty queue is easy case */ 3386 { 3387 skb_queue_head(&sk->receive_queue,skb); 3388 skb1= NULL; 3389 } 接收队列中没有数据包,这没什么好说的,直接挂到队列首部即可。 3390 else 3391 { 3392 for(skb1=sk->receive_queue.prev; ; skb1 = skb1->prev) 3393 { 3394 if(sk->debug) 3395 { 3396 printk("skb1=%p :", skb1); 3397 printk("skb1->h.th->seq = %ld: ", skb1->h.th->seq); 3398 printk("skb->h.th->seq = %ld\n",skb->h.th->seq); 3399 printk("copied_seq = %ld acked_seq = %ld\n", sk->copied_seq, 3400 sk->acked_seq); 3401 } 否则进行队列遍历,对队列中每个数据包进行检查,如果需要,打印调试信息,我们对此不关 心。 3402 /* 3403 * Optimisation: Duplicate frame or extension of previous frame from 3404 * same sequence point (lost ack case). 3405 * The frame contains duplicate data or replaces a previous frame 3406 * discard the previous frame (safe as sk->inuse is set) and put 3407 * the new one in its place. 3408 */ 3409 if (th->seq==skb1->h.th->seq && skb->len>= skb1->len) 3410 { 3411 skb_append(skb1,skb); 3412 skb_unlink(skb1); 3413 kfree_skb(skb1,FREE_READ); 3414 dup_dumped=1; 3415 skb1=NULL; 3416 break; 3417 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 如果已有数据包与新接收的数据包开始序列号相同,但新接收的数据包长度较长,这就发生了 覆盖的情况,此时删除已有旧数据包,将新接收的数据包挂接到原来的位置上。 3418 /* 3419 * Found where it fits 3420 */ 3421 if (after(th->seq+1, skb1->h.th->seq)) 3422 { 3423 skb_append(skb1,skb); 3424 break; 3425 } 新接收的数据包序列号比已有的数据包序列号大,插入到该数据包之后,注意在 for 循环中, 是从队列最后一个数据包向前遍历的,即从具有最大序列号的数据包开始比较的,所以一旦发 送一个数据包序列号比当前接收的数据包的序列号小,则可以停止比较,将新接收的数据包插 入该数据包之后即可。此时包括了数据重叠,连续,断裂的情况。如下图所示。 3426 /* 3427 * See if we've hit the start. If so insert. 3428 */ 3429 if (skb1 == skb_peek(&sk->receive_queue)) 3430 { 3431 skb_queue_head(&sk->receive_queue, skb); 3432 break; 3433 } 3434 } 3435 }//else 如果我们比较到最后,还没有找到一个序列号比当前所接收数据包小的原有数据包,则表示当 前数据包序列号最小,此时应该插入到 receive_queue 接收队列的首部。receive_queue 接收队列 中数据包是按其中所包含第一个数据字节序列号从小到大排列的。 将数据包插入的接收队列中正确位置后,下面就需要判断是否需要立刻发送一个 ACK 数据包以 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 及 ACK 数据包所使用的应答序列号。 3436 /* 3437 * Figure out what the ack value for this frame is 3438 */ 3439 th->ack_seq = th->seq + skb->len; 3440 if (th->syn) 3441 th->ack_seq++; 3442 if (th->fin) 3443 th->ack_seq++; 首先计算当前被接收数据包的应答序列号。并将该应答序列号暂存于所接收数据包 TCP 首部中 应答序列号(ack_seq)字段。注意 TCP 首部中应答序列号字段表示的是远端希望从本地接收的 下一个字节的序列号,同时也表示了在此序列号之前的数据都已被远端成功接收,本地可以从 其重发队列中删除序列号(指数据包最后一个字节的序列号)在此应答序列号之前的数据包。 3444 if (before(sk->acked_seq, sk->copied_seq)) 3445 { 3446 printk("*** tcp.c:tcp_data bug acked < copied\n"); 3447 sk->acked_seq = sk->copied_seq; 3448 } 此段代码对 sock 结构中当前相关字段进行合法性检查,其中 acked_seq 字段表示本地当前应答 对方的序列号或者说是期望从对方接收的下一个字节的序列号,而 copied_seq 表示已经提交给 用户应用程序的最后一个字节的序列号加 1,所以 copied_seq 一定小于或等于 acked_seq,如果 出现 copied_seq 大于 acked_seq 的情况,则表示内核出现不一致,可能存在系统 BUG,此处的 修复是简单的将 acked_seq 赋值为 copied_seq,其中所基于的思想是,既然上层声称已接收到序 列号到 copied_seq 的数据,而我所跟踪的情况是本地压根就没有请求到那一步,既然你上层这 样说,那就依你,我就跳过中间的一段数据,直接请求你上层所要求的下一个数据,到时出了 问题你上层自己解决。 3449 /* 3450 *Now figure out if we can ack anything. This is very messy because we really want two 3451 *receive queues, a completed and an assembly queue. We also want only one transmit 3452 *queue. 3453 */ 3454 if ((!dup_dumped && (skb1 == NULL || skb1->acked)) || before(th->seq, sk->acked_seq+1)) 3455 { 如上 if 语句为真时需要满足如下条件之一: 1>dup_dump=0 并且 skb1=null 或者已经对 skb1 进行了应答。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2>th->seq<=sk->acked_seq,即当前所接收数据包中第一个字节序列号小于或等于本地当前从远 端请求的下一个字节的序列号,对于这种情况,我们还需要进一步检查数据包中最后一个字节 的序列号是否也在 acked_seq 之内,如果在这之内,这就表示这是一个重复数据包;否则该数 据包就是一个合法的数据包。 对于条件一中,如果 dup_dump=0 的话,则 skb1 则一定不为 null,此时只可能 skb1->acked=1 即序列号比当前数据包稍小的数据包已经对其发送了一个 ACK 数据包给远端。对于这种情况的 处理方式是并不立即对当前接收的这个数据包回复一个 ACK 数据包,而是等待接收下一个数据 包,即一个 ACK 数据包对多个接收的数据包进行应答。所以对于条件一,我们还需要根据其他 条件决定是否发送一个 ACK 数据包,暂时无需特别处理。而对于条件二,此时也表示条件以不 满足,即此时可能已经积累了多个数据包没有回复 ACK 数据包了。对于这种情况我们进一步分 析其代码。 //对应以上分析中的第二种情况。以下我们分段进行说明。 3456 if (before(th->seq, sk->acked_seq+1)) 3457 { 3458 int newwindow; 3459 if (after(th->ack_seq, sk->acked_seq)) 3460 { 3461 newwindow = sk->window-(th->ack_seq - sk->acked_seq); 3462 if (newwindow < 0) 3463 newwindow = 0; 3464 sk->window = newwindow; 3465 sk->acked_seq = th->ack_seq; 3466 } 3467 skb->acked = 1; 这是对应当前可能已经累积了未应答数据包的情况,并且当前所接收数据包中包含了本地尚未 接收的新数据。此时根新窗口大小(从当前窗口中减去这些新的数据),并且检查更新后窗口大 小,如果小于 0,则设置为 0 即可。另外更新当前应答序列号位当前所接收数据包的应答序列 号,因为接下来就对刚刚接收到的这个数据包以及之前可能尚未进行应答的数据包一并应答一 个数据包。针对前文中分析的第二种情况,无论当前所接收数据包中是否包含有新的数据,都 需要进行应答,所以设置当前接收数据包 sk_buff 结构中 acked 应答标志字段为 1,表示该数据 包已经进行了应答(接下来要完成的工作)。 3468 /* 3469 * When we ack the fin, we do the FIN 3470 * processing. 3471 */ 3472 if (skb->h.th->fin) 3473 { 3474 tcp_fin(skb,sk,skb->h.th); 3475 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 检查当前所接收数据包是不是一个 FIN 数据包,如果是,则调用 tcp_fin 函数进行相应处理 (tcp_fin 函数刚刚讨论,主要是对本地套接字状态的更新)。 3476 for(skb2 = skb->next; 3477 skb2 != (struct sk_buff *)&sk->receive_queue; 3478 skb2 = skb2->next) 3479 { 注意 skb 表示当前刚接收的数据包,在前面代码中已经被插入 receive_queue 接收队列中了,所 以此处 for 循环遍历的是比当前刚接收的这个数据包序列号大的 receive_queue 队列中已有的数 据包。 3480 if (before(skb2->h.th->seq, sk->acked_seq+1)) 3481 { 3482 if (after(skb2->h.th->ack_seq, sk->acked_seq)) 3483 { 3484 newwindow = sk->window - 3485 (skb2->h.th->ack_seq - sk->acked_seq); 3486 if (newwindow < 0) 3487 newwindow = 0; 3488 sk->window = newwindow; 3489 sk->acked_seq = skb2->h.th->ack_seq; 3490 } 3491 skb2->acked = 1; 3492 /* 3493 * When we ack the fin, we do 3494 * the fin handling. 3495 */ 3496 if (skb2->h.th->fin) 3497 { 3498 tcp_fin(skb,sk,skb->h.th); 3499 } 3500 /* 3501 * Force an immediate ack. 3502 */ 3503 sk->ack_backlog = sk->max_ack_backlog; 3504 } 3480-3504 这个 if 语句块是寻求对多个数据包同时进行应答的情况,加上 3476 的 for 语句,就 是在寻找一个合适的应答序列号,从刚接收的数据包开始(见下面的分析),以序列号增大的顺 序,分析每个数据包,寻找序列号断裂处。3480,3482,3489 行代码进行了数据包的连续性判 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 断,请读者仔细体会,应该可以理解。这三行的功能与 3456,3459,3465 这三行代码作用于当 前所接收数据包的作用是一样的。如果读者稍微观察一下,就会发现,3480-3499 行代码 3456-3499 行代码完成的功能是一样的。所以刚刚说道在寻找一个合适的应答序列号时是从当前 所接收的数据包开始检查的。另外一个值得注意的一点是,一旦 3480 行 if 语句为真,则 3503 行代码一定会得到执行。前面将当前所接收数据包插入 receive_queue 队列的过程已经保证了该 队列中不存在重复数据包,所以一旦可以进入到 3480 行 if 语句块执行,则 3482 行所表示的语 句块也一定会得到执行,换句话说,3480 行为真,则表示此时至少有两个数据包需要进行应答, 将 sock 结构中 ack_backlog 字段设置为 max_ack_backlog 字段值,就是强制在下文中发送应答, 所基于的思想是一旦有两个或两个以上数据包累积,则立刻回复一个应答数据包。即可以对多 个数据包进行积累一并对他们进行应答,但数据包的个数达到两个,则必须立刻回复一个应答 数据包。防止累积过多产生成累积的时间过长,从而造成远端超时重传。 3505 else 3506 { 3507 break; 3508 } 这个 else 语句表示出现序列号断裂,也表示此时已经寻找到了一个合适的应答序列号。 3509 }//for(skb2 = skb->next;... 注意在寻找合适的应答序列号的过程中,对于序列号连续的所有数据包其对应 sk_buff 结构中 acked 字段均被设置为 1,表示对应数据包已经发送了应答数据包。原因很简单,因为我们寻找 应答序列号中的过程中,已经将应答序列号设置为包含这些数据包在内(这正是寻找的过程所 进行的工作)。 3510 /* 3511 * This also takes care of updating the window. 3512 * This if statement needs to be simplified. 3513 */ 3514 if (!sk->delay_acks || 3515 sk->ack_backlog >= sk->max_ack_backlog || 3516 sk->bytes_rcv > sk->max_unacked || th->fin) { 3517 /*tcp_send_ack(sk->sent_seq, sk->acked_seq,sk,th, saddr); */ 3518 } 3519 else 3520 { 3521 sk->ack_backlog++; 3522 if(sk->debug) 3523 printk("Ack queued.\n"); 3524 reset_xmit_timer(sk, TIME_WRITE, TCP_ACK_TIME); 3525 } 3526 } 3527 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 既然找到了一个合适的应答序列号,那么就要根据情况进行应答数据包的发送了。3514-3525 行代码完成的工作即是如此。sock 结构中 delay_acks 标志位字段决定是否采用对数据包累积应 答(而非一个数据包对应一个应答),如果值设置为 0,表示不使用累积应答,此时必须立刻发 送一个应答数据包;否则检查是否有两个或两个以上数据包累积(即通过比较 ack_backlog 字段 与 max_ack_backlog 字段值);如果没有检查到累积的情况,那么进一步检查接收的数据字节数 是否超过了 sock 结构设置未应答字节的最大值,如果超过,立刻回复应答数据包;再否则,则 检查刚接收的数据包是不是一个 FIN 数据包,此时就不能累积其后的数据包一起进行应答了, 因为这是对方发送的最后一个数据包了。只要以上条件之一满足,立刻发送一个应答数据包(调 用 tcp_send_ack 函数完成,但是,注意调用 tcp_send_ack 函数进行 ACK 数据包发送的 3517 行 被注释掉了,这点需要结合 3562-3565 行代码分析,见下文)。如果以上条件都不满足,则等待 接收更多数据包进行累积应答。但这个等待的时间不能无限期,否则造成远端超时重传,无谓 的增加网络拥挤度。所以设置一个定时器,定时器到期后,无论有没有累积到数据包,都要立 刻回复一个应答数据包。可以想见,这个定时时间间隔一定小于远端设置的超时重传间隔(实 际上也是本地设置的超时重传间隔,从网络栈实现代码本身既是本地端,也是远端这点考虑的 话)。 3528 /* 3529 * If we've missed a packet, send an ack. 3530 * Also start a timer to send another. 3531 */ 3532 if (!skb->acked) 3533 { 对于 skb(表示当前接收到的数据包)中 acked 字段为 0 的情况,从前文代码(3456 行代码不 满足)可以分析出,此时出现了序列号断裂的情况,即当前所接收的数据包并非本地想要的, 其中第一个字节的序列号大于本地的应答序列号(也即本地期望从远端接收的下一个字节的序 列号),此时很有可能出现了数据包丢失的情况,所以立刻发送一个 ACK 应答数据包告知对方 这种情况,三次发送同样请求序列号的 ACK 数据包会引起远端快速重传机制,从而重传可能丢 失的数据包,而非一定等待重传定时器超时(这个时间一般较长),这个过程从本地的角度而言, 也被称为快速恢复机制。3532 行 if 语句块所进行的工作即是进行可能的快速恢复。 3534 /* 3535 * This is important. If we don't have much room left, 3536 * we need to throw out a few packets so we have a good 3537 * window. Note that mtu is used, not mss, because mss is really 3538 * for the send side. He could be sending us stuff as large as mtu. 3539 */ 3540 while (sk->prot->rspace(sk) < sk->mtu) 3541 { 3542 skb1 = skb_peek(&sk->receive_queue); 3543 if (skb1 == NULL) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3544 { 3545 printk("INET: tcp.c:tcp_data memory leak detected.\n"); 3546 break; 3547 } 3548 /* 3549 * Don't throw out something that has been acked. 3550 */ 3551 if (skb1->acked) 3552 { 3553 break; 3554 } 3555 skb_unlink(skb1); 3556 kfree_skb(skb1, FREE_READ); 3557 }// while (sk->prot->rspace(sk) < sk->mtu) 因为现在可能丢失序列号排在前面的数据包,接收到一些序列号大的排在后面的数据包,而这 些排在后面的数据包有可能将本地接收缓冲区使用完,从而造成即使远端实行快速重发那些丢 失的排在前面的数据包,也由于接收缓冲区满,造成本地窗口很小,限制远端的发送(注意远 端发送数据包还需要受到本地所声明的窗口大小的节制),从而造成两难的境地:排在前面的数 据包由于丢失需要重传,而排在后面的数据包占用了空间造成了前面的数据包不能被重传。这 就是死锁。此处解决的方式是释放接收队列中一些数据包,空出接收空间,从而释放接收缓冲 区空间,以接收那些丢失被重传的数据包。在释放接收队列中数据包时,很直接的想法即是释 放那些序列号大的(即排在后面的)数据包。这样在后面还可以再对他们进行请求。此处的实 际做法是:遍历接收队列,凡是没有回复应答的数据包都被作为释放对象,直到释放出足够的 空间为止,这种方式的结果是从断裂处前后开始进行数据包释放,在都满足释放条件时,从序 列号较小的开始释放。从上面寻找应答序列号的过程来看,这种做法显然不经济。所以此处是 一个可以优化的地方。3540-3557 行代码完成这个遍历和释放的过程。 3558 tcp_send_ack(sk->sent_seq, sk->acked_seq, sk, th, saddr); 3559 sk->ack_backlog++; 3560 reset_xmit_timer(sk, TIME_WRITE, TCP_ACK_TIME); 3561 } 在释放出足够空间后,现在发送一个 ACK 数据包请求远端重传可能丢失的数据包。此处也设置 一个定时器,这个定时器的作用需要和 sock 结构中 ack_backlog 字段结合起来分析。以上是定 时器到期处理函数 retransmit_timer 中的代码片断: // retransmit_timer 604 if (sk->ack_backlog && !sk->zapped) 605 { 606 sk->prot->read_wakeup (sk); 607 if (! sk->dead) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 608 sk->data_ready(sk,0); 609 } 其中 sk->prot->read_wakeup 指向 tcp_read_wakeup 函数,该函数作用的就是向远端发送一个 ACK 数据包。tcp_read_wakeup 函数以 sock 结构中 ack_backlog 字段值决定是否发送一个 ACK 数据 包,如果该字段为 0,则直接返回。如下代码片断所示: //tcp_read_wakeup 1586 if (!sk->ack_backlog) 1587 return; 如下 else 语句对应 skb->acked=1 的情况,此处就要提到 3517 行代码被注释掉了的情况了,那 行未实现的功能在此处 else 模块中完成了。此处 else 模块实现有别于 3517 行的地方在于:3517 行明确要求了数据包累积,而此处并未明确要求,换句话说,既可以进行累积(对应 3476 行条 件为真,查找应答序列号的过程),也可以不进行累积(对应 3476 行条件为假的情况)。 3562 else 3563 { 3564 tcp_send_ack(sk->sent_seq, sk->acked_seq, sk, th, saddr); 3565 } 到此为止,我们已经完成了数据包的接纳(挂入接收队列中正确位置上)以及寻找合适应答序 列号进行了应答处理,现在我们需要通知上层应用有数据可用了,以防当前有进程阻塞读数据 请求上。如下 3569-3574 行代码完成这个通知的工作。 3566 /* 3567 * Now tell the user we may have some data. 3568 */ 3569 if (!sk->dead) 3570 { 3571 if(sk->debug) 3572 printk("Data wakeup.\n"); 3573 sk->data_ready(sk,0); 3574 } 3575 return(0); 3576 } 至此我们完成对 tcp_data 函数的分析,大体上,该函数完成如下工作: 1>完成数据包中数据合法性检查。 2>完成数据包插入到接收队列中的工作。 3>完成寻找合适应答序列号对数据包进行应答的工作。 4>完成对上层应用有可用数据的通知。 3577 /* 3578 * This routine is only called when we have urgent data 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3579 * signalled. Its the 'slow' part of tcp_urg. It could be 3580 * moved inline now as tcp_urg is only called from one 3581 * place. We handle URGent data wrong. We have to - as 3582 * BSD still doesn't use the correction from RFC961. 3583 */ 3584 static void tcp_check_urg(struct sock * sk, struct tcphdr * th) 3585 { 3586 unsigned long ptr = ntohs(th->urg_ptr); 3587 if (ptr) 3588 ptr--; 3589 ptr += th->seq; 3590 /* ignore urgent data that we've already seen and read */ 3591 if (after(sk->copied_seq, ptr)) 3592 return; 3593 /* do we already have a newer (or duplicate) urgent pointer? */ 3594 if (sk->urg_data && !after(ptr, sk->urg_seq)) 3595 return; 3596 /* tell the world about our new urgent pointer */ 3597 if (sk->proc != 0) { 3598 if (sk->proc > 0) { 3599 kill_proc(sk->proc, SIGURG, 1); 3600 } else { 3601 kill_pg(-sk->proc, SIGURG, 1); 3602 } 3603 } 3604 sk->urg_data = URG_NOTYET; 3605 sk->urg_seq = ptr; 3606 } tcp_check_urg 函数被 tcp_urg 函数调用计算当前接收数据包中紧急数据指针值。本版本网络代 码实现将紧急数据处理为一个字节,并且将 TCP 首部中紧急指针处理为指向紧急数据后的第一 个字节。所以 3588 行将紧急指针减 1,从而使得紧急指针指向紧急数据本身;另外紧急指针是 一个偏移量,是相对于 TCP 首部中 seq 字段而言的,所以 3589 加上这个 seq 字段获得紧急数据 的绝对序列号并保存在 sock 结构 urg_seq 字段中。3591 行检查紧急指针是否已经在被用户读取 的数据序列号之内,如果是,则没有必须标志出来,直接返回即可。3594 行检查是否之前已经 接收了紧急数据,由于 sock 结构只能标识一个紧急数据位置,所以以绝对序列号最大的为主, 如果此次紧急数据绝对序列号比之前声明的紧急数据序列号大,则保存本次声明的紧急数据序 列号,否则直接忽略之。3597-3603 行代码用于通知该套接字对应的用户进程,有紧急数据到达, 提示他们立刻进行读取。最后设置 sock 结构中 urg_seq 字段表示这个紧急数据字节序列号,并 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 将 urg_data 设置为 URG_NOTYET,表示紧急数据到达,但尚未从数据包中提取,urg_data 字段 值将在 tcp_urg 函数中被检查,从而提取紧急数据字节。参考下面对 tcp_urg 函数的分析。 3607 /* 3608 * This is the 'fast' part of urgent handling. 3609 */ 3610 extern __inline__ int tcp_urg(struct sock *sk, struct tcphdr *th, 3611 unsigned long saddr, unsigned long len) 3612 { 3613 unsigned long ptr; 3614 /* 3615 * Check if we get a new urgent pointer - normally not 3616 */ 3617 if (th->urg) 3618 tcp_check_urg(sk,th); 3619 /* 3620 * Do we wait for any urgent data? - normally not 3621 */ 3622 if (sk->urg_data != URG_NOTYET) 3623 return 0; 3624 /* 3625 * Is the urgent pointer pointing into this packet? 3626 */ 3627 ptr = sk->urg_seq - th->seq + th->doff*4; 3628 if (ptr >= len) 3629 return 0; 3630 /* 3631 * Ok, got the correct packet, update info 3632 */ 3633 sk->urg_data = URG_VALID | *(ptr + (unsigned char *) th); 3634 if (!sk->dead) 3635 sk->data_ready(sk,0); 3636 return 0; 3637 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 tcp_urg 函数被 tcp_rcv 函数调用用于处理数据包可能的紧急数据,该函数通过检查 TCP 首部中 URG 标志位值判断是否有紧急数据到达,如果该标志位被设置为 1,则表示存在紧急数据,此 时调用 tcp_check_urg 函数计算紧急数据位置,并将紧急数据绝对序列号保存在 sock 结构 urg_seq 字段中,同时设置 sock 结构 urg_data 字段值为 URG_NOTYET,表示尚未从数据包读取该紧急数 据保存,tcp_urg 函数接下来主要完成就是读取该紧急数据并将其保存在 urg_data 字段中。3627 行代码计算紧急数据相对于接收数据包第一个数据字节的偏移量。如果紧急数据偏移量超出全 部数据的长度,则直接返回。否则读取这个紧急数据(一个字节),保存到 urg_data 字段中,并 同时设置 URG_VALID 标志位,表示 urg_data 字段表示一个有效的紧急数据。注意 3633 行的赋 值,我们说紧急数据是一个字节,只占用低 8 比特,而 URG_VALID 常量使用高位比特,所以 不会对紧急数据造成影响。最后 3634-3635 通知用户进程读取紧急数据。 3638 /* 3639 * This will accept the next outstanding connection. 3640 */ 3641 static struct sock *tcp_accept(struct sock *sk, int flags) 3642 { 3643 struct sock *newsk; 3644 struct sk_buff *skb; 3645 /* 3646 * We need to make sure that this socket is listening, 3647 * and that it has something pending. 3648 */ 3649 if (sk->state != TCP_LISTEN) 3650 { 3651 sk->err = EINVAL; 3652 return(NULL); 3653 } 3654 /* Avoid the race. */ 3655 cli(); 3656 sk->inuse = 1; 3657 while((skb = tcp_dequeue_established(sk)) == NULL) 3658 { 3659 if (flags & O_NONBLOCK) 3660 { 3661 sti(); 3662 release_sock(sk); 3663 sk->err = EAGAIN; 3664 return(NULL); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3665 } 3666 release_sock(sk); 3667 interruptible_sleep_on(sk->sleep); 3668 if (current->signal & ~current->blocked) 3669 { 3670 sti(); 3671 sk->err = ERESTARTSYS; 3672 return(NULL); 3673 } 3674 sk->inuse = 1; 3675 } 3676 sti(); 3677 /* 3678 * Now all we need to do is return skb->sk. 3679 */ 3680 newsk = skb->sk; 3681 kfree_skb(skb, FREE_READ); 3682 sk->ack_backlog--; 3683 release_sock(sk); 3684 return(newsk); 3685 } tcp_accept 函数是 accept 系统调用的传输层 TCP 协议对应实现函数,该函数实现比较简单:从 侦听套接字接收队列中取数据包,查看其是否完成 TCP 三路握手建立过程 (tcp_dequeue_established 函数完成的功能),如果没有完成,则无限期等待。否则返回数据包 对应的 sock 结构。实际上本函数只是一个获取结果函数,并为完成任何实际工作,这些实际工 作( TCP 连接建立)均有其他函数完成了(tcp_conn_request, tcp_ack 等)。就本函数独立的来看, 只有一点需要指明,这在前文中已有交待:即侦听套接字接收队列中缓存的均是请求连接(完 成或尚未完成连接过程)的数据包,即都是由客户端发送的 SYN 数据包。具体连接完成后的双 方通信由新创建的数据通信套接字(即此处返回的 newsk,该新通信套接字是在 tcp_conn_request 函数中创建的,具体参照下文对 tcp_conn_request 函数的分析)负责。 3686 /* 3687 * This will initiate an outgoing connection. 3688 */ 3689 static int tcp_connect(struct sock *sk, struct sockaddr_in *usin, int addr_len) 3690 { 3691 struct sk_buff *buff; 3692 struct device *dev=NULL; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3693 unsigned char *ptr; 3694 int tmp; 3695 int atype; 3696 struct tcphdr *t1; 3697 struct rtable *rt; 3698 if (sk->state != TCP_CLOSE) 3699 { 3700 return(-EISCONN); 3701 } 3702 if (addr_len < 8) 3703 return(-EINVAL); 3704 if (usin->sin_family && usin->sin_family != AF_INET) 3705 return(-EAFNOSUPPORT); 3706 /* 3707 * connect() to INADDR_ANY means loopback (BSD'ism). 3708 */ 3709 if(usin->sin_addr.s_addr==INADDR_ANY) 3710 usin->sin_addr.s_addr=ip_my_addr(); 3711 /* 3712 * Don't want a TCP connection going to a broadcast address 3713 */ 3714 if ((atype=ip_chk_addr(usin->sin_addr.s_addr)) == IS_BROADCAST || atype==IS_MULTICAST) 3715 return -ENETUNREACH; 3716 sk->inuse = 1; 3717 sk->daddr = usin->sin_addr.s_addr; 3718 sk->write_seq = tcp_init_seq(); 3719 sk->window_seq = sk->write_seq; 3720 sk->rcv_ack_seq = sk->write_seq -1; 3721 sk->err = 0; 3722 sk->dummy_th.dest = usin->sin_port; 3723 release_sock(sk); 3724 buff = sk->prot->wmalloc(sk,MAX_SYN_SIZE,0, GFP_KERNEL); 3725 if (buff == NULL) 3726 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3727 return(-ENOMEM); 3728 } 3729 sk->inuse = 1; 3730 buff->len = 24; 3731 buff->sk = sk; 3732 buff->free = 0; 3733 buff->localroute = sk->localroute; 3734 t1 = (struct tcphdr *) buff->data; 3735 /* 3736 * Put in the IP header and routing stuff. 3737 */ 3738 rt=ip_rt_route(sk->daddr, NULL, NULL); 3739 /* 3740 * We need to build the routing stuff from the things saved in skb. 3741 */ 3742 tmp = sk->prot->build_header(buff, sk->saddr, sk->daddr, &dev, 3743 IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl); 3744 if (tmp < 0) 3745 { 3746 sk->prot->wfree(sk, buff->mem_addr, buff->mem_len); 3747 release_sock(sk); 3748 return(-ENETUNREACH); 3749 } 3750 buff->len += tmp; 3751 t1 = (struct tcphdr *)((char *)t1 +tmp); 3752 memcpy(t1,(void *)&(sk->dummy_th), sizeof(*t1)); 3753 t1->seq = ntohl(sk->write_seq++); 3754 sk->sent_seq = sk->write_seq; 3755 buff->h.seq = sk->write_seq; 3756 t1->ack = 0; 3757 t1->window = 2; 3758 t1->res1=0; 3759 t1->res2=0; 3760 t1->rst = 0; 3761 t1->urg = 0; 3762 t1->psh = 0; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3763 t1->syn = 1; 3764 t1->urg_ptr = 0; 3765 t1->doff = 6; 3766 /* use 512 or whatever user asked for */ 3767 if(rt!=NULL && (rt->rt_flags&RTF_WINDOW)) 3768 sk->window_clamp=rt->rt_window; 3769 else 3770 sk->window_clamp=0; /* 以下代码说明作者对于 MTU,MSS 概念不是很清楚,所以这段对于 MTU 赋值, * 以下对于 TCP * 首部中 MSS 选项值的选定都不正确! * MTU 表示最大传输单元长度,包括 IP 首部及其数据(TCP 首部以及 TCP 数据负载)。 * MSS 表示用户数据长度,即 TCP 数据负载部分(不包括 TCP 首部) */ 3771 if (sk->user_mss) 3772 sk->mtu = sk->user_mss; 3773 else if(rt!=NULL && (rt->rt_flags&RTF_MTU)) 3774 sk->mtu = rt->rt_mss; 3775 else 3776 { 3777 #ifdef CONFIG_INET_SNARL 3778 if ((sk->saddr ^ sk->daddr) & default_mask(sk->saddr)) 3779 #else 3780 if ((sk->saddr ^ sk->daddr) & dev->pa_mask) 3781 #endif 3782 sk->mtu = 576 - HEADER_SIZE; 3783 else 3784 sk->mtu = MAX_WINDOW; 3785 } 3786 /* 3787 * but not bigger than device MTU 3788 */ 3789 if(sk->mtu <32) 3790 sk->mtu = 32; /* Sanity limit */ 3791 sk->mtu = min(sk->mtu, dev->mtu - HEADER_SIZE); 3792 /* 3793 * Put in the TCP options to say MTU. 3794 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3795 ptr = (unsigned char *)(t1+1); 3796 ptr[0] = 2; 3797 ptr[1] = 4; 3798 ptr[2] = (sk->mtu) >> 8; 3799 ptr[3] = (sk->mtu) & 0xff; 3800 tcp_send_check(t1, sk->saddr, sk->daddr, 3801 sizeof(struct tcphdr) + 4, sk); 3802 /* 3803 * This must go first otherwise a really quick response will get reset. 3804 */ 3805 tcp_set_state(sk,TCP_SYN_SENT); 3806 sk->rto = TCP_TIMEOUT_INIT; 3807 #if 0 /* we already did this */ 3808 init_timer(&sk->retransmit_timer); 3809 #endif 3810 sk->retransmit_timer.function=&retransmit_timer; 3811 sk->retransmit_timer.data = (unsigned long)sk; 3812 reset_xmit_timer(sk, TIME_WRITE, sk->rto); /* Timer for repeating the SYN until an answer */ 3813 sk->retransmits = TCP_SYN_RETRIES; 3814 sk->prot->queue_xmit(sk, dev, buff, 0); 3815 reset_xmit_timer(sk, TIME_WRITE, sk->rto); 3816 tcp_statistics.TcpActiveOpens++; 3817 tcp_statistics.TcpOutSegs++; 3818 release_sock(sk); 3819 return(0); 3820 } tcp_connect 函数是 connect 系统调用传输层对于 TCP 协议的实现。该函数实现的功能顾名思义 是发送 SYN 请求连接数据包,用在主动打开(active open)的一端(一般我们称为客户端)。对 于该函数并没有什么值得特别说明的地方,需要注意的是该函数中对于 TCP 首部各字段的赋值 以及对于 socket 状态的更新(设置为 TCP_SYN_SENT)和最后超时重发定时器的设置(注意最 初定时时间间隔为 TCP_TIMEOUT_INIT)。TCP 协议连接建立期间通报各自的 MSS 值,这是 作为 TCP 选项而存在的,在 TCP 连接建立时,这个选项是必须的。函数中对于 MSS 值的设置 有些混淆,其直接使用 MTU 值,而其于 MSS 是完全不同的。MTU 表示最大传输单元大小, 解释为 IP 首部及其数据负载,IP 数据负载即包括 TCP 首部和 TCP 数据负载。而 MSS 值表示 最大段大小(Maximum Segment Size),仅指 TCP 数据负载大小。 即:MSS=MTU — sizeof(ip header ) — sizeof(tcp header). 实际上,本版本网络栈实现代码对于 很多数据(如分配单个数据包的缓存大小)处理的都比较随意,不可仔细考量! 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3821 /* This functions checks to see if the tcp header is actually acceptable. */ 3822 extern __inline__ int tcp_sequence(struct sock *sk, struct tcphdr *th, short len, 3823 struct options *opt, unsigned long saddr, struct device *dev) 3824 { 3825 unsigned long next_seq; 3826 next_seq = len - 4*th->doff; 3827 if (th->fin) 3828 next_seq++; 3829 /* if we have a zero window, we can't have any data in the packet.. */ 3830 if (next_seq && !sk->window) 3831 goto ignore_it; 3832 next_seq += th->seq; 3833 /* 3834 * This isn't quite right. sk->acked_seq could be more recent 3835 * than sk->window. This is however close enough. We will accept 3836 * slightly more packets than we should, but it should not cause 3837 * problems unless someone is trying to forge packets. 3838 */ 3839 /* have we already seen all of this packet? */ 3840 if (!after(next_seq+1, sk->acked_seq)) 3841 goto ignore_it; 3842 /* or does it start beyond the window? */ 3843 if (!before(th->seq, sk->acked_seq + sk->window + 1)) 3844 goto ignore_it; 3845 /* ok, at least part of this packet would seem interesting.. */ 3846 return 1; 3847 ignore_it: 3848 if (th->rst) 3849 return 0; 3850 /* 3851 * Send a reset if we get something not ours and we are 3852 * unsynchronized. Note: We don't do anything to our end. We 3853 * are just killing the bogus remote connection then we will 3854 * connect again and it will work (with luck). 3855 */ 3856 if (sk->state==TCP_SYN_SENT || sk->state==TCP_SYN_RECV) 3857 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3858 tcp_reset(sk->saddr,sk->daddr,th,sk->prot,NULL,dev, sk->ip_tos,sk->ip_ttl); 3859 return 1; 3860 } 3861 /* Try to resync things. */ 3862 tcp_send_ack(sk->sent_seq, sk->acked_seq, sk, th, saddr); 3863 return 0; 3864 } tcp_sequence 函数用于检查接收的数据包序列号是否正确,或者更准确的说,是否需要对该数据 包进行进一步的处理。 如果本地窗口为 0,数据包包含数据(注 FIN,SYN 均使用了一个序列号),则简单丢弃该数据 包。窗口为 0 表示本地接收缓冲区满,无法继续接收远端数据包。 如果数据包是一个副本(之前已经接收了同样的一份数据包),则简单丢弃。 如果数据包序列号在本地窗口之外,则违背了远端窗口数据流节制机制,也简单丢弃。 如果序列号正确,则返回 1,内核继续处理该数据包,否则根据当前套接字状态,发送一个 RST 数据包复位对方套接字,或者是简单的发送一个 ACK 数据包,敷衍了事。 3865 /* 3866 * When we get a reset we do this. 3867 */ 3868 static int tcp_std_reset(struct sock *sk, struct sk_buff *skb) 3869 { 3870 sk->zapped = 1; 3871 sk->err = ECONNRESET; 3872 if (sk->state == TCP_SYN_SENT) 3873 sk->err = ECONNREFUSED; 3874 if (sk->state == TCP_CLOSE_WAIT) 3875 sk->err = EPIPE; 3876 #ifdef TCP_DO_RFC1337 3877 /* 3878 * Time wait assassination protection [RFC1337] 3879 */ 3880 if(sk->state!=TCP_TIME_WAIT) 3881 { 3882 tcp_set_state(sk,TCP_CLOSE); 3883 sk->shutdown = SHUTDOWN_MASK; 3884 } 3885 #else 3886 tcp_set_state(sk,TCP_CLOSE); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3887 sk->shutdown = SHUTDOWN_MASK; 3888 #endif 3889 if (!sk->dead) 3890 sk->state_change(sk); 3891 kfree_skb(skb, FREE_READ); 3892 release_sock(sk); 3893 return(0); 3894 } tcp_std_reset 函数负责处理接收到的 RST 数据包,RST 数据包用于对连接进行复位(简单的说, 即断开连接)。如果我们发送一个请求连接数据包,远端并无对应的服务存在,在远端回送一个 RST 数据包,将本地请求复位,上层显示可能为:connection refused. sock 结构 zapped 字段专门用于标志被复位套接字。当 zapped 字段被设置为 1 时,表示本地套 接字已被复位(即远端断开了本地的连接或者请求),对于已被复位的连接,很多工作就无需进 行,其它相关函数在处理之前会检查该字段。tcp_std_reset 函数主要实现功能为根据当前套接字 的状态相应的设置新的状态。如果该套接字仍在使用(dead 字段为 0),则通过回调函数通知上 层应用该套接字状态的改变(sk->state_change 调用)。 4305 /* 4306 * A window probe timeout has occurred. 4307 */ 4308 void tcp_send_probe0(struct sock *sk) 4309 { 4310 if (sk->zapped) 4311 return; /* After a valid reset we can send no more */ 4312 tcp_write_wakeup(sk); 4313 sk->backoff++; 4314 sk->rto = min(sk->rto << 1, 120*HZ); 4315 reset_xmit_timer (sk, TIME_PROBE0, sk->rto); 4316 sk->retransmits++; 4317 sk->prot->retransmits ++; 4318 } tcp_send_probe0 函数用于进行远端窗口探测。远端窗口是一个远端对本地进行数据流节制的机 制。本地发送的数据包不可超过远端通报的窗口范围。本质上,远端窗口大小表示远端当前可 用接收缓冲区的大小。如果远端窗口大小通报为 0,表示远端接收缓冲区已满,本地不可继续 发送数据包。当远端应用程序读取部分数据包后,远端将发送一个窗口通报数据包,本地在接 收到该数据包后,方可继续发送待发送数据包。这种机制的隐患是,一旦远端通报非 0 窗口大 小的数据包丢失了,则双方将陷入死锁:本地等待非 0 窗口通报数据包,远端认为其已经发送 非 0 通报数据包,等待本地发送其它数据。为了防止非 0 窗口数据包可能丢失引起的死锁,本 地在接收到远端 0 窗口数据包后,启动窗口探测定时器,每隔一段时间发送一个窗口探测数据 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 包,如此即便对方非 0 窗口通报数据包发送丢失的行为,在接收该窗口探测数据包,其发送的 ACK 数据包中仍将通报一个非 0 窗口,从而解除之前可能的死锁行为。 具体的,tcp_send_probe0 函数是通过调用 tcp_write_wakeup 函数完成窗口探测数据包的发送。 tcp_write_wakeup 函数前文中已有论述,其发送一个旧序列号的数据包,这样做主要是因为目的 是探测,而非是发送数据。使用旧序列号的原因还在于与其它函数(主要指 tcp_sequeue)配合 使用,实际上探测数据包并非一定要使用一个旧序列号,使用当前序列号也无不可。从 tcp_sequeuce 函数如下实现片断即可看出使用旧序列号的原因: …… if (!after(next_seq+1, sk->acked_seq)) goto ignore_it; …… ignore_it: …… tcp_send_ack(sk->sent_seq, sk->acked_seq, sk, th, saddr); 探测数据包的目的主要是接收一个远端数据包(无论是 ACK 数据包还是普通数据包,其都会包 含本地需要知道的远端窗口大小),最好是 ACK 数据包(极端情况是可能本地也是 0 窗口,此 时只能接收 ACK 数据包),使用旧序列号以及 tcp_sequence 以上这段代码的配合,本地可以达 到接收一个远端 ACK 数据包的目的。 注意 tcp_send_probe0 函数对于探测数据包也使用指数退避算法计算发送时间间隔。 4319 /* 4320 * Socket option code for TCP. 4321 */ 4322 int tcp_setsockopt(struct sock *sk, int level, int optname, char *optval, int optlen) 4323 { 4324 int val,err; 4325 if(level!=SOL_TCP) 4326 return ip_setsockopt(sk,level,optname,optval,optlen); 4327 if (optval == NULL) 4328 return(-EINVAL); 4329 err=verify_area(VERIFY_READ, optval, sizeof(int)); 4330 if(err) 4331 return err; 4332 val = get_fs_long((unsigned long *)optval); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4333 switch(optname) 4334 { 4335 case TCP_MAXSEG: 4336 /* 4337 * values greater than interface MTU won't take effect. however at 4338 * the point when this call is done we typically don't yet know 4339 * which interface is going to be used 4340 */ 4341 if(val<1||val>MAX_WINDOW) 4342 return -EINVAL; 4343 sk->user_mss=val; 4344 return 0; 4345 case TCP_NODELAY: 4346 sk->nonagle=(val==0)?0:1; 4347 return 0; 4348 default: 4349 return(-ENOPROTOOPT); 4350 } 4351 } tcp_setsockopt 函数用于处理 TCP 选项设置,本版本支持的可设置选项为 MSS 值和 Nagle 算法 使能。如果选项不是针对 TCP,则调用 ip_setsocket 函数进行处理。 Nagle 算法是为解决网络中充斥大量小数据包问题而设计的。其算法基本思想是:如果之前发送 的一个数据包尚未得到应答,则本地不可继续发送其它数据包。欢聚换说,Nagle 算法实现的是 等待-发送策略,每次只能发送一个数据包,直到该数据包得到应答,方可发送下一个数据包。 这种算法有利于在等待上一个数据包应答期间,尽量积累上层数据,以便在下一个数据包中一 并发送出去,从而减少发送的数据包数量。Nagle 算法引起的问题是增加了程序之间交互的时间, 有些情况下必须禁止使用 Nagle 算法,这些情况大多存在于客户-服务体系架构中,如 X 窗口服 务必须禁止 Nagle 算法,如此方可对相关事件(如鼠标移动事件)作出快速反应,此时使用 Nagle 算法造成的延迟将是不可容忍的。 4352 int tcp_getsockopt(struct sock *sk, int level, int optname, char *optval, int *optlen) 4353 { 4354 int val,err; 4355 if(level!=SOL_TCP) 4356 return ip_getsockopt(sk,level,optname,optval,optlen); 4357 switch(optname) 4358 { 4359 case TCP_MAXSEG: 4360 val=sk->user_mss; 4361 break; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4362 case TCP_NODELAY: 4363 val=sk->nonagle; 4364 break; 4365 default: 4366 return(-ENOPROTOOPT); 4367 } 4368 err=verify_area(VERIFY_WRITE, optlen, sizeof(int)); 4369 if(err) 4370 return err; 4371 put_fs_long(sizeof(int),(unsigned long *) optlen); 4372 err=verify_area(VERIFY_WRITE, optval, sizeof(int)); 4373 if(err) 4374 return err; 4375 put_fs_long(val,(unsigned long *)optval); 4376 return(0); 4377 } tcp_getsockopt 函数用于获取选项值,其与 tcp_setsockopt 函数对应,此处不再叙述。 至此,关于 TCP 协议还剩下最后一个函数 tcp_rcv 尚未涉及,该函数是 TCP 协议数据包接收的 总入口函数,网络层协议(如 IP 协议)在判断数据包使用的是 TCP 协议后,将调用 tcp_rcv 函 数对该数据包进行传输层的相关处理。从实现上看,tcp_rcv 函数更像是一个任务分发器,其根 据数据包中各标志位的设置,将数据包进一步派送给其它相关函数进行具体处理,其本身并不 进行细节上的处理工作。 从大的方面来看,可以将数据包分为以下几种类型:SYN 请求连接数据包,ACK 应答数据包, RST 数据包,普通数据包,FIN 断开连接数据包。 其中在 TCP 连接期间,ACK 应答数据包和普通数据包是作为一个数据包来传输的。即数据包 中包含普通数据且 TCP 首部中 ACK 字段被设置为 1:也即发送本地数据的同时也对已成功接 收的远端数据进行应答。 由此,tcp_rcv 函数从功能上看,主要有以下几个模块组成: 1> 数据包合法性检测模块,对应函数:tcp_sequence 2> 请求连接处理模块,对应函数:tcp_conn_request 3> RST 数据包处理模块,对应函数:tcp_reset 4> 应答处理模块(完成连接建立模块),对应函数:tcp_ack 5> 数据处理模块,对应函数:tcp_urg, tcp_data 6> 断开连接处理模块(此时指处理对方发送的 FIN 数据包),对应函数:tcp_fin 下面我们将分段对该函数进行分析,首先对函数参数进行说明。 3895 /* 3896 * A TCP packet has arrived. 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3897 */ 3898 int tcp_rcv(struct sk_buff *skb, struct device *dev, struct options *opt, 3899 unsigned long daddr, unsigned short len, 3900 unsigned long saddr, int redo, struct inet_protocol * protocol) 3901 { 参数说明: skb:表示被接收的数据包。 dev:表示接收该数据包的网络设备,对于每个网络接收设备,内核均以 device 结构表示。 opt:表示被接收数据包可能的 IP 选项,IP 选项的处理是在 do_options(ip.c)函数中完成的。 daddr:IP 首部中的远端地址字段值,所以从本地接收的角度看,指的是本地 IP 地址。 len:IP 数据负载的长度:包括 TCP 首部以及 TCP 数据负载。 saddr:IP 首部中源端 IP 地址,从本地接收的角度,指的是发送端 IP 地址。 redo:这是一个标志位,准确地说,tcp_rcv 函数在两个地方被调用,其一就是上文中刚刚提到 的,被下层网络层模块调用,用于接收新的数据包,这是 redo 标志位设置为 0,表示这是一个 新的数据包;其二就是在 release_sock 函数中被调用,release_sock 函数对先前缓存在 sock 结构 back_log 队列中的数据包调用 tcp_rcv 函数进行重新接收。而 back_log 中数据包是由 tcp_rcv 函 数进行缓存的,读者在下文中即可看到,当 tcp_rcv 函数发送套接字当前正忙时(sock 结构 inuse 字段为 1),则将数据包缓存于 back_log 队列中后直接返回,此后由 release_sock 函数负责将数 据包再次递给 tcp_rcv 函数进行重新处理,此时 redo 字段即被设置为 1,表示这是先前被缓存数 据包的再次处理。 protocol:这是一个 inet_protocol 结构类型的变量,表示该套接字所使用的协议以及协议对应的 接收函数。inet_protocol 结构定义在 protocol.h 文件中,如下: /*net/inet/protocol.h*/ 24 /* This is used to register protocols. */ 25 struct inet_protocol { 26 int (*handler)(struct sk_buff *skb, struct device *dev, 27 struct options *opt, unsigned long daddr, 28 unsigned short len, unsigned long saddr, 29 int redo, struct inet_protocol *protocol); 30 int (*frag_handler)(struct sk_buff *skb, struct device *dev, 31 struct options *opt, unsigned long daddr, 32 unsigned short len, unsigned long saddr, 33 int redo, struct inet_protocol *protocol); 34 void (*err_handler)(int err, unsigned char *buff, 35 unsigned long daddr, 36 unsigned long saddr, 37 struct inet_protocol *protocol); 38 struct inet_protocol *next; 39 unsigned char protocol; 40 unsigned char copy:1; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 41 void *data; 42 char *name; 43 }; 而相关实例定义在 protocol.c 文件中,对于 TCP 协议对应的 inet_protocol 结构如下: /*net/inet/protocol.c*/ 44 static struct inet_protocol tcp_protocol = { 45 tcp_rcv, /* TCP handler */ 46 NULL, /* No fragment handler (and won't be for a long time) */ 47 tcp_err, /* TCP error control */ 48 NULL, /* next */ 49 IPPROTO_TCP, /* protocol ID */ 50 0, /* copy */ 51 NULL, /* data */ 52 "TCP" /* name */ 53 }; 这个结构定义了对应协议数据包接收函数,错误处理函数,协议编号(TCP 为 6)等等。UDP, ICMP 协议都有这样的一个结构对应,在分析 protocol.c 文件时读者将会看到。这些结构被网络 层模块使用,其根据 IP 首部中上层协议字段值判断该调用哪个函数进行数据包的进一步处理。 对于 TCP 协议,这个处理函数为 tcp_rcv。 在明白参数含义后,我们进入函数主体的分析,首先是对数据包合法性的检查。 3902 struct tcphdr *th; 3903 struct sock *sk; 3904 int syn_ok=0; 3905 if (!skb) 3906 { 3907 printk("IMPOSSIBLE 1\n"); 3908 return(0); 3909 } //数据包没有经过网口设备,怎们可能会被接收? 3910 if (!dev) 3911 { 3912 printk("IMPOSSIBLE 2\n"); 3913 return(0); 3914 } 3915 tcp_statistics.TcpInSegs++; //如果不是发送给本地的数据包,在网络层就已经被处理,根本不会跑到 //传输层来。 3916 if(skb->pkt_type!=PACKET_HOST) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3917 { 3918 kfree_skb(skb,FREE_READ); 3919 return(0); 3920 } 这些检查有些显得比较冗余,如对 skb 参数是否为 NULL 的检查;这些检查所依据的思想是保 险。虽然我们按常理分析,程序应该不会出现某种情况,但由于计算机在运行程序过程后,有 可能出现意想不到的极端情况,此时如果不对这些原本不应该出现的情况的监测,则进一步处 理的话,会使系统崩溃。所以在系统代码的很多地方,都会出现这些对“不应该”出现的条件 的监测。以上这段代码都是这个意思。 3921 th = skb->h.th; 3922 /* 3923 * Find the socket. 3924 */ 3925 sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); get_sock 函数定义在 af_inet.c 中,用于根据 TCP 套接字四元素查询本地对应的 sock 结构。 3926 /* 3927 * If this socket has got a reset it's to all intents and purposes 3928 * really dead. Count closed sockets as dead. 3929 * 3930 * Note: BSD appears to have a bug here. A 'closed' TCP in BSD 3931 * simply drops data. This seems incorrect as a 'closed' TCP doesn't 3932 * exist so should cause resets as if the port was unreachable. 3933 */ 3934 if (sk!=NULL && (sk->zapped || sk->state==TCP_CLOSE)) 3935 sk=NULL; 如果本地有对应的 sock 结构,但是本地状态显示该套接字已经被复位或者已经处于关闭状态, 则不可接收该数据包,此时通过将 sk 变量设置为 NULL 来完成,下面代码中将有对该字段的检 测,此处设置为 NULL,将阻止下文中的进一步处理。 3936 if (!redo) 3937 { 3938 if (tcp_check(th, len, saddr, daddr )) 3939 { 3940 skb->sk = NULL; 3941 kfree_skb(skb,FREE_READ); 3942 /* 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 3943 * We don't release the socket because it was 3944 * never marked in use. 3945 */ 3946 return(0); 3947 } 3948 th->seq = ntohl(th->seq); 3949 /* See if we know about the socket. */ 3950 if (sk == NULL) 3951 { 3952 /* 3953 * No such TCB. If th->rst is 0 send a reset (checked in tcp_reset) 3954 */ 3955 tcp_reset(daddr, saddr, th, &tcp_prot, opt,dev,skb->ip_hdr->tos,255); 3956 skb->sk = NULL; 3957 /* 3958 * Discard frame 3959 */ 3960 kfree_skb(skb, FREE_READ); 3961 return(0); 3962 } 3963 skb->len = len; 3964 skb->acked = 0; 3965 skb->used = 0; 3966 skb->free = 0; 3967 skb->saddr = daddr; 3968 skb->daddr = saddr; 3969 /* We may need to add it to the backlog here. */ 3970 cli(); 3971 if (sk->inuse) 3972 { 3973 skb_queue_tail(&sk->back_log, skb); 3974 sti(); 3975 return(0); 3976 } 3977 sk->inuse = 1; 3978 sti(); 3979 } 3936-3979 行对应的 if 语句块表示这是一个新的数据包,所以对此数据包的合法性需要进行检 查,如果数据包没有问题,则对表示该数据包的 skb 变量进行一些字段的赋值,合法性包括: 1>TCP 校验是否正确:tcp_check 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 2>本地是否有对应的套接字 如果以上两个条件都表示允许进一步的处理,则开始对表示该数据包的 sk_buff 结构中某些字段 进行赋值,这对应 3963-3968 行语句。最后检查当前套接字是否正在忙于处理其他事务(sk->inuse 是否为 1),如果是,则先将数据缓存到 sock 结构 back_log 队列中,稍后由 release_sock 函数再 次提交给本函数进行处理,此时对应的 redo 参数为 1,那么就进入下面的 else 模块。 3980 else 3981 { 3982 if (sk==NULL) 3983 { 3984 tcp_reset(daddr, saddr, th, &tcp_prot, opt,dev,skb->ip_hdr->tos,255); 3985 skb->sk = NULL; 3986 kfree_skb(skb, FREE_READ); 3987 return(0); 3988 } 3989 } 这个 else 语句块对应 redo=1 的情况,完成的工作只是对套接字存在性进行检查,如果在缓存过 程中套接字被复位或者被关闭,那么就不需要对该数据包进行接收,对于关闭的情况,即表示 本地不提供相关服务,此时回送一个 RST 数据包,复位对方请求,防止其一再进行数据包的发 送。 3990 if (!sk->prot) 3991 { 3992 printk("IMPOSSIBLE 3\n"); 3993 return(0); 3994 } sock 结构 prot 字段是一个 proto 类型结构变量,表示所使用的传输层协议处理函数集合,在创 建一个套接字时,会根据所使用流类型进行该字段的相应初始化,如对于 SOCK_STREAM 字 段,prot 字段将被初始化为 tcp_prot, 这个 tcp_prot 变量定义在 tcp.c 文件尾部,我们在分析完 tcp_rcv 函数后,最后查看该变量的定义。实际上,sock 结构中 prot 字段的初始化应根据 socket 系统调用中的第三个参数值,但一般我们在指定前两个参数后,第三个参数我们不指定,即由 于系统接口的局限性,前两个参数已经决定第三个参数的意义,如第一,二参数为 AF_INET, SOCK_STREAM,系统即认为使用 TCP 协议。因为对使用流式传输的协议,实际实现上只有 TCP 协议可供选择。 3995 /* 3996 * Charge the memory to the socket. 3997 */ 3998 if (sk->rmem_alloc + skb->mem_len >= sk->rcvbuf) 3999 { 4000 kfree_skb(skb, FREE_READ); 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4001 release_sock(sk); 4002 return(0); 4003 } 3998 行代码对接收缓冲区空余空间进行检查,以查看剩余空间是否足够接收当前数据包,如果 剩余空间过小,则简单丢弃该数据包返回。权当没“看见”,这将造成远端超时重发。当然这正 是本地想要的,因为或许之后会有足够空闲的缓冲区接收重发的数据包。 4004 skb->sk=sk; 4005 sk->rmem_alloc += skb->mem_len; 如果接收缓冲区空闲空间足够接收该数据包,则更新空闲缓冲区值:从当前空闲值中减去数据 包数据长度值。 一旦更新接收缓冲区值,我们即接收了该数据包,下面需要对该数据包的意图进行判断,根据 本地套接字当前的状态进行具体的处理。 4006 /* 4007 *This basically follows the flow suggested by RFC793, with the corrections in RFC1122.We 4008 *don't implement precedence and we process URG incorrectly (deliberately so) for BSD bug 4009 *compatibility. We also set up variables more thoroughly [Karn notes in the 4010 *KA9Q code the RFC793 incoming segment rules don't initialise the variables for all paths]. 4011 */ //这个 if 语句块管到 4173 行。 4012 if(sk->state!=TCP_ESTABLISHED) /* Skip this lot for normal flow */ 4013 { 4014 /* 4015 * Now deal with unusual cases. 4016 */ 套接字状态不是 TCP_ESTABLISHED,那么按双方协调的方式,此时数据包就是一个请求数据 包(而非数据传送数据包),下面将以此假设为前提,对本地对应的可用于表示或者提供请求的 套接字状态进行检查。 //这是一个侦听套接字,该 if 语句块管到 4047 行。 4017 if(sk->state==TCP_LISTEN) 4018 { 4019 if(th->ack)/* These use the socket TOS.. might want to be the received TOS */ 4020 tcp_reset(daddr,saddr,th,sk->prot,opt,dev,sk->ip_tos, sk->ip_ttl); 对于侦听套接字,其只响应连接请求(SYN 数据包),其他所有的数据包类型其都不负责,如 果这个数据包是一个 ACK 应答数据包,则表示这个数据包发错了地方,此时回复一个 RST 数 据包,复位对方的连接,阻止其一再发送无用的数据包给本地,浪费双方资源。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4021 /* 4022 * We don't care for RST, and non SYN are absorbed (old segments) 4023 * Broadcast/multicast SYN isn't allowed. Note - bug if you change the 4024 * netmask on a running connection it can go broadcast. Even Sun's have 4025 * this problem so I'm ignoring it 4026 */ 4027 if(th->rst || !th->syn || th->ack || ip_chk_addr(daddr)!=IS_MYADDR) 4028 { 4029 kfree_skb(skb, FREE_READ); 4030 release_sock(sk); 4031 return 0; 4032 } 4027 行语句块进行检查所依据的思想同 4019 行代码:侦听套接字只负责连接请求(请求对象 当然是本地),如果其一不满足,数据包即被简单丢弃。注意对于 RST,以及请求对象非本地的 情况,无需回复 RST 数据包,因为此时远端不会无休止的向本地发送无用数据包,浪费资源。 4033 /* 4034 * Guess we need to make a new socket up 4035 */ 4036 tcp_conn_request(sk, skb, daddr, saddr, opt, dev, tcp_init_seq()); 经过的以上的检查,我们可以认定这是一个 SYN 数据包(注意 4027 行代码中对 SYN 标志位的 检查),调用 tcp_conn_request 函数对连接请求做出响应。tcp_conn_request 函数在前文中已有分 析,这个函数主要完成新通信套接字的创建和初始化工作。一旦 tcp_conn_request 函数成功返回, 那么对应这个请求数据包发送端此后进行的所有工作将由这个新创建的套接字负责,侦听套接 字只是负责第一个 SYN 数据包,这一点非常重要!即对于同一个发送端发送的数据包,如还是 三路握手中的第三个 ACK 数据包,进入 tcp_rcv 函数处理时,查找到的对应本地 sock 结构不再 是侦听套接字对应的 sock 结构,而是这个新创建的套接字对应 sock 结构,换句话说,4049 行 对套接字状态的检查中,sk 表示的套接字与 4017 行是不同的,当然他们分属于两次不同的 tcp_rcv 调用,对于这段话的含义读者请想明白,在理解了这个后,对于 TCP 协议将有更进一步 的理解。 4037 /* 4038 * Now we have several options: In theory there is nothing else 4039 * in the frame. KA9Q has an option to send data with the syn, 4040 * BSD accepts data with the syn up to the [to be] advertised window 4041 * and Solaris 2.1 gives you a protocol error. For now we just ignore 4042 * it, that fits the spec precisely and avoids incompatibilities. It 4043 * would be nice in future to drop through and process the data. 4044 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4045 release_sock(sk); 对 release_sock 函数的调用是处理 back_log 队列中先前被缓存的其他连接请求,并做出响应。 4046 return 0; 4047 }//TCP_LISTEN 4048 /* retransmitted SYN? */ 4049 if (sk->state == TCP_SYN_RECV && th->syn && th->seq+1 == sk->acked_seq) 4050 { 4051 kfree_skb(skb, FREE_READ); 4052 release_sock(sk); 4053 return 0; 4054 }// TCP_SYN_RECV 在 tcp_conn_request 函数中,在创建一个新的专门用于此后数据交换的套接字后,该套接字状态 被设置为 TCP_SYN_RECV,并且该套接字以 TCP 套接字四元素(双方 IP 地址,双方端口号)为 计算条件通过 put_sock 插入到系统相关队列中,下一次在此处理从同一远端接收的数据包时, 查找到的 sock 结构(get_sock 函数)将是这个新创建的套接字对应 sock 结构,而不再是侦听套 接字了,或者说,侦听套接字与 tcp_conn_request 是绑定的,一旦 tcp_conn_request 函数被调用, 侦听套接字即脱离干系。 4049 行代码对重复发送的 SYN 数据包进行了检查,对于重发的 SYN 数据包,处理方式为简单 丢弃。一般远端重发 SYN 数据包,应该是因为本地发送的三路握手中的第二个数据包丢失了, 但这不会引起问题,因为本地也会超时重发。 4055 /* 4056 * SYN sent means we have to look for a suitable ack and either reset 4057 * for bad matches or go to connected 4058 */ //这个 if 语句块管到 4138 行,是对 TCP_SYN_SENT 状态进行的相应处理。 4059 if(sk->state==TCP_SYN_SENT) 4060 { 对 TCP_SYN_SENT 状态的处理,对于处于这一状态的套接字是作为主动连接端(一般为客户 端),在发送一个 SYN 请求连接数据包后,即设置进入 TCP_SYN_SENT 状态,此时等待远端 发送一个 SYN+ACK 数据包(意为 TCP 首部中 SYN,ACK 都被设置为 1,这个三路握手中一 般有服务器端发送的第二个数据包)。最主要是对本地 SYN 的应答,所以首先对所接收数据包 中 ACK 标志位进行检查,由此下面代码被分为两个模块,分别对应 if,else 语句。else 语句块 对可能的同时连接的情况进行处理。 4061 /* Crossed SYN or previous junk segment */ 4062 if(th->ack) 4063 { 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4062 行 if 语句对应 ACK 标志位被设置的情况,这种情况还需要对 SYN 标志位进行检查。如果 SYN 标志位没有被设置,还是无法完成三路握手连接过程。 4064 /* We got an ack, but it's not a good ack */ 4065 if(!tcp_ack(sk,th,saddr,len)) 4066 { 调用 tcp_ack 函数对 ACK 标志位被设置的情况进行处理,注意如果一切正常,本地套接字状态 在 tcp_ack 函数中将被设置为 TCP_ESTABLISHED。 tcp_ack 函数返回 1 表示正常,返回 0 表 示出现错误,对于错误的情况,本地简单丢弃该数据包并回复一个 RST 数据包(RST 数据包的 主要作用是复位对方连接,阻止其发送其他数据包给本地,本书下面讲到回复 RST 数据包时都 是这个意思,故不再做出解释)。 4067 /* Reset the ack - its an ack from a 4068 different connection [ th->rst is checked in tcp_reset()] */ 4069 tcp_statistics.TcpAttemptFails++; 4070 tcp_reset(daddr, saddr, th, 4071 sk->prot, opt,dev,sk->ip_tos,sk->ip_ttl); 4072 kfree_skb(skb, FREE_READ); 4073 release_sock(sk); 4074 return(0); 4075 } 4076 if(th->rst) 4077 return tcp_std_reset(sk,skb); 对方给了一个数据包,表示之前发送的 SYN 请求数据包所表示的服务对方不提供,所以回复一 个 RST 数据包,告知本地不要在发送 SYN 数据包了,即复位连接,本地调用 tcp_std_reset 函数 进行处理。tcp_std_reset 函数主要是关闭本地套接字。 4078 if(!th->syn) 4079 { 4080 /* A valid ack from a different connection 4081 start. Shouldn't happen but cover it */ 4082 kfree_skb(skb, FREE_READ); 4083 release_sock(sk); 4084 return 0; 4085 } 应答数据包中 SYN 标志位没有设置,无法完成连接过程,处理方式为简单丢弃,等待合适的数 据包。注意此时不能回复一个 RST 数据包,因为对于这种情况,本地还希望从远端接收其他数 据包完成连接建立过程。对于 SYN 标志位被设置的情况,表示对于本地而言,连接已经完成, 下面更新相关字段值。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4086 /* 4087 * Ok.. it's good. Set up sequence numbers and 4088 * move to established. 4089 */ 4090 syn_ok=1; /* Don't reset this connection for the syn */ 4091 sk->acked_seq=th->seq+1; 4092 sk->fin_seq=th->seq; 4093 tcp_send_ack(sk->sent_seq,sk->acked_seq,sk,th,sk->daddr); 4094 tcp_set_state(sk, TCP_ESTABLISHED); 4095 tcp_options(sk,th); 4096 sk->dummy_th.dest=th->source; 4097 sk->copied_seq = sk->acked_seq; 4098 if(!sk->dead) 4099 { 4100 sk->state_change(sk); 4101 sock_wake_async(sk->socket, 0); 4102 } 4103 if(sk->max_window==0) 4104 { 4105 sk->max_window = 32; 4106 sk->mss = min(sk->max_window, sk->mtu); 4107 } 4108 } 首先 4091 行更新本地应答序列号,这个序列号也表示本地希望从远端接收到的下一个字节的序 列号。对于 fin_seq 字段的更新较少,此处为其一,其二为接收到一个 FIN 数据包时。4093 行 回复一个应答数据包,从而帮助远端完成连接过程。之后设置本地状态为 TCP_ESTABLISHED, 并对数据包中 MSS 选项进行处理。对于 TCP 连接,其必须包含 MSS 选项,从而告知对方自己 可接收的数据包最大长度。4096 行对远端 IP 地址进行确认更新,4097 行对 copied_seq 字段进 行更细,该字段表示已经拷贝给上层应用的数据序列号,这个序列号对应拷贝的最后一个字节 的序列号加 1。由于状态进入 TCP_ESTABLISHED,通知使用该套接字的进程可以进行进一步的 处理的(一般是从 connect 系统调用中返回)。最后更新本地 MSS 值,用于节制此后发送的所有 数据包的大小。 4109 else 4110 { 这个 else 语句块对应 ACK 标志位没有被设置的情况,此时需要对可能的同时打开连接的情况 进行检查。对于同时打开连接的情况是指本地在发送 SYN 请求连接数据包后,在接收对应的应 答数据包之前,接收到了一个对方发送 SYN 请求数据包,此时即进入同时打开连接的状态。所 以首先代码对数据包 SYN 标志位进行检查。 4111 /* See if SYN's cross. Drop if boring */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4112 if(th->syn && !th->rst) 4113 { 4114 /* Crossed SYN's are fine - but talking to 4115 yourself is right out... */ 4116 if(sk->saddr==saddr && sk->daddr==daddr && 4117 sk->dummy_th.source==th->source && 4118 sk->dummy_th.dest==th->dest) 4119 { 4120 tcp_statistics.TcpAttemptFails++; 4121 return tcp_std_reset(sk,skb); 4122 } 4123 tcp_set_state(sk,TCP_SYN_RECV); 4124 /* 4125 * FIXME: 4126 * Must send SYN|ACK here 4127 */ 4128 }// if(th->syn && !th->rst) 4112-4128 行是对同时发起连接情况的处理,4116 行对这个 SYN 数据包的合法性进行检查,该 行 if 条件语句通过检查 TCP 套接字四要素判断这个接收的数据包是否是自己发送的:即试图自 己与自己通信的情况,这种情况是不允许的。通过 4116 行的检查就表示这是一个合法的 SYN 数据包,此时对应于同时打开连接的情况,4123 行将套接字状态设置为 TCP_SYN_RECV,这 是对应于同时打开情况的状态转换图,正常情况下是从 TCP_SYN_SENT 状态进入 TCP_ESTABLISHED。4124-4127 行注释表示应该在这儿发送一个 ACK 数据包。 4129 /* Discard junk segment */ 4130 kfree_skb(skb, FREE_READ); 4131 release_sock(sk); 4132 return 0; 4133 }//else 4134 /* 4135 * SYN_RECV with data maybe.. drop through 4136 */ 4137 goto rfc_step6; 对于本地原状态时 TCP_SYN_SENT 的情况,在处理完 ACK 数据包或者可能的同时打开连接的 情况后,直接跳转到 rfc_step6 标志符处进行执行,从下文的代码看,这跳过了其中对于数据包 序列号的检查,SYN 标志位的处理,ACK 标志位的处理,以及 RST 标志位的处理,直接进行 URG 标志位,以及普通数据的处理。跳过如上这些处理,是因为在 TCP_SYN_SENT 对应状态 的模块中已经对这些情况进行了处理。 4138 }// TCP_SYN_SENT 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 下面这段代码是对处于 2MSL 状态的服务器端套接字从同一客户端重新发起一个新的连接的情 况的处理。我们先从表层上进行阐述,然后深入本质查看具体的处理方式。一般而言,对于处 理 2MSL 状态的套接字(一般为服务器端),是不允许接受新的连接请求的,但套接字编程系统 接口允许应用程序通过设置一个 REUSEADDR 选项,达到使处于 2MSL 状态的套接字重新接受 从相同客户端发起的新的连接请求。很多教科书上都绘这么说,但其中有一个最为关键的矛盾 大家都避而不谈,那就是在客户端在请求连接时,服务器端是新创建一个套接字用于通信的, 侦听套接字依然处于侦听状态,其不进行任何实际数据的交换。那么现在双方关闭连接,那么 实际上从服务器的角度而言,处于 2MSL 等待状态的也是先前那个新创建的用于通信的套接字, 这时候侦听套接字依然处于侦听状态,而且所谓接不接受新的连接请求,只能对侦听套接字而 言,对于那个新创建的用于数据交换的套接字是不存在所谓的接不接受连接请求的情况的。那 么此处就涉及到一个低层 sock 数据结构的存储和查找的问题。前文中我们分析到 get_sock 函数, 该函数根据套接字四元素(双方 IP 地址,双方端口号)从系统队列中寻找满足条件的 sock 结 构。实际上对于侦听套接字 sock 结构只有本地 IP 地址,本地端口号进行标识,其不与任何远 端 IP 地址,远端端口号进行绑定,只有在处理一个连接请求时新创建的那个套接字方才进行四 元素的绑定,而且这个新创建的套接字本地 IP 地址,本地端口号大多数情况下(对于只有一个 网卡的普通主机而言,IP 地址,端口号必然相同,对于有多块网卡的路由器可能 IP 地址不同) 是和侦听套接字相同的。由于 sock 结构查找过程中首先是通过本地端口号进行数组元素队列的 寻址(请查询本书前文中对 get_sock 函数的分析,这个函数定义在 af_inet.c 文件中),所以对于 一个侦听套接字而言,其在调用 tcp_conn_request 函数处理一个连接请求时创建的新的套接字对 应的 sock 结构都与该侦听套接字 sock 结构处于同一个队列中。只不过在查找具体 sock 结构时, 是查找最佳匹配的 sock 结构,由于侦听套接字只绑定了本地 IP 地址,本地端口号两个元素, 而负责通信的新创建的套接字绑定了本地 IP 地址,本地端口号,远端 IP 地址,远端端口号四 个元素,在查找时匹配度较之侦听套接字高,所以通常在通信套接字处于非 TCP_CLOSE 状态, 其 sock 结构依然处于系统队列中时,返回的是通信套接字 sock 结构,而非侦听套接字 sock 结 构;那么对于新的连接请求,为何返回侦听套接字 sock 结构?这时候,侦听套接字之前创建的 所有的新的套接字本地 IP 地址以及本地端口号都符合条件,原因是:其一在查找对应 sock 结 构时,远端 IP 地址,远端端口号都没有匹配项;其二侦听套接字处于队列的最前端,虽然所有 的由侦听套接字之前创建的新的 sock 结构都满足本地 IP 地址,本地端口号匹配,但在查找规 则设置是除非找到更合适的(匹配元素更多的,这点参考 get_sock 函数实现即可一目了然),否 则返回第一个满足条件的 sock 结构。另外一个查找策略是如果套接字状态为 TCP_CLOSE,则 将该套接字排除在查找对象之外。综上所述,虽然在检查服务器端套接字状态为 2MSL 时使用 的是通信套接字,但在接受来自同一个远端(包括使用了相同远端 IP 地址,远端端口号)的连 接请求时,使用的却是侦听套接字。 4139 /* 4140 * BSD has a funny hack with TIME_WAIT and fast reuse of a port. There is 4141 * a more complex suggestion for fixing these reuse issues in RFC1644 4142 * but not yet ready for general use. Also see RFC1379. 4143 */ 4144 #define BSD_TIME_WAIT 4145 #ifdef BSD_TIME_WAIT 4146 if (sk->state == TCP_TIME_WAIT && th->syn && sk->dead && 4147 after(th->seq, sk->acked_seq) && !th->rst) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4148 { 4146 行 if 语句是对处于 2MSL 状态的套接字是否接受到一个连接请求进行判断,如果条件都满 足,则表示接收到一个具有相同远端地址,远端端口号的连接请求(这一点是由 4540 行代码保 证的)。在处理上是将听任原来的这个通信套接字释放,而将请求转移给侦听套接字,通过调用 tcp_conn_request 函数重新创建一个套接字用于通信。下面我们逐行分析这时如何完成的。 4149 long seq=sk->write_seq; 保存原来这个通信套接字本地发送序列号最后值,下面(4165 行)将这个序列号加上 128000 作为新创建套接字的初始序列号。 4150 if(sk->debug) 4151 printk("Doing a BSD time wait\n"); 4152 tcp_statistics.TcpEstabResets++; 4153 sk->rmem_alloc -= skb->mem_len; 4154 skb->sk = NULL; 4155 sk->err=ECONNRESET; 4156 tcp_set_state(sk, TCP_CLOSE); 4157 sk->shutdown = SHUTDOWN_MASK; 4158 release_sock(sk); 4152-4158 行代码将原来的这个通信套接字完全置于关闭状态,首先将这个新的连接请求数据包 与这个通信套接字脱离干系(4153,4154 行),并设置套接字状态为完全关闭(4156,4157 行)。 最后调用 release_sock 函数进行释放(注意 release_sock 函数除了对缓存与 sock 结构中 back_log 队列中数据包调用 tcp_rcv 进行重新处理外,对于处于 TCP_CLOSE 状态以及 sk->dead 设置为 1 的套接字进行释放操作(具体的通过设置一个定时器完成,定时器到期后方才进行释放)。换句 话说,以上这段代码是将原先的通信套接字完全置于“毁灭”。下面对于相同远端的连接请求通 过转移给侦听套接字,而侦听套接字通过调用 tcp_conn_request 函数创建一个新的通信套接字儿 完成。 4159 sk=get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); 当 4156 行代码将原来的处理那个相同远端的套接字被设置为 TCP_CLOSE 状态后,其就不具备 被查找的资格,所以 4159 行代码调用 get_sock 函数返回的就是侦听套接字 sock 结构!即从 4159 行开始,如下的 sk 变量指向的都是侦听套接字的 sock 结构,那么当然如果服务应用程序仍然 运行的话,4160 行为真,从进行 4165 行 tcp_conn_request 函数的调用处理连接请求(即又创建 一个新的通信套接字进行数据交互)。当然如果服务应用程序被关闭,简单丢弃此次连接请求。 如果远端再次发送连接请求,在对其下一个连接请求数据包进行处理时,本地会“毫不客气” 的回复一个 RST 数据包的。 4160 if (sk && sk->state==TCP_LISTEN) 4161 { 4162 sk->inuse=1; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4163 skb->sk = sk; 4164 sk->rmem_alloc += skb->mem_len; 4165 tcp_conn_request(sk, skb, daddr, saddr,opt, dev,seq+128000); 4166 release_sock(sk); 4167 return 0; 4168 } 4169 kfree_skb(skb, FREE_READ); 4170 return 0; 4171 } 4172 #endif 4173 }// if(sk->state!=TCP_ESTABLISHED) 通过 4146-4171 行代码的分析,读者现在应该明白通常我们所说的使用 REUSEADDR 选项的侦 听套接字究竟是如何处理的(注意 REUSEADDR 选项只可被用于侦听套接字)。在处理上,原 来的通信套接字仍然进行释放,只不过侦听套接字又创建了一个新的套接字用于同一远端的通 信。本质上,这与平常的处理连接请求的方式并无区别,仅仅在于现在这个请求发生在原来的 套接字还没有被释放(那么在处理上就加速了释放过程,从而为创建新的套接字扫清道路),而 且原来通信中可能被延迟的数据包会被发送到这个新创建的连接通道中。当然对于使用 REUSEADDR 选项的应用而言,如果真的发生这种情况,这也是自找的。对于以上代码的分析, 读者需要结合 get_sock 函数进行理解。 代码执行到此处,即表示可以对数据包中可能的数据进行处理了,但从上文代码来看,只在对 TCP_SYN_SENT 状态的处理模块中处理了各标志位,对于其他状态的处理都没有涉及,所以在 处理完 TCP_SYN_SENT 状态后,模块直接跳转到 rfc_step6 标志符处,而对于其他模块则进行 下面代码的执行,完成对各标志位的处理。由于被调用的相关函数前文中都有分析,而对各标 志位的处理方式前文中多有论述,故这段代码就由读者结合代码中注释自行分析理解,此处不 再阐述。 4174 /* 4175 * We are now in normal data flow (see the step list in the RFC) 4176 * Note most of these are inline now. I'll inline the lot when 4177 * I have time to test it hard and look at what gcc outputs 4178 */ //对数据包中数据序列号进行合法性检查。 4179 if(!tcp_sequence(sk,th,len,opt,saddr,dev)) 4180 { 4181 kfree_skb(skb, FREE_READ); 4182 release_sock(sk); 4183 return 0; 4184 } //这是一个 RST 数据包,调用 tcp_std_reset 函数进行 RST 标志位处理。 4185 if(th->rst) 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4186 return tcp_std_reset(sk,skb); 4187 /* 4188 * !syn_ok is effectively the state test in RFC793. 4189 */ //对于不在相应状态(TCP_LISTEN)的套接字发送 SYN 请求连接数据包 4190 if(th->syn && !syn_ok) 4191 { 4192 tcp_reset(daddr,saddr,th, &tcp_prot, opt, dev, skb->ip_hdr->tos, 255); 4193 return tcp_std_reset(sk,skb); 4194 } 4195 /* 4196 * Process the ACK 4197 */ //这是一个应答数据包,但应答序列号不合法。 4198 if(th->ack && !tcp_ack(sk,th,saddr,len)) 4199 { 4200 /* 4201 * Our three way handshake failed. 4202 */ //如果正处于三路握手连接建立过程,则连接建立失败。 //并发送一个 RST 复位数据包,阻止其继续发送不合法的数据包,浪费资源。 4203 if(sk->state==TCP_SYN_RECV) 4204 { 4205 tcp_reset(daddr, saddr, th,sk->prot, opt, dev,sk->ip_tos,sk->ip_ttl); 4206 } 4207 kfree_skb(skb, FREE_READ); 4208 release_sock(sk); 4209 return 0; 4210 } 如下 tcp_rcv 函数的最后这段代码才进行数据包中可能的数据处理,首先调用 tcp_urg 函数进行 紧急数据处理。此后调用 tcp_data 函数进行普通数据处理,注意如果处理过程中发生错误,则 表示数据包格式或者相关条件不满足接收,此时对数据包进行简单丢弃。至于对 release_sock 函数的一再调用是为了处理之前由于套接字忙而被tcp_rcv函数缓存于对应sock结构中back_log 队列中的数据包(重新递交给 tcp_rcv 函数处理)。 4211 rfc_step6: /* I'll clean this up later */ 4212 /* 4213 * Process urgent data 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4214 */ 4215 if(tcp_urg(sk, th, saddr, len)) 4216 { 4217 kfree_skb(skb, FREE_READ); 4218 release_sock(sk); 4219 return 0; 4220 } 4221 /* 4222 * Process the encapsulated data 4223 */ 4224 if(tcp_data(skb,sk, saddr, len)) 4225 { 4226 kfree_skb(skb, FREE_READ); 4227 release_sock(sk); 4228 return 0; 4229 } 4230 /* 4231 * And done 4232 */ 4233 release_sock(sk); 4234 return 0; 4235 } tcp_rcv 函数是 TCP 协议数据包处理的总中心,这个函数的作用有些类似于有关驱动程序中中断 处理函数的作用:检查中断产生的原因,调用对应函数进行具体的处理。此处是根据数据包设 置的标志为以及套接字当前的状态调用对应的其他函数完成针对性的具体处理。 虽然在分析时,采用的是从头到尾按函数定义顺序分析方式,不过在案这种方式分析完所有函 数之后,我们需要进行总体考虑,以 tcp_rcv 函数入手,理解代码对 TCP 协议的具体实现,这 对加深理解很有好处。所谓变换一个角度去理解问题,会很有收获。 文件最后是对 TCP 协议操作函数集合 proto 结构的定义,变量为 tcp_prot. 每个套接字在创建时, 系统都创建该套接字的对应的一个 socket 结构,一个 sock 结构。当应用程序进行套接字相关调 用,如 read 系统调用,调用将通过 socket 结构和 sock 结构以函数指针的方式逐层向下传递。如 read 函数调用首先通过系统接口层进入 sys_socketcall 总入口函数,该函数进一步将请求传递给 sock_read 函数处理,sock_read 函数根据 socket 结构中 ops 字段指向的处理函数集中 read 函数 指针指向的函数调用 inet_read 函数,而 inet_read 函数根据 sock 结构中 prot 指向的传输层协议 操作函数集中同样 read 函数指针指向的 tcp_read 完成数据读取工作,在这过程中用户缓冲区将 一直作为参数被传递,从而 tcp_read 函数可以将从接收队列中读取的数据直接复制到用户缓冲 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 区中,最终完成应用程序数据读取工作。 对于如下 tcp_prot 变量的定义,读者可结合 proto 结构的定义来看各字段的赋值情况。proto 结 构类型定义在 net/inet/sock.h 文件中。 4378 struct proto tcp_prot = { 4379 sock_wmalloc, 4380 sock_rmalloc, 4381 sock_wfree, 4382 sock_rfree, 4383 sock_rspace, 4384 sock_wspace, 4385 tcp_close, 4386 tcp_read, 4387 tcp_write, 4388 tcp_sendto, 4389 tcp_recvfrom, 4390 ip_build_header, 4391 tcp_connect, 4392 tcp_accept, 4393 ip_queue_xmit, 4394 tcp_retransmit, 4395 tcp_write_wakeup, 4396 tcp_read_wakeup, 4397 tcp_rcv, 4398 tcp_select, 4399 tcp_ioctl, 4400 NULL, 4401 tcp_shutdown, 4402 tcp_setsockopt, 4403 tcp_getsockopt, 4404 128, 4405 0, 4406 {NULL,}, 4407 "TCP", 4408 0, 0 4409 }; tcp.c 文件小结 TCP 协议实现代码是网络栈实现中最为复杂的部分,这主要是由于 TCP 协议本身的复杂性造成 的,其所要求的可靠性数据传输保证以及流式传输方式使得实现上必须进行数据重传以及重新 排序的处理措施。基于对每个数据进行编号的方式有效的解决了这个问题,但由于网络传输通 道的不稳定性,使得在代码实现上必须考虑某些极端情况,从而又有对拥塞处理,快速恢复机 制的支持。另外由于提供面向连接的传输方式,必须防止一方无故中断连接,而另一端不知的 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 情况发生,以及在采用窗口方式对数据传输速度进行节制时由于非 0 窗口通报数据包丢失的情 况发生,从而造成双方死锁,在实现上还必须采用主动探测的方式,而且又不可频繁探测,所 以必须设置定时器进行辅助。总之,TCP 协议实现要考虑的方面很多,在分析实现代码时,有 的地方也很难说清楚,但只要抓住 TCP 协议的核心:对数据进行编号和应答机制对很多问题都 容易理解。最后一点需要提及的是,无论何种协议都是为了主机进程之间的数据交换,网络特 别于同一台主机的区别在于其传输通道不可靠,所以需要定义一系列措施保证可靠性,仅此而 言,而 TCP 协议专门就是为了保证这种可靠性而设计的。当然如果不要求可靠性,那么就直接 传着吧,权当是数据都成功到达对方了。 2.5 net/inet/tcp.h 文件 在 inclue/linux/tcp.h 文件中定义的 TCP 协议基本信息,如 TCP 首部格式,而 net/inet/tcp.h 文件 是与 net/inet/tcp.c 文件中定义的函数进行声明,以及一些常量的定义。 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * Definitions for the TCP module. 7 * 8 * Version: @(#)tcp.h 1.0.5 05/23/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * 13 * This program is free software; you can redistribute it and/or 14 * modify it under the terms of the GNU General Public License 15 * as published by the Free Software Foundation; either version 16 * 2 of the License, or (at your option) any later version. 17 */ 18 #ifndef _TCP_H 19 #define _TCP_H 20 #include 21 #define MAX_SYN_SIZE 44 + MAX_HEADER 22 #define MAX_FIN_SIZE 40 + MAX_HEADER 23 #define MAX_ACK_SIZE 40 + MAX_HEADER 24 #define MAX_RESET_SIZE 40 + MAX_HEADER MAX_HEADER 常量定义在 include/linux/netdevice.h 文件中,值为 18。包括 MAC 首部 14 个字 节以及尾部冗余校验序列 4 个字节。21-24 分别定义了 SYN,FIN,ACK,RST 数据包的长度。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 其中 40 表示 20 字节 IP 首部加上 20 字节 TCP 首部。至于 SYN 数据包多出的 4 个字节用于 MSS 选项。对于 TCP 协议而言,在建立 TCP 连接时,必须使用 MSS 选项,声明本地可接受的最大 报文长度。 25 #define MAX_WINDOW 16384 26 #define MIN_WINDOW 2048 27 #define MAX_ACK_BACKLOG 2 MAX_WINDOW,MIN_WINDOW 表示本地最大,最小窗口。这两个字段主要是用在对接收缓 冲区当前大小的判断上(sock_rspace-sock.c)。我们在上文中一再提到,本地窗口值本质上表示 本地接收缓冲区大小。在使用 TCP 协议发送数据包时,TCP 首部中窗口字段表示的是本地接收 缓冲区中空闲区大小。MAX_ACK_BACKLOG 表示侦听套接字最大可接受的连接数(实际上该 常量没有被使用)。 28 #define MIN_WRITE_SPACE 2048 29 #define TCP_WINDOW_DIFF 2048 MIN_WRITE_SPACE 常量没有被使用,含义不明。TCP_WINDOW_DIFF 常量表示一个差值, 在 tcp_conn_request 函数中对新创建的套接字 sock 结构 max_unacked 字段进行初始化时被使用 到,如下: //inet/tcp.c 2402 newsk->max_unacked = MAX_WINDOW - TCP_WINDOW_DIFF; max_unacked 字段表示已成功接收但尚未对其进行应答的数据总量;这儿对于 MAX_WINDOW 常量的使用有些混乱,从此处赋值的含义来看,此时 MAX_WINDOW 表示的应是远端的最大 窗口值,TCP_WINDOW_DIFF 表示窗口中剩余空间大小。读者只要明白意思即可,对于这些早 期代码不可过于苛求。 30 /* urg_data states */ 31 #define URG_VALID 0x0100 32 #define URG_NOTYET 0x0200 33 #define URG_READ 0x0400 31-33 行这三个常量用于表示紧急数据处理的状态。URG_VALID 表示 sock 结构中 urg_data 字 段表示一个有效的紧急数据字节;URG_NOTYET 表示检测到紧急数据包,sock 结构中 urg_seq 表示该紧急数据序列号,但该紧急数据尚未被复制到 urg_data 字段中;而 URG_READ 则表示 urg_data 中紧急数据已被上层读取。读者可查看 tcp_read_urg, tcp_urg 函数对这三个字段进行理 解。 34 #define TCP_RETR1 7 /* 35 * This is how many retries it does before it 36 * tries to figure out if the gateway is 37 * down. 38 */ 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 39 #define TCP_RETR2 15 /* 40 * This should take at least 41 * 90 minutes to time out. 42 */ TCP_RETR1, TCP_RETR2 常量表示在丢弃本地一个数据包之前,尝试发送的次数。如果重传次 数超过 TCP_RETR1,则重新进行远端主机 ARP 请求,监测是否远端主机 MAC 地址发生变换; 如果重传次数超出 TCP_RETR2,则直接丢弃发送的数据包,放弃发送。 43 #define TCP_TIMEOUT_LEN (15*60*HZ) /* should be about 15 mins */ 44 #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to successfully 45 * close the socket, about 60 seconds */ 46 #define TCP_FIN_TIMEOUT (3*60*HZ) /* BSD style FIN_WAIT2 deadlock breaker */ 47 #define TCP_ACK_TIME (3*HZ) /* time to delay before sending an ACK */ 48 #define TCP_DONE_TIME 250 /* maximum time to wait before actually 49 * destroying a socket */ 50 #define TCP_WRITE_TIME 3000 /* initial time to wait for an ACK, 51 * after last transmit */ 52 #define TCP_TIMEOUT_INIT (3*HZ) /* RFC 1122 initial timeout value */ 53 #define TCP_SYN_RETRIES 5 /* number of times to retry opening a 54 * connection */ 55 #define TCP_PROBEWAIT_LEN 100 /* time to wait between probes when 56 * I've got something to write and 57 * there is no window */ 43-57 行是对 TCP 协议使用的定时器相关定时间隔时间的定义。 58 #define TCP_NO_CHECK 0 /* turn to one if you want the default 59 * to be no checksum */ 对于 TCP 协议而言,TCP 校验是必须的。UDP,ICMP 协议校验是可选的。 60 /* 61 * TCP option 62 */ 63 #define TCPOPT_NOP 1 /* Padding */ 64 #define TCPOPT_EOL 0 /* End of options */ 65 #define TCPOPT_MSS 2 /* Segment size negotiating */ 63-65 行是 TCP 协议最初规范 RFC-793 中定义的 TCP 使用的三种选项,其中 TCPOPT_MSS 选 项必须使用在连接建立过程中且也只可使用在该过程中。 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 66 /* 67 * We don't use these yet, but they are for PAWS and big windows 68 */ 69 #define TCPOPT_WINDOW 3 /* Window scaling */ 70 #define TCPOPT_TIMESTAMP 8 /* Better RTT estimations/PAWS */ 这是后来新添加的 TCP 选项。本版本网络代码没有对其进行实现。 71 /* 72 * The next routines deal with comparing 32 bit unsigned ints 73 * and worry about wraparound (automatic with unsigned arithmetic). 74 */ 75 extern __inline int before(unsigned long seq1, unsigned long seq2) 76 { 77 return (long)(seq1-seq2) < 0; 78 } 79 extern __inline int after(unsigned long seq1, unsigned long seq2) 80 { 81 return (long)(seq1-seq2) > 0; 82 } 83 /* is s2<=s1<=s3 ? */ 84 extern __inline int between(unsigned long seq1, unsigned long seq2, unsigned long seq3) 85 { 86 return (after(seq1+1, seq2) && before(seq1, seq3+1)); 87 } 88 /* 89 * List all states of a TCP socket that can be viewed as a "connected" 90 * state. This now includes TCP_SYN_RECV, although I am not yet fully 91 * convinced that this is the solution for the 'getpeername(2)' 92 * problem. Thanks to Stephen A. Wood -FvK 93 */ 94 extern __inline const int 95 tcp_connected(const int state) 96 { 97 return(state == TCP_ESTABLISHED || state == TCP_CLOSE_WAIT || 98 state == TCP_FIN_WAIT1 || state == TCP_FIN_WAIT2 || 99 state == TCP_SYN_RECV); 100 } 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 tcp_connected 函数用于检测连接是否被断开,监测的依据是只要有一方可以发送数据,连接就 不算是断开的,即双方还处于连接。当然此处是以通常情况进行推理,不计其他特殊状况,如 一方突然崩溃。 //对 tcp_prot 全局变量的声明。 101 extern struct proto tcp_prot; 102 extern void tcp_err(int err, unsigned char *header, unsigned long daddr, 103 unsigned long saddr, struct inet_protocol *protocol); 104 extern void tcp_shutdown (struct sock *sk, int how); 105 extern int tcp_rcv(struct sk_buff *skb, struct device *dev, 106 struct options *opt, unsigned long daddr, 107 unsigned short len, unsigned long saddr, int redo, 108 struct inet_protocol *protocol); 109 extern int tcp_ioctl(struct sock *sk, int cmd, unsigned long arg); 110 extern int tcp_select_window(struct sock *sk); 111 extern void tcp_send_check(struct tcphdr *th, unsigned long saddr, 112 unsigned long daddr, int len, struct sock *sk); 113 extern void tcp_send_probe0(struct sock *sk); 114 extern void tcp_enqueue_partial(struct sk_buff *, struct sock *); 115 extern struct sk_buff * tcp_dequeue_partial(struct sock *); 116 #endif /* _TCP_H */ 102-115 行是对一些函数的声明。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 2.6 net/inet/udp.c 文件 udp.c 文件是 UDP 协议的实现文件,相比较 TCP 协议而言,这个实现要简单多了。RFC768 是 UDP 协议的正式描述。UDP 协议提供不可靠的数据传送方式,如同 IP 协议一样,其尽 量传输,但不保证远端一定会接收到传输的数据。需要数据可靠到达的应用程序可以使用 TCP 协议,或者自行提供某种可靠性实现。UDP 协议实现所完成的工作较少,简单的说就 是将用户数据从用户缓冲区复制到内核缓冲区并进行封装,之后发往下层 IP 协议进行处理。 udp.c 文件定义了 UDP 协议操作函数集,并类似 TCP 协议,定义了一个 udp_prot 全局变量, 使用 UDP 的套接字在创建时其对应 sock 结构 prot 字段将被初始化为 udp_prot 变量(参考 inet_create 函数实现)。下面我们对 UDP 协议实现文件 udp.c 进行分析。 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * The User Datagram Protocol (UDP). 7 * 8 * Version: @(#)udp.c 1.0.13 06/02/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * 13 * Fixes: 14 * Alan Cox : verify_area() calls 15 * Alan Cox : stopped close while in use off icmp 16 * messages. Not a fix but a botch that 17 * for udp at least is 'valid'. 18 * Alan Cox : Fixed icmp handling properly 19 * Alan Cox : Correct error for oversized datagrams 20 * Alan Cox : Tidied select() semantics. 21 * Alan Cox : udp_err() fixed properly, also now 22 * select and read wake correctly on errors 23 * Alan Cox : udp_send verify_area moved to avoid mem leak 24 * Alan Cox : UDP can count its memory 25 * Alan Cox : send to an unknown connection causes 26 * an ECONNREFUSED off the icmp, but 27 * does NOT close. 28 * Alan Cox : Switched to new sk_buff handlers. No more backlog! 29 * Alan Cox : Using generic datagram code. Even smaller and the PEEK 30 * bug no longer crashes it. 31 * Fred Van Kempen : Net2e support for sk->broadcast. 32 * Alan Cox : Uses skb_free_datagram 33 * Alan Cox : Added get/set sockopt support. 34 * Alan Cox : Broadcasting without option set returns EACCES. 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 35 * Alan Cox : No wakeup calls. Instead we now use the callbacks. 36 * Alan Cox : Use ip_tos and ip_ttl 37 * Alan Cox : SNMP Mibs 38 * Alan Cox : MSG_DONTROUTE, and 0.0.0.0 support. 39 * Matt Dillon : UDP length checks. 40 * Alan Cox : Smarter af_inet used properly. 41 * Alan Cox : Use new kernel side addressing. 42 * Alan Cox : Incorrect return on truncated datagram receive. 43 * 44 * 45 * This program is free software; you can redistribute it and/or 46 * modify it under the terms of the GNU General Public License 47 * as published by the Free Software Foundation; either version 48 * 2 of the License, or (at your option) any later version. 49 */ 50 #include 51 #include 52 #include 53 #include 54 #include 55 #include 56 #include 57 #include 58 #include 59 #include 60 #include 61 #include 62 #include 63 #include 64 #include 65 #include "snmp.h" 66 #include "ip.h" 67 #include "protocol.h" 68 #include "tcp.h" 69 #include 70 #include "sock.h" 71 #include "udp.h" 72 #include "icmp.h" 73 #include "route.h" 74 /* 75 * SNMP MIB for the UDP layer 76 */ 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 77 struct udp_mib udp_statistics; udp_mib 结构类型定义在 net/inet/snmp.h 文件中,主要用于统计 UDP 各种数据,如接收或发 送的数据包个数等。对于 IP,TCP,ICMP 协议都定义有类似这样一个数据结构。目前对于 这个我们并不关心。读者如有兴趣,可阅读 SNMP 协议对应规范 RFC1157。 78 static int udp_deliver(struct sock *sk, struct udphdr *uh, struct sk_buff *skb, struct device *dev, long saddr, long daddr, int len); 79 #define min(a,b) ((a)<(b)?(a):(b)) 78 行是对 udp_deliver 函数的声明,79 行定义了 min 宏,取两个数中最小者。 80 /* 81 * This routine is called by the ICMP module when it gets some 82 * sort of error condition. If err < 0 then the socket should 83 * be closed and the error returned to the user. If err > 0 84 * it's just the icmp type << 8 | icmp code. 85 * Header points to the ip header of the error packet. We move 86 * on past this. Then (as it used to claim before adjustment) 87 * header points to the first 8 bytes of the udp header. We need 88 * to find the appropriate port. 89 */ 90 void udp_err(int err, unsigned char *header, unsigned long daddr, 91 unsigned long saddr, struct inet_protocol *protocol) 92 { udp_err 函数的作用类似于 tcp_err, 被 ICMP 协议实现模块调用,当接收到一个错误信息时, 首先 IP 协议实现模块调用 ICMP 模块进行处理,而 ICMP 模块根据使用的协议调用传输层 模块处理。对于 UDP 协议,被 ICMP 模块调用的函数即为 udp_err。从 ICMP 模块调用原型 中可以看出各参数的意义: err:bit0-bit7,ICMP 首部中 code 值;bit8-bit15,ICMP 首部中 type 值。 header:引 起 该 ICMP 数据包产生的原数据包 IP 首部以及 8 字节的传输层数据(传输层协议 首部)。 daddr:发送该错误通知数据包的远端 IP 地址。 saddr:本地 IP 地址。 protocol:该参数的意义同 tcp_rcv,tcp_err,udp_rcv。 93 struct udphdr *th; 94 struct sock *sk; 95 struct iphdr *ip=(struct iphdr *)header; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 96 header += 4*ip->ihl; header 变量指向回送的 8 字节传输层协议首部数据。有些读者之前没有接触过协议,此处需 要做出一些说明,如果本地发送了一个数据包,远端或者中间路由器发生错误(如远端无对 应进程活动在本地请求端口上,或者路由器没有目的端路由路径),此时远端或者中间路由 器都会回送一个 ICMP 错误通知数据包给本地,该数据包首先传递给网络层 IP 协议模块处 理,IP 协议模块再将该数据包传递给 ICMP 协议模块处理,ICMP 协议模块进而将该数据包 传递给传输层协议模块处理,对于 UDP 协议而言,此处就是由 udp_err 函数进行处理。所 谓处理,无外乎根据具体错误进行套接字状态的更新以及更新状态所需的必要措施。 97 /* 98 * Find the 8 bytes of post IP header ICMP included for us 99 */ 100 th = (struct udphdr *)header; 101 sk = get_sock(&udp_prot, th->source, daddr, th->dest, saddr); 102 if (sk == NULL) 103 return; /* No socket for error */ 将 th 变量设置为指向原数据包中 UDP 首部,根据 UDP 首部中端口号以及双方 IP 地址查找 本地对应 sock 结构。如果本地没有对应的 sock 结构,则无需进一步处理,直接返回。 104 if (err & 0xff00 ==(ICMP_SOURCE_QUENCH << 8)) 105 { /* Slow down! */ 106 if (sk->cong_window > 1) 107 sk->cong_window = sk->cong_window/2; 108 return; 109 } 这是对类型码进行检查,如果类型码为源端节制,就表示远端处理速度跟不上本地发送速度, 故其要求本地节制数据包的发送,本地在接收到这种 ICMP 通知数据包后,需要相应的减缓 本地发送数据包的速度。以上代码通过将拥塞窗口值减半达到的,拥塞窗口表示本地发送出 去但尚未得到远端应答的最大数据包个数,对于 UDP 协议而言,其不存在应答机制,所以 无所谓拥塞窗口。这儿只是形式上的实现,实际上 UDP 协议实现模块在发送数据包并不检 查拥塞窗口大小。正因如此,UDP 协议提供不可靠的数据传输方式。 110 /* 111 * Various people wanted BSD UDP semantics. Well they've come 112 * back out because they slow down response to stuff like dead 113 * or unreachable name servers and they screw term users something 114 * chronic. Oh and it violates RFC1122. So basically fix your 115 * client code people. 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 116 */ 117 #ifdef CONFIG_I_AM_A_BROKEN_BSD_WEENIE 118 /* 119 * It's only fatal if we have connected to them. I'm not happy 120 * with this code. Some BSD comparisons need doing. 121 */ 122 if (icmp_err_convert[err & 0xff].fatal && sk->state == TCP_ESTABLISHED) 123 { 124 sk->err = icmp_err_convert[err & 0xff].errno; 125 sk->error_report(sk); 126 } 127 #else 128 if (icmp_err_convert[err & 0xff].fatal) 129 { 130 sk->err = icmp_err_convert[err & 0xff].errno; 131 sk->error_report(sk); 132 } 133 #endif 134 } 117-134 行代码根据错误的严重性进行处理,设置 sock 结构 err 字段,并通知等待该 sock 结 构的进程,进程此后在操作该 sock 结构时,会被通知到这个错误的(返回 err 字段值)。 sock 结构 error_report 函数指针在 inet_create(af_inet.c)函数中被初始化为 def_callback1,该函 数实现如下: //net/inet/af_inet.c 430 static void def_callback1(struct sock *sk) 431 { 432 if(!sk->dead) 433 wake_up_interruptible(sk->sleep); 434 } udp_check 函数用于计算 UDP 校验值,UDP 校验是可选的。函数返回一个 16bits 值,计算 过程虽然使用内联编程方式,但实现思想较为简单,就是连续相加。有关内联编程,请参考 本书附录 B,其中介绍了有关内联编程的内容。 135 static unsigned short udp_check(struct udphdr *uh, int len, unsigned long saddr, unsigned long daddr) 136 { 137 unsigned long sum; 138 __asm__( "\t addl %%ecx,%%ebx\n" 139 "\t adcl %%edx,%%ebx\n" 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 140 "\t adcl $0, %%ebx\n" 141 : "=b"(sum) 142 : "0"(daddr), "c"(saddr), "d"((ntohs(len) << 16) + IPPROTO_UDP*256) 143 : "cx","bx","dx" ); 144 if (len > 3) 145 { 146 __asm__("\tclc\n" 147 "1:\n" 148 "\t lodsl\n" 149 "\t adcl %%eax, %%ebx\n" 150 "\t loop 1b\n" 151 "\t adcl $0, %%ebx\n" 152 : "=b"(sum) , "=S"(uh) 153 : "0"(sum), "c"(len/4) ,"1"(uh) 154 : "ax", "cx", "bx", "si" ); 155 } 156 /* 157 * Convert from 32 bits to 16 bits. 158 */ 159 __asm__("\t movl %%ebx, %%ecx\n" 160 "\t shrl $16,%%ecx\n" 161 "\t addw %%cx, %%bx\n" 162 "\t adcw $0, %%bx\n" 163 : "=b"(sum) 164 : "0"(sum) 165 : "bx", "cx"); 166 /* 167 * Check for an extra word. 168 */ 169 if ((len & 2) != 0) 170 { 171 __asm__("\t lodsw\n" 172 "\t addw %%ax,%%bx\n" 173 "\t adcw $0, %%bx\n" 174 : "=b"(sum), "=S"(uh) 175 : "0"(sum) ,"1"(uh) 176 : "si", "ax", "bx"); 177 } 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 178 /* 179 * Now check for the extra byte. 180 */ 181 if ((len & 1) != 0) 182 { 183 __asm__("\t lodsb\n" 184 "\t movb $0,%%ah\n" 185 "\t addw %%ax,%%bx\n" 186 "\t adcw $0, %%bx\n" 187 : "=b"(sum) 188 : "0"(sum) ,"S"(uh) 189 : "si", "ax", "bx"); 190 } 191 /* 192 * We only want the bottom 16 bits, but we never cleared the top 16. 193 */ 194 return((~sum) & 0xffff); 195 } 注意 udp_check 函数最后(194 行)对相加计算结果的取反,实际上,采用何种计算方式并 不重要,重要的是大家都保持一致。另外该函数只负责对一系列连续数据进行计算,下面介 绍的 udp_send_check 首先将 UDP 首部中校验值字段清零,然后调用 udp_check 函数计算校 验值,并将该校验值赋值给 UDP 首部中校验值字段。 196 /* 197 * Generate UDP checksums. These may be disabled, eg for fast NFS over ethernet 198 * We default them enabled.. if you turn them off you either know what you are 199 * doing or get burned... 200 */ 201 static void udp_send_check(struct udphdr *uh, unsigned long saddr, 202 unsigned long daddr, int len, struct sock *sk) 203 { 204 uh->check = 0; 205 if (sk && sk->no_check) 206 return; 207 uh->check = udp_check(uh, len, saddr, daddr); 208 /* 209 * FFFF and 0 are the same, pick the right one as 0 in the 210 * actual field means no checksum. 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 211 */ 212 if (uh->check == 0) 213 uh->check = 0xffff; 214 } 函数 212-213 行检查计算后校验值字段是否为 0,如果为 0,则改为全 1。其实这两种情况 从远端的角度来看,没有什么区别。计算校验值得目的就是远端接收到该数据包后通过重新 计算校验值与本地计算的值进行比较,如果相同,就认为数据包传输过程中没有受到破坏; 如果不相同,则简单丢弃。但远端在重新计算校验值时,并不是同本地一样,先将校验值字 段清零,然后才计算,而是连同首部中校验值字段(注意该字段被设置为本地计算的校验值) 一起计算,如果计算结果为 0,则表示数据包正常。这一点可以从数学上计算出来,那么也 可以从数学上进行推导,得出将校验值字段设置为犬 0 和全 1 是同样的效果(注意在计算校 验值时,双方使用的是同一种算法,更确切的说应该是同一个函数:udp_check)。 对于 TCP 协议,在发送数据时,我们使用较多的是 write 函数,因为 TCP 是面向连接的, 在发送数据之前,已经建立了与远端的连接,所以此后每次发送的数据都发送到同一个远端, 当然无需再指定地址。而对于 UDP 协议,在发送数据时,我们却通常使用 sendto,send 函 数,其原因当然需要和 TCP 协议对照起来说明,我们读者应该明白。因为 UDP 没有连接一 说,故每次发送数据时都需要指定数据发送的目的端。当然 UDP 协议也支持 connect 函数, 虽然对应 UDP 协议的 connect 函数并无任何网络数据的传送,但此后的数据发送都无需继 续指定远端地址,换句话说,此后,如果不明确指明远端地址的话,都表示数据是发送到之 前调用 connect 时指定的地址。如下的 udp_send 函数是上层系统调用 send 函数的传输层实 现。由于 UDP 协议不提供数据包可靠性传输保证,其主要完成的工作即从用户缓冲区复制 数据到内核缓冲区,并对数据进行封装后,发往下层网络层模块进行处理。 215 static int udp_send(struct sock *sk, struct sockaddr_in *sin, 216 unsigned char *from, int len, int rt) 217 { 218 struct sk_buff *skb; 219 struct device *dev; 220 struct udphdr *uh; 221 unsigned char *buff; 222 unsigned long saddr; 223 int size, tmp; 224 int ttl; 225 /* 226 * Allocate an sk_buff copy of the packet. 227 */ 228 size = sk->prot->max_header + len; 计算所需要分配的封装数据的缓冲区大小。此时 sock 结构 prot 字段为 udp_prot, 对应 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 max_header 值为 128,这个长度足以容纳各协议首部:MAC 首部 14 字节,IP 首部 60 字节, UDP 首部 8 字节。 229 skb = sock_alloc_send_skb(sk, size, 0, &tmp); sock_alloc_send_skb 函数定义在 net/inet/sock.c 中,用于分配指定大小的 sk_buff 结构用于封 装数据。该函数还对当前套接字状态和错误标志进行检查等。在发送缓冲区暂不满足分配空 间时,该函数会进入睡眠等待。 230 if (skb == NULL) 231 return tmp; 232 skb->sk = NULL; /* to avoid changing sk->saddr */ 233 skb->free = 1; 当前内核版本数据重发队列的创建是在网络层进行的,换句话,虽然 TCP 协议属于传输层, 但其提供的可靠性数据传输中超时重发队列是在网络层进行更新的。使用 TCP 协议的数据 包将其对应 sk_buff 结构中 free 字段设置为 0,表示网络层模块在将该数据包发往下层处理 之前,需要将数据包缓存到重发队列中,以便超时重发从而提供可靠性数据传输。对于 UDP 协议,其不提供可靠性数据传输,所以使用 UDP 协议的所有数据包对应的 sk_buff 结构中 free 字段设置为 1,表示网络层模块进行相应处理后,可直接发往下层,无需缓存到重发队 列中,或者在下层无法接收该数据包时,直接简单丢弃。 234 skb->localroute = sk->localroute|(rt&MSG_DONTROUTE); 234 行代码设置路有查询方式,有关路有相关内容在介绍 route.c 文件中着重说明。此处按下 不表。 235 /* 236 * Now build the IP and MAC header. 237 */ 既然已经成功分配内核缓冲区,现在进行数据复制,不过在这之前,首先创建 MAC 首部和 IP 首部,其实顺序先后重要性不大。 238 buff = skb->data; 239 saddr = sk->saddr; 240 dev = NULL; 241 ttl = sk->ip_ttl; 242 #ifdef CONFIG_IP_MULTICAST 243 if (MULTICAST(sin->sin_addr.s_addr)) 244 ttl = sk->ip_mc_ttl; 如果目的地址是多播,则设置 TTL 值为 1,表示局限于本地网络,不可跨越路有器。 245 #endif 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 246 tmp = sk->prot->build_header(skb, saddr, sin->sin_addr.s_addr, 247 &dev, IPPROTO_UDP, sk->opt, skb->mem_len,sk->ip_tos,ttl); 248 skb->sk=sk; /* So memory is freed correctly */ 249 /* 250 * Unable to put a header on the packet. 251 */ 252 if (tmp < 0 ) 253 { 254 sk->prot->wfree(sk, skb->mem_addr, skb->mem_len); 255 return(tmp); 256 } 如果 ip_build_header(246 行实际调用的函数)返回负值,表示首部创建失败,此时取消本地 发送,返回错误给上层应用(注意对于 send 函数返回负值表示发送失败,否则返回值为实 际发送的字节数,其他发送函数如 write 等类似)。 257 buff += tmp; 258 saddr = skb->saddr; /*dev->pa_addr;*/ 259 skb->len = tmp + sizeof(struct udphdr) + len; /* len + UDP + IP + MAC */ 260 skb->dev = dev; 继续更新数据封装结构中部分字段值。 261 /* 262 * Fill in the UDP header. 263 */ 264 uh = (struct udphdr *) buff; 265 uh->len = htons(len + sizeof(struct udphdr)); 266 uh->source = sk->dummy_th.source; 267 uh->dest = sin->sin_port; 268 buff = (unsigned char *) (uh + 1); 264-267 创建 UDP 首部,主要是双方端口号字段的初始化。 269 /* 270 * Copy the user data. 271 */ 272 memcpy_fromfs(buff, from, len); memcpy_fromfs 函数复制用户缓冲区数据到内核缓冲区中。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 273 /* 274 * Set up the UDP checksum. 275 */ //计算 UDP 校验值。 276 udp_send_check(uh, saddr, sin->sin_addr.s_addr, skb->len - tmp, sk); 277 /* 278 * Send the datagram to the interface. 279 */ 280 udp_statistics.UdpOutDatagrams++; //调用 ip_queue_xmit 函数将数据包发完网络层模块处理。 281 sk->prot->queue_xmit(sk, dev, skb, 1); 282 return(len); 283 } 注意 281 行实际调用的是 ip_queue_xmit 函数,对于 UDP 协议对应操作函数集 udp_prot 各 函数指针指向的实际函数在 udp.c 文件结尾处给出。其中 queue_xmit 指向是 ip_queue_xmit 函数。 284 static int udp_sendto(struct sock *sk, unsigned char *from, int len, int noblock, 285 unsigned flags, struct sockaddr_in *usin, int addr_len) 286 { 287 struct sockaddr_in sin; 288 int tmp; 289 /* 290 * Check the flags. We support no flags for UDP sending 291 */ 292 if (flags&~MSG_DONTROUTE) 293 return(-EINVAL); 294 /* 295 * Get and verify the address. 296 */ 297 if (usin) 298 { 299 if (addr_len < sizeof(sin)) 300 return(-EINVAL); 301 memcpy(&sin,usin,sizeof(sin)); 302 if (sin.sin_family && sin.sin_family != AF_INET) 303 return(-EINVAL); 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 304 if (sin.sin_port == 0) 305 return(-EINVAL); 306 } 307 else 308 { 309 if (sk->state != TCP_ESTABLISHED) 310 return(-EINVAL); 311 sin.sin_family = AF_INET; 312 sin.sin_port = sk->dummy_th.dest; 313 sin.sin_addr.s_addr = sk->daddr; 314 } 315 /* 316 * BSD socket semantics. You must set SO_BROADCAST to permit 317 * broadcasting of data. 318 */ 319 if(sin.sin_addr.s_addr==INADDR_ANY) 320 sin.sin_addr.s_addr=ip_my_addr(); 321 if(!sk->broadcast && ip_chk_addr(sin.sin_addr.s_addr)==IS_BROADCAST) 322 return -EACCES; /* Must turn broadcast on first */ 323 sk->inuse = 1; 324 /* Send the packet. */ 325 tmp = udp_send(sk, &sin, from, len, flags); 326 /* The datagram has been sent off. Release the socket. */ 327 release_sock(sk); 328 return(tmp); 329 } udp_sendto 函数通过调用 udp_send 函数发送数据包,不过该函数在发送数据包之前对远端 地址的合法性进行了检查,这种检查也只是表面,由于不涉及网络数据传送,所以无法验证 这个地址存在性。首先 292 行对标志位进行检查,除了 MSG_DONTROUTE 外,UDP 协议 不支持任何其他标志位,如果设置了其他标志位,则返回错误。此后 297,307 分别对应本 地调用明确指定远端地址和没有指定的情况,对于明确指定的情况,则直接对给出的地址进 行检查,如果没有明确执行,那么检查之前是否调用了 connect 函数进行了地址绑定,如果 进行了绑定,则将远端地址设置为这个绑定的地址,否则出错返回。319 行处理尚未指定本 地地址的情况;321 处理广播的情况,代码都容易理解。最后 325 行调用 udp_send 函数将 用户数据发送出去。 330 /* 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 331 * In BSD SOCK_DGRAM a write is just like a send. 332 */ 333 static int udp_write(struct sock *sk, unsigned char *buff, int len, int noblock, 334 unsigned flags) 335 { 336 return(udp_sendto(sk, buff, len, noblock, flags, NULL, 0)); 337 } udp_write 函数直接对 udp_sendto 函数进行封装,不直接调用 udp_send 函数的目的在于 write 系统调用必须有一个默认远端地址,所以需要 udp_sendto 函数中的地址处理代码。 338 /* 339 * IOCTL requests applicable to the UDP protocol 340 */ 341 int udp_ioctl(struct sock *sk, int cmd, unsigned long arg) 342 { 343 int err; 344 switch(cmd) 345 { 346 case TIOCOUTQ: 347 { 348 unsigned long amount; 349 if (sk->state == TCP_LISTEN) return(-EINVAL); 350 amount = sk->prot->wspace(sk)/*/2*/; 351 err=verify_area(VERIFY_WRITE,(void *)arg, 352 sizeof(unsigned long)); 353 if(err) 354 return(err); 355 put_fs_long(amount,(unsigned long *)arg); 356 return(0); 357 } 358 case TIOCINQ: 359 { 360 struct sk_buff *skb; 361 unsigned long amount; 362 if (sk->state == TCP_LISTEN) return(-EINVAL); 363 amount = 0; 364 skb = skb_peek(&sk->receive_queue); 365 if (skb != NULL) { 366 /* 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 367 * We will only return the amount 368 * of this packet since that is all 369 * that will be read. 370 */ 371 amount = skb->len; 372 } 373 err=verify_area(VERIFY_WRITE,(void *)arg, 374 sizeof(unsigned long)); 375 if(err) 376 return(err); 377 put_fs_long(amount,(unsigned long *)arg); 378 return(0); 379 } 380 default: 381 return(-EINVAL); 382 } 383 return(0); 384 } udp_ioctl 函数支持的选项不多,TIOCOUTQ 选项用于检测当前发送缓冲区中空闲空间大小; TIOCINQ 选项用于查询下一个可读取数据包中数据长度。注意 UDP 协议是面向报文的,不 是面向流的,即对于 UDP 协议而言,其接收的每个数据包被认为是独立的,可以是从不同 的远端发送的。所以在查询可读数据量时,不可如同 TCP 协议一样,对多个序列号连续的 数据包进行汇总。以上代码较为浅显,读者自行分析理解。 385 /* 386 * This should be easy, if there is something there we\ 387 * return it, otherwise we block. 388 */ 389 int udp_recvfrom(struct sock *sk, unsigned char *to, int len, 390 int noblock, unsigned flags, struct sockaddr_in *sin, 391 int *addr_len) 392 { 393 int copied = 0; 394 int truesize; 395 struct sk_buff *skb; 396 int er; 397 /* 398 * Check any passed addresses 399 */ 400 if (addr_len) 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 401 *addr_len=sizeof(*sin); 402 /* 403 * From here the generic datagram does a lot of the work. Come 404 * the finished NET3, it will do _ALL_ the work! 405 */ 406 skb=skb_recv_datagram(sk,flags,noblock,&er); 407 if(skb==NULL) 408 return er; 409 truesize = skb->len; 410 copied = min(len, truesize); 411 /* 412 * FIXME : should use udp header size info value 413 */ 414 skb_copy_datagram(skb,sizeof(struct udphdr),to,copied); 415 sk->stamp=skb->stamp; 416 /* Copy the address. */ 417 if (sin) 418 { 419 sin->sin_family = AF_INET; 420 sin->sin_port = skb->h.uh->source; 421 sin->sin_addr.s_addr = skb->daddr; 422 } 423 skb_free_datagram(skb); 424 release_sock(sk); 425 return(truesize); 426 } udp_recvfrom 函数是 UDP 协议数据读取函数,只有一点需要提请注意,每次读取只可从接 收队列(sock 结构 receive_queue 字段指向的队列)读取单个数据包。414 行 skb_recv_datagram 函数完成的工作就是从 receive_queue 队列中取下一个数据包,如果暂时队列中无数据包的 话,则睡眠等待。skb_recv_datagram 函数定义在 net/inet/datagram.c 中,除了从接收队列中 取数据包外,其还完成一系列检查,如对 sock 结构 err 字段的检查,检查套接字是否发生错 误,对 NON_BLOCK 标志位的检查,从而在当前无数据包可读取时,是否睡眠等待等等。 如果该函数返回 NULL,则其一使用了 NON_BLOCK 选项,且当前无可读数据包;其二在 睡眠等待数据包过程中,被其它条件中断。无论如何,只要返回 NULL,就表示无数据包可 读,此时返回可能的错误状态值,当然如果没有发生错误的话,返回值将是 0,表示没有读 取到数据。如果成功返回可以可读数据包,则对读取的数据长度进行检查,410 行取数据包 可读长度和用户要求长度中的最小值,这会造成两种结果,一是用户要求长度大于实际可读 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 数量,则此时可以读取多少就返回多少,这是我们一般使用 UDP 协议时常见的情况(此时 我们要求的长度实际上是用户缓冲区大小);二是用户要求长度小于实际可读取量(无论是 用户缓冲区分配的太小,还是用户真正只要求了这么多数据),此时多出的那部分数据将被 简单丢弃,无法如同 TCP 协议,在下层读取时返回这些这次没有读完的数据,根本原因还 是 UDP 协议是面向报文的,其接收到的每个数据包都是独立的,可能来自不同的远端。当 然内核实现上,也可以将数据没有被读完的数据包从小回插入接收队列中,从而在应用下次 读取时进行返回。这是实现相关的,就看实现支持不支持这种情况。本版本网络代码不支持 这种分多次读取一个数据包的情况。414 行完成数据从内核缓冲区到用户缓冲区的复制。417 行处理返回发送数据包的远端地址的情况,如果对应参数要求返回远端地址的话。423 行完 成数据包的释放。 427 /* 428 * Read has the same semantics as recv in SOCK_DGRAM 429 */ 430 int udp_read(struct sock *sk, unsigned char *buff, int len, int noblock, 431 unsigned flags) 432 { 433 return(udp_recvfrom(sk, buff, len, noblock, flags, NULL, NULL)); 434 } udp_read 函数是对 udp_recvfrom 函数的直接封装,只不过其不要求返回远端地址,因为是 用 read,write 函数调用 UDP 套接字时,在这之前一定通过 connect 系统调用对远端地址进 行了设置。 下面我们即看 UDP 协议 connect 函数究竟完成了哪些工作。 435 int udp_connect(struct sock *sk, struct sockaddr_in *usin, int addr_len) 436 { 437 struct rtable *rt; 438 unsigned long sa; 439 if (addr_len < sizeof(*usin)) 440 return(-EINVAL); 441 if (usin->sin_family && usin->sin_family != AF_INET) 442 return(-EAFNOSUPPORT); 443 if (usin->sin_addr.s_addr==INADDR_ANY) 444 usin->sin_addr.s_addr=ip_my_addr(); 445 if(!sk->broadcast && ip_chk_addr(usin->sin_addr.s_addr)==IS_BROADCAST) 446 return -EACCES; /* Must turn broadcast on first */ 447 rt=ip_rt_route(usin->sin_addr.s_addr, NULL, &sa); 448 if(rt==NULL) 449 return -ENETUNREACH; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 450 sk->saddr = sa; /* Update source address */ 451 sk->daddr = usin->sin_addr.s_addr; 452 sk->dummy_th.dest = usin->sin_port; 453 sk->state = TCP_ESTABLISHED; 454 return(0); 455 } udp_connect 函数完成任务如下: 1> 进行远端合法性检查,包括地址长度及地址类型(AF_INET),以及地址可达性,即本地 路由中是否包含到达该远端地址的路由项。另外对于远端地址设置为广播地址的情况也 进行了检查(445 行),如果本地套接字不支持广播,则返回错误:EACCESS,无法访 问。 2> 在通过地址合法性检查后,设置 sock 结构中对应远端地址各字段。 3> 最后设置本地套接字状态为 TCP_ESTABLISHED,这是 UDP 协议模拟 TCP 协议进行的状 态设置,实际上 UDP 协议在完成连接建立的过程中实际没有发送一个数据包到网络介 质上。 在调用 connect 函数后,此后应用程序就可以使用 read,write 函数如同 TCP 协议一样进行 数据读取和发送。从而避免了每次函数调用时都要传递远端地址。 456 static void udp_close(struct sock *sk, int timeout) 457 { 458 sk->inuse = 1; 459 sk->state = TCP_CLOSE; 460 if (sk->dead) 461 destroy_sock(sk); 462 else 463 release_sock(sk); 464 } 如同 udp_connect 函数一样,upd_close 也不涉及实际数据包的传送。主要还是状态的设置以 及本地已接收数据包的处理。如果 sock 结构 dead 字段已经设置为 1,则可以立刻进行套接 字相关结构释放,否则继续处理已接收的数据包。 465 /* 466 * All we need to do is get the socket, and then do a checksum. 467 */ 468 int udp_rcv(struct sk_buff *skb, struct device *dev, struct options *opt, 469 unsigned long daddr, unsigned short len, 470 unsigned long saddr, int redo, struct inet_protocol *protocol) 471 { 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 udp_rcv 函数作用如同 tcp_rcv,它是 UDP 协议数据包处理总入口函数,不过他的处理方式 没有 tcp_rcv 那么麻烦,要分那么多种情况。根本原因在于 UDP 协议是一个无状态协议。没 有状态的转换之类的控制,所以对于下层传递上来的数据包的处理方式较为直接:直接挂接 到对应 sock 结构的 receive_queue 指向的接收队列中。不过从函数实现来看,代码还是不短, 原因在于要判断该不该接收这个数据包还是一件麻烦事!有关 udp_rcv 函数调用中各函数含 义同 tcp_rcv 函数。 472 struct sock *sk; 473 struct udphdr *uh; 474 unsigned short ulen; 475 int addr_type = IS_MYADDR; 476 if(!dev || dev->pa_addr!=daddr) 477 addr_type=ip_chk_addr(daddr); 首先我们要检查一下这个数据包是不是发给本地的。dev 变量表示接收该数据包的网口设 备。虽然 dev 变量为 NULL 不大可能发生,但有可能 daddr!=dev->pa_addr, 因为或者这是 一个多播或者广播数据包。ip_chk_addr 函数的目的就是对此进行检查。dev->pa_addr 表示 网口被赋予的 IP 地址,另外 dev->ha_addr 表示网口的硬件地址。 478 /* 479 * Get the header. 480 */ 481 uh = (struct udphdr *) skb->h.uh; 482 ip_statistics.IpInDelivers++; 483 /* 484 * Validate the packet and the UDP length. 485 */ 486 ulen = ntohs(uh->len); 487 if (ulen > len || len < sizeof(*uh) || ulen < sizeof(*uh)) 488 { 489 printk("UDP: short packet: %d/%d\n", ulen, len); 490 udp_statistics.UdpInErrors++; 491 kfree_skb(skb, FREE_WRITE); 492 return(0); 493 } 486 行 ulen 变量被初始化为 UDP 首部长度字段值,该字段表示的长度包括 UDP 首部及其用 户数据负载的长度。这个值应该小于传入的 len 参数值,参数 len 在网络层 IP 模块调用该 udp_rcv 函数被初始化为 IP 负载的长度:即 UDP 首部及其用户数据负载的长度再加上填充 数据长度。因为当前使用最多的是以太网,网络上的每台主机竞争使用网络介质,为了让网 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 卡设备有时间探测到数据发送冲突(具体请查阅对 CSMA/CD 协议的介绍),对于发送的数 据包长度有个最小限制,如果不计最后 4 字节的冗余校验的话,这个值就是 60 字节。或者 我们通常所说的 46 字节(60 字节减去 14 字节 MAC 首部长度)所以应该有:ulen<=len。 487 行其它判断容易理解,如果数据包被检测出不合法,则在更新相关统计信息后丢弃。 494 if (uh->check && udp_check(uh, len, saddr, daddr)) 495 { 496 /* wants to know, who sent it, to 497 go and stomp on the garbage sender... */ 498 printk("UDP: bad checksum. From %08lX:%d to %08lX:%d ulen %d\n", 499 ntohl(saddr),ntohs(uh->source), 500 ntohl(daddr),ntohs(uh->dest), 501 ulen); 502 udp_statistics.UdpInErrors++; 503 kfree_skb(skb, FREE_WRITE); 504 return(0); 505 } 506 len=ulen; 494-505 行进行数据包校验值检查。506 行将 len 参数值赋值为实际数据长度。 507 #ifdef CONFIG_IP_MULTICAST 508 if (addr_type!=IS_MYADDR) 509 { 510 /* 511 * Multicasts and broadcasts go to each listener. 512 */ 513 struct sock *sknext=NULL; 514 sk=get_sock_mcast(udp_prot.sock_array[ntohs(uh->dest)&(SOCK_ARRAY_SIZE-1)], uh->dest, 515 saddr, uh->source, daddr); 516 if(sk) 517 { 518 do 519 { 520 struct sk_buff *skb1; 521 sknext=get_sock_mcast(sk->next, uh->dest, saddr, uh->source, daddr); 522 if(sknext) 523 skb1=skb_clone(skb,GFP_ATOMIC); 524 else 525 skb1=skb; 526 if(skb1) 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 527 udp_deliver(sk, uh, skb1, dev,saddr,daddr,len); 528 sk=sknext; 529 } 530 while(sknext!=NULL); 531 } 532 else 533 kfree_skb(skb, FREE_READ); 534 return 0; 535 } 536 #endif 507-536 行代码是对多播情况的处理。首先 508 行判断出该数据包目的地址不是本地地址, 排除了单播的情况。此后 514 行调用 get_sock_mcast 函数返回可能的侦听在对应端口上的本 地多播套接字队列,此后对队列进行遍历,对满足条件的所有套接字都复制一份该数据包。 get_sock_mcast 函数定义在 af_inet.c 中,本书前文中已经对该函数进行了分析,此处再次列 出,便于查看: //net/inet/af_inet.c 1275 #ifdef CONFIG_IP_MULTICAST 1276 /* 1277 * Deliver a datagram to broadcast/multicast sockets. 1278 */ 1279 struct sock *get_sock_mcast(struct sock *sk, 1280 unsigned short num, 1281 unsigned long raddr, 1282 unsigned short rnum, unsigned long laddr) 1283 { 1284 struct sock *s; 1285 unsigned short hnum; 1286 hnum = ntohs(num); 1287 /* 1288 * SOCK_ARRAY_SIZE must be a power of two. This will work better 1289 * than a prime unless 3 or more sockets end up using the same 1290 * array entry. This should not be a problem because most 1291 * well known sockets don't overlap that much, and for 1292 * the other ones, we can just be careful about picking our 1293 * socket number when we choose an arbitrary one. 1294 */ 1295 s=sk; 1296 for(; s != NULL; s = s->next) 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 1297 { 1298 if (s->num != hnum) 1299 continue; 1300 if(s->dead && (s->state == TCP_CLOSE)) 1301 continue; 1302 if(s->daddr && s->daddr!=raddr) 1303 continue; 1304 if (s->dummy_th.dest != rnum && s->dummy_th.dest != 0) 1305 continue; 1306 if(s->saddr && s->saddr!=laddr) 1307 continue; 1308 return(s); 1309 } 1310 return(NULL); 1311 } 1312 #endif 注意 get_sock_mcast 函数第一个参数是一个 sock 结构,即调用该函数时必须已经找到了多 播套接字 sock 结构队列,对于 udp_rcv 函数,这是在 514 行完成的。在 508 行判断出这不 是一个发往本地单播地址的数据包后,直接以本地远端口(uh->dest)作为索引提出系统数 组中 sock 结构队列,至于队列中存不存在多播套接字,则通过数据报的目的地址查询即可。 一旦 get_sock_mcast 返回一个 sock 结构,则表示本地存在有相关的多播套接字,那么就对 该数据包进行接收。注意对于广播的处理包含在对多播的处理中。数据包的具体接收是通过 udp_deliver 函数完成的。在分析完 udp_rcv 函数后,我们再看 udp_deliver 函数。 537 sk = get_sock(&udp_prot, uh->dest, saddr, uh->source, daddr); 538 if (sk == NULL) 539 { 540 udp_statistics.UdpNoPorts++; 541 if (addr_type == IS_MYADDR) 542 { 543 icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0, dev); 544 } 545 /* 546 * Hmm. We got an UDP broadcast to a port to which we 547 * don't wanna listen. Ignore it. 548 */ 549 skb->sk = NULL; 550 kfree_skb(skb, FREE_WRITE); 551 return(0); 552 } 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 处理完可能的多播的情况,现在就要进行单播处理,如果 537 没有找到一个本地对应的套接 字,则表示这个数据包的目的进程不存在或者已经死亡,此时本地将回复一个端口不可达的 ICMP 错误报文(这就是 543 行完成的工作),更新相关信息,丢弃该数据包。 553 return udp_deliver(sk,uh,skb,dev, saddr, daddr, len); 554 } 如果一切正常,那么就调用 udp_deliver 函数进行接收。该函数定义如下: 555 static int udp_deliver(struct sock *sk, struct udphdr *uh, struct sk_buff *skb, struct device *dev, long saddr, long daddr, int len) 556 { 557 skb->sk = sk; 558 skb->dev = dev; 559 skb->len = len; 560 /* 561 * These are supposed to be switched. 562 */ 563 skb->daddr = saddr; 564 skb->saddr = daddr; 565 /* 566 * Charge it to the socket, dropping if the queue is full. 567 */ 568 skb->len = len - sizeof(*uh); 569 if (sock_queue_rcv_skb(sk,skb)<0) 570 { 571 udp_statistics.UdpInErrors++; 572 ip_statistics.IpInDiscards++; 573 ip_statistics.IpInDelivers--; 574 skb->sk = NULL; 575 kfree_skb(skb, FREE_WRITE); 576 release_sock(sk); 577 return(0); 578 } 579 udp_statistics.UdpInDatagrams++; 580 release_sock(sk); 581 return(0); 582 } 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 Udp_deliver 函数首先对数据包对应的 sk_buff 封装结构中一些字段进行初始化,之后调用 sock_queue_rcv_skb 函数将数据包挂接到 sk 变量表示的套接字的接收队列中,从而完成套接 字的接收。sock_queue_rcv_skb 函数定义在 net/inet/sock.c 中,如下: //net/inet/sock.c 446 /* 447 * Queue a received datagram if it will fit. Stream and sequenced protocols 448 * can't normally use this as they need to fit buffers in and play with them. 449 */ 450 int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) 451 { 452 unsigned long flags; 453 if(sk->rmem_alloc + skb->mem_len >= sk->rcvbuf) 454 return -ENOMEM; 455 save_flags(flags); 456 cli(); 457 sk->rmem_alloc+=skb->mem_len; 458 skb->sk=sk; 459 restore_flags(flags); 460 skb_queue_tail(&sk->receive_queue,skb); 461 if(!sk->dead) 462 sk->data_ready(sk,skb->len); 463 return 0; 464 } sock_queue_rcv_skb 函数对接收缓冲区进行更新,将数据包插入接收队列尾部,最后通知可 能等待读取数据被置于睡眠的进程进行数据包的读取。 如同 tcp.c 文件最后对 tcp_prot 变量的定义,udp.c 最后也是对 UDP 协议操作函数集和的这 样一个定义:udp_prot。 583 struct proto udp_prot = { 584 sock_wmalloc, 585 sock_rmalloc, 586 sock_wfree, 587 sock_rfree, 588 sock_rspace, 589 sock_wspace, 590 udp_close, 591 udp_read, 592 udp_write, 593 udp_sendto, 594 udp_recvfrom, 595 ip_build_header, 596 udp_connect, 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 597 NULL, 598 ip_queue_xmit, 599 NULL, 600 NULL, 601 NULL, 602 udp_rcv, 603 datagram_select, 604 udp_ioctl, 605 NULL, 606 NULL, 607 ip_setsockopt, 608 ip_getsockopt, 609 128, 610 0, 611 {NULL,}, 612 "UDP", 613 0, 0 614 }; 2.7 net/inet/udp.h 头文件 该头文件主要是对一些函数原型的声明,故简单列出如下。 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * Definitions for the UDP module. 7 * 8 * Version: @(#)udp.h 1.0.2 05/07/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * 13 * Fixes: 14 * Alan Cox : Turned on udp checksums. I don't want to 15 * chase 'memory corruption' bugs that aren't! 16 * 17 * This program is free software; you can redistribute it and/or 18 * modify it under the terms of the GNU General Public License 19 * as published by the Free Software Foundation; either version 20 * 2 of the License, or (at your option) any later version. 21 */ 22 #ifndef _UDP_H 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 23 #define _UDP_H 24 #include 25 #define UDP_NO_CHECK 0 26 extern struct proto udp_prot; 27 extern void udp_err(int err, unsigned char *header, unsigned long daddr, 28 unsigned long saddr, struct inet_protocol *protocol); 29 extern int udp_recvfrom(struct sock *sk, unsigned char *to, 30 int len, int noblock, unsigned flags, 31 struct sockaddr_in *sin, int *addr_len); 32 extern int udp_read(struct sock *sk, unsigned char *buff, 33 int len, int noblock, unsigned flags); 34 extern int udp_connect(struct sock *sk, 35 struct sockaddr_in *usin, int addr_len); 36 extern int udp_rcv(struct sk_buff *skb, struct device *dev, 37 struct options *opt, unsigned long daddr, 38 unsigned short len, unsigned long saddr, int redo, 39 struct inet_protocol *protocol); 40 extern int udp_ioctl(struct sock *sk, int cmd, unsigned long arg); 41 #endif /* _UDP_H */ 在介绍完传输层两个最主要的协议之后,我们对这一层两个至关重要的数据结构尚未介绍: sock 结构(该结构实际上在本书的前面在介绍 af_inet.c 文件时已经作出了分析),proto 结构。 另外还有 TCP,UDP 用来操作发送和接收缓冲区的功能函数如 sock_rspace, sock_rmalloc, sock_wmalloc 等都为介绍,这些函数或者变量定义在两个文件中:sock.c, sock.h。下面我们 就对这两个文件进行分析。首先从 sock.h 头文件入手先对 sock 结构和 proto 结构进行分析。 2.8 net/inet/sock.h 头文件 对该文件的分析将主要集中在对 sock 结构和 proto 结构的分析上。在本书前文分析 af_inet.c 文件时,已经对 sock 结构中各字段的含义进行了较为详细的介绍,另外 proto 结构定义为操 作函数集,其主要由函数指针组成,前文中 tcp_prot, udp_prot 全局变量都是 proto 结构类型, 分别定义了 TCP 协议和 UDP 协议的操作函数集。此处为保证本书的完整性,系统的给出这 两个结构的定义,但不再进行重复说明。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * Definitions for the AF_INET socket handler. 7 * 8 * Version: @(#)sock.h 1.0.4 05/13/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * Corey Minyard 13 * Florian La Roche 14 * 15 * Fixes: 16 * Alan Cox : Volatiles in skbuff pointers. See 17 * skbuff comments. May be overdone, 18 * better to prove they can be removed 19 * than the reverse. 20 * Alan Cox : Added a zapped field for tcp to note 21 * a socket is reset and must stay shut up 22 * Alan Cox : New fields for options 23 * Pauline Middelink : identd support 24 * 25 * This program is free software; you can redistribute it and/or 26 * modify it under the terms of the GNU General Public License 27 * as published by the Free Software Foundation; either version 28 * 2 of the License, or (at your option) any later version. 29 */ 30 #ifndef _SOCK_H 31 #define _SOCK_H 32 #include 33 #include /* struct options */ 34 #include /* struct tcphdr */ 35 #include 36 #include /* struct sk_buff */ 37 #include "protocol.h" /* struct inet_protocol */ 38 #ifdef CONFIG_AX25 39 #include "ax25.h" 40 #endif 41 #ifdef CONFIG_IPX 42 #include "ipx.h" 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 43 #endif 44 #ifdef CONFIG_ATALK 45 #include 46 #endif 47 #include 48 #define SOCK_ARRAY_SIZE 256 /* Think big (also on some systems a byte is faster */ 49 /* 50 * This structure really needs to be cleaned up. 51 * Most of it is for TCP, and not used by any of 52 * the other protocols. 53 */ 54 struct sock { 55 struct options *opt; 56 volatile unsigned long wmem_alloc; 57 volatile unsigned long rmem_alloc; 58 unsigned long write_seq; 59 unsigned long sent_seq; 60 unsigned long acked_seq; 61 unsigned long copied_seq; 62 unsigned long rcv_ack_seq; 63 unsigned long window_seq; 64 unsigned long fin_seq; 65 unsigned long urg_seq; 66 unsigned long urg_data; 67 /* 68 * Not all are volatile, but some are, so we 69 * might as well say they all are. 70 */ 71 volatile char inuse, 72 dead, 73 urginline, 74 intr, 75 blog, 76 done, 77 reuse, 78 keepopen, 79 linger, 80 delay_acks, 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 81 destroy, 82 ack_timed, 83 no_check, 84 zapped, /* In ax25 & ipx means not linked */ 85 broadcast, 86 nonagle; 87 unsigned long lingertime; 88 int proc; 89 struct sock *next; 90 struct sock *prev; /* Doubly linked chain.. */ 91 struct sock *pair; 92 struct sk_buff * volatile send_head; 93 struct sk_buff * volatile send_tail; 94 struct sk_buff_head back_log; 95 struct sk_buff *partial; 96 struct timer_list partial_timer; 97 long retransmits; 98 struct sk_buff_head write_queue, 99 receive_queue; 100 struct proto *prot; 101 struct wait_queue **sleep; 102 unsigned long daddr; 103 unsigned long saddr; 104 unsigned short max_unacked; 105 unsigned short window; 106 unsigned short bytes_rcv; 107 /* mss is min(mtu, max_window) */ 108 unsigned short mtu; /* mss negotiated in the syn's */ 109 volatile unsigned short mss; /* current eff. mss - can change */ 110 volatile unsigned short user_mss; /* mss requested by user in ioctl */ 111 volatile unsigned short max_window; 112 unsigned long window_clamp; 113 unsigned short num; 114 volatile unsigned short cong_window; 115 volatile unsigned short cong_count; 116 volatile unsigned short ssthresh; 117 volatile unsigned short packets_out; 118 volatile unsigned short shutdown; 119 volatile unsigned long rtt; 120 volatile unsigned long mdev; 121 volatile unsigned long rto; 122 /* currently backoff isn't used, but I'm maintaining it in case 123 * we want to go back to a backoff formula that needs it 124 */ 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 125 volatile unsigned short backoff; 126 volatile short err; 127 unsigned char protocol; 128 volatile unsigned char state; 129 volatile unsigned char ack_backlog; 130 unsigned char max_ack_backlog; 131 unsigned char priority; 132 unsigned char debug; 133 unsigned short rcvbuf; 134 unsigned short sndbuf; 135 unsigned short type; 136 unsigned char localroute; /* Route locally only */ 137 #ifdef CONFIG_IPX 138 ipx_address ipx_dest_addr; 139 ipx_interface *ipx_intrfc; 140 unsigned short ipx_port; 141 unsigned short ipx_type; 142 #endif 143 #ifdef CONFIG_AX25 144 /* Really we want to add a per protocol private area */ 145 ax25_address ax25_source_addr,ax25_dest_addr; 146 struct sk_buff *volatile ax25_retxq[8]; 147 char ax25_state,ax25_vs,ax25_vr,ax25_lastrxnr,ax25_lasttxnr; 148 char ax25_condition; 149 char ax25_retxcnt; 150 char ax25_xx; 151 char ax25_retxqi; 152 char ax25_rrtimer; 153 char ax25_timer; 154 unsigned char ax25_n2; 155 unsigned short ax25_t1,ax25_t2,ax25_t3; 156 ax25_digi *ax25_digipeat; 157 #endif 158 #ifdef CONFIG_ATALK 159 struct atalk_sock at; 160 #endif 161 /* IP 'private area' or will be eventually */ 162 int ip_ttl; /* TTL setting */ 163 int ip_tos; /* TOS */ 164 struct tcphdr dummy_th; 165 struct timer_list keepalive_timer; /* TCP keepalive hack */ 166 struct timer_list retransmit_timer; /* TCP retransmit timer */ 167 struct timer_list ack_timer; /* TCP delayed ack timer */ 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 168 int ip_xmit_timeout; /* Why the timeout is running */ 169 #ifdef CONFIG_IP_MULTICAST 170 int ip_mc_ttl; /* Multicasting TTL */ 171 int ip_mc_loop; /* Loopback (not implemented yet) */ 172 char ip_mc_name[MAX_ADDR_LEN]; /* Multicast device name */ 173 struct ip_mc_socklist *ip_mc_list; /* Group array */ 174 #endif 175 /* This part is used for the timeout functions (timer.c). */ 176 int timeout; /* What are we waiting for? */ 177 struct timer_list timer; /* This is the TIME_WAIT/receive timer when we are doing IP */ 178 struct timeval stamp; 179 /* identd */ 180 struct socket *socket; 181 /* Callbacks */ 182 void (*state_change)(struct sock *sk); 183 void (*data_ready)(struct sock *sk,int bytes); 184 void (*write_space)(struct sock *sk); 185 void (*error_report)(struct sock *sk); 186 }; 187 struct proto { 188 struct sk_buff * (*wmalloc)(struct sock *sk, 189 unsigned long size, int force, 190 int priority); 191 struct sk_buff * (*rmalloc)(struct sock *sk, 192 unsigned long size, int force, 193 int priority); 194 void (*wfree)(struct sock *sk, struct sk_buff *skb, 195 unsigned long size); 196 void (*rfree)(struct sock *sk, struct sk_buff *skb, 197 unsigned long size); 198 unsigned long (*rspace)(struct sock *sk); 199 unsigned long (*wspace)(struct sock *sk); 200 void (*close)(struct sock *sk, int timeout); 201 int (*read)(struct sock *sk, unsigned char *to, 202 int len, int nonblock, unsigned flags); 203 int (*write)(struct sock *sk, unsigned char *to, 204 int len, int nonblock, unsigned flags); 205 int (*sendto)(struct sock *sk, 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 206 unsigned char *from, int len, int noblock, 207 unsigned flags, struct sockaddr_in *usin, 208 int addr_len); 209 int (*recvfrom)(struct sock *sk, 210 unsigned char *from, int len, int noblock, 211 unsigned flags, struct sockaddr_in *usin, 212 int *addr_len); 213 int (*build_header)(struct sk_buff *skb, 214 unsigned long saddr, 215 unsigned long daddr, 216 struct device **dev, int type, 217 struct options *opt, int len, int tos, int ttl); 218 int (*connect)(struct sock *sk, 219 struct sockaddr_in *usin, int addr_len); 220 struct sock * (*accept) (struct sock *sk, int flags); 221 void (*queue_xmit)(struct sock *sk, 222 struct device *dev, struct sk_buff *skb, 223 int free); 224 void (*retransmit)(struct sock *sk, int all); 225 void (*write_wakeup)(struct sock *sk); 226 void (*read_wakeup)(struct sock *sk); 227 int (*rcv)(struct sk_buff *buff, struct device *dev, 228 struct options *opt, unsigned long daddr, 229 unsigned short len, unsigned long saddr, 230 int redo, struct inet_protocol *protocol); 231 int (*select)(struct sock *sk, int which, 232 select_table *wait); 233 int (*ioctl)(struct sock *sk, int cmd, 234 unsigned long arg); 235 int (*init)(struct sock *sk); 236 void (*shutdown)(struct sock *sk, int how); 237 int (*setsockopt)(struct sock *sk, int level, int optname, 238 char *optval, int optlen); 239 int (*getsockopt)(struct sock *sk, int level, int optname, 240 char *optval, int *option); 241 unsigned short max_header; 242 unsigned long retransmits; 243 struct sock * sock_array[SOCK_ARRAY_SIZE]; 244 char name[80]; 245 int inuse, highestinuse; 246 }; 注意 243 行 proto 结构中 sock_array 字段,SOCK_ARRAY_SIZE 常量在本文件前面被定义为 256。sock_array 字段表示一个 sock 结构数组,每个数组元素表示一个 sock 结构队列。我们 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 在前面代码分析中使用 get_sock, put_sock 函数对 sock 函数进行查找或者将其挂入到系统队 列中时,操作的对象就此处 sock_array 表示的数组中各元素指向的 sock 结构队列。要解释 清楚这个问题,我们以 TCP 协议为例(UDP 协议类同)。在分析 tcp.c 文件时,在文件尾部 定义了一个 tcp_prot 全局变量,这是一个系统全局变量,工作该版本网络代码之上的所有使 用 TCP 协议进行网络数据传输的进程所使用的套接字(抱歉,有点长),都使用这个 tcp_prot 变量,换句话说,本地所有使用 TCP 协议的套接字所对应的 sock 结构都挂接在 tcp_prot 这 个 proto 结构类型的 sock_array 数组中。UDP 协议也一样,本机上所有使用 UDP 协议的套 接字所对应的 sock 结构都挂接在 udp_prot 这个 proto 结构类型的 sock_array 数组中。系统在 创建一个新的套接字时,都会对应创建一个 sock 结构,这个 sock 结构 prot 字段的初始化: 如果使用 TCP 协议,则被初始化为 tcp_prot;如果使用 UDP 协议,则被初始化为 udp_prot。 同时在 sock 结构初始化完成后,其本身被插入 tcp_prot 中 sock_array 数组元素的相应队列中, 数组元素的索引即为本地端口号对数组长度取余(本地端口号%数组长度-1)。在接收到一 个新的数据包时,我们调用 get_sock 函数或者本地对应的 sock 结构,如下再次给出 get_sock 函数代码进行分析: //net/inet/af_inet.c 1197 struct sock *get_sock(struct proto *prot, unsigned short num, 1198 unsigned long raddr, 1199 unsigned short rnum, unsigned long laddr) 1200 { 我们再给出对应 TCP 协议和 UDP 协议调用该函数时的参数设置情况。 //net/inet/udp.c—udp_rcv 537 sk = get_sock(&udp_prot, uh->dest, saddr, uh->source, daddr); uh 表示所接收数据包的 UDP 首部。 //net/inet/tcp.c –-tcp_rcv 3925 sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); th 表示所接收数据包的 TCP 首部。 那么意义就很明显了,get_sock 函数中 num 表示的是本地端口号,再看 get_sock 函数中 1205, 1214 行代码,首先将网络字节序(Big-endian)转换为主机字节序,之后以本地端口号作为 索引对 udp_prot 或者 tcp_prot 中 sock_array 数组进行寻址,得到对应元素指向的那个 sock 结构队列。进而以其他参数进行合适的 sock 结构查找。 1201 struct sock *s; 1202 struct sock *result = NULL; 1203 int badness = -1; 1204 unsigned short hnum; 1205 hnum = ntohs(num); 1206 /* 1207 * SOCK_ARRAY_SIZE must be a power of two. This will work better 1208 * than a prime unless 3 or more sockets end up using the same 1209 * array entry. This should not be a problem because most 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 1210 * well known sockets don't overlap that much, and for 1211 * the other ones, we can just be careful about picking our 1212 * socket number when we choose an arbitrary one. 1213 */ 1214 for(s = prot->sock_array[hnum & (SOCK_ARRAY_SIZE - 1)]; 1215 s != NULL; s = s->next) 1216 { 1217 int score = 0; 1218 if (s->num != hnum) 1219 continue; 1220 if(s->dead && (s->state == TCP_CLOSE)) 1221 continue; 1222 /* local address matches? */ 1223 if (s->saddr) { 1224 if (s->saddr != laddr) 1225 continue; 1226 score++; 1227 } 1228 /* remote address matches? */ 1229 if (s->daddr) { 1230 if (s->daddr != raddr) 1231 continue; 1232 score++; 1233 } 1234 /* remote port matches? */ 1235 if (s->dummy_th.dest) { 1236 if (s->dummy_th.dest != rnum) 1237 continue; 1238 score++; 1239 } 1240 /* perfect match? */ 1241 if (score == 3) 1242 return s; 1243 /* no, check if this is the best so far.. */ 1244 if (score <= badness) 1245 continue; 1246 result = s; 1247 badness = score; 1248 } 1249 return result; 1250 } 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 此处所要表达的意义是:tcp_prot 对于 TCP 协议是唯一的,所有使用 TCP 协议进行跨主机 间通信的进程(或者更近一步:套接字)都要使用这个变量,而且套接字对应的 sock 结构 都被插入到 tcp_prot 所表示 proto 结构中 sock_array 数组对应元素指向的 sock 结构队列中(这 可真拗口)。 至于是 sock_array 数组中哪个元素,由本地端口进行索引。udp_prot 变量之于 UDP 协议的 作用类同。 247 #define TIME_WRITE 1 //超时重传 248 #define TIME_CLOSE 2 //2MSL 定时 249 #define TIME_KEEPOPEN 3 //保活 250 #define TIME_DESTROY 4 //套接字释放 251 #define TIME_DONE 5 /* used to absorb those last few packets */ 252 #define TIME_PROBE0 6 //非 0 窗口探测 以上这些变量定义了 retransmit_timer 定时器当前定时的目的。TCP 协议规范上定义了 4 个 系统定时器:超时重发定时器,窗口探测定时器,保活(Keepopen or Keepalive)定时器, 2MSL 定时器。本版本 sock 结构中虽然对应有这四个定时器字段,但实际上只使用了超时 重传定时器,2MSL 定时器。而窗口探测,保活都是使用超时重传定时器,由于多个功能集 中于使用同一个定时器,所以 sock 结构中 ip_xmit_timeout 字段专门用于指定超时重传定时 器当前的定时目的。 253 #define SOCK_DESTROY_TIME 1000 /* about 10 seconds */ 254 #define PROT_SOCK 1024/* Sockets 0-1023 can't be bound too unless you are superuser */ PROT_SOCK 常量定义了自动分配本地通信端口号的起始值或者说最小开始值。 255 #define SHUTDOWN_MASK 3 //完全关闭 256 #define RCV_SHUTDOWN 1 //接收通道被关闭(远端发送了 FIN 数据包) 257 #define SEND_SHUTDOWN 2 //发送通道关闭(本地主动发送 FIN 数据包) 以下是对一些函数的声明。 258 extern void destroy_sock(struct sock *sk); 259 extern unsigned short get_new_socknum(struct proto *, unsigned short); 260 extern void put_sock(unsigned short, struct sock *); 261 extern void release_sock(struct sock *sk); 262 extern struct sock *get_sock(struct proto *, unsigned short, 263 unsigned long, unsigned short, 264 unsigned long); 265 extern struct sock *get_sock_mcast(struct sock *, unsigned short, 266 unsigned long, unsigned short, 267 unsigned long); 268 extern struct sock *get_sock_raw(struct sock *, unsigned short, 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 269 unsigned long, unsigned long); 270 extern struct sk_buff *sock_wmalloc(struct sock *sk, 271 unsigned long size, int force, 272 int priority); 273 extern struct sk_buff *sock_rmalloc(struct sock *sk, 274 unsigned long size, int force, 275 int priority); 276 extern void sock_wfree(struct sock *sk, struct sk_buff *skb, 277 unsigned long size); 278 extern void sock_rfree(struct sock *sk, struct sk_buff *skb, 279 unsigned long size); 280 extern unsigned long sock_rspace(struct sock *sk); 281 extern unsigned long sock_wspace(struct sock *sk); 282 extern int sock_setsockopt(struct sock *sk,int level,int op,char *optval,int optlen); 283 extern int sock_getsockopt(struct sock *sk,int level,int op,char *optval,int *optlen); 284 extern struct sk_buff *sock_alloc_send_skb(struct sock *skb, unsigned long size, int noblock, int *errcode); 285 extern int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb); 286 /* declarations from timer.c */ 287 extern struct sock *timer_base; 288 void delete_timer (struct sock *); 289 void reset_timer (struct sock *, int, unsigned long); 290 void net_timer (unsigned long); 291 #endif /* _SOCK_H */ 对本文件的理解着重于对 sock 结构和 proto 结构中 sock_array 数组,以及两个全局变量 tcp_prot, udp_prot 和其他套接字 sock 结构之间关系的理解。分清他们之间的关系,将对网络 实现代码进入了更深层次的理解。 2.9 net/inet/sock.c 文件 sock.c 文件中定义函数完成的功能独立于传输层协议,但为传输层协议工作。如内存分配函 数,接收或发送缓冲区大小设置等等。所谓独立于传输层协议是指其将传输层协议同样的地 方抽出来,单独实现,避免各自有一套相同的代码,减少了代码的冗余。从这个大的方向把 握,有助于我们对 sock.c 文件的理解。 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 3 * operating system. INET is implemented using the BSD Socket 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 4 * interface as the means of communication with the user level. 5 * 6 * Generic socket support routines. Memory allocators, sk->inuse/release 7 * handler for protocols to use and generic option handler. 8 * 9 * 10 * Version: @(#)sock.c 1.0.17 06/02/93 11 * 12 * Authors: Ross Biro, 13 * Fred N. van Kempen, 14 * Florian La Roche, 15 * Alan Cox, 16 * 17 * Fixes: 18 * Alan Cox : Numerous verify_area() problems 19 * Alan Cox : Connecting on a connecting socket 20 * now returns an error for tcp. 21 * Alan Cox : sock->protocol is set correctly. 22 * and is not sometimes left as 0. 23 * Alan Cox : connect handles icmp errors on a 24 * connect properly. Unfortunately there 25 * is a restart syscall nasty there. I 26 * can't match BSD without hacking the C 27 * library. Ideas urgently sought! 28 * Alan Cox : Disallow bind() to addresses that are 29 * not ours - especially broadcast ones!! 30 * Alan Cox : Socket 1024 _IS_ ok for users. (fencepost) 31 * Alan Cox : sock_wfree/sock_rfree don't destroy sockets, 32 * instead they leave that for the DESTROY timer. 33 * Alan Cox : Clean up error flag in accept 34 * Alan Cox : TCP ack handling is buggy, the DESTROY timer 35 * was buggy. Put a remove_sock() in the handler 36 * for memory when we hit 0. Also altered the timer 37 * code. The ACK stuff can wait and needs major 38 * TCP layer surgery. 39 * Alan Cox : Fixed TCP ack bug, removed remove sock 40 * and fixed timer/inet_bh race. 41 * Alan Cox : Added zapped flag for TCP 42 * Alan Cox : Move kfree_skb into skbuff.c and tidied up surplus code 43 * Alan Cox : for new sk_buff allocations wmalloc/rmalloc now call alloc_skb 44 * Alan Cox : kfree_s calls now are kfree_skbmem so we can track skb resources 45 * Alan Cox : Supports socket option broadcast now as does udp. Packet and raw need fixing. 46 * Alan Cox : Added RCVBUF,SNDBUF size setting. It suddenly occurred to me 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 how easy it was so... 47 * Rick Sladkey : Relaxed UDP rules for matching packets. 48 * C.E.Hawkins : IFF_PROMISC/SIOCGHWADDR support 49 * Pauline Middelink : identd support 50 * Alan Cox : Fixed connect() taking signals I think. 51 * Alan Cox : SO_LINGER supported 52 * Alan Cox : Error reporting fixes 53 * Anonymous : inet_create tidied up (sk->reuse setting) 54 * Alan Cox : inet sockets don't set sk->type! 55 * Alan Cox : Split socket option code 56 * Alan Cox : Callbacks 57 * Alan Cox : Nagle flag for Charles & Johannes stuff 58 * Alex : Removed restriction on inet fioctl 59 * Alan Cox : Splitting INET from NET core 60 * Alan Cox : Fixed bogus SO_TYPE handling in getsockopt() 61 * Adam Caldwell : Missing return in SO_DONTROUTE/SO_DEBUG code 62 * Alan Cox : Split IP from generic code 63 * Alan Cox : New kfree_skbmem() 64 * Alan Cox : Make SO_DEBUG superuser only. 65 * Alan Cox : Allow anyone to clear SO_DEBUG 66 * (compatibility fix) 67 * 68 * To Fix: 69 * 70 * 71 * This program is free software; you can redistribute it and/or 72 * modify it under the terms of the GNU General Public License 73 * as published by the Free Software Foundation; either version 74 * 2 of the License, or (at your option) any later version. 75 */ 76 #include 77 #include 78 #include 79 #include 80 #include 81 #include 82 #include 83 #include 84 #include 85 #include 86 #include 87 #include 88 #include 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 89 #include 90 #include 91 #include 92 #include 93 #include 94 #include 95 #include "ip.h" 96 #include "protocol.h" 97 #include "arp.h" 98 #include "rarp.h" 99 #include "route.h" 100 #include "tcp.h" 101 #include "udp.h" 102 #include 103 #include "sock.h" 104 #include "raw.h" 105 #include "icmp.h" 106 #define min(a,b) ((a)<(b)?(a):(b)) 宏 min 的作用显然:取两数中小值。 接下来分析的两个函数为参数设置函数,我们前文在分析 TCP,UDP 协议时,他们各自也 有相应的参数设置函数如 tcp_setsockopt 诸如此类。但这些嵌入在具体协议实现代码中的参 数设置所对应的参数是协议相关的,如对 TCP 协议 Nagle 标志位的设置。而本文件中设置 的参数是传输层协议无关的,换句话说,对于 TCP,UDP 协议,都有这样的参数需要设置 如发送缓冲区大小,如此就没有必要分别在 TCP 和 UDP 协议中设置,而是将这个共性抽出 来,作为一个单独的模块实现,诚如上文刚刚所述,这就是 sock.c 文件的本质。 我们首先看参数设置函数,对于参数获取函数,是与之对应的,之后我们不再进行分析,只 列出其代码。 107 /* 108 * This is meant for all protocols to use and covers goings on 109 * at the socket level. Everything here is generic. 110 */ 111 int sock_setsockopt(struct sock *sk, int level, int optname, 112 char *optval, int optlen) 113 { 114 int val; 115 int err; 116 struct linger ling; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 117 if (optval == NULL) 118 return(-EINVAL); 这一点毫无疑义,对于参数设置而言,如果没有指定参数值,那么就没有必要进行下面的处 理工作。 119 err=verify_area(VERIFY_READ, optval, sizeof(int)); 120 if(err) 121 return err; verify_area 函数验证参数地址空间是否存在,如果不存在,还需要进行分配和映射。 122 val = get_fs_long((unsigned long *)optval); 123 switch(optname) 124 { 获取参数值,然后根据具体的参数名称进行参数设置。 125 case SO_TYPE: 126 case SO_ERROR: 127 return(-ENOPROTOOPT); 128 case SO_DEBUG: 129 if(val && !suser()) 130 return(-EPERM); 131 sk->debug=val?1:0; 132 return 0; 133 case SO_DONTROUTE: 134 sk->localroute=val?1:0; 135 return 0; 136 case SO_BROADCAST: 137 sk->broadcast=val?1:0; 138 return 0; 139 case SO_SNDBUF: 140 if(val>32767) 141 val=32767; 142 if(val<256) 143 val=256; 144 sk->sndbuf=val; 145 return 0; 146 case SO_LINGER: 147 err=verify_area(VERIFY_READ,optval,sizeof(ling)); 148 if(err) 149 return err; 150 memcpy_fromfs(&ling,optval,sizeof(ling)); 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 151 if(ling.l_onoff==0) 152 sk->linger=0; 153 else 154 { 155 sk->lingertime=ling.l_linger; 156 sk->linger=1; 157 } 158 return 0; SO_LINGER 选项可以让我们设置服务器端套接字在设置为 TCP_CLOSE 状态依然“驻留” 的时间。所谓驻留(Linger)表示套接字在关闭后保持为“被使用(inuse)“状态的时间。 保持为”被使用“状态,换句话说,在这期间,不可被重新使用,类似于我们前文中介绍的 TCP_TIME_WAIT 状态时 2MSL 等待时间的概念。不过一般我们是对服务器端套接字使用 SO_LINGER 选项,该选项在套接字被创建时被设置为 0,表示套接字不进行关闭后”驻留 “等待。linger 结构定义在 include/linux/socket.h 文件中,如下所示: //include/linux/socket.h 8 struct linger { 9 int l_onoff; /* Linger active */ 10 int l_linger; /* How long to linger for */ 11 }; 其中 l_onff 是表示是否进行关闭后等待的标志位;l_linger 则表示等待时间。 159 case SO_RCVBUF: 160 if(val>32767) 161 val=32767; 162 if(val<256) 163 val=256; 164 sk->rcvbuf=val; 165 return(0); SO_RCVBUF 选项用于设置接收缓冲区的大小。139 行代码是对发送缓冲区大小的设置。 166 case SO_REUSEADDR: 167 if (val) 168 sk->reuse = 1; 169 else 170 sk->reuse = 0; 171 return(0); 172 case SO_KEEPALIVE: 173 if (val) 174 sk->keepopen = 1; 175 else 176 sk->keepopen = 0; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 177 return(0); SO_KEEPALIVE 选项用于设置是否进行保活探测。这个是由 sock 结构中 keepopen 字段决 定的。 178 case SO_OOBINLINE: 179 if (val) 180 sk->urginline = 1; 181 else 182 sk->urginline = 0; 183 return(0); OOB 数据称为带外数据(Out Of Band),这种数据与紧急数据其实是不同的,带外数据的传 输需要重建数据通道,而紧急数据是嵌入在普通数据流中的,通过 TCP 首部中紧急数据指 针指出紧急数据范围。sock 结构中 urginline 标志位表示怎样对待紧急数据,如果该标志位 被设置为 1,则将紧急数据处理为普通数据。即在读取时,不再进行区别对待。 184 case SO_NO_CHECK: 185 if (val) 186 sk->no_check = 1; 187 else 188 sk->no_check = 0; 189 return(0); SO_NO_CHECK 选项用于设置是否进行校验和计算,TCP 协议校验和计算是必须的。无论 此处设置为何值,都会进行 TCP 校验和计算。UDP,ICMP 协议根据此处的设置决定是否进 行校验和计算。 190 case SO_PRIORITY: 191 if (val >= 0 && val < DEV_NUMBUFFS) 192 { 193 sk->priority = val; 194 } 195 else 196 { 197 return(-EINVAL); 198 } 199 return(0); SO_PRIORITY 选项用于设置对应该套接字的所有发送数据包缓存到硬件缓冲区队列中的优 先级。网络层模块在发送数据包给链路层时(此处层次的概念只是为了便于理解,实际代码 实现上相互耦合),链路层模块将数据包缓存到硬件队列中,硬件队列区分优先级,或者更 准确的说,共有 DEV_NUMBUFFS 个硬件队列,硬件驱动在发送数据时,从第一个队列开 始遍历处理,这样由于发送先后上的顺序就自然了形成了数据包不同的发送优先级。此处 SO_PRIORITY 选项的作用就是设置该套接字发送的数据包所缓存的硬件队列。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 200 default: 201 return(-ENOPROTOOPT); 202 } 203 } 以上对几个重要的比较难以理解的选项进行了解释,其他选项的意义都比较明显,读者可自 行分析理解。 204 int sock_getsockopt(struct sock *sk, int level, int optname, 205 char *optval, int *optlen) 206 { 207 int val; 208 int err; 209 struct linger ling; 210 switch(optname) 211 { 212 case SO_DEBUG: 213 val = sk->debug; 214 break; 215 case SO_DONTROUTE: 216 val = sk->localroute; 217 break; 218 case SO_BROADCAST: 219 val= sk->broadcast; 220 break; 221 case SO_LINGER: 222 err=verify_area(VERIFY_WRITE,optval,sizeof(ling)); 223 if(err) 224 return err; 225 err=verify_area(VERIFY_WRITE,optlen,sizeof(int)); 226 if(err) 227 return err; 228 put_fs_long(sizeof(ling),(unsigned long *)optlen); 229 ling.l_onoff=sk->linger; 230 ling.l_linger=sk->lingertime; 231 memcpy_tofs(optval,&ling,sizeof(ling)); 232 return 0; 233 case SO_SNDBUF: 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 234 val=sk->sndbuf; 235 break; 236 case SO_RCVBUF: 237 val =sk->rcvbuf; 238 break; 239 case SO_REUSEADDR: 240 val = sk->reuse; 241 break; 242 case SO_KEEPALIVE: 243 val = sk->keepopen; 244 break; 245 case SO_TYPE: 246 #if 0 247 if (sk->prot == &tcp_prot) 248 val = SOCK_STREAM; 249 else 250 val = SOCK_DGRAM; 251 #endif 252 val = sk->type; 253 break; 254 case SO_ERROR: 255 val = sk->err; 256 sk->err = 0; 257 break; 258 case SO_OOBINLINE: 259 val = sk->urginline; 260 break; 261 case SO_NO_CHECK: 262 val = sk->no_check; 263 break; 264 case SO_PRIORITY: 265 val = sk->priority; 266 break; 267 default: 268 return(-ENOPROTOOPT); 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 269 } 270 err=verify_area(VERIFY_WRITE, optlen, sizeof(int)); 271 if(err) 272 return err; 273 put_fs_long(sizeof(int),(unsigned long *) optlen); 274 err=verify_area(VERIFY_WRITE, optval, sizeof(int)); 275 if(err) 276 return err; 277 put_fs_long(val,(unsigned long *)optval); 278 return(0); 279 } sock_getsockopt 函数与 sock_setsockopt 函数是相对应的。此处不再讨论。 280 struct sk_buff *sock_wmalloc(struct sock *sk, unsigned long size, int force, int priority) 281 { 282 if (sk) 283 { 284 if (sk->wmem_alloc + size < sk->sndbuf || force) 285 { 286 struct sk_buff * c = alloc_skb(size, priority); 287 if (c) 288 { 289 unsigned long flags; 290 save_flags(flags); 291 cli(); 292 sk->wmem_alloc+= c->mem_len; 293 restore_flags(flags); /* was sti(); */ 294 } 295 return c; 296 } 297 return(NULL); 298 } 299 return(alloc_skb(size, priority)); 300 } sock_wmalloc 是发送数据包时内核缓冲区分配函数,其在分配之前,主要是检查当前发送缓 冲区是否有足够剩余空闲空间分配指定大小的缓冲区。此处我们可以澄清一个概念,所谓发 送缓冲区并非是指系统分配一段连续的指定大小的缓冲区,然后发送的数据都依次填充到这 个区域被内核网络代码处理。实际上所谓发送缓冲区(接收缓冲区也如此)紧急指定的时一 个大小,至于具体的缓冲区空间则随时随地进行分配,换句话说,这些分配的缓冲区不存在 连续的概念,内核所关心的只是这些分配的缓冲区大小的总和是否超过了指定的大小。一旦 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 超出这个指定值,就表示缓冲区溢出,此时无法在分配缓冲区。所以一言以蔽之,即发送或 者接收缓冲区只是一个大小上的限定,没有固定分配一段内存空间之说。sock_wmalloc 函数 以及下面将要介绍的 sock_rmalloc, sock_wfree, sock_rfree, sock_rspace, sock_wspace 等函数 都被传输层协议使用完成对应的工作。读者可查看 tcp_prot, udp_prot 变量的定义方式进行验 证。以上这些函数的意义如同其函数名称一样,都是用于操作发送或者接收缓冲区,具体功 能在下文分析中一一道来,首先看 sock_wmalloc 函数。 282 检查缓冲区分配是否与一个套接字绑定,如果进行了绑定,则分配的缓冲区需要进行统 计,如果对应套接字当前分配的缓冲区大小已经超出限制值,则返回 NULL,表示无法分配 缓冲区,发送缓冲区已经溢出。具体的分配工作交给 alloc_skb 函数,alloc_skb 函数定义在 net/inet/sk_buff.c 中,其调用 kmalloc 函数完成内存分配,并且将这段分配内存封装在 sk_buff 结构中,并对 sk_buff 结构相关字段进行初始化。具体情况在分析 sk_buff.c 文件时进行说明。 如果缓冲区分配不与一个套接字绑定,则直接分配要求数量的内存,此时无需进行缓冲区分 配大小统计字段的更新。 301 struct sk_buff *sock_rmalloc(struct sock *sk, unsigned long size, int force, int priority) 302 { 303 if (sk) 304 { 305 if (sk->rmem_alloc + size < sk->rcvbuf || force) 306 { 307 struct sk_buff *c = alloc_skb(size, priority); 308 if (c) 309 { 310 unsigned long flags; 311 save_flags(flags); 312 cli(); 313 sk->rmem_alloc += c->mem_len; 314 restore_flags(flags); /* was sti(); */ 315 } 316 return(c); 317 } 318 return(NULL); 319 } 320 return(alloc_skb(size, priority)); 321 } sock_rmalloc 函数用于分配接收缓冲区,内存分配基本相同,只不过此时更新的是接收缓冲 区当前分配大小值,这一点是在 313 行完成的。305 行进行预先检查。 322 unsigned long sock_rspace(struct sock *sk) 323 { 324 int amt; 325 if (sk != NULL) 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 326 { 327 if (sk->rmem_alloc >= sk->rcvbuf-2*MIN_WINDOW) 328 return(0); 329 amt = min((sk->rcvbuf-sk->rmem_alloc)/2-MIN_WINDOW, MAX_WINDOW); 330 if (amt < 0) 331 return(0); 332 return(amt); 333 } 334 return(0); 335 } sock_rspace 函数用于检查接收缓冲区空闲空间大小,该函数被调用的较为频繁,而且在处 理上有些“欺诈“性质,其根本原因在于这个函数返回值将直接被用于 TCP 首部中窗口字 段的创建。该窗口字段用于向远端表明本地当前可接收的数据量,为了防止产生小数据包, 所以本地缓冲区空闲空间计算上并不是将限制值减去当前使用值,而是预留了一个最小窗 口,327 行将这个最小窗口设置为 2*MIN_WINDOW, MIN_WINDOW 定义在 tcp.h 中为 2048。 如果当前接收缓冲区空闲空间小于这个最小窗口,则作为 0 对待。329 行代码计算空闲空间 的方式很特别,其将真正空闲空间减半后再减去 MIN_WINDOW 后的结果作为当前接收缓 冲区空闲大小。通过这种方式计算出的空闲空间大小大大缩水了。此处所要说明的是,这种 计算方式是实现相关的,其基本底线是本地向远端声明的本地窗口大小最好不要发生缩减的 情况(即本地声明的窗口大小小于上一次声明的值),而且这个窗口大小不要小于一个报文 长度(以防促使远端产生小数据包,当然如果远端自己需要产生小数据包,则另当别论,如 Telnet,Rlogin 应用等)。 336 unsigned long sock_wspace(struct sock *sk) 337 { 338 if (sk != NULL) 339 { 340 if (sk->shutdown & SEND_SHUTDOWN) 341 return(0); 342 if (sk->wmem_alloc >= sk->sndbuf) 343 return(0); 344 return(sk->sndbuf-sk->wmem_alloc ); 345 } 346 return(0); 347 } 对于发送缓冲区当前空闲空间大小,sock_wspace 比较直接,因为这个返回值被本地使用, 既然“大家都是一家人“,就”不说两家话“了,我有多少空闲空间,就通知你多少(344 行代码)。 348 void sock_wfree(struct sock *sk, struct sk_buff *skb, unsigned long size) 349 { 350 #ifdef CONFIG_SKB_CHECK 351 IS_SKB(skb); 352 #endif 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 353 kfree_skbmem(skb, size); 354 if (sk) 355 { 356 unsigned long flags; 357 save_flags(flags); 358 cli(); 359 sk->wmem_alloc -= size; 360 restore_flags(flags); 361 /* In case it might be waiting for more memory. */ 362 if (!sk->dead) 363 sk->write_space(sk); 364 return; 365 } 366 } 367 void sock_rfree(struct sock *sk, struct sk_buff *skb, unsigned long size) 368 { 369 #ifdef CONFIG_SKB_CHECK 370 IS_SKB(skb); 371 #endif 372 kfree_skbmem(skb, size); 373 if (sk) 374 { 375 unsigned long flags; 376 save_flags(flags); 377 cli(); 378 sk->rmem_alloc -= size; 379 restore_flags(flags); 380 } 381 } sock_wfree, sock_rfree 函数用于释放发送和接收缓冲区,并更新相关统计字段值。这两个函 数实现较为浅显,不必多说。 382 /* 383 * Generic send/receive buffer handlers 384 */ 385 struct sk_buff *sock_alloc_send_skb(struct sock *sk, unsigned long size, int noblock, int *errcode) 386 { 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 sk 表示哪个套接字需要分配内存;size 为数据帧长度(包括各协议首部长度和用户数据长 度);noblock 表示如果暂时无法分配内存时是否进行睡眠等待;errcode 是一个地址,用于 返回可能出现的错误。 387 struct sk_buff *skb; 388 int err; 389 sk->inuse=1; 390 do 391 { 390 行这个 do-while 循环用于“坚持不懈“的分配一个指定大小的 sk_buff 封装数据结构。 392 if(sk->err!=0) 393 { 394 cli(); 395 err= -sk->err; 396 sk->err=0; 397 sti(); 398 *errcode=err; 399 return NULL; 400 } 392 行这个 if 语句检查在分配内存的过程中,是否发生了错误,如果返回错误,则终止内存 分配过程,返回这个错误值。 401 if(sk->shutdown&SEND_SHUTDOWN) 402 { 403 *errcode=-EPIPE; 404 return NULL; 405 } sock_alloc_send_skb 函数用于分配发送数据包,如果对应套接字已经关闭发送通道,则返回 EPIPE 错误。因为这种可能“费尽了千辛万苦“分配得到的内存最后却被简单释放,实在是 浪费资源,所以直接返回错误。 406 skb = sock_wmalloc(sk, size, 0, GFP_KERNEL); sock_alloc_send_skb 函数封装对 sock_wmalloc 函数的调用更新发送缓冲区,并分配内存空 间。 407 if(skb==NULL) 408 { 409 unsigned long tmp; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 410 sk->socket->flags |= SO_NOSPACE; 411 if(noblock) 412 { 413 *errcode=-EAGAIN; 414 return NULL; 415 } 416 if(sk->shutdown&SEND_SHUTDOWN) 417 { 418 *errcode=-EPIPE; 419 return NULL; 420 } 421 tmp = sk->wmem_alloc; 422 cli(); 423 if(sk->shutdown&SEND_SHUTDOWN) 424 { 425 sti(); 426 *errcode=-EPIPE; 427 return NULL; 428 } 429 if( tmp <= sk->wmem_alloc) 430 { 431 sk->socket->flags &= ~SO_NOSPACE; 432 interruptible_sleep_on(sk->sleep); 433 if (current->signal & ~current->blocked) 434 { 435 sti(); 436 *errcode = -ERESTARTSYS; 437 return NULL; 438 } 439 } 440 sti(); 441 }// if(skb==NULL) 407 行表示内存分配失败,此时根据 noblock 参数的设置决定是否睡眠等待。416 行对发送 通道的再次检查源于对 sock_wmalloc 函数的调用可能发生睡眠等待,在这过程中,可能发 送通道由于某种原因被关闭,所以此处进行再次检查;而 423 行的检查源于 cli()函数调用。 一种解释就是在开启中断后,可能当前进程被中断,中断返回后可能调度其他进程运行,那 么在本进程被重新换回来之后,需要再次对发送通道进行检查。代码中一再对发送通道进行 检查的原因前文中已经交待:防止做无用功。429-439 行代码等待睡眠,等待发送缓冲区中 有足够的空闲空间。 一旦被唤醒,而且唤醒不是因为被信号中断(此种情况直接返回处理信号中断),那么回到 406 行重新进行缓冲区分配。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 442 } 443 while(skb==NULL); 444 return skb; 445 } sock_alloc_send_skb 函数被 udp_send 函数调用用于分配一个 sk_buff 封装数据结构。TCP 协 议在分配接收或者发送缓冲区时,直接调用 sock_wmalloc, sock_rmalloc 函数进行操作。而 UDP 协议则通过 sock_alloc_send_skb 以及下文将要介绍的 sock_queue_rcv_skb 函数进行分 配。 446 /* 447 * Queue a received datagram if it will fit. Stream and sequenced protocols 448 * can't normally use this as they need to fit buffers in and play with them. 449 */ 450 int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) 451 { 452 unsigned long flags; 453 if(sk->rmem_alloc + skb->mem_len >= sk->rcvbuf) 454 return -ENOMEM; 455 save_flags(flags); 456 cli(); 457 sk->rmem_alloc+=skb->mem_len; 458 skb->sk=sk; 459 restore_flags(flags); 460 skb_queue_tail(&sk->receive_queue,skb); 461 if(!sk->dead) 462 sk->data_ready(sk,skb->len); 463 return 0; 464 } sock_queue_rcv_skb 函数被 udp_deliver 函数调用将接收到的数据包插入到接收队列中。453 行判断当前接收缓冲区是否有足够空闲空间接收该数据包。如果有,则更新(457 行)统计 字段值。此后调用(460 行)skb_queue_tail 将接收到的数据包插入到接收队列尾部,最后 通知应用进程有数据包到达,可以进行处理了。 465 void release_sock(struct sock *sk) 466 { 467 unsigned long flags; 468 #ifdef CONFIG_INET 469 struct sk_buff *skb; 470 #endif 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 471 if (!sk->prot) 472 return; 473 /* 474 * Make the backlog atomic. If we don't do this there is a tiny 475 * window where a packet may arrive between the sk->blog being 476 * tested and then set with sk->inuse still 0 causing an extra 477 * unwanted re-entry into release_sock(). 478 */ 479 save_flags(flags); 480 cli(); 481 if (sk->blog) 482 { 483 restore_flags(flags); 484 return; 485 } 486 sk->blog=1; 487 sk->inuse = 1; 488 restore_flags(flags); 489 #ifdef CONFIG_INET 490 /* See if we have any packets built up. */ 491 while((skb = skb_dequeue(&sk->back_log)) != NULL) 492 { 493 sk->blog = 1; 494 if (sk->prot->rcv) 495 sk->prot->rcv(skb, skb->dev, sk->opt, 496 skb->saddr, skb->len, skb->daddr, 1, 497 /* Only used for/by raw sockets. */ 498 (struct inet_protocol *)sk->pair); 499 } 500 #endif 501 sk->blog = 0; 502 sk->inuse = 0; 503 #ifdef CONFIG_INET 504 if (sk->dead && sk->state == TCP_CLOSE) 505 { 506 /* Should be about 2 rtt's */ 507 reset_timer(sk, TIME_DONE, min(sk->rtt * 2, TCP_DONE_TIME)); 508 } 509 #endif 510 } release_sock 函数被调用的最为频繁,如果读者自己留意一下,在 TCP 协议(以及 UDP 协 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 议)实现中,大多数函数“有事没事“都调用一下该函数,那么这个函数究竟有什么”吸引 之处“?秘密就在 491-499 行。网络层模块在将一个数据包传递给传输层模块处理时 (tcp_rcv),如果当前对应的套接字正忙,则将数据包插入到 sock 结构 back_log 队列中。但 插入该队列中的数据包并不能算是被接收,该队列中的数据包需要进行一系列处理后插入 receive_queue 接收队列中时,方才算是完成接收。而 release_sock 函数就是从 back_log 中取 数据包重新调用 tcp_rcv 函数对数据包进行接收。所谓 back_log 队列只是数据包暂居之所, 不可久留,所以也就必须对这个队列中数据包尽快进行处理,那么也就表示必须对 release_sock 函数进行频繁调用。这就是 release_sock 函数如此受”照顾“的原因。504-508 行代码在套接字关闭后等待一段时间后将内核相关数据结构完全释放。reset_timer 函数定义 在 net/inet/timer.c 中,用新的定时间隔重置定时器。reset_timer 函数使用的是 sock 结构中 timer 字段指向的定时器,而定时器到期执行函数为 net_timer, net_timer 函数也是定义在 timer.c 中。 该函数中对应 TIME_DONE 的处理代码如下,注意 111 行对 destroy_sock 函数的调用,用于 释放对应套接字(destroy_sock 函数定义在 af_inet.c 中,在本书上文中已经进行了分析)。 //net/inet/timer.c 104 case TIME_DONE: 105 if (! sk->dead || sk->state != TCP_CLOSE) 106 { 107 printk ("non dead socket in time_done\n"); 108 release_sock (sk); 109 break; 110 } 111 destroy_sock (sk); 112 break; 至此,完成 sock.c 文件的分析,该文件作为一个传输层通用功能实现文件而存在,主要是对 发送和接收缓冲区分配进行处理。 2.10 net/inet/datagram.c 文件 实际上在介绍 udp.c 文件实现之前,就应该先介绍 datagram.c 文件,该文件定义了几个功能 函数被 UDP 协议使用,主要涉及数据包接收和释放以及数据复制,此外还有一个查询函数。 我们下面进入对该文件的分析。 1 /* 2 * SUCS NET3: 3 * 4 * Generic datagram handling routines. These are generic for all protocols. Possibly a generic IP version on top 5 * of these would make sense. Not tonight however 8-). 6 * This is used because UDP, RAW, PACKET and the to be released IPX layer all have identical select code and mostly 7 * identical recvfrom() code. So we share it here. The select was shared before but buried in udp.c so I moved it. 8 * 9 * Authors: Alan Cox . (datagram_select() from old udp.c code) 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 10 * 11 * Fixes: 12 * Alan Cox : NULL return from skb_peek_copy() understood 13 * Alan Cox : Rewrote skb_read_datagram to avoid the skb_peek_copy stuff. 14 * Alan Cox : Added support for SOCK_SEQPACKET. IPX can no longer use the SO_TYPE hack but 15 * AX.25 now works right, and SPX is feasible. 16 * Alan Cox : Fixed write select of non IP protocol crash. 17 * Florian La Roche: Changed for my new skbuff handling. 18 * Darryl Miles : Fixed non-blocking SOCK_SEQPACKET. 19 * 20 * Note: 21 * A lot of this will change when the protocol/socket separation 22 * occurs. Using this will make things reasonably clean. 23 */ 24 #include 25 #include 26 #include 27 #include 28 #include 29 #include 30 #include 31 #include 32 #include 33 #include 34 #include 35 #include "ip.h" 36 #include "protocol.h" 37 #include "route.h" 38 #include "tcp.h" 39 #include "udp.h" 40 #include 41 #include "sock.h" 42 /* 43 * Get a datagram skbuff, understands the peeking, nonblocking wakeups and possible 44 * races. This replaces identical code in packet,raw and udp, as well as the yet to 45 * be released IPX support. It also finally fixes the long standing peek and read 46 * race for datagram sockets. If you alter this routine remember it must be 47 * re-entrant. 48 */ 49 struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned flags, int noblock, int *err) 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 50 { 51 struct sk_buff *skb; 52 unsigned long intflags; 53 /* Socket is inuse - so the timer doesn't attack it */ 54 save_flags(intflags); 55 restart: 56 sk->inuse = 1; 57 while(skb_peek(&sk->receive_queue) == NULL) /* No data */ 58 { 59 /* If we are shutdown then no more data is going to appear. We are done */ 60 if (sk->shutdown & RCV_SHUTDOWN) 61 { 62 release_sock(sk); 63 *err=0; 64 return NULL; 65 } 66 if(sk->err) 67 { 68 release_sock(sk); 69 *err=-sk->err; 70 sk->err=0; 71 return NULL; 72 } 73 /* Sequenced packets can come disconnected. If so we report the problem */ 74 if(sk->type==SOCK_SEQPACKET && sk->state!=TCP_ESTABLISHED) 75 { 76 release_sock(sk); 77 *err=-ENOTCONN; 78 return NULL; 79 } 80 /* User doesn't want to wait */ 81 if (noblock) 82 { 83 release_sock(sk); 84 *err=-EAGAIN; 85 return NULL; 86 } 87 release_sock(sk); 88 /* Interrupts off so that no packet arrives before we begin sleeping. 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 89 Otherwise we might miss our wake up */ 90 cli(); 91 if (skb_peek(&sk->receive_queue) == NULL) 92 { 93 interruptible_sleep_on(sk->sleep); 94 /* Signals may need a restart of the syscall */ 95 if (current->signal & ~current->blocked) 96 { 97 restore_flags(intflags);; 98 *err=-ERESTARTSYS; 99 return(NULL); 100 } 101 if(sk->err != 0) /* Error while waiting for packet 102 eg an icmp sent earlier by the 103 peer has finally turned up now */ 104 { 105 *err = -sk->err; 106 sk->err=0; 107 restore_flags(intflags); 108 return NULL; 109 } 110 } 111 sk->inuse = 1; 112 restore_flags(intflags); 113 } 114 /* Again only user level code calls this function, so nothing interrupt level 115 will suddenly eat the receive_queue */ 116 if (!(flags & MSG_PEEK)) 117 { 118 skb=skb_dequeue(&sk->receive_queue); 119 if(skb!=NULL) 120 skb->users++; 121 else 122 goto restart; /* Avoid race if someone beats us to the data */ 123 } 124 else 125 { 126 cli(); 127 skb=skb_peek(&sk->receive_queue); 128 if(skb!=NULL) 129 skb->users++; 130 restore_flags(intflags); 131 if(skb==NULL) /* shouldn't happen but .. */ 132 *err=-EAGAIN; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 133 } 134 return skb; 135 } skb_recv_datagram 函数被 UDP 协议中 udp_recvfrom 函数调用从接收队列中取数据包。这个 函数虽然较长,但实现思想非常简单:查看套接字接收队列中是否有数据包,如有,则直接 返回该数据包,否则睡眠等待。当然在进行睡眠等待之前必须检查等待的必要性,这些检查 包括: 1〉 套接字是否已经被关闭接收通道,对于这种情况,盲目等待是不可取的。此时调用 release_sock 函数从可能的其他缓存队列中转移数据包(实际上调用 release_sock 函数对 使用 UDP 协议的套接字接收队列不会造成任何影响,因为 UDP 协议根本没有使用 back_log 暂存队列),并直接返回 NULL。 2〉 套接字在处理过程中是否发生错误,如果发生错误,则首先需要处理这些错误,此时也 返回。 3〉 如果 noblock 标志位被设置,则调用者要求不进行睡眠等待,当然也就不能进行睡眠等 待。 4〉 对 SOCK_SEQPACKET 类型套接字,如果套接字当前状态不是 TCP_ESTABLISHED,则 也返回,并设置连接错误标志。 对于 SOCK_SEQPACKET 类型套接字,此处需要简单介绍一下,这个套接字类如同 SOCK_STREAM 套接字类型一样,提供面向连接的可靠性数据传输。二者的区别在于 SOCK_SEQPACKET 是面向报文的,而 SOCK_STREAM 是面向流的。所谓面向报文指数据 包之间是独立的记录,接收端在读取时不可跨越数据包进行数据读取,所以在对数据包的操 作上类似于 UDP 协议(如果一个数据包中数据没有读取完,则数据包中剩余的数据很可能 被丢弃)。而面向流是指数据包只是作为数据传输的一种方式,所有包中的数据是连续的, 可以跨越包进行数据读取,一个数据包中数据可以分几次进行读取。总之,对于 SOCK_SEQPACKET 类型套接字的处理,当考虑两台主机之间数据包交换时,我们将其类 比于 TCP 协议,其提供可靠性数据传输,TCP 协议使用的那一套对数据进行编号和应答机 制,它也使用;但是当数据包在发送或者被远端接收后,应用程序进行读取时,其操作的方 式我们将其类比于 UDP 协议,其处理上是以包为边界的。这是 SOCK_SEQPACKET 类型套 接字应该工作的方式,但是实际在实现上究竟对于 SOCK_SEQPACKET 这个套接字类型如 何处理是实现相关的。如本版本网络代码实现 SOCK_SEQPACKET 类型在数据包处理上完 全等同于 SOCK_STREAM,我们可以从 inet_create 函数实现看出: //net/inet/af_inet.c ---inet_create 469 case SOCK_STREAM: 470 case SOCK_SEQPACKET: 471 if (protocol && protocol != IPPROTO_TCP) 472 { 473 kfree_s((void *)sk, sizeof(*sk)); 474 return(-EPROTONOSUPPORT); 475 } 476 protocol = IPPROTO_TCP; 477 sk->no_check = TCP_NO_CHECK; 478 prot = &tcp_prot; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 479 break; inet_create函数将 SOCK_STREAM, SOCK_SEQPACKET套接字类型都用TCP协议实现进行 处理。而且在内核网络栈代码的其他地方也未发现对二者处理上的区别。在 skb_recv_datagram 函数中突然冒出对 SOCK_SEQPACKET 类型的特殊“照顾”倒是令人诧 异。 90-110 行代码进行睡眠等待以及被唤醒后对唤醒原因的检查和对是否发生错误的检查,这写 代码都约定俗成了。当我们跳出 57 行的 while 循环时,就表示接收队列中已经存在数据包 供读取了。116-123 行代码处理正常读取的情况,124-133 行代码处理预先读取的情况。最 后返回表示数据包的 sk_buff 结构。 136 void skb_free_datagram(struct sk_buff *skb) 137 { 138 unsigned long flags; 139 save_flags(flags); 140 cli(); 141 skb->users--; 142 if(skb->users>0) 143 { 144 restore_flags(flags); 145 return; 146 } 147 /* See if it needs destroying */ 148 if(!skb->next && !skb->prev) /* Been dequeued by someone - ie it's read */ 149 kfree_skb(skb,FREE_READ); 150 restore_flags(flags); 151 } skb_free_datagram 函数释放一个数据包,141 行递减用户计输,每个使用该数据包的进程都 回增加该 sk_buff 结构的 users 字段,一旦该字段为 0,表示这是一个游离的数据包,可以进 行释放,否则表示还有进程在使用该数据包,此时不可进行释放,直接返回。148 行检查数 据包是否仍然处于系统某个队列中,如果数据包还被挂接在系统队列中,也不可对其进行释 放。否则调用 kfree_skb 函数释放数据包所占用的内存空间。 152 void skb_copy_datagram(struct sk_buff *skb, int offset, char *to, int size) 153 { 154 /* We will know all about the fraglist options to allow >4K receives 155 but not this release */ 156 memcpy_tofs(to,skb->h.raw+offset,size); 157 } skb_copy_datagram 函数被 udp_recvfrom 函数调用,将内核缓冲区中数据复制到用户缓冲区 中。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 158 /* 159 * Datagram select: Again totally generic. Moved from udp.c 160 * Now does seqpacket. 161 */ 162 int datagram_select(struct sock *sk, int sel_type, select_table *wait) 163 { 164 select_wait(sk->sleep, wait); 165 switch(sel_type) 166 { 167 case SEL_IN: 168 if (sk->type==SOCK_SEQPACKET && sk->state==TCP_CLOSE) 169 { 170 /* Connection closed: Wake up */ 171 return(1); 172 } 173 if (skb_peek(&sk->receive_queue) != NULL || sk->err != 0) 174 { /* This appears to be consistent 175 with other stacks */ 176 return(1); 177 } 178 return(0); 179 case SEL_OUT: 180 if (sk->type==SOCK_SEQPACKET && sk->state==TCP_SYN_SENT) 181 { 182 /* Connection still in progress */ 183 return(0); 184 } 185 if (sk->prot && sk->prot->wspace(sk) >= MIN_WRITE_SPACE) 186 { 187 return(1); 188 } 189 if (sk->prot==NULL && sk->sndbuf-sk->wmem_alloc >= MIN_WRITE_SPACE) 190 { 191 return(1); 192 } 193 return(0); 194 case SEL_EX: 195 if (sk->err) 196 return(1); /* Socket has gone into error state (eg icmp error) */ 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 197 return(0); 198 } 199 return(0); 200 } datagram_select 函数的作用如同 tcp_select 函数,我们将这类函数称为信息查询函数,如查 询内核发送缓冲区当前可用空间大小,查询当前是否存在可接收的连接请求,如果读者对应 socket 应用程序编程较熟的话,那么这些传输层 select 函数就是系统调用 select 函数的底层 实现。 从函数实现上来看,分为三种查询情况: 1〉 对当前可接收状态进行查询:查询是否存在可接收的数据。对于侦听套接字而言,就表 示是否有连接请求到达。注意 SOCK_SEQPACKET 不同于 SOCK_STREAM 的地方在于 内核和应用层接口对数据处理上,在内核处理上同于 SOCK_STREAM,使用 TCP 协议 进行连接和交互数据。 2〉 对当前可发送状态进行查询,主要通过查询当前发送缓冲可用空间大小是否满足发送一 个数据包。 3〉 对当前错误情况进行查询,一旦套接字在其处理过程中出现错误,则将错误保存在 sock 结构 err 字段,一般在用户应用程序请求套接字某种操作时,会返回这个错误,当然也 可如同此处进行明确查询,返回可能发生的错误状态。 datagram.c 文件在早期版本中是不存在的,其实现的功能直接内嵌于 UDP 协议实现文件中, 随着网络栈代码的演化,这些可以独立出来的功能逐渐被分离成独立的文件。sk_buff.c 文件 就是一个典型,这个文件专门用于处理 sk_buff 相关功能如 sk_buff 结构分配和释放等。早 期代码这些分配和释放是随时随地完成的。随着代码层次性逐渐清晰,这些代码也逐渐分离 出来作为专门文件形式而存在。当然这在一定程度上增加了代码分析的复杂度,有时为了理 解一个函数,我们不得不查阅好几个文件,不像早期版本,所有要实现的功能均“当地”进 行实现,不需要调用其他包装函数。这也是本书选择 1.2.13 这个版本作为分析对象的主要原 因,虽然这个版本代码也开始进行封装,但程度不深,而且从整体文件结构上还未进行细分 (IP 协议,TCP 协议等实现文件都还在一个文件夹中),便于我们集中进行分析。 2.11 net/inet/icmp.c 文件 将 ICMP 协议放在此处进行分析,即认为 ICMP 协议从属于传输层协议,此话一出,某些“专 家”就要跳出来,大发一通“危言耸论”了。单从协议角度看,我们一般将 ICMP 协议放在 网络层讨论,即与 IP 协议同层,但实际上 ICMP 数据是作为 IP 协议数据负载形式存在的。 所以从实现角度上,ICMP 协议工作在 IP 协议之上,但其又不与 TCP 协议或者 UDP 协议同 级,而是工作在他们的下一级,即一般 ICMP 模块在处理完后,还需要进一步调用 TCP 协 议或者 UDP 协议模块进行处理。所以在 ICMP 协议层次归属上确实很难决定将其到底是归 入到网络层还是传输层。不过既然其工作在 IP 模块之上,当然在介绍 IP 协议实现之前,先 介绍 ICMP 协议实现。ICMP 协议的目的并非是为了传送数据,而是用于通报错误或者探测 远端主机信息。有关 ICMP 协议首部格式以及各种 ICMP 类型在本书第一章中已有介绍,此 处不再讨论,我们直接进入对 ICMP 协议实现文件的分析。 1 /* 2 * INET An implementation of the TCP/IP protocol suite for the LINUX 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 3 * operating system. INET is implemented using the BSD Socket 4 * interface as the means of communication with the user level. 5 * 6 * Internet Control Message Protocol (ICMP) 7 * 8 * Version: @(#)icmp.c 1.0.11 06/02/93 9 * 10 * Authors: Ross Biro, 11 * Fred N. van Kempen, 12 * Mark Evans, 13 * Alan Cox, 14 * Stefan Becker, 15 * 16 * Fixes: 17 * Alan Cox : Generic queue usage. 18 * Gerhard Koerting: ICMP addressing corrected 19 * Alan Cox : Use tos/ttl settings 20 * Alan Cox : Protocol violations 21 * Alan Cox : SNMP Statistics 22 * Alan Cox : Routing errors 23 * Alan Cox : Changes for newer routing code 24 * Alan Cox : Removed old debugging junk 25 * Alan Cox : Fixed the ICMP error status of net/host unreachable 26 * Gerhard Koerting : Fixed broadcast ping properly 27 * Ulrich Kunitz : Fixed ICMP timestamp reply 28 * A.N.Kuznetsov : Multihoming fixes. 29 * Laco Rusnak : Multihoming fixes. 30 * Alan Cox : Tightened up icmp_send(). 31 * Alan Cox : Multicasts. 32 * Stefan Becker : ICMP redirects in icmp_send(). 33 * 34 * 35 * 36 * This program is free software; you can redistribute it and/or 37 * modify it under the terms of the GNU General Public License 38 * as published by the Free Software Foundation; either version 39 * 2 of the License, or (at your option) any later version. 40 */ 41 #include 42 #include 43 #include 44 #include 45 #include 46 #include 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 47 #include 48 #include 49 #include 50 #include "snmp.h" 51 #include "ip.h" 52 #include "route.h" 53 #include "protocol.h" 54 #include "icmp.h" 55 #include "tcp.h" 56 #include "snmp.h" 57 #include 58 #include "sock.h" 59 #include 60 #include 61 #include 62 #include 63 #define min(a,b) ((a)<(b)?(a):(b)) 64 /* 65 * Statistics 66 */ 67 struct icmp_mib icmp_statistics={0,}; 如同 UDP,TCP 协议实现文件一样,ICMP 实现文件在开始处也定义了一个信息统计结构。 类似这样的结构都定义在 snmp.h 文件中。我们对这样的统计信息不是很关心。 68 /* An array of errno for error messages from dest unreach. */ 69 struct icmp_err icmp_err_convert[] = { 70 { ENETUNREACH, 0 }, /* ICMP_NET_UNREACH */ 71 { EHOSTUNREACH, 0 }, /* ICMP_HOST_UNREACH */ 72 { ENOPROTOOPT, 1 }, /* ICMP_PROT_UNREACH */ 73 { ECONNREFUSED, 1 }, /* ICMP_PORT_UNREACH */ 74 { EOPNOTSUPP, 0 }, /* ICMP_FRAG_NEEDED */ 75 { EOPNOTSUPP, 0 }, /* ICMP_SR_FAILED */ 76 { ENETUNREACH, 1 }, /* ICMP_NET_UNKNOWN */ 77 { EHOSTDOWN, 1 }, /* ICMP_HOST_UNKNOWN */ 78 { ENONET, 1 }, /* ICMP_HOST_ISOLATED */ 79 { ENETUNREACH, 1 }, /* ICMP_NET_ANO */ 80 { EHOSTUNREACH, 1 }, /* ICMP_HOST_ANO */ 81 { EOPNOTSUPP, 0 }, /* ICMP_NET_UNR_TOS */ 82 { EOPNOTSUPP, 0 } /* ICMP_HOST_UNR_TOS */ 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 83 }; 69-83 行代码定义了 ICMP 各种错误类型或者说是错误代码,icmp_err 结构定义在 include/linux/icmp.h 中,如下: //include/linux/icmp.h 66 struct icmp_err { 67 int errno; 68 unsigned fatal:1; 69 }; 第二个字段是一个 1bit 的标志位,表示对应的错误是否可恢复,或者说是内核网络实现代 码在接收到对应错误时是否继续处理套接字,还是将错误返回并释放套接字。以上 69-83 行 代码定义了一系列常见错误及其是否为 fatal 的标志位。 84 /* 85 * Send an ICMP message in response to a situation 86 * 87 * Fixme: Fragment handling is wrong really. 88 */ 89 void icmp_send(struct sk_buff *skb_in, int type, int code, unsigned long info, struct device *dev) 90 { icmp_send 函数被 IP 模块实现代码调用最多,例如当 IP 模块发现无法找到一个有效的路由 选项发送该数据包时,就会调用该函数回复一个 ICMP 错误通报数据包。例如下调用原型: //net/inet/ip.c 1122 rt = ip_rt_route(iph->daddr, NULL, NULL); 1123 if (rt == NULL) 1124 { 1125 /* 1126 * Tell the sender its packet cannot be delivered. Again 1127 * ICMP is screened later. 1128 */ 1129 icmp_send(skb, ICMP_DEST_UNREACH, ICMP_NET_UNREACH, 0, dev); 1130 return; 1131 } 1122 行调用 ip_rt_route 函数查询路由表获取可发送该数据包的路由表项,如果没有找到, 则表示数据包无法发送,此时调用 icmp_send 回复一个网络不可达错误通报数据包。注意其 中参数设置情况,第一个参数是引起错误的原数据包,其他参数含义易见(info 参数表示自 定义信息,一般不使用,简单设置为 0 即可)。 91 struct sk_buff *skb; 92 struct iphdr *iph; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 93 int offset; 94 struct icmphdr *icmph; 95 int len; 96 struct device *ndev=NULL; /* Make this =dev to force replies on the same interface */ 97 unsigned long our_addr; 98 int atype; 99 /* 100 * Find the original IP header. 101 */ 102 iph = (struct iphdr *) (skb_in->data + dev->hard_header_len); 103 /* 104 * No replies to MAC multicast 105 */ 106 if(skb_in->pkt_type!=PACKET_HOST) 107 return; 如果数据包目的地址不是指向本地的单播地址,则不产生 ICMP 错误通报数据包。对于数据 包目的地址类型主要定义又如下常量: //include/linux/sk_buff.h 64 #define PACKET_HOST 0 /* To us */ 65 #define PACKET_BROADCAST 1 66 #define PACKET_MULTICAST 2 67 #define PACKET_OTHERHOST 3 /* Unmatched promiscuous */ 网口驱动程序在调用 alloc_skb 函数分配一个 sk_buff 结构封装数据时默认将 pkt_type 字段设 置为 PACKET_HOST。该字段在 alloc_skb 中被初始化为 PACKET_HOST 后一直未曾改变, 读者可以将该字段的含义理解为链路层目的地址类型,而非网络层目的地址类型。真正网络 层地址的检查是用以下 111 行调用 ip_chk_addr 函数完成的。 108 /* 109 * No replies to IP multicasting 110 */ 111 atype=ip_chk_addr(iph->daddr); 112 if(atype==IS_BROADCAST || IN_MULTICAST(iph->daddr)) 113 return; 111 行调用 ip_chk_addr 检查原数据包最终目的地址类型,如果是一个多播或者广播地址, 则不满足产生 ICMP 数据包的条件,直接返回到调用者。由于 ICMP 错误报文产生的条件请 参见函数后的说明。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 114 /* 115 * Only reply to first fragment. 116 */ 117 if(ntohs(iph->frag_off)&IP_OFFSET) 118 return; 117 行代码检查是否是第一个数据包分片,注意这种情况也包含了对单分片数据包(即未分 片)情况的检查。ICMP 错误报文只对第一个分片产生。 119 /* 120 * We must NEVER NEVER send an ICMP error to an ICMP error message 121 */ 122 if(type==ICMP_DEST_UNREACH||type==ICMP_REDIRECT|| type==ICMP_SOURCE_QUENCH||type==ICMP_TIME_EXCEEDED) 123 { 122 行检查原数据包本身是否是一个 ICMP 错误报文,对于这种情况,不可产生另一个 ICMP 错误报文,否则就陷入死循环了。135 行是对原 ICMP 错误报文错误值的进一步检查。 124 /* 125 * Is the original packet an ICMP packet? 126 */ 127 if(iph->protocol==IPPROTO_ICMP) 128 { 129 icmph = (struct icmphdr *) ((char *) iph + 130 4 * iph->ihl); 131 /* 132 * Check for ICMP error packets (Must never reply to 133 * an ICMP error). 134 */ 135 if (icmph->type == ICMP_DEST_UNREACH || 136 icmph->type == ICMP_SOURCE_QUENCH || 137 icmph->type == ICMP_REDIRECT || 138 icmph->type == ICMP_TIME_EXCEEDED || 139 icmph->type == ICMP_PARAMETERPROB) 140 return; 141 } 142 } 143 icmp_statistics.IcmpOutMsgs++; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 144 /* 145 * This needs a tidy. 146 */ 经过了以上检查,现在可以安全的发送一个 ICMP 错误通报数据包了,不过首先我们需要更 新一下 ICMP 错误报文统计信息。icmp_statistics 变量定义了 icmp.c 文件开始处。 147 switch(type) 148 { 149 case ICMP_DEST_UNREACH: 150 icmp_statistics.IcmpOutDestUnreachs++; 151 break; 152 case ICMP_SOURCE_QUENCH: 153 icmp_statistics.IcmpOutSrcQuenchs++; 154 break; 155 case ICMP_REDIRECT: 156 icmp_statistics.IcmpOutRedirects++; 157 break; 158 case ICMP_ECHO: 159 icmp_statistics.IcmpOutEchos++; 160 break; 161 case ICMP_ECHOREPLY: 162 icmp_statistics.IcmpOutEchoReps++; 163 break; 164 case ICMP_TIME_EXCEEDED: 165 icmp_statistics.IcmpOutTimeExcds++; 166 break; 167 case ICMP_PARAMETERPROB: 168 icmp_statistics.IcmpOutParmProbs++; 169 break; 170 case ICMP_TIMESTAMP: 171 icmp_statistics.IcmpOutTimestamps++; 172 break; 173 case ICMP_TIMESTAMPREPLY: 174 icmp_statistics.IcmpOutTimestampReps++; 175 break; 176 case ICMP_ADDRESS: 177 icmp_statistics.IcmpOutAddrMasks++; 178 break; 179 case ICMP_ADDRESSREPLY: 180 icmp_statistics.IcmpOutAddrMaskReps++; 181 break; 182 } 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 在更新了统计信息后,下面就创建一个 ICMP 报文进行回复。如下这段代码实现思想非常简 单,就是创建一个 ICMP 协议数据包,这包括 MAC,IP 首部创建,以及 ICMP 首部创建, 因为 ICMP 报文是使用 IP 协议发送的。 183 /* 184 * Get some memory for the reply. 185 */ 186 len = dev->hard_header_len + sizeof(struct iphdr) + sizeof(struct icmphdr) + 187 sizeof(struct iphdr) + 32; /* amount of header to return */ 188 skb = (struct sk_buff *) alloc_skb(len, GFP_ATOMIC); 189 if (skb == NULL) 190 { 191 icmp_statistics.IcmpOutErrors++; 192 return; 193 } 194 skb->free = 1; 195 /* 196 * Build Layer 2-3 headers for message back to source. 197 */ 198 our_addr = dev->pa_addr; 199 if (iph->daddr != our_addr && ip_chk_addr(iph->daddr) == IS_MYADDR) 200 our_addr = iph->daddr; dev 参数表示接收原数据包的网络设备,每个网络设备都会分配一个网络地址,如果原数据 包目的地址不是接收该数据包的网络设备对应的地址,但是原数据包目的地址确实属于本 机,那么表示这台主机有多个 IP 地址(可能有多块网卡,而原数据包目的地址是其他网卡 设备的 IP 地址),既然如此,我们在回复 ICMP 报文时,本地地址就设置为原数据包中使用 的目的地址,以保持一致。 201 offset = ip_build_header(skb, our_addr, iph->saddr, 202 &ndev, IPPROTO_ICMP, NULL, len, 203 skb_in->ip_hdr->tos,255); ip_build_header 函数定义在 ip.c 文件中,用于创建 MAC,IP 首部,返回两个首部的总长度。 如果返回值为负值,就表示创建失败,此时 204-210 行代码更新统计信息,释放原数据包。 204 if (offset < 0) 205 { 206 icmp_statistics.IcmpOutErrors++; 207 skb->sk = NULL; 208 kfree_skb(skb, FREE_READ); 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 209 return; 210 } 211 /* 212 * Re-adjust length according to actual IP header size. 213 */ 214 skb->len = offset + sizeof(struct icmphdr) + sizeof(struct iphdr) + 8; 215 /* 216 * Fill in the frame 217 */ 218 icmph = (struct icmphdr *) (skb->data + offset); 219 icmph->type = type; 220 icmph->code = code; 221 icmph->checksum = 0; 222 icmph->un.gateway = info; /* This might not be meant for 223 this form of the union but it will 224 be right anyway */ 218-222 行代码创建 ICMP 首部,这个首部的创建较为简单,因为各字段值(最重要是 type, code 字段)都已经确定。 225 memcpy(icmph + 1, iph, sizeof(struct iphdr) + 8); 225 行代码表示 ICMP 错误通报数据包中 ICMP 数据负载为原数据包 IP 首部加上传输层 8 个字节。这样从传输层 8 个字节可以获取对应的端口号,从而通知相关上层应用程序进程。 226 icmph->checksum = ip_compute_csum((unsigned char *)icmph, 227 sizeof(struct icmphdr) + sizeof(struct iphdr) + 8); 228 /* 229 * Send it and free it once sent. 230 */ 231 ip_queue_xmit(NULL, ndev, skb, 1); 226 行代码在计算 ICMP 校验值后,直接调用(231 行)ip_queue_xmit 函数将数据包送往下 层进行发送。ip_queue_xmit 函数定义在 IP 模块,使提供给传输层的接口函数,传输层在处 理完毕本层工作后,调用该函数进入网络层的处理。TCP,UDP 协议都使用该函数(通过 prot->queue_xmit 函数指针调用)将数据包发往网络层进行处理。 232 }//end of icmp_send 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 icmp_send 函数被调用发送一个 ICMP 错误通报数据包。ICMP 错误通报数据包一般在接收 到远端一个不合法的数据包后产生的,用于通知远端发生错误的原因,例如远端发送的一个 数据包中远端端口不合法,则远端将产生一个端口不可达 ICMP 错误通报数据包给本地,本 地在接收到这个数据包响应的作出响应,如停止发送到不可达端口的数据包,但远端并非在 任何错误的情况下都会回复这样一个 ICMP 数据包,如接收到的数据包满足如下条件之一, 不可产生 ICMP 错误通报数据包: 1〉 一个 ICMP 错误通报数据包。 2〉 数据包远端地址是一个广播地址或者是一个多播地址。 3〉 链路广播数据包(MAC 远端地址设置为全 1)。 4〉 分片数据包中非第一个数据包(换句话说,只对所有分片中第一个分片产生 ICMP 错误 通报数据包,原因很简单,只有第一个分片才包含有传输层协议头部)。 5〉 数据包源端地址非单播地址,即全 0 地址,回环地址,多播或广播地址。 满足以上 5 个条件中之一,即不可产生一个 ICMP 错误通报数据包。 icmp_send 函数实现代码虽然较长,但实现思想非常简单,首先判断引起错误的数据包是否 满足以上 5 种条件之一,如果满足,则直接返回调用这,不产生 ICMP 错误通报数据包,否 则更新本地 ICMP 统计信息后,构建一个 ICMP 错误通报数据包并直接调用 ip_queue_xmit 函数发送个 IP 模块进行处理从而将这个 ICMP 错误通报数据包发送给引起该错误的原数据 包的发送端。具体情况请参见前文分析。 233 /* 234 * Handle ICMP_UNREACH and ICMP_QUENCH. 235 */ 236 static void icmp_unreach(struct icmphdr *icmph, struct sk_buff *skb) 237 { 238 struct inet_protocol *ipprot; 239 struct iphdr *iph; 240 unsigned char hash; 241 int err; 242 err = (icmph->type << 8) | icmph->code; 243 iph = (struct iphdr *) (icmph + 1); 244 switch(icmph->code & 7) 245 { 246 case ICMP_NET_UNREACH: 247 break; 248 case ICMP_HOST_UNREACH: 249 break; 250 case ICMP_PROT_UNREACH: 251 printk("ICMP: %s:%d: protocol unreachable.\n", 252 in_ntoa(iph->daddr), ntohs(iph->protocol)); 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 253 break; 254 case ICMP_PORT_UNREACH: 255 break; 256 case ICMP_FRAG_NEEDED: 257 printk("ICMP: %s: fragmentation needed and DF set.\n", 258 in_ntoa(iph->daddr)); 259 break; 260 case ICMP_SR_FAILED: 261 printk("ICMP: %s: Source Route Failed.\n", in_ntoa(iph->daddr)); 262 break; 263 default: 264 break; 265 } 266 /* 267 * Get the protocol(s). 268 */ 269 hash = iph->protocol & (MAX_INET_PROTOS -1); 270 /* 271 * This can't change while we are doing it. 272 */ 273 ipprot = (struct inet_protocol *) inet_protos[hash]; 274 while(ipprot != NULL) 275 { 276 struct inet_protocol *nextip; 277 nextip = (struct inet_protocol *) ipprot->next; 278 /* 279 * Pass it off to everyone who wants it. 280 */ 281 if (iph->protocol == ipprot->protocol && ipprot->err_handler) 282 { 283 ipprot->err_handler(err, (unsigned char *)(icmph + 1), 284 iph->daddr, iph->saddr, ipprot); 285 } 286 ipprot = nextip; 287 } 288 kfree_skb(skb, FREE_READ); 289 } 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 icmp_unreach 函数处理的 ICMP 错误类型比较广,这点可以从下文中介绍的 icmp_rcv 总入 口函数看出。此处我们就事论事,单方面进行该函数的分析。函数实现思想较为简单,对相 关错误打印错误信息后,调用传输层协议错误处理函数进行处理(tcp_err, udp_err)。注意 242 行代码 err 变量被初始化为 ICMP 类型和代码值。最低 8bit 为代码(code)值,次低 8bit 为类型(type)值。243 行代码初始化 iph 变量指向原数据包中 IP 首部以及传输层 8 字节数 据,从而传输层错误处理函数可以由此获知上层对应应用进程。269 行代码计算传输层协议 在 inet_protos 数组中的位置。inet_protos 一个元素类型为 inet_protocol 结构的数组,数组中 每个元素对应一个传输层协议,协议在数组中的位置由协议编号索引,如 TCP 协议对应索 引号为 6,UDP 为 17。274-287 行代码完成对传输层协议的遍历,调用协议对应错误处理函 数进行返回错误的处理。注意 inet_protos 数组中每个元素对应一个协议,所以 274 行在一次 循环后会退出,但这种编码方式提高了程序的可扩展性。 290 /* 291 * Handle ICMP_REDIRECT. 292 */ 293 static void icmp_redirect(struct icmphdr *icmph, struct sk_buff *skb, 294 struct device *dev, unsigned long source) 295 { icmp_redirect 函数处理重定向报文,有关重定向的说明,请看下文函数后的说明。 ICMP 重定向报文格式如下图所示: 其中类型(Type),代码(Code),校验和(Checksum)以及原数据包 IP 首部加上 8 字节传 输层数据所有的 ICMP 数据包类型都具有这些字段,对于重定向报文来说,多出一个网关 IP 地址字段,这个字段表示更优路由器的 IP 地址。 296 struct rtable *rt; 297 struct iphdr *iph; 298 unsigned long ip; 299 /* 300 * Get the copied header of the packet that caused the redirect 301 */ 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 302 iph = (struct iphdr *) (icmph + 1); iph 变量被初始化为原数据包 IP 首部。所谓原数据包即本地原先发送的数据包,该数据包引 起远端(或中间路由器)回复了一个 ICMP 重定向报文,这个 ICMP 重定向报文就是我们当 前正在处理的数据包,这个 ICMP 重定向数据包在 ICMP 首部之后包含了原先本地发送数据 包的 IP 首部以及 8 字节传输层数据。此处 iph 变量就是被初始化为指向原数据包中 IP 首部。 303 ip = iph->daddr; ip 字段被初始化为要发送数据包的远端 IP 地址。注意 iph 表示本地发送的数据包的 IP 首部, 所以 iph->daddr 表示的是远端 IP 地址(当然是从本地的角度看)。 以下对重定向的类型进行判断,代码支持目的主机和网络重定向,暂不支持服务类型重定向 报文。所有重定向报文的类型值为 5,但代码字段可取如下 4 个值,分别表示 4 种重定向报 文。 在 include/linux/icmp.h 中定义有如下常量: //include/linux/icmp.h 46 /* Codes for REDIRECT. */ 47 #define ICMP_REDIR_NET 0 /* Redirect Net */ 48 #define ICMP_REDIR_HOST 1 /* Redirect Host */ 49 #define ICMP_REDIR_NETTOS 2 /* Redirect Net for TOS */ 50 #define ICMP_REDIR_HOSTTOS 3 /* Redirect Host for TOS */ 如下代码即根据具体的重定向类型分别进行处理。 304 switch(icmph->code & 7) 305 { 306 case ICMP_REDIR_NET: 307 /* 308 * This causes a problem with subnetted networks. What we should do 309 * is use ICMP_ADDRESS to get the subnet mask of the problem route 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 310 * and set both. But we don't.. 311 */ 312 #ifdef not_a_good_idea 313 ip_rt_add((RTF_DYNAMIC | RTF_MODIFIED | RTF_GATEWAY), 314 ip, 0, icmph->un.gateway, dev,0, 0); 315 break; 316 #endif 对于网络重定向类型,直接添加路由表项,对于划分子网的情况,由于无法获得确切的子网 掩码,所以这个添加的表项可能无法正常工作。另外对于使用重定向报文创建或者修改的路 有表项都需要设置 M(RTF_MODIFIED)标志位。RTF_DYNAMIC 标志位表示这是一个动 态路由表项,动态路由表项是相对于永久表项而言的。动态路由表项会设置定时器进行刷新 处理。RTF_GATEWAY 标志位表示添加的是一个网关地址。 317 case ICMP_REDIR_HOST: 318 /* 319 * Add better route to host. 320 * But first check that the redirect 321 * comes from the old gateway.. 322 * And make sure it's an ok host address 323 * (not some confused thing sending our 324 * address) 325 */ 326 rt = ip_rt_route(ip, NULL, NULL); 327 if (!rt) 328 break; 329 if (rt->rt_gateway != source || ip_chk_addr(icmph->un.gateway)) 330 break; 331 printk("redirect from %s\n", in_ntoa(source)); 332 ip_rt_add((RTF_DYNAMIC | RTF_MODIFIED | RTF_HOST | RTF_GATEWAY), 333 ip, 0, icmph->un.gateway, dev,0, 0); 334 break; 对于主机重定向的处理较为谨慎。首先不可直接创建主机重定向路由表项,而只能更改原有 的表项,327 行代码保证了这一点,即如果不存在原表项,则返回。另外发送这个重定向报 文的路由器必须是原表项中指定的路由器,换句话说,路由器重定向报文只可将其自身替换 掉(替换成一个更好的路由器发送通道),另外所提供的新的路由器不可是本地地址,多播 地址或者是广播地址(即 ip_chk_addr 返回值不可为非 0)。通过 329 行检查后,332 行调用 ip_rt_add 对原表项进行修改,将原路由器替换为重定向报文中声明的路由器。注意 RTF_MODIFIED 标志位被设置,表示这是经过 ICMP 重定位数据包修改过的。 335 case ICMP_REDIR_NETTOS: 336 case ICMP_REDIR_HOSTTOS: 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 337 printk("ICMP: cannot handle TOS redirects yet!\n"); 338 break; 对于服务类型和网络,以及服务类型和主机重定向报文本版本不支持。即便支持,我们也可 以预见其处理方式,对于修改表项操作方式而言是一致的,区别在于发送这些报文时所依据 的判断条件。对于这两种重定向报文,考虑的不单是更短的路由途径,还需要考虑服务质量。 339 default: 340 break; 341 } 342 /* 343 * Discard the original packet 344 */ 345 kfree_skb(skb, FREE_READ); 346 } icmp_redirect 函数处理路有重定向 ICMP 数据包。这种数据包是由路由器发送,用于通知本 地到达某个网络或主机的一个更好的路由途径。路由器为特定的目的主机发送重定向消息将 主机重定向到一个更优的路由器或者告诉主机目的主机实际上是在同一链路上的邻居节点。 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 如上图所示,PC 机要发送一个数据包到小型机,其原路有表项表示到达小型机需要通过左 路有器转发,但左路由器发现被转发的数据包发送接口与接收该数据包的接口是同一个,这 就表示这个目的主机要么就在本地链路上,要么下一站路由器在本地链路上,这种情况下, PC 机直接将数据包发送到这个目的主机或者这个“下一站路由器”(下图中即为右路由器) 即可。那么此时左路由器就会产生一个 ICMP 重定向报文,通知 PC 机一个更合理的路有表 项,PC 机在更新这个表项后,以后发送给小型机的所有数据包都直接发送给右路由器进行 转发即可,这样极大的提高了网络利用率以及节省左路由器的资源。 347 /* 348 * Handle ICMP_ECHO ("ping") requests. 349 */ PC 左路由器 右路由器 令牌环 小型机 重定向 PC 左路由器 右路由器 令牌环 小型机 “以后发往小型机的业务量 应向右路由器重定向” 重定向消息 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 350 static void icmp_echo(struct icmphdr *icmph, struct sk_buff *skb, struct device *dev, 351 unsigned long saddr, unsigned long daddr, int len, 352 struct options *opt) 353 { 本函数实现功能单一,即回复一个 Echo 应答数据包,所以完成的主要工作即进行数据包创 建,主要是协议首部创建。函数所传入参数中 saddr 从本地角度看,实际表示的是远端地址, 而 daddr 表示的是本地地址。 Echo 请求和应答 ICMP 首部格式相同,不同的只是 Type 类型字段取值,具体格式如下:Code 代码值均设置为 0。 其中 Identifier(标识)和 Sequence Number(序列号)字段用于匹配请求和应答报文。根据 以上 ECHO 请求和应答报文 ICMP 首部格式,387-392 行代码应不难理解,并注意到 include/linux/icmp.h 中如下常量定义: //include/linux/icmp.h 19 #define ICMP_ECHOREPLY 0 /* Echo Reply */ 23 #define ICMP_ECHO 8 /* Echo Request */ include/linux/icmp.h 中定义有 ICMP 所有报文类型常量。这个文件在本书第一章中已有说明。 354 struct icmphdr *icmphr; 355 struct sk_buff *skb2; 356 struct device *ndev=NULL; 357 int size, offset; 358 icmp_statistics.IcmpOutEchoReps++; 359 icmp_statistics.IcmpOutMsgs++; 360 size = dev->hard_header_len + 64 + len; 第二章 网络协议实现文件分析 LINUX-1.2.13 内核网络栈实现源代码分析 此处对这个长度字段的赋值需要进行一下说明,dev->hard_header_len 表示 MAC 首部长度, len 表示 Echo 请求数据包中 IP 数据负载长度:包括 ICMP 首部长度和可能的用户数据,Echo 应答数据包必须原样返回这些数据(另外 Echo 请求数据包中 ICMP 首部中标识字段和序列 号字段也要原样返回)。我们从包含有 IP 选项的情况考虑(如对 PING 使用-R 选项),此时 IP 首部需要 60 字节,