3

Linux Network Protocol Stack

 2 years ago
source link: https://qftm.github.io/2020/03/25/Linux-Network-Protocol-Stack/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Linux Network Protocol Stack

最近,发现网络协议栈这部分有缺陷,底层基础知识一点要熟练掌握。于是,打算开始深入学习理解Linux网络协议栈 23333!!!一切事物只有熟悉其底层原理,那么所有的问题都会迎刃而解。

Linux网络协议栈源码是Linux内核源码的重要组成部分,它不仅支持TCP/IP协议,还支持Ethernet透明网桥协议,提供IP防火墙、IP服务质量(QoS)管理及其他的安全特性。配置好的Linux系统所能提供的这些功能可以与目前使用的中档路由器、网桥等网络设备相媲美。Linux 系统还支持多种不同的网络协议,如ATM、蓝牙协议等。下面将重点讨论基于Ethernet的TCP/IP协议的设计和实现技术。

Linux网络协议栈在设计和实现思路上体现了以下3个重要特点

网络系统在设计上采用了层次化的体系结构。这种层次结构为网络协议的设计与实现提供了很大方便,上层协议可以通过相邻层之间预先定义的服务接口直接使用下层协议提供的功能,从而保证任何一-层协议实现技术的改变不会影响整个网络系统的正常运行。由于操作系统不仅要考虑网络子系统如何与其他子系统(如文件子系统和调度子系统)协调工作,还要支持各种不同的网络设备,让系统中的各个部分协调工作,所以必须采用层次化的设计方法。Linux网络协议栈的层次结构如图所示。

image-20200320092829021
image-20200320092829021

网络编程接口层

位于层次结构最高层的是网络编程接口层,它主要提供符合BSD socket API规范的接口函数,这部分就是编写网络应用程序时经常采用的socket函数,例如socket、bind、accept等,它们是应用程序使用网络服务的主要途径。

系统调用接口层

在网络编程接口层之下的是系统调用接口层,它可以实现通过虚拟文件系统的接口访问网络的功能,如在报文发送和接收流程中使用的read和write函数。

硬件设备层

位于层次结构最底层的是由各种具体硬件设备组成的硬件设备层,各种设备通过硬件厂商自己提供的驱动程序来完成工作。

虚拟设备接口层

位于硬件设备层之上的是虚拟网络设备层,Linux中采用net_device 结构来抽象地表示系统中的每一个网络硬件设备。struct net_device中定义了所有网络设备都必须保存的信息和必须支持的操作。虚拟网络设备层还可以看做是一个适配层,其作用类似于面向对象设计中的接口,它可以屏蔽底层各种硬件本身的差异,使得上层可以通过一个统一的接口来操作各种网络设备,完成报文接收与发送任务。

网络协议实现层

虚拟网络设备层之上是网络协议实现层,其中实现各种网络协议,如TCP、IP、ARP协议等。

通用网络接口层

Linux网络协议栈在网络协议实现层之上增加了一个通用网络接口层。设置通用网络接口层的目的与虚拟网络设备层十分类似,它为各种网络协议提供了一个服务接口。这样系统中的其他子系统就可以直接使用网络服务,而无需依赖特定的协议或硬件。

一方面,层次化设计本身就要求把每一层作为一个独立的模块来实现;另一方面,由于Linux网络协议栈支持的网络协议和实现的网络功能比较多,所以通常情况下这些功能或协议不会同时被使用,这就要求网络协议栈能够根据实际情况,采取“按需加载”的方法,选择所要求的功能与协议。通过采用模块化的实现方式,不仅能够提高系统的工作效率,而且便于以后增加新的功能或协议。下图给出Linux中TCP/IP协议栈中的各个主要功能模块之间的关系示意图。

image-20200320094302966
image-20200320094302966

每个模块的设计目标都很明确,一个模块只用来完成一项任务,如IPv4 模块只完成IPv4分组的接收、发送、合法性判断、分片重组等任务,而路由选择功能则交给IP路由模块来完成。为降低模块之间的耦合度,减小相互之间的影响,模块与模块之间的界面多采用函数指针作为接口。这样,当一个模块的实现方式发生变化时,不影响另一个模块的正常工作。

面向对象的设计

Linux内核里很多子系统都采用了面向对象的设计方法,最典型的例子就是虚拟文件系统(virtual file system, VFS)。Linux 内核支持很多种文件系统,比如VFAT、EXT2、JFS等。但是在内核里面,采用一个抽象的基类VFS来表示所有的文件系统,并且定义了文件系统的操作界面(接口函数),然后每一种文件系统再根据VFS进行实例化。

网络协议栈部分也有多处采用了面向对象的设计方法,典型的例子有以下两个

ARP协议是获取网络结点IP地址与MAC地址映射关系的协议。但在其他协议族(如ATM、X25)中还存在另外几种不同的地址映射关系。Linux 网络协议栈对此进行了适当的抽象,采用“邻居”的概念来管理相邻的计算机,这样不同协议族都采用相同的接口来管理邻居。

socket接口

由于Linux网络协议栈支持20多种协议,因此它在内核中采取向用户层提供一个统一的BSD socket接口的方法。这样做的最大优点是允许应用程序在利用不同的网络协议通信时,能够采用相同的函数完成数据的发送与接收。

采用面向对象设计方法的主要目的是能够获得多态特性。这种设计在实现上采用函数指针,同样一个调用形式,被执行时会根据指针赋值情况的不同而调用不同的函数,从而表现出不同的行为。当需要支持新的协议时,只需要提供实现了所需功能的处理函数,然后让函数指针指向新协议的处理函数即可,这样便可以在不需要修改调用函数代码的情况下,动态增加对新协议的支持。这种设计方式极大地提高了代码的可扩展性和
灵活性。

需要注意的是:灵活性与复杂性总是相伴而生的。采用函数指针作为函数调用的形式之后,会给阅读源代码、追踪和分析程序的处理流程带来很大困难,即无法从调用语句本身确定被调用的函数。因此,在本章分析报文发送和接收流程时,会特别强调代码执行过程中对函数指针的处理情况。

固定实现模式

除了上面介绍的3个设计特点之外,Linux网络协议栈的代码中还使用了很多固定的实现模式,下面会介绍几个模式。

网络协议栈为了提高处理效率而使用缓存,具体的实例包括保存路由结果的struct rtable结构和ARP缓存等。通常使用哈希表来实现缓存,内核中提供了用于实现哈希表的基本数据结构:数组、单向和双向链表。具体的哈希函数要根据缓存对象的特征来进行选择,有些情况下会在键值中加人一些随机特征来防止针对哈希表的拒绝服务(Denial of Service,DoS)攻击。

内核中的很多数据结构都可能被不同CPU上的不同进程所共享,这里就涉及如何进行垃圾收集的问题,即只有不被任何进程使用的数据结构才能被释放,否则就会引起空指针等严重的问题。因此网络协议栈中的很多数据结构都使用了一个引用计数字段。使用该结构的用户在使用之前增加引用计数的值,在使用之后再减少计数值。当引用计数值减少到0时,就表明这块数据已经不再有用户使用,应该释放其占用的内存。在Linux内核源代码中包含引用计数字段的数据结构往往会同时提供两个专门的函数来增加和减少计数值,这类函数的名称通常为: xxx_ hold 和xxx_ relcase(或xxx_ put, 例如net_ device 结构提供的dev_ put 函数)。如果在释放数据结构时忘记调用xxx_ release 函数减少计数值,则可能造成数据永远得不到释放,导致内存泄露;如果在使用数据结构时忘记调用xxx_hold函数来增加计数值,则可能导致当前使用的数据被提前释放,造成空指针问题。特别需要指出的是,在使用哈希表或链表提供的查询函数获取其中元素时,查询函数往往会自动增加该元素:的计数值,所以在使用完之后不能忘记手工减少计数值。

通过在结构体中定义函数指针成员,C语言也可以编写出具有面向对象特性的程序。使用函数指针的最大优点是可以根据情况将指针初始化为不同的函数,从而做到同一种静态调用形式具有不同动态行为的能力,即多态性。在Linux网络协议栈中使用函数指针的情况主要有3种。第一,作为层与层之间的接口实现一对多的映射关系,如BSD socket层和具体协议族的socket实现之间就借助于一组函数指针(定义在proto_ ops 结构中),获得接口和多态的特性;第二,根据状态或其他模块的处理结果来选择具体的处理函数,如ARP模块的发送函数指针output会根据ARP缓存的状态来决定采用哪一种发送丽数;第三,实现一些自定义的处理,例如在net_ device 中定义的函数指针init 就可以执行设备提供的自定义初始化函数来完成特殊处理,无需特殊处理时函数指针为空。

函数指针的最大缺点是会给阅读源代码带来困难。当在某条代码执行路径上遇到函数指针时,必须首先找出对该指针的初始化情况。常见的初始化条件包括某些报文首部字段或状态,例如,在将报文投递给上层处理函数时,会根据报头中上层协议字段的值来初始化函数指针;在ARP模块中则会根据缓存状态让函数指针指向合适的处理函数。

TCP/IP协议栈中主要模块简介

下面按照从上层到下层的顺序来对网络协议栈的各个子模块进行介绍。

路由子系统

路由是IP层的主要任务之一,也是IP层最复杂的模块。当主机向外发送IP分组时,要根据IP分组的目的地址查询它的路由。Linux网络协议栈将与IP路由有关的信息集中存放在转发信息块(Forwarding Information Block,FIB)中,转发路由信息库包括路由规则(Routing Rules)和路由表( Routing Tables) 两个部分。路由规则用来实现基于规则的路由,又称为策略路由,即对满足某些条件的报文,应用特定的路由表来决定路由信息。系统中最多可以定义255张路由表,在不启用策略路由的情况下只使用其中两张:主路由表(main_table)用于描述主机间路由规则;局部路由表(local_table)只用于记录本地地址信息。如果分组的路由决策来自局部路由表,则说明这是发给本机的分组。通过这样一个特殊的路由表,路由子系统能够以统一的方式来处理所有IP分组。

路由表本身不是一个单一的结构,而是由多个结构组合而成,并且采用层次结构来组织和管理这些实现路由表的结构。这些数据结构之间的关系如下图所示

image-20200325093657873
image-20200325093657873

Internet上的大多数网络应用都属于单播通信,即将分组从一台主机发送到另一台主机。但有些应用(如视频会议)要求同时有多个发送主机和多个接收主机,这样的通信方式称为组播(或多播)。上面介绍的IP路由子系统主要是针对单播模式,但Linux网络协议栈同样也支持组播模式。要实现组播通信,网络协议栈必须实现两种功能:管理组播通信的成员、高效地把数据分发给组播成员。

对于第一种功能,Linux协议栈实现了标准的IGMP协议,具体的代码在net/ipv4/igmp.c中。IGMP协议数据必须作为IP分组的负载,在IP报头中会指明负载类型,进而调用接收IGMP报文的处理函数igmp_rev。

对于第二种功能,通常在组成员之间建立一棵组播树,发送主机作为树的根节点,把分组发送给作为树的叶子节点的接收主机,这个过程称为组播路由算法。树的中间节点只负责转发而不需要接收组播分组,因此网络协议栈也必须区分接收和转发的情况。目前常用的组播路由算法包括DVMRP,MOSPF等。组播路由算法大多以守护程序的形式在用户空间实现。但转发和接收组播报文的处理必须在内核协议栈中完成,这部分代码在net/
ipv4/ipmr.c中。

IPv6模块

IPv6将地址长度从32bit扩展为128bit,同时其在报头格式、报头扩展选项等方面与IPv4存在差异。Linux 网络协议栈中实现IPv6的代码保存在net/ipv6 目录下及头文件include/net/ipv6.h中。IPv6协议的代码以IPv4的实现为基础,尽可能复用一些与具体IP协议版本无关的处理代码。

  • 处理数据链路层报头时,发现网络层使用IPv6协议, 调用函数ipv6_rcv来接收并处理该报文

  • 当上层协议发送报文时,调用IPv6的发送函数 ip6_xmit

  • 通过IPv6来发送ICMP报文时调用icmpv6_send函数

报文过滤和防火墙模块

Linux使用Netfilter框架来实现防火墙的功能。从Linux2. 4内核开始,内核设计者在网络协议栈中预留出若干函数接口,开发人员可以通过这些接口实现报文过滤、报文处理、NAT等功能,这套函数接口构成了Netfilter 框架。Netfilter 框架包含以下3个部分:

  • 为每种网络协议(IPv4/v6)定义一套钩子函数,钩子函数在报文流过协议栈的几个关键点调用。在这几个钩子函数点上,协议栈将把报文及钩子函数标号作为参数传递给Netfilter框架。

  • 内核中的任何模块都可以在每种协议的一个或多个钩子点 上注册,实现挂接。

  • 用户进程可以采用异步方式处理传递到用户空间的报文。

Linux中所有报文过滤和NAT等功能都基于该框架。目前Netfilter 框架已在IPv4和IPv6协议栈中实现了。

下图显示了Netfilter框架中钩子函数在协议栈中的位置,从图中可以看到IPv4共有5个钩子函数点

image-20200325095747404
image-20200325095747404

Iptables是一个基于Netfilter框架的报文过滤和修改工具。Iptables模块可以创建规则表(table) ,并要求报文流经指定的规则表。在Iptables 中,预先定义了3张规则表,分别实现3种不同的概念:报文过滤(filter表)、网络地址转换(nat表)及报文处理(mangle表)。

  • 报文过滤(filter表)

NF_IP_LOCAL_IN、NF_IP_FORWARD、NF_IP_LOCAL_OUT

  • 网络地址转换(nat表)

NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING、NF_IP_LOCAL_OUT

  • 报文处理(mangle表)

NF_IP_PRE_ROUTING、NF_IP_LOCAL_OUT

Netfilter是表的容器,表是链的容器,链是规则的容器(规则定义形式为“如果报头符合某个条件,就按照某种方法处理这个报文” )

Linux网络协议栈中实现的防火墙不仅可以实现报文过滤,还可以跟踪会话状态。

邻居子系统

网络转发分组过程中,需要将分组转发给相邻主机,需要知道相邻主机的第2层和第3层地址间的映射关系。IPv4的ARP协议,IPv6的邻居发现协议。

不同的协议有不同的方法解决地址映射问题,linux 邻居子系统把公用部分抽象出来,形成一个与具体协议无关的服务接口,主要功能:缓存机制和超时机制。

  • 3层和2层地址映射关系,2层帧头

  • 老化和超时机制维护映射关系

  • 网桥是一种工作在第2层的网络互联设备,用于数据链路层上将两个或多个物理上独立的局域网连接成 一个网段。

  • 网桥和交换机本质上相同,前者是描述第2层连接设备和说明生成树(STP)算法,而后者多指实际的网络设备。

  • Linux网络协议栈中实现了透明网桥的功能。

  • 通过和Netfilter/Iptables和Ebtables框架结合,构建网桥式防火墙。

流量控制管理

  • Linux网络协议栈既可以控制向外的流量,也可以控制向内的流量。

  • 流量控制通过3种对象实现,Qdisc(排队规则)、Class(分类)和Filter(过滤器)。

原始套接字和PACKET协议族

在Linux中实现的TCP/IP网络协议栈源码不仅能够完成TCP和UDP协议的发送和接收流程,还支持两类比较特殊的报文处理方法: INET协议族的原始套接字(RAW socket)和PACKET协议族。

INET协议族的原始套接字

  • 发送:用户空间构造协议报文,利用RAW Socket将数据传 给内核,内核增加合法的IP报头发送。
  • 接收:程序中创建了一个SOCK_RAW类型的socket,并制定 目标协议之后,系统使用raw_v4_htable对网卡收到数据包 匹配,将ip报头中指定的协议类型和socket指定的协议类型 相同、socket绑定的本地地址和IP报头目的地址相同、 socket绑定的目的地址和IP报头的源地址相同,可能有多个 socket同时满足条件。

PF_PACKET类型的socket

  • 是一种允许应用程序不经过网络协议栈而直接读写网络设备的接口。

  • 应用程序创建的PF_PACKET报文直接交给网络接口设备发送,接收到报文,直接投递给相应的socket。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK