Linux-内核网络教程-全-

Linux 内核网络教程(全)

原文:Linux kernel networking

协议:CC BY-NC-SA 4.0

零、前言

这本书引导你深入了解当前的 Linux 内核网络实现及其背后的理论。近十年来,没有关于 Linux 网络的新书问世。十年动态和快节奏的 Linux 内核开发是一段相当长的时间。有一些重要的内核网络子系统没有在任何其他书中描述;例如,IPv6、IPsec、无线(IEEE 802.11)、IEEE 802.15.4、NFC、InfiniBand 等等。关于这些子系统的实现细节,网上的信息也很少。出于所有这些原因,我写了这本书。

大约十年前,我迈出了内核编程的第一步。我是一家初创公司的开发人员,参与了一个基于 Linux 的机顶盒(STB)的 VoIP 项目。一些 USB 摄像头的 USB 堆栈出现崩溃,我们不得不钻研代码来试图找到解决方案,因为该机顶盒的供应商不想花时间来解决问题。事实上,他们并不是不想做,而是不知道怎么做。在那些日子里,几乎没有关于 USB 堆栈的文档。当年 O'Reilly 出的 Linux 设备驱动书只有第二版(第三版才加了 USB 一章)。作为一家初创公司,该项目的成功对我们来说至关重要。在解决 USB 崩溃的过程中,我学到了很多关于内核编程的知识。后来,我们有一个项目需要 NAT 穿越解决方案。用户空间解决方案太重,以至于设备很快就崩溃了。当我提出一个内核解决方案时,我的经理非常怀疑,但他们确实让我尝试了。事实证明,内核解决方案非常稳定,占用的 CPU 比用户空间解决方案少得多。从那以后,我参加了许多内核网络项目。这本书是我多年开发和研究的成果。

这本书是给谁的

本书面向从事网络相关项目的计算机专业人员,包括开发人员、软件架构师、设计人员、项目经理和首席技术官。这些项目可能涉及广泛的专业领域,如通信、数据中心、嵌入式设备、虚拟化、安全等。此外,处理网络项目或网络研究或操作系统研究的学生和学院研究人员和理论家将在本书中找到大量帮助。

这本书的结构

在第一章中,你会看到 Linux 内核和 Linux 网络栈的概述。本章的其他主题包括网络设备的实现、套接字缓冲区以及 Rx 和 Tx 路径。第一章以一个关于 Linux 内核网络开发模型的部分结束。

在第二章中,您将了解 netlink 套接字,它提供了用户空间和内核之间的双向通信机制,网络子系统和其他子系统都使用它。你还会在本章中找到一个关于通用 netlink 套接字的部分,它可以被视为高级 netlink 套接字,你会在第十二章和浏览内核网络源代码时遇到。

在第三章中,您将了解 ICMP 协议,它通过发送有关网络层(L3)的错误和控制消息来帮助保持系统正常运行。您将了解 ICMP 协议在 IPv4 和 IPv6 中的实现。

第四章深入探讨 IPv4 协议——互联网和现代生活离不开它。您将了解 IPv4 报头的结构、Rx 和 Tx 路径、IP 选项、碎片和碎片整理及其必要性,以及转发数据包,这是 IPv4 的重要任务之一。

第五章和第六章专门讨论 IPv4 路由子系统。在第五章中,您将了解路由子系统中的查找是如何执行的,路由表是如何组织的,IPv4 路由子系统中使用了哪些优化,以及 IPv4 路由缓存的移除。第六章讨论高级路由主题,如多播路由、策略路由和多路径路由。

第七章试图解释邻近的子系统。您将了解 IPv4 中使用的 ARP 协议,IPv6 中使用的 NDISC 协议,以及这两种协议之间的一些差异。您还将了解 IPv6 中的重复地址检测(DAD)机制。

第八章讨论 IPv6 协议,这似乎是解决 IPv4 地址短缺的必然方案。本章介绍了 IPv6 的实施,并讨论了 IPv6 地址、IPv6 报头和扩展报头、IPv6 中的自动配置、Rx 路径和转发等主题。它还描述了 MLD 协议。

第九章讲述了 netfilter 子系统。您将了解 netfilter 挂钩及其注册方式、连接跟踪、IP 表和网络地址转换(NAT)以及连接跟踪和 NAT 使用的回调。

第十章讨论 IPsec,它是最复杂的网络子系统之一。像 ike 协议(在用户空间中实现)和 IPsec 的加密方面的主题被简单地讨论了(完整的讨论超出了本书的范围)。您将了解 XFRM 框架,它是 Linux IPsec 子系统的基础,以及它的两个最重要的结构:XFRM 策略和 XFRM 状态。简要介绍了 ESP 协议,以及传输模式下的 IPsec Rx 路径和 Tx 路径。这一章以 XFRM 查找的一节和 NAT 穿越的一小段结束。

第十一章描述了四种第 4 层协议,从最常用的协议 UDP 和 TCP 开始,以两种较新的协议 SCTP 和 DCCP 结束。

第十二章讨论 Linux (IEEE 802.11)中的无线技术。您将了解 mac80211 子系统及其实现、各种无线网络拓扑、节能模式以及 IEEE 802.11n 和数据包聚合。本章中还有一节专门讨论无线网状网络。

第十三章深入探讨 InfiniBand 子系统,这是一项在数据中心越来越受欢迎的技术。您将了解 RDMA 堆栈组织、InfiniBand 中的寻址、InfiniBand 数据包的组织以及 RDMA API。

第十四章以对高级主题的讨论结束了这本书,例如 Linux 名称空间,特别是网络名称空间、忙轮询套接字、蓝牙子系统、IEEE 802.15.4 子系统、近场通信(NFC)子系统、PCI 子系统等等。

附录 A“Linux API”和 C,“词汇表”,为书中讨论的许多主题提供了完整的参考信息。附录 B“网络管理”提供了使用 Linux 内核网络时需要的各种工具的信息。

约定

在整本书中,我保持了一贯的风格。所有代码片段,无论是在文本段落内还是在它们自己的行上,连同库路径、shell 命令、URL 和其他与代码相关的元素,都以等宽字体设置,就像这样。新术语用斜体表示,其他强调可能用粗体表示。

一、简介

这本书讨论了 Linux 内核网络栈的实现及其背后的理论。在接下来的几页中,您将看到对网络子系统及其架构的深入而详细的分析。我不会用与网络不直接相关的主题来增加您的负担,您可能会在阅读内核网络代码时遇到这些主题(例如,锁定和同步、SMP、原子操作等等)。关于这些主题有很多资源。另一方面,专注于内核网络本身的最新资源非常少。我的意思是主要描述包在 Linux 内核网络栈中的遍历及其与各种网络层和子系统的交互——以及各种网络协议是如何实现的。

这本书也不是一个繁琐的,逐行代码演练。我重点关注每个网络层实现的本质,以及导致这种实现的理论指导方针和原则。近年来,Linux 操作系统已经证明了自己是一个成功、可靠、稳定和受欢迎的操作系统。它的受欢迎程度似乎在稳步增长,种类繁多,从大型机、数据中心、核心路由器和 web 服务器到嵌入式设备,如无线路由器、机顶盒、医疗仪器、导航设备(如 GPS 设备)和消费电子设备。许多半导体供应商使用 Linux 作为他们的板支持包(bsp)的基础。Linux 操作系统始于 1991 年一个名叫 Linus Torvalds 的芬兰学生的项目,基于 UNIX 操作系统,被证明是一个严肃可靠的操作系统,是老牌专有操作系统的竞争对手。

Linux 最初是基于 Intel x86 的操作系统,但是已经移植到非常广泛的处理器上,包括 ARM、PowerPC、MIPS、SPARC 等等。基于 Linux 内核的 Android 操作系统在今天的平板电脑和智能手机中很常见,并且似乎有可能在未来的智能电视中获得普及。除了 Android 之外,Google 还贡献了一些合并到主线内核中的内核网络特性。

Linux 是一个开源项目,因此它比其他专有操作系统更有优势:它的源代码可以在通用公共许可证(GPL)下免费获得。其他开源操作系统,如不同类型的 BSD,就没那么受欢迎了。在这种情况下,我还应该提到 OpenSolaris 项目,该项目基于通用开发和分发许可证(CDDL)。这个由 Sun 微系统公司发起的项目并没有像 Linux 那样流行。在活跃的 Linux 开发人员的大型社区中,有些人代表他们工作的公司贡献代码,有些人自愿贡献代码。所有的内核开发过程都可以通过内核邮件列表访问。有一个中央邮件列表,Linux 内核邮件列表(LKML),,许多子系统都有自己的邮件列表。贡献代码是通过向适当的内核邮件列表和维护者发送补丁来完成的,这些补丁在邮件列表中讨论。

Linux 内核网络栈是 Linux 内核的一个非常重要的子系统。很难找到一个基于 Linux 的系统,无论是台式机、服务器、移动设备还是任何其他嵌入式设备,不使用任何类型的网络。即使在机器没有任何硬件网络设备的罕见情况下,当您使用 X-Windows 时,您仍然会使用网络(可能是无意识的),因为 X-Windows 本身是基于客户机-服务器网络的。从核心路由器到小型嵌入式设备,许多项目都与 Linux 网络堆栈有关。其中一些项目处理添加特定于供应商的特性。例如,一些硬件供应商在一些网络设备中实现通用分段卸载(GSO)。GSO 是内核网络堆栈的一项网络功能,它将 Tx 路径中的一个大数据包分成较小的数据包。许多硬件供应商在其网络设备的硬件中实现校验和。校验和是一种验证数据包在传输过程中未被损坏的机制,通过计算数据包中的一些哈希并将其附加到数据包中。许多项目为 Linux 提供了一些安全性增强。有时候,这些增强需要在网络子系统中做一些改变,例如,你会在第三章中看到,当讨论 Openwall GNU/*/Linux 项目时。例如,在嵌入式设备领域,有许多基于 Linux 的无线路由器;运行 Linux 的 WRT54GL Linksys 路由器就是一个例子。还有一个开源的、基于 Linux 的操作系统,可以在这个设备(以及其他一些设备)上运行,名为 OpenWrt,拥有一个庞大而活跃的开发者社区(见https://openwrt.org/)。了解 Linux 内核网络栈是如何实现各种协议的,并熟悉其中的主要数据结构和包的主要路径,对于更好地理解它是必不可少的。

Linux 网络堆栈

根据开放系统互连(OSI)模型,有七个逻辑网络层。最低层是物理层,即硬件,最高层是应用层,用户空间软件进程在此运行。让我们来描述这七层:

  1. 物理层: 处理电信号和底层细节。
  2. 数据链路层: 处理端点之间的数据传输。最常见的数据链路层是以太网。Linux 以太网设备驱动程序位于这一层。
  3. 网络层: 处理包转发和主机寻址。在本书中,我讨论了 Linux 内核网络子系统最常见的网络层:IPv4 或 IPv6。Linux 还实现了其他一些不太常见的网络层,比如 DECnet,但我们不会讨论它们。
  4. 协议层/传输层: 处理节点间的数据发送。TCP 和 UDP 协议是最著名的协议。
  5. 会话层: 处理端点之间的会话。
  6. 表示层: 处理交付和格式化。
  7. 应用层: 为最终用户应用提供网络服务。

图 1-1 显示了 OSI 模型的七层。

9781430261964_Fig01-01.jpg

图 1-1 。OSI 七层模型

图 1-2 显示了 Linux 内核网络栈处理的三层。此图中的 L2、L3 和 L4 层分别对应于七层模型中的数据链路层、网络层和传输层。Linux 内核栈的本质是将传入的数据包从 L2(网络设备驱动程序)传递到 L3(网络层,通常是 IPv4 或 IPv6) ,然后传递到 L4(传输层,在那里你有,例如,TCP 或 UDP 监听套接字),如果它们是用于本地传递,或者当数据包应该被转发时返回到 L2 进行传输。本地生成的传出数据包从 L4 传递到 L3,然后通过网络设备驱动程序传递到 L2 进行实际传输。沿着这条路有许多阶段,许多事情会发生。例如:

  • 由于协议规则(例如,由于 IPsec 规则或 NAT 规则),数据包可以被改变。
  • 该分组可以被丢弃。
  • 该数据包会导致发送错误消息。
  • 该分组可以被分段。
  • 可以对数据包进行碎片整理。
  • 应该为数据包计算校验和。

9781430261964_Fig01-02.jpg

图 1-2 。Linux 内核网络层

内核不处理 L4 以上的任何层;这些层(会话层、表示层和应用层)由用户空间应用单独处理。物理层(L1)也不由 Linux 内核处理。

如果你感到不知所措,不要担心。在接下来的章节中,你将会更深入地了解这里所描述的一切。

网络设备

如图 1-2 中的所示,下层,第二层(L2)是链路层。网络设备驱动程序位于这一层。这本书不是关于网络设备驱动程序开发的,因为它关注的是 Linux 内核网络栈。我将在这里简单描述一下代表网络设备的net_device结构,以及与之相关的一些概念。为了更好地理解网络堆栈,您应该对网络设备结构有一个基本的了解。设备的参数(如 MTU 的大小,以太网设备通常为 1,500 字节)决定了数据包是否应该被分段。net_device是一个非常大的结构,由如下设备参数组成:

  • 设备的 IRQ 号。
  • 设备的 MTU。
  • 设备的 MAC 地址。
  • 设备的名称(如eth0eth1)。
  • 设备的标志(例如,是打开还是关闭)。
  • 与设备相关的多播地址列表。
  • promiscuity计数器(将在本节稍后讨论)。
  • 设备支持的功能(如 GSO 或 GRO 卸载)。
  • 网络设备回调的一个对象(net_device_ops object),由函数指针组成,比如打开和停止一个设备,开始传输,改变网络设备的 MTU 等等。
  • 一个ethtool回调的对象,它支持通过运行命令行ethtool实用程序来获取关于设备的信息。
  • 当设备支持多队列时,发送和接收队列的数量。
  • 此设备上最后一次传输数据包的时间戳。
  • 此设备上最后一次接收数据包的时间戳。

下面是一些net_device结构成员的定义,给你一个初步印象:

struct net_device {
    unsigned int            irq;            /* device IRQ number    */
    . . .
    const struct net_device_ops *netdev_ops;
    . . .
    unsigned int            mtu;
    . . .
    unsigned int            promiscuity;
    . . .
    unsigned char           *dev_addr;
    . . .
};
(include/linux/netdevice.h)

本书的附录 A 包含了对net_device结构及其大部分成员的详细描述。在那个附录中你可以看到irqmtu以及本章前面提到的其他成员。

promiscuity计数器大于 0 时,网络堆栈不会丢弃目的地不是本地主机的数据包。例如,像tcpdumpwireshark这样的数据包分析器(“嗅探器”)就使用这种方法,它们在用户空间中打开原始套接字,并希望接收这种类型的流量。它是一个计数器,而不是一个布尔值,以便能够同时打开几个嗅探器:打开每一个这样的嗅探器都会使计数器加 1。当嗅探器关闭时,promiscuity计数器减 1;如果达到 0,则不再有嗅探器运行,设备退出混杂模式。

在浏览内核联网核心源代码时,在各个地方你很可能会遇到 NAPI (New API)这个术语,这是现在大多数网络设备驱动都实现的一个特性。你应该知道它是什么,为什么网络设备驱动程序使用它。

网络设备中的新 API (NAPI)

旧的网络设备驱动程序工作在中断驱动模式下,这意味着对于每个收到的数据包,都有一个中断。事实证明,这在高负载流量下的性能方面是低效的。一种新的软件技术被开发出来,称为新 API (NAPI),现在几乎所有的 Linux 网络设备驱动程序都支持它。NAPI 最初是在 2.5/2.6 内核中引入的,后来被移植到 2.4.20 内核中。对于 NAPI,在高负载下,网络设备驱动程序工作在轮询模式,而不是中断驱动模式。这意味着每个收到的包不会触发中断。相反,数据包被缓存在驱动程序中,内核不时地轮询驱动程序以获取数据包。使用 NAPI 可以提高高负载下的性能。对于需要尽可能低的延迟并愿意为更高的 CPU 利用率付出代价的套接字应用,Linux 从内核 3.11 和更高版本添加了对套接字进行繁忙轮询的功能。这项技术将在第十四章的“忙轮询套接字”一节中讨论。

有了关于网络设备的新知识,现在是时候了解包在 Linux 内核网络栈中的遍历了。

接收和发送数据包

网络设备驱动程序的主要任务如下:

  • 接收目的地为本地主机的数据包,并将它们传递到网络层(L3),然后从那里传递到传输层(L4)
  • 传输本地主机生成并发送到外部的传出数据包,或者转发本地主机收到的数据包

对于每个数据包,无论是传入的还是传出的,都会在路由子系统中进行查找。基于路由子系统中的查找结果来决定是否应该转发数据包以及应该在哪个接口上发送数据包,我将在第五章和第六章中对此进行详细描述。路由子系统中的查找并不是决定数据包在网络堆栈中遍历的唯一因素。例如,网络堆栈中有五个点可以注册 netfilter 子系统(通常称为 netfilter 钩子)的回调。在执行路由查找之前,接收到的数据包的第一个 netfilter 挂钩点是 NF_INET_PRE_ROUTING。当一个包被这样一个回调处理时,这个回调由一个名为 NF_HOOK()的宏调用,它将根据这个回调的结果继续在网络堆栈中遍历(也称为verdict)。例如,如果verdict是 NF_DROP,数据包将被丢弃,如果verdict是 NF_ACCEPT,数据包将照常继续遍历。Netfilter 钩子回调由nf_register_hook()方法或nf_register_hooks()方法注册,例如,在各种 netfilter 内核模块中,你会遇到这些调用。内核 netfilter 子系统是众所周知的iptables用户空间包的基础设施。第九章描述了 netfilter 子系统和 netfilter 挂钩,以及 netfilter 的连接跟踪层。

除了 netfilter 挂钩之外,数据包遍历还会受到 IPsec 子系统的影响,例如,当它与配置的 IPsec 策略匹配时。IPsec 提供了一个网络层安全解决方案,它使用 ESP 和 AH 协议。根据 IPv6 规范,IPsec 是强制性的,而在 IPv4 中是可选的,尽管包括 Linux 在内的大多数操作系统也在 IPv4 中实现了 IPsec。IPsec 有两种操作模式:传输模式和隧道模式。它被用作许多虚拟专用网络(VPN) 解决方案、的基础,尽管也有非 IPsec VPN 解决方案。您将在第十章中了解 IPsec 子系统和 IPsec 策略,该章还讨论了通过 NAT 使用 IPsec 时出现的问题,以及 IPsec NAT 穿越解决方案。

还有其他因素会影响数据包的遍历,例如,正在转发的数据包的 IPv4 报头中的ttl字段的值。此ttl在每个转发设备中递减 1。当它达到 0 时,数据包被丢弃,并且发送回一个带有“超过 TTL 计数”代码的“超时”ICMPv4 消息。这样做是为了避免由于某些错误而导致转发数据包的无休止的旅程。此外,每次成功转发数据包并且ttl减 1 时,都应该重新计算 IPv4 报头的校验和,因为其值取决于 IPv4 报头,并且ttl是 IPv4 报头成员之一。第四章,处理 IPv4 子系统,更多的谈论这个。在 IPv6 中有一些类似的东西,但是 IPv6 报头中的跳计数器被命名为hop_limit而不是ttl。您将在第八章中了解到这一点,该章涉及 IPv6 子系统。您还将在第三章的中了解 IPv4 和 IPv6 中的 ICMP。

该书的很大一部分讨论了数据包在网络堆栈中的遍历,无论是在接收路径(Rx 路径,也称为入口流量)还是传输路径(Tx 路径,也称为出口流量)。这种遍历是复杂的,并且有许多变化:大的包在发送之前可能被分段;另一方面,应该将碎片化的数据包组装起来(在第四章的中讨论)。不同类型的数据包被不同地处理。例如,多播数据包是可以由一组主机处理的数据包(与单播数据包相反,单播数据包的目的地是指定的主机)。例如,多播可以用于流媒体应用中,以便消耗更少的网络资源。处理 IPv4 多播流量在第四章的中讨论。您还将了解主机如何加入和离开多播组;在 IPv4 中,互联网组管理协议(IGMP)协议处理多播成员资格。然而,也有主机被配置为组播路由器的情况,组播流量应该被转发而不是传送到本地主机。这些情况更加复杂,因为它们应该与用户空间多播路由守护进程一起处理,如pimd守护进程或mrouted守护进程。这些情况称为多播路由,在第六章中讨论。

为了更好地理解包遍历,您必须了解包在 Linux 内核中是如何表示的。sk_buff结构表示一个输入或输出的数据包,包括其报头(include/linux/skbuff.h)。在本书的许多地方,我将一个sk_buff对象称为 SKB,因为这是表示sk_buff对象的通用方式(SKB 代表套接字缓冲区)。套接字缓冲区(sk_buff)结构是一个很大的结构——在本章中我将只讨论这个结构的几个成员。

套接字缓冲区

sk_buff结构在附录 A 中有详细描述。当你需要了解更多关于 SKB 成员或者如何使用 SKB API 时,我推荐你参考这个附录。请注意,在使用 SKBs 时,您必须遵守 SKB API。因此,举例来说,当你想要推进skb->data指针时,你不直接这样做,而是用skb_pull_inline()方法或skb_pull()方法(你将在本节后面看到一个这样的例子)。如果您想从 SKB 获取 L4 报头(传输报头),您可以通过调用skb_transport_header()方法来完成。同样,如果您想获取 L3 报头(网络报头),您可以通过调用skb_network_header()方法来完成,如果您想获取 L2 报头(MAC 报头),您可以通过调用skb_mac_header()方法来完成。这三种方法将 SKB 作为单个参数。

下面是sk_buff结构的(部分)定义:

struct sk_buff {
    . . .
    struct sock             *sk;
    struct net_device       *dev;
    . . .
    __u8                    pkt_type:3,
    . . .
    __be16                  protocol;
    . . .
    sk_buff_data_t          tail;
    sk_buff_data_t          end;
    unsigned char           *head,
                            *data;

    sk_buff_data_t          transport_header;
    sk_buff_data_t          network_header;
    sk_buff_data_t          mac_header;
    . . .

};
(include/linux/skbuff.h)

当网络上接收到一个数据包时,网络设备驱动程序会分配一个 SKB,通常是通过调用netdev_alloc_skb()方法(或dev_alloc_skb()方法,这是一个调用第一个参数为空的netdev_alloc_skb()方法的遗留方法)。在包遍历的过程中,有时会丢弃一个包,这是通过调用kfree_skb()dev_kfree_skb()来实现的,这两个函数都以单个参数的形式获得一个指向 SKB 的指针。SKB 的一些成员是在链路层(L2)确定的。例如,pkt_typeeth_type_trans()方法根据目的以太网地址确定。如果这个地址是组播地址,pkt_type将被设置为 PACKET _ MULTICAST 如果该地址是广播地址,则pkt_type将被设置为 PACKET _ BROADCAST 如果这个地址是本地主机的地址,那么pkt_type将被设置为 PACKET_HOST。大多数以太网网络驱动程序在它们的 Rx 路径中调用eth_type_trans()方法。eth_type_trans()方法还根据以太网报头的ethertype设置 SKB 的protocol字段。eth_type_trans()方法还通过调用skb_pull_inline()方法将 SKB 的data指针提前 14 (ETH_HLEN),这是以太网报头的大小。这样做的原因是skb->data应该指向它当前所在的层的头。当数据包在 L2 时,在网络设备驱动程序的 Rx 路径中,skb->data指向了 L2(以太网)报头;既然数据包将被移动到第 3 层,在调用eth_type_trans()方法后,skb->data将立即指向网络(L3)报头,这在以太网报头后立即开始(见图 1-3 )。

9781430261964_Fig01-03.jpg

图 1-3 。一个 IPv4 数据包

SKB 包括数据包报头(L2、L3 和 L4 报头)和数据包有效载荷。在网络堆栈中的数据包遍历中,可以添加或删除报头。例如,对于由套接字在本地生成并传输到外部的 IPv4 数据包,网络层(IPv4)会在 SKB 中添加 IPv4 报头。IPv4 报头大小最小为 20 字节。添加 IP 选项时,IPv4 报头最大可达 60 字节。IP 选项在第四章中描述,该章讨论了 IPv4 协议的实现。图 1-3 显示了一个带有 L2、L3 和 L4 报头的 IPv4 数据包的例子。图 1-3 中的例子是一个 UDPv4 包。首先是 14 字节的以太网报头(L2)。然后是最小大小为 20 字节到 60 字节的 IPv4 报头(L3 ),之后是 8 字节的 UDPv4 报头(L4)。然后是数据包的有效载荷。

每个 SKB 都有一个dev成员,它是net_device结构的一个实例。对于传入数据包,它是传入网络设备,对于传出数据包,它是传出网络设备。有时需要连接到 SKB 的网络设备来获取信息,这些信息可能会影响 SKB 在 Linux 内核网络堆栈中的遍历。例如,如前所述,网络设备的 MTU 可能需要分段。每个传输的 SKB 都有一个与之关联的sock对象(sk)。如果数据包是转发的数据包,那么sk为空,因为它不是在本地主机上生成的。

每个收到的数据包都应该由匹配的网络层协议处理程序来处理。例如,IPv4 数据包应该由ip_rcv()方法处理,IPv6 数据包应该由ipv6_rcv()方法处理。您将在第四章的中学习使用dev_add_pack()方法注册 IPv4 协议处理程序,并在第八章的中学习使用dev_add_pack()方法注册 IPv6 协议处理程序。此外,我将跟踪 IPv4 和 IPv6 中传入和传出数据包的遍历。例如,在ip_rcv()方法中,大多数情况下会执行健全性检查,如果一切正常,数据包会进入 NF_INET_PRE_ROUTING 钩子回调(如果此回调已注册),如果此钩子没有丢弃数据包,则下一步是ip_rcv_finish()方法,在路由子系统中执行查找。路由子系统中的查找建立了目的缓存条目(dst_entry对象)。在描述 IPv4 路由子系统的第五章和第六章中,您将了解到dst_entry以及与之相关的inputoutput回调方法。

在 IPv4 中,存在地址空间有限的问题,因为 IPv4 地址只有 32 位。组织使用 NAT(在第九章中讨论)向其主机提供本地地址,但 IPv4 地址空间仍在逐年减少。开发 IPv6 协议的主要原因之一是,与 IPv4 地址空间相比,它的地址空间非常大,因为 IPv6 地址长度为 128 位。但是 IPv6 协议不仅仅是关于更大的地址空间。IPv6 协议包含了许多变化和补充,这是多年来使用 IPv4 协议所获得的经验的结果。例如,与 IPv4 报头相比,IPv6 报头具有 40 字节的固定长度,而 IP v4 报头的长度是可变的(从最小 20 字节到 60 字节),这是由于 IP 选项可以扩展它。在 IPv4 中处理 IP 选项是复杂的,并且在性能方面相当繁重。另一方面,在 IPv6 中,您根本无法扩展 IPv6 报头(如上所述,它的长度是固定的)。取而代之的是一种扩展报头机制,它在性能方面比 IPv4 中的 IP 选项更有效。另一个显著的变化是 ICMP 协议;在 IPv4 中,它仅用于错误报告和信息性消息。在 IPv6 中,ICMP 协议用于许多其他目的:邻居发现(ND)、多播侦听发现(MLD)等等。第三章专门针对 ICMP(IP v4 和 IPv6)。IPv6 邻居发现协议在第七章的中描述,MLD 协议在第八章的中讨论,它涉及 IPv6 子系统。

如前所述,收到的数据包由网络设备驱动程序传递到网络层,即 IPv4 或 IPv6。如果数据包用于本地传送,它们将被传送到传输层(L4)以供监听套接字处理。最常见的传输协议是 UDP 和 TCP,在第十一章中讨论,其中讨论了第 4 层,即传输层。本章还介绍了两种较新的传输协议,即流控制传输协议(SCTP)和数据报拥塞控制协议(DCCP)。你会发现,SCTP 和 DCCP 都采用了一些 TCP 特性和 UDP 特性。已知 SCTP 协议与长期演进(LTE)协议结合使用;到目前为止,DCCP 还没有在更大规模的互联网环境中测试过。

本地主机生成的数据包由第 4 层套接字创建,例如 TCP 套接字或 UDP 套接字。它们是由用户空间应用用套接字 API 创建的。套接字主要有两种:数据报套接字和套接字。这两种类型的套接字和基于 POSIX 的套接字 API 也将在第十一章中讨论,在那里您还将了解套接字的内核实现(struct socket,它提供了到用户空间的接口,struct sock,它提供了到第 3 层的接口)。本地生成的数据包被传递到网络层 L3(在第四章的【发送 IPv4 数据包】一节中描述),然后被传递到网络设备驱动程序(L2)进行传输。在有些情况下,碎片发生在第 3 层,即网络层,这也在第四章的中讨论。

每个第 2 层网络接口都有一个 L2 地址来标识它。在以太网的情况下,这是一个 48 位地址,由制造商为每个以太网网络接口分配的 MAC 地址,据说是唯一的(尽管您应该考虑到大多数网络接口的 MAC 地址可以通过用户空间命令如ifconfigip来更改)。每个以太网数据包都以一个 14 字节长的以太网报头开始。它由以太网类型(2 字节)、源 MAC 地址(6 字节)和目的 MAC 地址(6 字节)组成。例如,IPv4 的以太网类型值为 0x0800,IPv6 的以太网类型值为 0x86DD。对于每个传出的数据包,应该构建一个以太网报头。当用户空间套接字发送一个包时,它指定其目的地址(可以是 IPv4 或 IPv6 地址)。这不足以构建数据包,因为目的 MAC 地址应该是已知的。根据 IP 地址找到主机的 MAC 地址是相邻子系统的任务,在第七章的中讨论。邻居发现由 IPv4 中的 ARP 协议和 IPv6 中的 NDISC 协议处理。这些协议是不同的:ARP 协议依赖于发送广播请求,而 NDISC 协议依赖于发送 ICMPv6 请求,这些请求实际上是多播数据包。ARP 协议和 NDSIC 协议也在第七章中讨论。

网络堆栈应该与用户空间进行通信,以执行诸如添加或删除路由、配置邻居表、设置 IPsec 策略和状态等任务。用户空间和内核之间的通信是通过 netlink 套接字完成的,在第二章中有所描述。基于 netlink 套接字的用户空间包也在第二章的中讨论,以及通用 netlink 套接字及其优点。

无线子系统将在第十二章中讨论。如前所述,这个子系统是单独维护的;它有自己的树和自己的邮件列表。无线堆栈中有一些普通网络堆栈中不存在的独特功能,例如省电模式(当工作站或接入点进入睡眠状态时)。Linux 无线子系统还支持特殊的拓扑,,比如网状网络、自组织网络等等。这些拓扑有时需要使用特殊功能。例如,网状网络使用一种叫做混合无线网状协议(HWMP)的路由协议,在第十二章中讨论。该协议工作在第 2 层,处理 MAC 地址,与 IPV4 路由协议相反。第十二章还讨论了 mac80211 框架,无线设备驱动程序使用它。无线子系统的另一个非常有趣的特性是 IEEE 802.11n 中的块确认机制,也在第十二章中讨论过。

近年来,InfiniBand 技术在企业数据中心越来越受欢迎。InfiniBand 基于一种称为远程直接内存访问(RDMA)的技术。在版本 2.6.11 中,RDMA API 被引入到 Linux 内核中。在第十三章中,你会找到关于 Linux Infiniband 实现、RDMA API 及其基本数据结构的很好的解释。

虚拟化解决方案也变得越来越受欢迎,尤其是由于 Xen 或 KVM 等项目。此外,硬件的改进,如用于英特尔处理器的 VT-x 或用于 AMD 处理器的 AMD-V,使虚拟化更加高效。还有另一种形式的虚拟化,可能不太为人所知,但有自己的优势。这种虚拟化基于一种不同的方法:流程虚拟化。它在 Linux 中是通过名称空间实现的。Linux 目前支持六种名称空间,将来可能会有更多。名称空间特性已经被 Linux Containers ( http://lxc.sourceforge.net/)和 Userspace 中的 check point/Restore(CRIU)等项目所使用。为了支持名称空间,内核中增加了两个系统调用:unshare()setns();六个新标志被添加到 CLONE_标志中,每个标志对应一种名称空间类型。我在第十四章中特别讨论了名称空间和网络名称空间。第十四章也讨论了蓝牙子系统,并简要介绍了 PCI 子系统,因为许多网络设备驱动程序都是 PCI 设备。我不深入研究 PCI 子系统内部,因为那超出了本书的范围。第十四章中讨论的另一个有趣的子系统是 IEEE 8012.15.4,它适用于低功耗和低成本设备。这些设备有时会与物联网* (IoT) 概念一起提及,后者涉及将支持 IP 的嵌入式设备连接到 IP 网络。事实证明,在这些设备上使用 IPv6 可能是个好主意。该解决方案被称为低功率无线个人区域网上的 IPv6(6 lowpan)。它有自己的挑战,例如扩展 IPv6 邻居发现协议以适合这种偶尔进入睡眠模式的设备(与普通 IPv6 网络相反)。IPv6 邻居发现协议的这些变化还没有实现,但是考虑这些变化背后的理论是很有趣的。除此之外,在第十四章中还有关于其他高级主题的章节,如 NFC、cgroups、Android 等等。

为了更好地理解 Linux 内核网络栈或参与其开发,您必须熟悉其开发是如何处理的。

Linux 内核网络开发模型

内核网络子系统非常复杂,它的开发相当动态。像任何 Linux 内核子系统一样,开发是由通过邮件列表(有时不止一个邮件列表)发送的git补丁完成的,这些补丁最终被该子系统的维护者接受或拒绝。出于许多原因,了解内核网络开发模型是很重要的。为了更好地理解代码,为了调试和解决基于 Linux 内核网络的项目中的问题,为了实现性能改进和优化补丁,或者为了实现新的特性,在许多情况下,您需要学习很多东西,例如:

  • 如何应用补丁
  • 如何阅读和解释补丁
  • 如何找到可能导致给定问题的修补程序
  • 如何恢复修补程序
  • 如何找到与某些功能相关的补丁
  • 如何将项目调整到旧的内核版本(反向移植)
  • 如何将项目调整到较新的内核版本(升级)
  • 如何克隆一棵树
  • 如何重置一棵git
  • 如何找出在哪个内核版本中应用了指定的git补丁

有些情况下,您需要使用刚刚添加的新功能,为此,您需要知道如何使用最新的、前沿的树。有些情况下,当您遇到一些 bug,或者您想要向网络堆栈添加一些新功能时,您需要准备一个补丁并提交它。与内核的其他部分一样,Linux 内核网络子系统由 Linus Torvalds 开发的源代码管理(SCM)系统git管理。如果你打算为主线内核发送补丁,或者如果你的项目由git管理,你必须学会使用git工具。

有时你甚至需要安装一个git服务器来开发本地项目。即使您不打算发送任何补丁,您也可以使用git工具来检索大量关于代码和代码开发历史的信息。网上有很多关于git的资源;我推荐斯科特·沙孔的免费在线书籍 Pro Git ,在http://git-scm.com/book可以买到。如果您打算将您的补丁提交到主线,您必须遵守一些关于编写、检查和提交补丁的严格规则,这样您的补丁才会被应用。您的补丁应该符合内核编码风格,并且应该经过测试。你还需要有耐心,因为有时即使是微不足道的补丁也要过几天才能贴上。我建议学习配置一台主机,使用git send-email命令提交补丁(尽管提交补丁可以用其他邮件客户端完成,甚至是流行的 Gmail 网络邮件客户端)。网上有很多关于如何使用git准备和发送内核补丁的指南。我还建议在提交你的第一个补丁之前阅读内核树中的Documentation/SubmittingPatchesDocumentation/CodingStyle

我推荐使用以下 PERL 脚本:

  • scripts/checkpatch.pl检查补丁的正确性
  • 要找出补丁应该发给哪个维护者

最重要的信息资源之一是内核网络开发邮件列表,netdev : netdev@vger.kernel.org,存档在www.spinics.net/lists/netdev。这是一个高容量列表。大多数帖子是新代码的补丁和征求意见稿(RFC ),以及关于补丁的评论和讨论。这个邮件列表处理 Linux 内核网络堆栈和网络设备驱动程序,除了处理具有特定邮件列表和特定git存储库的子系统的情况(例如无线子系统,在第十二章中讨论)。iproute2ethtool用户空间包的开发也在netdev邮件列表中处理。这里应该提到的是,并不是每个网络子系统都有自己的邮件列表;例如,IPsec 子系统(在第十章中讨论)没有邮件列表,IEEE 802.15.4 子系统也没有邮件列表。一些网络子系统有自己特定的git树、维护者和邮件列表,比如无线邮件列表和蓝牙邮件列表。这些子系统的维护者不时通过netdev邮件列表发送对他们的git树的请求。另一个信息来源是内核树中的Documentation/networking。它在许多文件中包含了关于各种网络主题的大量信息,但是请记住,您在那里找到的文件并不总是最新的。

Linux 内核网络子系统在两个git存储库中维护。补丁和 RFC 被发送到两个存储库的netdev邮件列表。这是两株git树:

  • net: http://git.kernel.org/?p=linux/kernel/git/davem/net.git:针对主线树中已经存在的代码进行修复
  • net-next: http://git.kernel.org/?p=linux/kernel/git/davem/net-next.git:未来内核发布的新代码

网络子系统的维护者 David Miller 不时通过 LKML 向 Linus 发送对这些git树的主线的拉请求。你应该知道,在与主线合并的过程中,有一段时间 net-next git树是关闭的,不应该发送补丁。通过netdev邮件列表发送一个通知,告知这段时间何时开始,另一个通知何时结束。

image 本书基于内核 3.9。所有代码片段都来自这个版本,除非另有明确说明。内核树可以从www.kernel.org作为一个tar文件获得。或者,您可以下载一个带有git clone的内核git树(例如,使用前面提到的git net树或git net-next树的 URL,或者其他git内核库)。互联网上有很多关于如何配置、构建和引导 Linux 内核的指南。也可以在http://lxr.free-electrons.com/在线浏览各种内核版本。这个网站让你了解每个方法和每个变量被引用的地方;此外,您可以通过点击鼠标轻松导航到 Linux 内核的以前版本。如果您正在使用您自己版本的 Linux 内核树,其中一些更改是在本地进行的,您可以在本地 Linux 机器上本地安装和配置 Linux 交叉引用服务器(LXR)。参见http://lxr.sourceforge.net/en/index.shtml

摘要

本章是对 Linux 内核网络子系统的简短介绍。我描述了使用 Linux(一个流行的开源项目)和内核网络开发模型的好处。我还描述了网络设备结构(net_device)和套接字缓冲区结构(sk_buff),这是网络子系统的两个最基本的结构。你应该参考附录 A 中关于这些结构的几乎所有成员及其用途的详细描述。本章涵盖了与数据包在内核网络堆栈中的遍历相关的其他重要主题,例如路由子系统中的查找、碎片和碎片整理、协议处理程序注册等。其中一些协议将在后面的章节中讨论,包括 IPv4、IPv6、ICMP4 和 ICMP6、ARP 和邻居发现。几个重要的子系统,包括无线子系统、蓝牙子系统和 IEEE 812.5.4 子系统,也将在后面的章节中介绍。第二章从 netlink sockets 开始内核网络堆栈之旅,它提供了一种用户空间和内核之间的双向通信方式,这将在其他几章中讨论。

二、网络链接套接字

第一章讨论了 Linux 内核网络子系统的角色及其运行的三个层次。netlink socket 接口最早出现在 2.2 Linux 内核中,名为 AF_NETLINK socket。它是作为用户空间进程和内核之间笨拙的 IOCTL 通信方法的一种更灵活的替代方法而创建的。IOCTL 处理程序不能从内核向用户空间发送异步消息,而 netlink 套接字可以。为了使用 IOCTL,还有另一层复杂性:您需要定义 IOCTL 编号。netlink 的操作模型非常简单:您使用 socket API 在用户空间中打开并注册一个 netlink socket,这个 netlink socket 处理与内核 netlink socket 的双向通信,通常发送消息来配置各种系统设置,并从内核获得响应。

本章描述了 netlink 协议的实现和 API,并讨论了它的优点和缺点。我还谈到了新的通用 netlink 协议,讨论了它的实现及其优点,并给出了一些使用libnl库的示例。最后,我讨论了套接字监控接口。

Netlink 系列

netlink 协议是基于套接字的进程间通信(IPC)机制,基于 RFC 3549,“Linux Netlink 作为 IP 服务协议”它在用户空间和内核之间或者内核本身的某些部分之间提供了一个双向通信通道。Netlink 是标准套接字实现的扩展。netlink 协议实现主要位于net/netlink下,在这里您可以找到以下四个文件:

  • af_netlink.c
  • af_netlink.h
  • genetlink.c
  • diag.c

除了它们之外,还有一些头文件。其实最常用的是af_netlink模块;它提供了 netlink 内核套接字 API,而genetlink模块提供了一个新的通用 netlink API,使用它可以更容易地创建 netlink 消息。diag监控接口模块(diag.c)提供一个 API 来转储和获取关于 netlink 套接字的信息。我将在本章后面的“套接字监控接口”一节中讨论diag模块

我应该在这里提到,理论上 netlink 套接字可以用于两个或更多用户空间进程之间的通信(包括发送多播消息),尽管这通常不被使用,也不是 netlink 套接字的最初目标。UNIX 域套接字为 IPC 提供了一个 API,它们广泛用于两个用户空间进程之间的通信。

与用户空间和内核之间的其他通信方式相比,Netlink 有一些优势。例如,当使用 netlink 套接字时,不需要轮询。一个用户空间应用打开一个 socket 然后调用recvmsg(),如果内核没有发送消息就进入阻塞状态;例如,参见iproute2包的rtnl_listen()方法(lib/libnetlink.c)。另一个优点是内核可以发起向用户空间发送异步消息,而不需要用户空间触发任何动作(例如,通过调用某个 IOCTL 或者通过写入某个sysfs条目)。另一个优点是 netlink 套接字支持多播传输。

您可以使用socket()系统调用从用户空间创建 netlink 套接字。netlink 套接字可以是 SOCK_RAW 套接字或 SOCK_DGRAM 套接字。

Netlink 套接字可以在内核或用户空间中创建;内核 netlink 套接字由netlink_kernel_create()方法创建;和用户空间 netlink 套接字由socket()系统调用创建。从用户空间或内核创建 netlink 套接字会创建一个netlink_sock对象。当从用户空间创建套接字时,它由netlink_create()方法处理。在内核中创建套接字时,由__netlink_kernel_create()处理;此方法设置 NETLINK_KERNEL_SOCKET 标志。最终,两个方法都调用__netlink_create()以公共方式分配一个套接字(通过调用sk_alloc()方法)并初始化它。图 2-1 显示了如何在内核和用户空间中创建 netlink 套接字。

9781430261964_Fig02-01.jpg

图 2-1 。在内核和用户空间中创建 netlink 套接字

您可以从用户空间创建一个 netlink 套接字,方法与普通 BSD 风格的套接字非常相似,例如:socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)。然后你应该创建一个sockaddr_nl对象(netlink 套接字地址结构的实例),初始化它,并使用标准的 BSD 套接字 API(比如bind()sendmsg()recvmsg()等等)。sockaddr_nl结构表示用户空间或内核中的 netlink 套接字地址。

Netlink 套接字库为 netlink 套接字提供了一个方便的 API。我将在下一节讨论它们。

Netlink 套接字库

我推荐您使用libnl API 来开发用户空间应用,它通过 netlink 套接字发送或接收数据。libnl包是为基于 netlink 协议的 Linux 内核接口提供 API 的库集合。如前所述,iproute2包使用了libnl库。除了核心库(libnl),它还支持通用的 netlink 家族(libnl-genl)、routing 家族(libnl-route)和 netfilter 家族(libnl-nf)。这个包主要是由托马斯·格拉夫开发的。这里我还应该提到一个叫做libmnl的库,它是一个面向 netlink 开发者的极简用户空间库。libmnl库主要由 Pablo Neira Ayuso 编写,Jozsef Kadlecsik 和 Jan Engelhardt 也有贡献。(http://netfilter.org/projects/libmnl/)。

sockaddr_nl 结构

我们来看一下sockaddr_nl结构,它代表一个 netlink 套接字地址:

struct sockaddr_nl {
    __kernel_sa_family_t    nl_family;    /* AF_NETLINK                */
    unsigned short          nl_pad;       /* zero                      */
    __u32                   nl_pid;       /* port ID                   */
    __u32                   nl_groups;    /* multicast groups mask     */
};

(include/uapi/linux/netlink.h)

  • nl_family:应该一直是 AF_NETLINK。
  • nl_pad:应始终为 0。
  • nl_pid:netlink 套接字的单播地址。对于内核 netlink sockets ,应该是 0。用户空间应用有时会将nl_pid设置为它们的进程 id ( pid)。在用户空间应用中,当您将nl_pid显式设置为 0,或者根本不设置它,然后调用bind()时,内核方法netlink_autobind()会为nl_pid赋值。它尝试分配当前线程的进程 id。如果你在用户空间中创建两个套接字,那么你要负责它们的nl_pid是唯一的,以防你不调用 bind。Netlink 套接字不仅用于网络;其他子系统,如 SELinux、audit、uevent 等,使用 netlink 套接字。rtnelink 套接字是专门用于网络的 netlink 套接字;它们用于路由消息、相邻消息、链路消息和更多网络子系统消息。
  • nl_groups:组播组(或组播组掩码)。

下一节将讨论iproute2和旧的net-tools包。iproute2包基于 netlink 套接字,在本章后面的“在路由表中添加和删除路由条目”一节中,您将在iproute2中看到一个使用 netlink 套接字的示例。我提到net-tools包,它比较老,将来可能会被弃用,以强调作为iproute2的替代,它的功率和能力都比较低。

用于控制 TCP/IP 网络的用户空间包

有两个用户空间包用于控制 TCP/IP 网络和处理网络设备:net-toolsiproute2iproute2包包括如下命令:

  • ip:用于管理网络表和网络接口
  • tc:用于交通管制管理
  • ss:转储套接字统计
  • lnstat:转储 linux 网络统计
  • bridge:用于管理网桥地址和设备

iproute2包主要基于从用户空间向内核发送请求,并通过 netlink 套接字获得回复。在iproute2中使用 IOCTLs 也有一些例外。例如,ip tuntap命令使用 IOCTLs 来添加/删除一个 TUN/TAP 设备。如果您查看 TUN/TAP 软件驱动程序代码,您会发现它定义了一些 IOCTL 处理程序,但是它不使用 rtnetlink 套接字。net-tools包基于 IOCTLs,包括如下已知命令:

  • ifconifg
  • arp
  • route
  • netstat
  • hostname
  • rarp

iproute2包的一些高级功能在net-tools包中不可用。

下一节讨论内核 netlink 套接字——通过交换不同类型的 netlink 消息来处理用户空间和内核之间的通信的核心引擎。了解内核 netlink 套接字对于理解 netlink 层提供给用户空间的接口至关重要。

内核网络链接套接字

您可以在内核网络堆栈中创建几个 netlink 套接字。每个内核套接字处理不同类型的消息:例如,应该处理 netlink_ROUTE 消息的 NETLINK 套接字在rtnetlink_net_init() : 中创建

static int __net_init rtnetlink_net_init(struct net *net) {
    ...
    struct netlink_kernel_cfg cfg = {
        .groups    = RTNLGRP_MAX,
        .input        = rtnetlink_rcv,
        .cb_mutex    = &rtnl_mutex,
        .flags        = NL_CFG_F_NONROOT_RECV,
    };

    sk = netlink_kernel_create(net, NETLINK_ROUTE, &cfg);
    ...
}

注意,rtnetlink 套接字知道网络名称空间;网络名称空间对象(struct net)包含一个名为rtnl ( rtnetlink套接字)的成员。在rtnetlink_net_init()方法中,通过调用netlink_kernel_create()创建 rtnetlink 套接字后,将其赋给相应网络名称空间对象的rtnl指针。

让我们看看netlink_kernel_create()原型:

struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)

  • 第一个参数(net)是网络名称空间。

  • 第二个参数是 netlink 协议(例如,rtnetlink 消息的 NETLINK_ROUTE,IPsec 的 NETLINK_XFRM 或审计子系统的 NETLINK_AUDIT)。有 20 多种 netlink 协议,但它们的数量被限制为 32 (MAX_LINKS)。这是创建通用 netlink 协议的原因之一,你将在本章后面看到。netlink 协议的完整列表在include/uapi/linux/netlink.h中。

  • 第三个参数是对netlink_kernel_cfg的引用,它由创建 netlink 套接字的可选参数组成:

    struct netlink_kernel_cfg {
        unsigned int    groups;
        unsigned int    flags;
        void        (*input)(struct sk_buff *skb);
        struct mutex    *cb_mutex;
        void        (*bind)(int group);
    };
    (include/uapi/linux/netlink.h)
    

groups成员用于指定多播组(或多播组的掩码)。可以通过设置sockaddr_nl对象的nl_groups来加入一个组播组(也可以通过libnlnl_join_groups()方法来完成)。然而,这样你只能加入 32 个小组。从内核版本 2.6.14 开始,可以使用 NETLINK _ ADD _ MEMBERSHIP/NETLINK _ DROP _ MEMBERSHIP 套接字选项分别加入/离开一个多播组。使用套接字选项,您可以加入更多的组。libnlnl_socket_add_memberships()/nl_socket_drop_membership()方法使用这个套接字选项。

flags成员可以是 NL_CFG_F_NONROOT_RECV 或 NL_CFG_F_NONROOT_SEND。

设置 CFG_F_NONROOT_RECV 时,非超级用户可以绑定到多播组;在netlink_bind()中有如下代码:

static int netlink_bind(struct socket *sock, struct sockaddr *addr,
                         int addr_len)
 {
  ...
  if (nladdr->nl_groups) {
         if (!netlink_capable(sock, NL_CFG_F_NONROOT_RECV))
                         return -EPERM;
    }

对于非超级用户,如果没有设置 NL_CFG_F_NONROOT_RECV,那么当绑定到一个多播组时,netlink_capable()方法将返回 0,并且您将得到–EPRM 错误。

当 NL_CFG_F_NONROOT_SEND 标志被设置时,允许非超级用户发送多播。

input成员用于回调;当netlink_kernel_cfg中的input成员为空时,内核套接字将无法从用户空间接收数据(尽管从内核向用户空间发送数据是可能的)。对于 rtnetlink 内核套接字,rtnetlink_rcv()方法被声明为input回调;因此,从用户空间通过 rtnelink 套接字发送的数据将由rtnetlink_rcv()回调处理。

对于uevent内核事件,你只需要将数据从内核发送到用户空间;因此,在lib/kobject_uevent.c中,你有一个 netlink 套接字的例子,其中input回调是未定义的:

static int uevent_net_init(struct net *net)
{
    struct uevent_sock *ue_sk;
    struct netlink_kernel_cfg cfg = {
        .groups    = 1,
        .flags    = NL_CFG_F_NONROOT_RECV,
    };

    ...
    ue_sk->sk = netlink_kernel_create(net, NETLINK_KOBJECT_UEVENT, &cfg);
    ...
}
(lib/kobject_uevent.c)

netlink_kernel_cfg对象中的互斥(cb_mutex)是可选的;当没有定义互斥体时,使用默认的cb_def_mutex(互斥体结构的一个实例;参见net/netlink/af_netlink.c。事实上,大多数 netlink 内核套接字是在没有在netlink_kernel_cfg对象中定义互斥体的情况下创建的。比如前面提到的 uevent 内核 NETLINK socket(NETLINK _ ko object _ UEVENT)。此外,审计内核 netlink 套接字(NETLINK_AUDIT)和其他 NETLINK 套接字不定义互斥体。rtnetlink 套接字是一个例外,它使用rtnl_mutex。下一节讨论的通用 netlink 套接字也定义了自己的互斥锁:genl_mutex

netlink_kernel_create()方法通过调用netlink_insert()方法在名为nl_table的表中创建一个条目。对nl_table的访问受名为nl_table_lock的读写锁保护;通过netlink_lookup()方法在该表中进行查找,指定协议和端口 id。指定消息类型的回调注册由rtnl_register()完成;在网络内核代码中有几个地方可以注册这样的回调。例如,在rtnetlink_init()中,你为一些消息注册回调,比如 RTM_NEWLINK(创建一个新链接)、RTM_DELLINK(删除一个链接)、RTM_GETROUTE(转储路由表)等等。在net/core/neighbour.c中,为 RTM_NEWNEIGH 消息(创建一个新邻居)、RTM_DELNEIGH(删除一个邻居)、RTM_GETNEIGHTBL 消息(转储邻居表)等等注册回调。我在第五章和第七章中深入讨论了这些行动。您还可以在 FIB 代码(ip_fib_init())、多播代码(ip_mr_init())、IPv6 代码和其他地方注册对其他类型消息的回调。

使用 netlink 内核套接字的第一步是注册它。我们来看看rtnl_register()方法原型:

extern void rtnl_register(int protocol, int msgtype,
                  rtnl_doit_func,
                  rtnl_dumpit_func,
                  rtnl_calcit_func);

第一个参数是protocol族(当你不针对某个特定协议时,是 PF _ UNSPEC);您将在include/linux/socket.h中找到所有协议族的列表。

第二个参数是 netlink 消息类型,比如 RTM_NEWLINK 或 RTM_NEWNEIGH。这些是 rtnelink 协议添加的专用 netlink 消息类型。消息类型的完整列表在include/uapi/linux/rtnetlink.h中。

最后三个参数是回调:doitdumpitcalcit。回调是您想要执行的处理消息的动作,并且您通常只指定一个回调。

doit回调用于添加/删除/修改等动作;dumpit回调用于检索信息,calcit回调用于计算缓冲区大小。rtnetlink 模块有一个名为rtnl_msg_handlers的表。该表按协议编号进行索引。表中的每个条目本身就是一个表,按消息类型进行索引。表中的每个元素都是rtnl_link的一个实例,它是一个由这三个回调的指针组成的结构。用rtnl_register()注册回调时,将指定的回调添加到该表中。

注册回调是这样做的,比如:rtnl_register(PF_UNSPEC, RTM_NEWLINK, rtnl_newlink, NULL, NULL) in net/core/rtnetlink.c。这将在相应的rtnl_msg_handlers条目中添加rtnl_newlink作为 RTM_NEWLINK 消息的doit回调。

rtnelink 消息的发送通过rtmsg_ifinfo()完成。例如,在dev_open()中你创建了一个新链接,所以你调用了:rtmsg_ifinfo()方法中的rtmsg_ifinfo(RTM_NEWLINK, dev, IFF_UP|IFF_RUNNING);,首先调用了nlmsg_new()方法来分配一个大小合适的sk_buff。然后创建两个对象:netlink 消息头(nlmsghdr)和一个ifinfomsg对象,它位于 netlink 消息头之后。这两个对象由rtnl_fill_ifinfo()方法初始化。然后调用rtnl_notify()发送数据包;发送数据包实际上是通过通用网络链接方法nlmsg_notify()(在net/netlink/af_netlink.c中)完成的。图 2-2 显示了使用rtmsg_ifinfo()方法发送 rtnelink 消息的各个阶段。

9781430261964_Fig02-02.jpg

图 2-2 。使用 rtmsg_ifinfo()方法发送 rtnelink 消息

下一节是关于 netlink 消息,它在用户空间和内核之间交换。netlink 消息总是以 netlink 消息头开始,因此学习 netlink 消息的第一步是研究 netlink 消息头格式。

Netlink 消息头

网络链接消息应该遵循 RFC 3549“作为 IP 服务协议的 Linux 网络链接”第 2.2 节“消息格式”中规定的特定格式 netlink 消息以固定大小的 netlink 报头开始,其后是有效载荷。本节描述 netlink 消息头的 Linux 实现。

netlink 报文头由include/uapi/linux/netlink.h : 中的struct nlmsghdr定义

struct nlmsghdr
{
  __u32 nlmsg_len;
  __u16 nlmsg_type;
  __u16 nlmsg_flags;
  __u32 nlmsg_seq;
  __u32 nlmsg_pid;
};
(include/uapi/linux/netlink.h)

每个 netlink 数据包都以 netlink 消息头开始,用struct nlmsghdr表示。nlmsghdr的长度为 16 字节。它包含五个字段:

  • nlmsg_len是包括标题在内的消息长度。

  • nlmsg_type是报文类型;有四种基本的 netlink 报文头类型:

  • NLMSG_NOOP:没有操作,消息必须被丢弃。

  • NLMSG_ERROR:出现错误。

  • NLMSG_DONE:多部分消息被终止。

  • NLMSG_OVERRUN: Overrun notification: error, data was lost.

    (include/uapi/linux/netlink.h)

    但是,系列可以添加他们自己的 netlink 消息头类型。例如,rtnetlink 协议族增加了 RTM_NEWLINK、RTM_DELLINK、RTM_NEWROUTE 等消息头类型(见include/uapi/linux/rtnetlink.h)。有关 rtnelink 系列添加的 netlink 消息头类型的完整列表,以及对每种类型的详细解释,请参见:man 7 rtnetlink。注意,小于 NLMSG_MIN_TYPE (0x10)的消息类型值是为控制消息保留的,不能使用。

  • nlmsg_flags字段可以如下:

  • NLM 请求:当它是一个请求消息时。

  • NLM_F_MULTI:当它是一个多部分的消息时。多部分消息用于表转储。通常消息的大小被限制为一个页面(PAGE_SIZE)。所以大消息被分成小消息,每个小消息(除了最后一个)都设置了 NLM_F_MULTI 标志。最后一条消息设置了 NLMSG_DONE 标志。

  • NLM_F_ACK:当你希望消息的接收者用 ACK 回复时。Netlink ACK 消息通过netlink_ack()方法(net/netlink/af_netlink.c)发送。

  • NLM _ F _ 转储:检索关于表/条目的信息。

  • NLM 根:指定树根。

  • NLM_F_MATCH:返回所有匹配条目。

  • NLM_F_ATOMIC: This flag is deprecated.

    以下标志是创建条目的修饰符:

  • NLM_F_REPLACE:覆盖现有条目。

  • NLM_F_EXCL:不要触摸入口,如果它存在。

  • NLM_F_CREATE:创建条目,如果它不存在。

  • NLM _ F _ 附加:将条目添加到列表末尾。

  • NLM_F_ECHO: Echo this request.

    我已经展示了最常用的旗帜。完整列表见include/uapi/linux/netlink.h

  • nlmsg_seq是序列号(用于消息序列)。与某些第 4 层传输协议不同,序列号没有严格的强制要求。

  • nlmsg_pid is the sending port id. When a message is sent from the kernel, the nlmsg_pid is 0. When a message is sent from userspace, the nlmsg_pid can be set to be the process id of that userspace application which sent the message.

    图 2-3 显示了 netlink 消息头。

    9781430261964_Fig02-03.jpg

    图 2-3 。nlmsg 标题

    报头之后是有效载荷。netlink 消息的有效载荷由一组属性组成,这些属性以类型-长度-值(TLV)格式表示。对于 TLV,类型和长度的大小是固定的(通常为 1-4 字节),而值字段的大小是可变的。TLV 表示也用于网络代码的其他地方,例如 IPv6(参见 RFC 2460)。TLV 提供了灵活性,使得将来的扩展更容易实现。属性可以嵌套,这使得复杂的属性树结构成为可能。

    每个 netlink 属性头由struct nlattr定义:

    struct nlattr {
       __u16   nla_len;
       __u16   nla_type;
    };
    (include/uapi/linux/netlink.h)
    
    
  • nla_len:属性的大小,以字节为单位。

  • nla_type:属性类型。例如,nla_type的值可以是 NLA_U32(用于 32 位无符号整数)、NLA _ 字符串(用于可变长度字符串)、NLA _ 嵌套(用于嵌套属性)、NLA_UNSPEC(用于任意类型和长度)等等。您可以在include/net/netlink.h中找到可用类型的列表。

每个 netlink 属性必须用一个 4 字节的边界(NLA_ALIGNTO)对齐。

每个家族可以定义一个属性验证策略,该策略表示关于接收到的属性的期望。这个验证策略由nla_policy对象表示。事实上,nla_policy structstruct nlattr : 的内容完全一样

 struct nla_policy {
   u16  type;
   u16  len;
};
(include/uapi/linux/netlink.h)

属性验证策略是一组nla_policy对象;这个数组由属性号索引。对于每个属性(固定长度属性除外),如果nla_policy对象中的len的值为 0,则不应该执行任何验证。如果属性是字符串类型之一(比如NLA_STRING),那么len应该是字符串的最大长度,没有终止的空字节。如果属性类型是 NLA _ 未用或未知,len应该设置为属性有效载荷的精确长度。如果属性类型是 NLA _ 标志,则不使用len。(原因是属性的存在本身就隐含了一个值true,属性的不存在隐含了一个值false)。

在内核中接收通用 netlink 消息由genl_rcv_msg()处理。如果是转储请求(当设置了NLM_F_DUMP标志时),您可以通过调用netlink_dump_start()方法来转储该表。如果不是转储请求,就用nlmsg_parse()方法解析有效负载。nlmsg_parse()方法通过调用validate_nla() ( lib/nlattr.c)来执行属性验证。如果有类型超过 maxtype 的属性,为了向后兼容,它们将被忽略。在验证失败的情况下,您不能继续执行genl_rcv_msg()中的下一步(运行doit()回调),并且genl_rcv_msg()返回一个错误代码。

下一节描述 NETLINK_ROUTE 消息,这是网络子系统中最常用的消息。

NETLINK_ROUTE 消息

rtnetlink (netlink_ROUTE)消息不限于网络路由子系统:还有相邻子系统消息、接口设置消息、防火墙消息、NETLINK 队列消息、策略路由消息和许多其他类型的 rtnetlink 消息,您将在后面的章节中看到。

NETLINK_ROUTE 消息可以分为几类:

  • 链接(网络接口)
  • ADDR(网络地址)
  • 路由(路由消息)
  • 邻居(相邻子系统消息)
  • 规则(策略路由规则)
  • 排队规则
  • 流量类别
  • 动作(数据包动作 API,见net/sched/act_api.c)
  • 邻表
  • 地址标签

每个系列都有三种类型的消息:用于创建、删除和检索信息。因此,对于路由消息,有用于创建路由的 RTM_NEWROUTE 消息类型、用于删除路由的 RTM_DELROUTE 消息类型和用于检索路由的 RTM_GETROUTE 消息类型。对于链接消息,除了用于创建、删除和信息检索的三种方法之外,还有用于修改链接的附加消息:RTM_SETLINK。

有些情况下会出现错误,您会发送一条错误消息作为回复。netlink 错误信息由nlmsgerr struct表示:

struct nlmsgerr {
    int        error;
    struct nlmsghdr msg;
};
(include/uapi/linux/netlink.h)

事实上,正如你在图 2-4 中看到的,netlink 错误信息是由 netlink 信息头和错误代码组成的。当错误代码不为 0 时,导致错误的原始请求的 netlink 消息头被附加在错误代码字段之后。

9781430261964_Fig02-04.jpg

图 2-4 。Netlink 错误消息

如果您发送了一个错误构造的消息(例如,nlmsg_type无效),那么会发回一个 netlink 错误消息,并且根据发生的错误设置错误代码。例如,当nlmsg_type无效时(负值,或高于允许的最大值),错误代码被设置为–EOPNOTSUPP。参见net/core/rtnetlink.c中的rtnetlink_rcv_msg()方法。在错误消息中,序列号被设置为导致错误的请求的序列号。

发送方可以请求获得 netlink 消息的 ACK。这是通过将 netlink 报文头类型(nlmsg_type)设置为 NLM_F_ACK 来实现的。当内核发送 ACK 时,它使用错误消息(该消息的 netlink 消息头类型设置为 NLMSG_ERROR ),错误代码为 0。在这种情况下,请求的原始 netlink 标头不会附加到错误消息中。具体实现参见net/netlink/af_netlink.c中的netlink_ack()方法实现。

了解了 NETLINK_ROUTE 消息后,您就可以查看使用 NETLINK_ROUTE 消息在路由表中添加和删除路由条目的示例了。

在路由表中添加和删除路由条目

在幕后,让我们看看当添加和删除路由条目时,在 netlink 协议的上下文中内核发生了什么。例如,您可以通过运行以下命令向路由表添加路由条目:

ip route add 192.168.2.11 via 192.168.2.20

该命令通过 rtnetlink 套接字从用户空间(RTM_NEWROUTE)发送 netlink 消息,用于添加路由条目。该消息由 rtnetlink 内核套接字接收,并由rtnetlink_rcv()方法处理。最终,通过调用net/ipv4/fib_frontend.c中的inet_rtm_newroute()来添加路由条目。随后,用fib_table_insert()方法完成到转发信息库(FIB)的插入,该转发信息库是路由数据库;然而,插入路由表并不是fib_table_insert()的唯一任务。您应该通知所有注册了 RTM_NEWROUTE 消息的侦听器。怎么做?当插入一个新的路由条目时,用 RTM_NEWROUTE 调用rtmsg_fib()方法。rtmsg_fib()方法构建一个 netlink 消息,并通过调用rtnl_notify() 发送它,以通知注册到 RTNLGRP_IPV4_ROUTE 组的所有侦听器。这些 RTNLGRP_IPV4_ROUTE 监听器既可以在内核中注册,也可以在用户空间中注册(就像在iproute2中一样,或者在一些用户空间路由守护进程中,比如xorp)。您将很快看到iproute2的用户空间守护进程如何订阅各种 rtnelink 多播组。

当删除一个路由条目时,会发生类似的事情。您可以通过运行以下命令提前删除路由条目:

ip route del 192.168.2.11

该命令通过 rtnetlink 套接字从用户空间(RTM_DELROUTE)发送 netlink 消息,用于删除路由条目。该消息再次由 rtnetlink 内核套接字接收,并由rtnetlink_rcv()回调处理。最终,通过调用net/ipv4/fib_frontend.c中的inet_rtm_delroute()回调来删除路由条目。随后,用调用rtmsg_fib()fib_table_delete()从 FIB 中删除,这次是用 RTM_DELROUTE 消息。

您可以使用如下的iproute2 ip命令监控网络事件:

ip monitor route

例如,如果你打开一个终端并在那里运行ip monitor route,然后打开另一个终端并运行ip route add 192.168.1.10 via 192.168.2.200,在第一个终端上你会看到这一行:192.168.1.10 via 192.168.2.200 dev em1。当您在第二个终端上运行ip route del 192.168.1.10时,在第一个终端上将出现以下文本:Deleted 192.168.1.10 via 192.168.2.200 dev em1

运行ip monitor route运行一个守护进程,它打开一个 netlink 套接字并订阅 RTNLGRP_IPV4_ROUTE 多播组。现在,添加/删除一个路由,如本例中所做的,将导致这样的结果:用rtnl_notify()发送的消息将被守护进程接收并显示在终端上。

您可以通过这种方式订阅其他多播组。例如,要订阅 RTNLGRP_LINK 多播组,运行ip monitor link。这个守护进程从内核接收 netlink 消息——例如,在添加/删除链接时。因此,如果您打开一个终端并运行ip monitor link,然后打开另一个终端并通过第一个终端上的vconfig add eth1 200,添加一个 VLAN 接口,您会看到这样的行:

4: eth1.200@eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN
    link/ether 00:e0:4c:53:44:58 brd ff:ff:ff:ff:ff:ff

如果您通过brctl addbr mybr在第二个终端上添加一个桥,在第一个终端上您会看到这样的行:

5: mybr: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN
    link/ether a2:7c:be:62:b5:b6 brd ff:ff:ff:ff:ff:ff

您已经看到了什么是 netlink 消息以及如何创建和处理它。您已经看到了如何处理 netlink 套接字。接下来,您将了解为什么创建通用 netlink 家族(在内核 2.6.15 中引入),以及它在 Linux 中的实现。

通用网络链接协议

netlink 协议的缺点之一是协议族的数量被限制为 32 (MAX_LINKS)。这是创建通用 netlink 系列的主要原因之一——为添加更多系列提供支持。它充当 netlink 多路复用器,与单个 netlink 系列(NETLINK_GENERIC)一起工作。通用 netlink 协议基于 netlink 协议并使用其 API。

要添加 netlink 协议族,您应该在include/linux/netlink.h中添加一个协议族定义。但是有了通用的 netlink 协议,就不需要这样了。通用 netlink 协议也可用于联网以外的其他子系统,因为它提供了一个通用的通信信道。例如,它也被 acpi 子系统(参见drivers/acpi/event.cacpi_event_genl_family的定义)、任务统计代码(参见kernel/taskstats.c)、热事件代码等等使用。

通用的 netlink 内核套接字是通过如下的netlink_kernel_create()方法创建的:

static int __net_init genl_pernet_init(struct net *net) {
    ..
           struct netlink_kernel_cfg cfg = {
                 .input          = genl_rcv,
                 .cb_mutex       = &genl_mutex,
                 .flags          = NL_CFG_F_NONROOT_RECV,
         };
         net->genl_sock = netlink_kernel_create(net, NETLINK_GENERIC, &cfg);
 ...
 }
(net/netlink/genetlink.c)

注意,像前面描述的 netlink 套接字一样,通用 netlink 套接字也知道网络名称空间;网络名称空间对象(struct net)包含一个名为genl_sock(通用 netlink 套接字)的成员。如您所见,网络名称空间genl_sock指针在genl_pernet_init()方法中被赋值。

genl_rcv()方法被定义为genl_sock对象的input回调,该对象由genl_pernet_init()方法创建。因此,通过通用 netlink 套接字从用户空间发送的数据在内核中由genl_rcv()回调处理。

您可以使用socket()系统调用创建一个通用的 netlink 用户空间套接字,尽管使用libnl-genl API 会更好(在本节稍后讨论)。

创建通用 netlink 内核套接字后,立即注册控制器系列(genl_ctrl):

static struct genl_family genl_ctrl = {
         .id = GENL_ID_CTRL,
         .name = "nlctrl",
         .version = 0x2,
         .maxattr = CTRL_ATTR_MAX,
         .netnsok = true,
};

static int __net_init genl_pernet_init(struct net *net) {
...
err = genl_register_family_with_ops(&genl_ctrl, &genl_ctrl_ops, 1)
...

genl_ctrl有一个固定的id0x 10(GENL_ID_CTRL);事实上,它是用固定 id 初始化的genl_family的唯一实例;所有其他实例都用GENL_ID_GENERATE作为 id 进行初始化,随后用一个动态赋值替换。

通过定义一个genl_multicast_group对象并调用genl_register_mc_group(),支持在通用 netlink 套接字中注册多播组;例如,在近场通信(NFC)子系统、?? 中,您有以下:

static struct genl_multicast_group nfc_genl_event_mcgrp = {
         .name = NFC_GENL_MCAST_EVENT_NAME,
 };

int __init nfc_genl_init(void)
{
...
 rc = genl_register_mc_group(&nfc_genl_family, &nfc_genl_event_mcgrp);
...
}
(net/nfc/netlink.c)

多播组的名称应该是唯一的,因为它是查找的主键。

在组播组中,id也是在注册组播组时通过调用genl_register_mc_group()中的find_first_zero_bit()方法动态生成的。只有一个多播组notify_grp,它有一个固定的 ID GENL _ ID _ CTRL。

要在内核中使用通用 netlink 套接字,您应该执行以下操作:

  • 创建一个genl_family对象,并通过调用genl_register_family()注册它。
  • 创建一个genl_ops对象,并通过调用genl_register_ops()注册它。

或者,您可以调用genl_register_family_with_ops()并向其传递一个genl_family对象、genl_ops数组及其大小。该方法将首先调用genl_register_family(),然后如果成功,将为指定数组genl_ops的每个genl_ops元素调用genl_register_ops()

genl_register_family()genl_register_ops()以及genl_familygenl_opsinclude/net/genetlink.h中定义。

无线子系统使用通用 netlink 套接字:

int nl80211_init(void)
{
    int err;

    err = genl_register_family_with_ops(&nl80211_fam,
        nl80211_ops, ARRAY_SIZE(nl80211_ops));
...
}
(net/wireless/nl80211.c)

通用 netlink 协议由一些用户空间包使用,例如hostapd包和iw包。hostapd包 ( http://hostap.epitest.fi)为无线接入点和认证服务器提供了一个用户空间守护进程。iw包用于操作无线设备及其配置(见http://wireless.kernel.org/en/users/Documentation/iw)。

iw包基于nl80211libnl库。第十二章更详细地讨论了nl80211。旧的用户空间无线包叫做wireless-tools ,是基于发送 IOCTLs 的。

以下是nl80211genl_familygenl_ops的定义:

static struct genl_family nl80211_fam = {
    .id        = GENL_ID_GENERATE, /* don't bother with a hardcoded ID */
    .name      = "nl80211",    /* have users key off the name instead */
    .hdrsize   = 0,        /* no private header */
    .version   = 1,        /* no particular meaning now */
    .maxattr   = NL80211_ATTR_MAX,
    .netnsok   = true,
    .pre_doit  = nl80211_pre_doit,
    .post_doit = nl80211_post_doit,
};

  • name:必须是唯一的名称。

  • id : id在这种情况下是 GENL_ID_GENERATE,实际上是 0。GENL_ID_GENERATE 告诉通用 netlink 控制器,当您向genl_register_family()注册系列时,为通道分配一个唯一的通道号。genl_register_family()分配一个 id,范围为 16 (GENL_MIN_ID,即 0x10)到 1023 (GENL_MAX_ID)。

  • hdrsize:私有头的大小。

  • maxattr:  NL80211_ATTR_MAX, which is the maximum number of attributes supported.

    nl80211_policy验证策略数组有 nl 80211 _ ATTR _ 最大元素(每个属性在数组中都有一个条目):

  • netnsok : true,表示家族可以处理网络命名空间。

  • pre_doit:在doit()回调之前调用的钩子。

  • post_doit: A hook that can, for example, undo locking or any required private tasks after the doit() callback.

    您可以使用genl_ops结构添加一个或多个命令。让我们看看genl_ops struct 的定义,然后看看它在nl80211中的用法:

    struct genl_ops {
       u8                      cmd;
       u8                      internal_flags;
       unsigned int            flags;
       const struct nla_policy *policy;
       int                    (*doit)(struct sk_buff *skb,
                                      struct genl_info *info);
       int                    (*dumpit)(struct sk_buff *skb,
                                        struct netlink_callback *cb);
       int                    (*done)(struct netlink_callback *cb);
       struct list_head        ops_list;
    };
    
    
  • cmd:命令标识符(genl_ops struct定义了一个单独的命令及其doit / dumpit处理程序)。

  • internal_flags : 家族定义使用的私有旗帜。比如在nl80211中,有很多定义内部标志的操作(比如 NL80211_FLAG_NEED_NETDEV_UP,NL80211_FLAG_NEED_RTNL 等等)。nl80211 pre_doit()post_doit()回调根据这些标志执行动作。参见net/wireless/nl80211

  • flags : 操作标志。值可以是以下值:

  • GENL_ADMIN_PERM:设置了这个标志,就意味着操作需要 CAP_NET_ADMIN 权限;参见net/netlink/genetlink.c中的genl_rcv_msg()方法。

  • GENL_CMD_CAP_DO:如果genl_ops struct实现了doit()回调,则设置该标志。

  • GENL_CMD_CAP_DUMP:如果genl_ops struct实现了dumpit()回调,则设置该标志。

  • GENL_CMD_CAP_HASPOL:如果genl_ops struct定义了属性验证策略(nla_policy数组),则设置该标志。

  • policy : 属性验证策略将在本节稍后描述有效负载时讨论。

  • doit:标准命令回调。

  • dumpit:回拨转储。

  • done:转储完成回调。

  • ops_list:操作列表。

    static struct genl_ops nl80211_ops[] = {
        {
    
        ...
          {
            .cmd = NL80211_CMD_GET_SCAN,
            .policy = nl80211_policy,
            .dumpit = nl80211_dump_scan,
          },
        ...
    }
    

注意,必须为genl_ops(在本例中为nl80211_ops)的每个元素指定一个doitdumpit回调,否则函数将因-EINVAL 而失败。

genl_ops中的这个条目添加了nl80211_dump_scan()回调作为 NL80211_CMD_GET_SCAN 命令的处理程序。nl80211_policy是一个由nla_policy对象组成的数组,定义了属性的预期数据类型及其长度。

当从用户空间运行扫描命令时,例如通过iw dev wlan0 scan,您从用户空间通过通用 netlink 套接字发送一条通用 netlink 消息,其命令为 NL80211_CMD_GET_SCAN。在更新的libnl版本中,消息通过nl_send_auto_complete()方法或nl_send_auto()发送。nl_send_auto()填充 netlink 报文头中缺失的比特和片断。如果不需要任何自动消息完成功能,可以直接使用nl_send()

消息由nl80211_dump_scan()方法处理,它是这个命令(net/wireless/nl80211.c)的dumpit回调。nl80211_ops对象中有 50 多个条目用于处理命令,包括 NL80211_CMD_GET_INTERFACE、NL80211_CMD_SET_INTERFACE、NL80211_CMD_START_AP 等等。

为了向内核发送命令,用户空间应用应该知道系列 id。系列名称在用户空间中是已知的,但是系列 id 在用户空间中是未知的,因为它只在内核运行时确定。为了获得家族 id,用户空间应用应该向内核发送一个通用的 netlink CTRL_CMD_GETFAMILY 请求。这个请求由ctrl_getfamily()方法处理。它返回家族 id 和其他信息,比如家族支持的操作。然后,用户空间可以向内核发送命令,指定它在回复中获得的家族 id。我将在下一节详细讨论这一点。

创建和发送通用网络链接消息

通用 netlink 消息以 netlink 报头开始,接着是通用 netlink 消息报头,然后是可选的用户特定报头。只有在所有这些之后,你才能找到可选的有效载荷,正如你在图 2-5 中看到的。

9781430261964_Fig02-05.jpg

图 2-5 。通用网络链接消息。

这是通用 netlink 消息头:

struct genlmsghdr {
   __u8    cmd;
   __u8    version;
  __u16    reserved;
};
(include/uapi/linux/genetlink.h)

  • cmd是通用的 netlink 消息类型;您注册的每个通用族都添加了自己的命令。比如上面提到的nl80211_fam家族,它添加的命令(比如 NL80211_CMD_GET_INTERFACE)就是用nl80211_commands enum来表示的。有 60 多个命令(见include/linux/nl80211.h)。
  • version可用于版本支持。用nl80211就是 1,没有特别的意义。版本成员允许在不破坏向后兼容性的情况下更改消息的格式。
  • reserved是为了将来使用。

通过以下方法为通用 netlink 消息分配缓冲区:

sk_buff *genlmsg_new(size_t payload, gfp_t flags)

这实际上是对nlmsg_new()的包装。

在用genlmsg_new()分配了一个缓冲区之后,调用genlmsg_put()来创建通用 netlink 头,它是genlmsghdr的一个实例。您用genlmsg_unicast()发送一个单播通用 netlink 消息,它实际上是对nlmsg_unicast()的包装。您可以用两种方式发送多播通用网络链接消息:

  • genlmsg_multicast():该方法将消息发送到默认的网络名称空间net_init
  • genlmsg_multicast_allns():该方法将消息发送到所有网络名称空间。

(本节提到的所有方法的原型都在include/net/genetlink.h中。)

您可以从用户空间创建一个通用的 netlink 套接字,如下所示:socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);这个调用在内核中由netlink_create()方法处理,就像普通的、非通用的 netlink 套接字一样,正如您在上一节中看到的。你可以使用 socket API 来执行进一步的调用,比如bind()sendmsg()或者recvmsg();然而,建议使用libnl库。

libnl-genl提供通用 netlink API,用于管理控制器、系列和命令注册。使用libnl-genl,可以调用genl_connect() 来创建本地套接字文件描述符,并将套接字绑定到 NETLINK_GENERIC netlink 协议。

让我们简单地看一下,当使用libnl库和libnl-genl库通过通用 netlink 套接字向内核发送命令时,在一个简短的典型用户空间-内核会话中会发生什么。

iw包使用了libnl-genl库。当您运行类似iw dev wlan0 list的命令时,会出现以下序列(省略不重要的细节):

state->nl_sock = nl_socket_alloc()

分配一个套接字(注意这里使用的是libnl核心 API,而不是通用的 netlink 家族(libnl-genl)。

genl_connect(state->nl_sock)

用 NETLINK_GENERIC 调用socket()并在这个套接字上调用bind()genl_connect()libnl-genl库的一个方法。

genl_ctrl_resolve(state->nl_sock, "nl80211");

此方法将通用 netlink 系列名称("nl80211")解析为相应的数字系列标识符。用户空间应用必须将它的后续消息发送到内核,指定这个 id。

genl_ctrl_resolve()方法调用genl_ctrl_probe_by_name(),它实际上用 CTRL_CMD_GETFAMILY 命令向内核发送一个通用的 netlink 消息。

在内核中,通用 netlink 控制器("nlctrl")通过ctrl_getfamily()方法处理 CTRL_CMD_GETFAMILY 命令,并将系列 id 返回给用户空间。这个 id 是在创建套接字时生成的。

image 注意通过运行genl ctrl list可以使用genl(属于iproute2)的用户空间工具获得所有注册的通用 netlink 家族的各种参数(如生成 id、头大小、最大属性等)。

现在,您已经准备好学习套接字监视接口了,它可以让您获得关于套接字的信息。socket monitoring 接口用于像ss这样的用户空间工具,它显示各种套接字类型的套接字信息和统计数据,以及其他项目,您将在下一节中看到。

套接字监控接口

netlink 套接字提供了一个基于 netlink 的子系统,可以用来获取关于套接字的信息。这个特性被添加到内核中,以支持 Linux 在用户空间(CRIU)中的检查点/恢复功能。为了支持这个功能,需要关于套接字的附加数据。例如,/procfs并没有说明哪些是 UNIX 域套接字(AF_UNIX)的对等体,这些信息是检查点/恢复支持所需要的。这些额外的数据不是通过/proc导出的,对procfs条目进行修改并不总是可取的,因为这可能会破坏用户空间应用。sock_diag netlink 套接字提供了一个 API 来访问这些附加数据。这个 API 在 CRIU 项目和ss util 中都有使用。没有sock_diag,在检查点一个进程(将一个进程的状态保存到文件系统)之后,您不能重建它的 UNIX 域套接字,因为您不知道对等体是谁。

为了支持ss工具使用的监控接口,创建了一个基于 netlink 的内核套接字(NETLINK_SOCK_DIAG)。ss工具是iproute2包的一部分,它使您能够以类似于netstat的方式获得套接字统计数据。它可以显示比其他工具更多的 TCP 和状态信息。

您为sock_diag创建一个 netlink 内核套接字,如下所示:

static int __net_init diag_net_init(struct net *net)
{
    struct netlink_kernel_cfg cfg = {
        .input    = sock_diag_rcv,
    };

    net->diag_nlsk = netlink_kernel_create(net, NETLINK_SOCK_DIAG, &cfg);
    return net->diag_nlsk == NULL ? -ENOMEM : 0;
}
(net/core/sock_diag.c)

sock_diag模块有一个名为sock_diag_handlerssock_diag_handler 对象表。该表由协议号索引(协议号列表见include/linux/socket.h)。

sock_diag_handler struct很简单:

struct sock_diag_handler {
__u8 family;
int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh);
};
(net/core/sock_diag.c)

每个想要向该表添加套接字监控接口条目的协议首先定义一个处理程序,然后调用sock_diag_register()、来指定其处理程序。例如,对于 UNIX 套接字,在net/unix/diag.c中有以下内容:

第一步是定义处理程序:

static const struct sock_diag_handler unix_diag_handler = {
    .family = AF_UNIX,
    .dump = unix_diag_handler_dump,
};

第二步是处理程序的注册:

static int __init unix_diag_init(void)
{
    return sock_diag_register(&unix_diag_handler);
}

现在,with ss –xss --unix,您可以转储由 UNIX diag模块收集的统计数据。以非常相似的方式,还有用于其他协议的diag模块,例如 UDP ( net/ipv4/udp_diag.c)、TCP ( net/ipv4/tcp_diag.c)、DCCP ( /net/dccp/diag.c)和 AF_PACKET ( net/packet/diag.c)。

netlink 套接字本身也有一个diag模块。/proc/net/netlink条目提供了关于 netlink 套接字(netlink_sock对象)的信息,如portidgroups、套接字的 inode 号等等。如果你想知道细节,转储/proc/net/netlinknet/netlink/af_netlink.c中的netlink_seq_show()处理。有一些netlink_sock字段/proc/net/netlink没有提供——例如dst_groupdst_portid或 32 以上的组。为此,增加了 netlink 套接字监控接口(net/netlink/diag.c)。您应该能够使用iproute2ss工具读取 netlink 套接字信息。netlink diag代码也可以构建为内核模块。

摘要

本章介绍了 netlink 套接字,它为用户空间和内核之间的双向通信提供了一种机制,并被网络子系统广泛使用。您已经看到了一些使用 netlink 套接字的例子。我还讨论了 netlink 消息,它们是如何创建和处理的。本章涉及的另一个重要主题是通用 netlink 套接字,包括它们的优点和用法。下一章将介绍 ICMP 协议,包括它在 IPv4 和 IPv6 中的使用和实现。

快速参考

我用 netlink 和通用 netlink 子系统的重要方法的简短列表来结束这一章。本章提到了其中一些:

int netlink _ rcv _ skb(struct sk _ buf * skb,int(* CB)(struct sk _ buf *,
struct nlmsgid *)

此方法处理接收 netlink 消息。它是从 netlink 家族的输入回调中调用的(例如,在 rtnetlink 家族的rtnetlink_rcv()方法中,或者在sock_diag家族的sock_diag_rcv()方法中)。该方法执行健全性检查,比如确保 netlink 消息头的长度不超过允许的最大长度(NLMSG_HDRLEN)。在消息是控制消息的情况下,它还避免调用指定的回调。如果 ACK 标志(NLM_F_ACK)被置位,它通过调用netlink_ack()方法发送一个错误消息。

struct sk _ buff * netlink _ alloc _ skb(struct sock * SSK,无符号 int size,
u32 dst_portid,gfp_t gfp_mask)

这个方法分配一个指定大小和gfp_mask的 size 其他参数(sskdst_portid)在使用内存映射 netlink IO (NETLINK_MMAP)时使用。该功能不在本章讨论,位于此:net/netlink/af_netlink.c

结构 netlink_sock *nlk_sk(结构 sock *sk)

这个方法返回netlink_sock对象,它有一个sk作为成员,位于这里:net/netlink/af_netlink.h

struct sock * net link _ kernel _ create(struct net * net,int unit,struct netlink_kernel_cfg *cfg)

这个方法创建一个内核 netlink 套接字。

struct nlmsg HDR * nlmsg _ HDR(const struct sk _ buf * skb)

该方法返回由skb->data指向的 netlink 消息头。

struct nlmsghdr * _ _ nlmsg _ put(struct sk _ buff * skb,u32 portid,
u32 seq,int type,int len,int flags)

该方法根据指定的参数建立一个 netlink 报文头,放在skb中,位于此:include/linux/netlink.h

struct sk_buff *nlmsg_new(size_t 有效载荷,gfp_t 标志)

该方法通过调用alloc_skb()分配一个具有指定消息有效负载的新 netlink 消息。如果指定的有效载荷为 0,则用NLMSG_HDRLEN调用alloc_skb()(在与 NLMSG_ALIGN 宏对齐后)。

int nlmsg_msg_size(int 有效载荷)

此方法返回 netlink 消息的长度(消息头长度和有效负载),不包括填充。

见 rtnl_register(int protocol,int msgtype,rtnl _ doit _ func 必须,rtnl_dumpit_func dumpit,
rtnl_calcit_func calcit)

此方法用三个指定的回调注册指定的 rtnetlink 消息类型。

static int rtnetlink _ rcv _ msg(struct sk _ buf * skb,struct nlmsghdr *nlh)

此方法处理 rtnetlink 消息。

静态 int rtnl_fill_ifinfo(结构 sk_buff *skb,结构 net_device *dev,
int 类型,u32 pid,u32 seq,u32 change,
无符号 int 标志,u32 ext_filter_mask)

这个方法创建了两个对象:一个 netlink 消息头(nlmsghdr)和一个ifinfomsg对象,位于 netlink 消息头之后。

void rtnl _ notify(struct sk _ buff * skb,struct net *net,u32 pid,u32 group,
struct nlmsghdr *nlh,gfp_t 标志)

此方法发送 rtnetlink 消息。

int gen l _ register _ MC _ group(struct gen l _ family * family,
struct gen l _ multicast _ group * grp)

该方法注册指定的多播组,通知用户空间,并在成功时返回 0 或负错误代码。指定的多播组必须有一个名称。除了具有固定 id 0x 10(GENL _ ID _ CTRL)的notify_grp之外,所有多播组的find_first_zero_bit()方法在该方法中动态生成多播组 ID。

void genl_unregister_mc_group(结构 genl_family *family,
结构 genl_multicast_group *grp)

这个方法注销指定的多播组,并通知用户空间。该组中的所有当前侦听器都会被删除。在取消注册家族之前,没有必要取消注册所有多播组—取消注册家族会导致所有分配的多播组自动取消注册。

int gen l _ register _ ops(struct gen l _ family * family,struct genl_ops *ops)

此方法注册指定的操作,并将它们分配给指定的系列。必须指定doit()dumpit()回调,否则操作将因-EINVAL 而失败。每个命令标识符只能注册一个操作结构。如果成功,它将返回 0 或负的错误代码。

int genl _ unregister _ ops(struct genl _ family * family,struct genl_ops *ops)

此方法注销指定的操作,并从指定的系列中取消对它们的分配。该操作会一直阻止,直到当前消息处理完成,并且直到取消注册过程完成后才会再次开始。在取消注册该系列之前,不必取消注册所有操作—取消注册该系列会导致所有分配的操作自动取消注册。如果成功,它将返回 0 或负的错误代码。

int gen l _ register _ family(struct gen l _ family * family)

此方法首先验证指定的族,然后注册它。只有一个家族可以用相同的姓氏或标识符注册。家族 id 可以等于 GENL_ID_GENERATE,导致自动生成和分配唯一的 ID。

int gen l _ register _ family _ with _ ops(struct gen l _ family * family,
struct genl_ops *ops,size_t n_ops)

此方法注册指定的系列和操作。只有一个家族可以用相同的姓氏或标识符注册。家族 id 可以等于 GENL_ID_GENERATE,导致自动生成和分配唯一的 ID。必须为每个注册的操作指定一个doitdumpit回调,否则函数将失败。每个命令标识符只能注册一个操作结构。这相当于对表中的每个操作条目先调用genl_register_family(),然后调用genl_register_ops(),注意在错误路径上取消注册家族。如果成功,该方法返回 0 或负的错误代码。

int gen l _ unregister _ family(struct gen l _ family * family)

此方法注销指定的系列,并在成功时返回 0 或负错误代码。

void * genlmsg _ put(struct sk _ buff * skb,u32 portid,u32 seq,
struct genl_family *family,int flags,u8 cmd)

此方法将通用 netlink 标头添加到 netlink 消息中。

int genl _ register _ family(struct genl _ family * family)
int genl _ unregister _ family(struct genl _ family * family)

此方法注册/注销通用 netlink 系列。

int genl _ register _ ops(struct genl _ family * family,struct genl_ops *ops)
int genl _ unregister _ ops(struct genl _ family * family,struct genl _ ops * ops)

此方法注册/注销通用 netlink 操作。

见 genl_lock(见)
见 genl_unlock(见)

该方法锁定/解锁通用网络链接互斥锁(genl_mutex)。例如在net/l2tp/l2tp_netlink.c中使用。

三、互联网控制信息协议

第二章讨论了 netlink 套接字的实现,以及 netlink 套接字如何被用作内核和用户空间之间的通信通道。本章介绍 ICMP 协议,这是一种第 4 层协议。用户空间应用可以通过使用套接字 API(最著名的例子可能是ping实用程序)来使用 ICMP 协议(发送和接收 ICMP 数据包)。本章讨论了内核如何处理这些 ICMP 数据包,并给出了一些例子。

ICMP 协议主要用作发送有关网络层(L3)的错误和控制消息的强制机制。该协议能够通过发送 ICMP 消息来获得关于通信环境中问题的反馈。这些消息提供错误处理和诊断。ICMP 协议相对简单,但对于确保正确的系统行为非常重要。ICMPv4 的基本定义在 RFC 792“互联网控制消息协议”中。这个 RFC 定义了 ICMPv4 协议的目标和各种 ICMPv4 消息的格式。我在本章中还提到了 RFC 1122(“对互联网主机—通信层的要求”),它定义了一些关于 ICMP 消息的要求;RFC 4443,它定义了 ICMPv6 协议;RFC 1812 定义了对路由器的要求。我还描述了存在哪些类型的 ICMPv4 和 ICMPv6 消息,它们是如何发送的,以及它们是如何被处理的。我将介绍 ICMP 套接字,包括为什么添加它们以及如何使用它们。请记住,ICMP 协议也用于各种安全攻击;例如,Smurf 攻击是一种拒绝服务攻击,在这种攻击中,大量带有目标受害者假冒源 IP 的 ICMP 数据包通过广播发送到使用 IP 广播地址的计算机网络。

icmpv 4

ICMPv4 消息可以分为两类:错误消息和信息消息(它们在 RFC 1812 中被称为“查询消息”)。ICMPv4 协议用于诊断工具,如pingtraceroute。著名的ping实用程序实际上是一个用户空间应用(来自iputils包),它打开一个原始套接字并发送一个 ICMP_ECHO 消息,应该得到一个 ICMP_REPLY 消息作为响应。Traceroute是一个实用程序,用于查找主机和给定目的 IP 地址之间的路径。traceroute实用程序基于为生存时间(TTL)设置不同的值,TTL 是 IP 报头中表示跳数的字段。traceroute实用程序利用了这样一个事实:当数据包的 TTL 达到 0 时,转发机器将发回 ICMP_TIME_EXCEED 消息。traceroute实用程序通过发送 TTL 为 1 的消息开始,并且随着每个接收到的代码为 ICMP_TIME_EXCEED 的 ICMP_DEST_UNREACH 作为回复,它将 TTL 增加 1 并再次发送到相同的目的地。它使用返回的 ICMP“超时”消息建立数据包经过的路由器列表,直到到达目的地,并返回 ICMP“回应回复”消息。默认情况下,Traceroute 使用 UDP 协议。ICMPv4 模块是net/ipv4/icmp.c。请注意,ICMPv4 不能构建为内核模块。

ICMPv4 初始化

ICMPv4 初始化是在启动阶段调用的inet_init()方法中完成的。inet_init()方法调用icmp_init()方法,后者又调用icmp_sk_init()方法来创建内核 ICMP 套接字以发送 ICMP 消息,并将一些 ICMP procfs变量初始化为默认值。(在本章的后面,你会遇到这些procfs变量。)

ICMPv4 协议的注册与其他 IPv4 协议的注册一样,在inet_init()中完成:

static const struct net_protocol icmp_protocol = {
    .handler        =  icmp_rcv,
    .err_handler    =  icmp_err,
    .no_policy      =  1,
    .netns_ok       =  1,
};

(net/ipv4/af_inet.c)

  • icmp_rcv:回调handler。这意味着,对于 IP 报头中的协议字段等于 IPPROTO_ICMP (0x1)的传入数据包,将调用icmp_rcv()

  • 该标志被设置为 1,这意味着不需要执行 IPsec 策略检查;例如,在ip_local_deliver_finish()中没有调用xfrm4_policy_check()方法,因为设置了no_policy标志。

  • netns_ok:该标志设置为 1,表示协议知道网络名称空间。在net_device部分的附录 A 中描述了网络名称空间。对于netns_ok字段为 0 的协议,inet_add_protocol()方法将失败,错误为-EINVAL

    static int __init inet_init(void) {
    . . .
        if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
            pr_crit("%s: Cannot add ICMP protocol\n", __func__);
    . . .
    
    int __net_init icmp_sk_init(struct net *net)
    {
        . . .
        for_each_possible_cpu(i) {
            struct sock *sk;
    
            err = inet_ctl_sock_create(&sk, PF_INET,
                           SOCK_RAW, IPPROTO_ICMP, net);
            if (err < 0)
                goto fail;
    
                    net->ipv4.icmp_sk[i] = sk;
                 . . .
                    sock_set_flag(sk, SOCK_USE_WRITE_QUEUE);
            inet_sk(sk)->pmtudisc = IP_PMTUDISC_DONT;
        }
        . . .
    
    }
    

icmp_sk_init()方法中,为每个 CPU 创建一个原始 ICMPv4 套接字,并保存在一个数组中。电流sk可以通过icmp_sk(struct net *net)方法访问。这些套接字用于icmp_push_reply()方法。ICMPv4 procfs条目在icmp_sk_init()方法中初始化;我在本章中提到了它们,并在本章末尾的“快速参考”部分对它们进行了总结。每个 ICMP 数据包都以 ICMPv4 报头开始。在讨论如何接收和传输 ICMPv4 消息之前,以下部分描述了 ICMPv4 头,以便您更好地理解 ICMPv4 消息是如何构建的。

ICMPv4 报头

ICMPv4 报头由类型(8 位)、代码(8 位)、校验和(16 位)和 32 位可变部分成员(其内容根据 ICMPv4 类型和代码而变化)组成,如图 3-1 所示。在 ICMPv4 报头之后是有效载荷,它应该包括原始数据包的 IPv4 报头及其有效载荷的一部分。根据 RFC 1812,它应该包含尽可能多的原始数据报,而 ICMPv4 数据报的长度不超过 576 字节。这个大小符合 RFC 791,RFC 791 规定“所有主机必须准备好接受最多 576 个八位字节的数据报。”

9781430261964_Fig03-01.jpg

图 3-1 。ICMPv4 标头

ICMPv4 报头由struct icmphdr : 表示

struct icmphdr {
  __u8        type;
  __u8        code;
  __sum16    checksum;
  union {
    struct {
        __be16    id;
        __be16    sequence;
    } echo;
    __be32    gateway;
    struct {
        __be16    __unused;
        __be16    mtu;
    } frag;
  } un;
};

(include/uapi/linux/icmp.h)

您将在www.iana.org/assignments/icmp-parameters/icmp-parameters.xml找到当前分配的 ICMPv4 消息类型编号和代码的完整列表。

ICMPv4 模块定义了一个名为icmp_pointersicmp_control对象数组,该数组由 ICMPv4 消息类型索引。让我们看看icmp_control的结构定义和icmp_pointers数组:

struct icmp_control {
    void (*handler)(struct sk_buff *skb);
    short error;        /* This ICMP is classed as an error message */
};

static const struct icmp_control icmp_pointers[NR_ICMP_TYPES+1];

NR_ICMP_TYPES 是最高的 ICMPv4 类型,为 18。

(include/uapi/linux/icmp.h)

此数组的icmp_control对象的错误字段仅对于错误消息类型为 1,如“目的地不可达”消息(ICMP_DEST_UNREACH),对于信息消息(如 echo (ICMP_ECHO))为 0(隐式)。有些处理程序被分配给多种类型。接下来,我将讨论处理程序和它们管理的 ICMPv4 消息类型。

ping_rcv()处理接收 ping 应答(ICMP_ECHOREPLY)。在 ICMP 套接字代码net/ipv4/ping.c中实现了ping_rcv()方法。在 3.0 之前的内核中,为了发送 ping,您必须在用户空间中创建一个原始套接字。当收到对 ping 的回复(ICMP_ECHOREPLY 消息)时,发送 ping 的原始套接字会对其进行处理。为了理解这是如何实现的,让我们看一看ip_local_deliver_finish(),这是一种处理传入的 IPv4 包并将它们传递给应该处理它们的套接字的方法:

static int ip_local_deliver_finish(struct sk_buff *skb)
{
    . . .
        int protocol = ip_hdr(skb)->protocol;
        const struct net_protocol *ipprot;
        int raw;

    resubmit:
        raw = raw_local_deliver(skb, protocol);
        ipprot = rcu_dereference(inet_protos[protocol]);
            if (ipprot != NULL) {
                    int ret;
                    . . .
                    ret = ipprot->handler(skb);
                    . . .

(net/ipv4/ip_input.c)

ip_local_deliver_finish()方法接收到一个 ICMP_ECHOREPLY 包时,它首先尝试将它传递给一个侦听原始套接字,该套接字将处理它。因为在用户空间中打开的原始套接字处理 ICMP_ECHOREPLY 消息,所以不需要对它做任何进一步的处理。所以当ip_local_deliver_finish()方法收到 ICMP_ECHOREPLY 时,首先调用raw_local_deliver()方法通过一个原始套接字对其进行处理,然后调用ipprot->handler(skb)(这是 ICMPv4 数据包情况下的icmp_rcv()回调)。因为数据包已经被一个原始套接字处理过了,所以没有什么要做的了。因此,通过调用 ICMP_ECHOREPLY 消息的处理程序icmp_discard()方法,数据包被无声地丢弃。

当 ICMP 套接字(“ping 套接字”)被集成到内核 3.0 中的 Linux 内核中时,这种情况被改变了。Ping 套接字将在本章后面的“ICMP 套接字(“Ping 套接字”)一节中讨论。在这个上下文中,我应该注意到,对于 ICMP 套接字,ping的发送者也可以是而不是原始套接字。例如,您可以创建这样一个套接字:socket (PF_INET, SOCK_DGRAM, PROT_ICMP)并用它来发送ping数据包。此套接字不是原始套接字。因此,echo 回复不会传递给任何原始套接字,因为没有相应的原始套接字进行侦听。为了避免这个问题,ICMPv4 模块使用ping_rcv()回调来处理接收 ICMP_ECHOREPLY 消息。ping模块位于 IPv4 层(net/ipv4/ping.c)。然而,net/ipv4/ping.c中的大部分代码是双栈代码(适用于 IPv4 和 IPv6)。因此,ping_rcv()方法也处理 IPv6 的 ICMPV6_ECHO_REPLY 消息(参见net/ipv6/icmp.c)中的icmpv6_rcv())。我将在本章的后面更多地讨论 ICMP 套接字。

icmp_discard() 是一个空的处理程序,用于不存在的消息类型(头文件中编号没有对应声明的消息类型)和一些不需要处理的消息,例如 ICMP _ TIMESTAMPREPLY。ICMP_TIMESTAMP 和 ICMP _ TIMESTAMPREPLY 消息用于时间同步;发送者在 ICMP_TIMESTAMP 请求中发送originate timestamp;接收方发送带有三个时间戳的 ICMP _ timestamp preply:时间戳请求发送方发送的起始时间戳,以及接收时间戳和发送时间戳。有比 ICMPv4 时间戳消息更常用的时间同步协议,如网络时间协议(NTP)。我还应该提到地址掩码请求(ICMP_ADDRESS ),它通常由主机发送给路由器,以便获得适当的子网掩码。收件人应该用地址掩码回复邮件来回复此邮件。过去由icmp_address()方法和icmp_address_reply()方法处理的 ICMP_ADDRESS 和 ICMP_ADDRESSREPLY 消息现在也由icmp_discard()处理。原因是有其他方法可以获得子网掩码,比如 DHCP。

icmp_unreach()处理 ICMP_DEST_UNREACH、ICMP_TIME_EXCEED、ICMP_PARAMETERPROB 和 ICMP_QUENCH 消息类型。

可以在各种条件下发送一条ICMP_DEST_UNREACH消息。本章的“发送 ICMPv4 消息:无法到达目的地”一节描述了其中的一些情况。

在两种情况下发送ICMP_TIME_EXCEEDED消息:

ip_forward()中,每个数据包的 TTL 递减。根据 RFC 1700,IPv4 协议的建议 TTL 为 64。如果 TTL 达到 0,这表示应该丢弃数据包,因为可能存在某种循环。因此,如果 TTL 在ip_forward()中达到 0,就会调用icmp_send()方法:

icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
(net/ipv4/ip_forward.c)

在这种情况下,发送一个 ICMP_TIME_EXCEEDED 消息,代码为 ICMP_EXC_TTL,释放 SKB,InHdrErrors SNMP 计数器(IPSTATS _ MIB _ INHDRERRORS)递增,该方法返回 NET_RX_DROP。

ip_expire()中,当一个片段超时时,会发生以下情况:

icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
(net/ipv4/ip_fragment.c)

ip_options_compile()方法或ip_options_rcv_srr()方法(net/ipv4/ip_options.c)中解析 IPv4 报头选项失败时,发送 ICMP_PARAMETERPROB 消息。这些选项是 IPv4 报头的可选可变长度字段(最多 40 个字节)。IP 选项在第四章的中讨论。

ICMP_QUENCH 消息类型实际上已被否决。根据 RFC 1812,4.3.3.3 部分(源抑制):“路由器不应该发起 ICMP 源抑制消息”,此外,“路由器可以忽略它收到的任何 ICMP 源抑制消息。”ICMP_QUENCH 消息旨在减少拥塞,但事实证明这是一个无效的解决方案。

icmp_redirect() 处理 ICMP_REDIRECT 消息;根据 RFC 1122 3.2.2.2 部分,主机不应发送 ICMP 重定向消息;重定向只能由网关发送。icmp_redirect()处理 ICMP_REDIRECT 消息。过去,icmp_redirect()调用ip_rt_redirect(),但是现在不再需要ip_rt_redirect()调用,因为协议处理程序现在可以正确地将重定向传播回路由代码。事实上,在内核 3.6 中,ip_rt_redirect()方法被移除了。因此,icmp_redirect()方法首先执行健全性检查,然后调用icmp_socket_deliver(),它将数据包传递给原始套接字并调用协议错误处理程序(如果它存在的话)。第六章更深入地讨论了 ICMP_REDIRECT 消息。

icmp_echo() 通过用icmp_reply()发送回应回复(ICMP_ECHOREPLY)来处理回应(“ping”)请求(ICMP_ECHO)。如果设置了案例net->ipv4.sysctl_icmp_echo_ignore_all,则不会发送回复。关于配置 ICMPv4 procfs条目,请参见本章末尾的“快速参考”部分,以及Documentation/networking/ip-sysctl.txt

icmp_timestamp()通过用icmp_reply()发送 ICMP _ TIMESTAMPREPLY 来处理 ICMP 时间戳请求(ICMP_TIMESTAMP)。

在讨论通过icmp_reply()方法和icmp_send()方法发送 ICMP 消息之前,我应该描述一下在这两种方法中使用的icmp_bxm(“ICMP 构建 xmit 消息”)结构:

struct icmp_bxm {
    struct sk_buff *skb;
    int offset;
    int data_len;

    struct {
        struct icmphdr icmph;
        __be32           times[3];
    } data;
    int head_len;
    struct ip_options_data replyopts;
};

  • skb:对于icmp_reply()方法,这个skb是请求包;icmp_param对象(icmp_bxm的实例)就是从它构建的(在icmp_echo()方法和icmp_timestamp()方法中)。对于icmp_send()方法,这个skb是由于某些条件触发发送 ICMPv4 消息的方法;在本节中,您将看到几个此类消息的示例。
  • offset:在skb_network_header(skb)skb->data之间的差异(偏移)。
  • data_len : ICMPv4 数据包有效负载大小。
  • icmph:ICMP v4 报头。
  • times[3]:三个时间戳的数组,填入icmp_timestamp()
  • head_len:icmp v4 报头的大小(在icmp_timestamp()的情况下,时间戳有额外的 12 个字节)。
  • replyopts:一个ip_options数据对象。IP 选项是 IP 报头后的可选字段,最多 40 个字节。它们支持高级功能,如严格路由/松散路由、记录路由、时间戳等。它们是用ip_options_echo()方法初始化的。第四章讨论知识产权期权。

接收 ICMPv4 消息

ip_local_deliver_finish()方法处理本地机器的数据包。当获取 ICMP 数据包时,该方法将该数据包传递给已执行 ICMPv4 协议注册的原始套接字。在icmp_rcv()方法中,首先增加InMsgs SNMP 计数器(ICMP_MIB_INMSGS)。随后,检验校验和的正确性。如果校验和不正确,两个 SNMP 计数器增加,InCsumErrorsInErrors(分别为 ICMP_MIB_CSUMERRORS 和 ICMP_MIB_INERRORS),SKB 被释放,该方法返回 0。在这种情况下,icmp_rcv()方法不会返回错误。事实上,icmp_rcv()方法总是返回 0;在校验和错误的情况下返回 0 的原因是,当接收到错误的 ICMP 消息时,除了丢弃它之外,不应该做任何特殊的事情;当协议处理程序返回否定错误时,会再次尝试处理数据包,这种情况下不需要这样做。更多细节,请参考ip_local_deliver_finish()方法的实现。然后检查 ICMP 报头,以便找到它的类型;相应的procfs消息类型计数器递增(每个 ICMP 消息类型都有一个procfs计数器),并且执行健全性检查以验证它不高于最高允许值(NR_ICMP_TYPES)。根据 RFC 1122 的第 3.2.2 节,如果收到未知类型的 ICMP 消息,它必须被无声地丢弃。因此,如果消息类型超出范围,InErrors SNMP 计数器(ICMP_MIB_INERRORS)将递增,SKB 将被释放。

如果数据包是广播或组播,并且是 ICMP_ECHO 消息或 ICMP_TIMESTAMP 消息,则通过读取变量net->ipv4.sysctl_icmp_echo_ignore_broadcasts检查是否允许广播/组播 ECHO 请求。该变量可通过写入/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts经由procfs进行配置,默认情况下其值为 1。如果设置了该变量,数据包将被无声地丢弃。这是根据 RFC 1122 的 3.2.2.6 部分完成的:“发往 IP 广播或 IP 多播地址的 ICMP 回应请求可能会被无声地丢弃。”根据 RFC 的 3.2.2.8 部分,“发往 IP 广播或 IP 多播地址的 ICMP 时间戳请求消息可能会被无声地丢弃。”然后,执行检查以检测该类型是否允许广播/多播(ICMP_ECHO, ICMP_TIMESTAMP, ICMP_ADDRESS, and ICMP_ADDRESSREPLY). If it is not one of these message types, the packet is dropped and 0 is returned. Then according to its type, the corresponding entry in the icmp_pointers数组被取出,并且适当的处理程序被调用。让我们看看icmp_control调度表中的 ICMP_ECHO 条目:```sh`

static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = {
...
  [ICMP_ECHO] = {
        .handler = icmp_echo,
    },
...
}
```sh

所以当接收到 ping(消息的类型是“Echo Request”,ICMP_ECHO)时,它由`icmp_echo()`方法处理。`icmp_echo()`方法将 ICMP 头中的类型改为 ICMP_ECHOREPLY,并通过调用`icmp_reply()`方法发送回复。除了`ping`,唯一需要响应的其他 ICMP 消息是时间戳消息(ICMP _ TIMESTAMP);它由`icmp_timestamp()`方法处理,与 ICMP_ECHO 的情况非常相似,该方法将类型更改为 ICMP _ TIMESTAMPREPLY,并通过调用`icmp_reply()`方法发送回复。发送由`ip_append_data()`和`ip_push_pending_frames()`完成。接收 ping 应答(ICMP_ECHOREPLY)由`ping_rcv()`方法`.` 处理

您可以使用以下命令禁用对 pings 的回复:

```
echo 1 >  /proc/sys/net/ipv4/icmp_echo_ignore_all
```sh

有一些回调处理不止一种 ICMP 类型。例如,`icmp_discard()`回调处理类型不由 Linux ICMPv4 实现处理的 ICMPv4 数据包,以及 ICMP _ TIMESTAMPREPLY、ICMP_INFO_REQUEST、ICMP_ADDRESSREPLY 等消息。

发送 ICMPv4 消息:“目的地不可达”

发送 ICMPv4 消息有两种方法:第一种是`icmp_reply()`方法,它作为对 ICMP_ECHO 和 ICMP_TIMESTAMP 这两种类型的 ICMP 请求的响应发送。第二种是`icmp_send()`方法,本地机器在特定条件下发起发送 ICMPv4 消息(在本节中描述)。这两种方法最终都会调用`icmp_push_reply()`来实际发送数据包。作为对来自`icmp_echo()`方法的 ICMP_ECHO 消息的响应,调用`icmp_reply()`方法,并作为对来自`icmp_timestamp()`方法的 ICMP_TIMESTAMP 消息的响应。从 IPv4 网络堆栈中的许多地方调用`icmp_send()`方法——例如,从 netfilter、从转发代码(`ip_forward.c` ) *、*从类似`ipip`和`ip_gre`的隧道等等。

本节研究发送“目的地不可达”消息的一些情况(类型为 ICMP_DEST_UNREACH)。

代码 2: ICMP_PROT_UNREACH(协议不可达)

当 IP 报头的协议(它是一个 8 位字段)是一个不存在的协议时,ICMP _ DEST _ un reach/ICMP _ PROT _ un reach 被发送回发送方,因为这样的协议没有协议处理程序(协议处理程序数组由协议号索引,因此对于不存在的协议,将没有处理程序)。所谓*不存在的*协议,我的意思是,由于某些错误,IPv4 报头的协议号确实没有出现在协议号列表中(您可以在`include/uapi/linux/in.h` *、*中找到 IPv4 的协议号列表),或者内核是在不支持该协议的情况下构建的,因此,该协议没有被注册,并且在协议处理程序数组中没有它的条目。因为无法处理这样的数据包,所以应该向发送方回复一个“目的地不可达”的 ICMPv4 消息;ICMPv4 回复中的 ICMP_PROT_UNREACH 代码表示错误原因,“协议不可达”见下文:

```
static int ip_local_deliver_finish(struct sk_buff *skb)
  {
    ...
    int protocol = ip_hdr(skb)->protocol;
    const struct net_protocol *ipprot;
    int raw;

resubmit:
    raw = raw_local_deliver(skb, protocol);

    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot != NULL) {
        ...
    } else {
    if (!raw) {
    if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
            IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
            icmp_send(skb, ICMP_DEST_UNREACH,ICMP_PROT_UNREACH, 0);
              }
        ...
  }
```sh

```
(net/ipv4/ip_input.c)
```sh

在这个例子中,通过协议在`inet_protos`阵列中执行查找;因为没有找到条目,这意味着该协议没有在内核中注册。

代码 3: ICMP_PORT_UNREACH(“端口不可达”)

接收 UDPv4 数据包时,会搜索匹配的 UDP 套接字。如果没有找到匹配的套接字,则检验校验和的正确性。如果它是错误的,数据包将被无声地丢弃。如果正确,则更新统计数据,并发回“目的地不可达”/“端口不可达”ICMP 消息:

```
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, int proto)
{
        struct sock *sk;
        ...
        sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable)
        ...
        if (sk != NULL) {
        ...
        }

        /* No socket. Drop packet silently, if checksum is wrong */
    if (udp_lib_checksum_complete(skb))
        goto csum_error;

        UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
        ...
        }
...

}
```sh

```
(net/ipv4/udp.c)
```sh

通过`__udp4_lib_lookup_skb()`方法执行查找,如果没有套接字,则更新统计数据,并发回一个 ICMP_PORT_UNREACH 代码为的 ICMP_DEST_UNREACH 消息。

代码 4:需要 ICMP _ FRAG _ NEEDED

当转发长度大于传出链路 MTU 的数据包时,如果 IPv4 报头(IP_DF)中的不分段(DF)位被置位,则该数据包被丢弃,并且带有 ICMP_FRAG_NEEDED 代码的 ICMP_DEST_UNREACH 消息被发送回发送方:

```
int ip_forward(struct sk_buff *skb)
{
        ...
        struct rtable *rt;      /* Route we use */
        ...
        if (unlikely(skb->len > dst_mtu(&rt->dst) && !skb_is_gso(skb) &&
                     (ip_hdr(skb)->frag_off & htons(IP_DF))) && !skb->local_df) {
                IP_INC_STATS(dev_net(rt->dst.dev), IPSTATS_MIB_FRAGFAILS);
                icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
                          htonl(dst_mtu(&rt->dst)));
                goto drop;
        }
        ...
}
```sh

```
(net/ipv4/ip_forward.c)
```sh

代码 5: ICMP_SR_FAILED

当转发带有严格路由选项和网关设置的数据包时,会发回一条带有 ICMP_SR_FAILED 代码的“目的地不可达”消息,数据包会被丢弃:

```
int ip_forward(struct sk_buff *skb)
 {
         struct ip_options *opt  = &(IPCB(skb)->opt);
         ...
         if (opt->is_strictroute && rt->rt_uses_gateway)
                 goto sr_failed;
         ...
sr_failed:
          icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
          goto drop;
}
```sh

```
(net/ipv4/ip_forward.c)
```sh

有关所有 IPv4“目的地不可达”代码的完整列表,请参见本章末尾“快速参考”部分的表 3-1 。注意,用户可以使用`iptables`拒绝目标和`--reject-with`限定符配置一些规则,这些规则可以根据选择发送“目的地不可达”消息;本章末尾的“快速参考”部分有更多信息。

`icmp_reply()`和`icmp_send()`方法都支持速率限制;他们调用`icmpv4_xrlim_allow()`,如果速率限制检查允许发送数据包(`icmpv4_xrlim_allow()`返回`true`),他们就发送数据包。这里应该提到的是,速率限制不是自动对所有类型的流量执行的。以下是不执行速率限制检查的条件:

*   消息类型未知。
*   该数据包属于 PMTU 发现。
*   该设备是一个环回设备。
*   速率掩码中没有启用 ICMP 类型。

如果所有这些条件都不匹配,则通过调用`inet_peer_xrlim_allow()`方法来执行速率限制。您可以在本章末尾的“快速参考”部分找到更多关于速率屏蔽的信息。

让我们看看`icmp_send()`方法的内部。首先,这是它的原型:

```
void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info)
```sh

skb `_in`是导致调用`icmp_send()`方法的 skb,`type`和`code`分别是 ICMPv4 消息`type`和`code`。最后一个参数`info`用于以下情况:

*   对于 ICMP_PARAMETERPROB 消息类型,它是发生解析问题的 IPv4 标头中的偏移量。
*   对于带有 ICMP_FRAG_NEEDED 代码的 ICMP_DEST_UNREACH 消息类型,它是 MTU。
*   对于带有 ICMP_REDIR_HOST 代码的 ICMP_REDIRECT 消息类型,它是发起 SKB 的 IPv4 报头中的目的地址的 IP 地址。

当进一步研究`icmp_send()`方法时,首先有一些健全性检查。那么多播/广播分组被拒绝。通过检查 IPv4 报头的`frag_off`字段来检查分组是否是片段。如果数据包被分段,则会发送一条 ICMPv4 消息,但只针对第一个分段。根据 RFC 1812 的 4.3.2.7 部分,不得因收到 ICMP 错误消息而发送 ICMP 错误消息。因此,首先执行检查以发现要发送的 ICMPv4 消息是否是错误消息,如果是,则执行另一检查以发现发起 SKB 是否包含错误 ICMPv4 消息,如果是,则该方法返回而不发送 ICMPv4 消息。此外,如果类型是未知的 ICMPv4 类型(高于 NR_ICMP_TYPES ),该方法返回时不发送 ICMPv4 消息,尽管 RFC 没有明确指定这一点。然后根据`net->ipv4.sysctl_icmp_errors_use_inbound_ifaddr`值的值确定源地址(更多细节在本章末尾的“快速参考”一节)。然后调用`ip_options_echo()`方法来复制调用 SKB 的 IPv4 报头的 IP 选项。一个`icmp_bxm`对象`(icmp_param)`正在被分配和初始化,并且在路由子系统中使用`icmp_route_lookup()`方法执行查找。然后调用`icmp_push_reply()`方法。

让我们看一下`icmp_push_reply()`方法,它实际上发送了数据包。`icmp_push_reply()`首先通过调用以下命令找到应该发送数据包的套接字:

```
sk = icmp_sk(dev_net((*rt)->dst.dev));
```sh

`dev_net()`方法返回传出网络设备的网络名称空间。(第十四章和附录 A 中讨论了`dev_net()`方法和网络名称空间。)然后,`icmp_sk()`方法获取套接字(因为在 SMP 中每个 CPU 有一个套接字)。然后调用`ip_append_data()`方法将数据包移动到 IP 层。如果`ip_append_data()`方法失败,通过增加 ICMP_MIB_OUTERRORS 计数器来更新统计数据,并且调用`ip_flush_pending_frames()`方法来释放 SKB。我在第四章的中讨论了`ip_append_data()`方法和`ip_flush_pending_frames()`方法。

既然您已经对 ICMPv4 了如指掌,那么是时候继续学习 ICMPv6 了。

ICMPv6

在网络层(L3)报告错误方面,ICMPv6 与 ICMPv4 有许多相似之处。ICMPv6 还有一些在 ICMPv4 中不执行的任务。本节讨论 ICMPv6 协议、它的新功能(在 ICMPv4 中没有实现)以及类似的功能。RFC 4443 中定义了 ICMPv6。如果您深入研究 ICMPv6 代码,您可能迟早会遇到提到 RFC 1885 的注释。事实上,RFC 1885“用于互联网协议版本 6 (IPv6)的互联网控制消息协议(ICMPv6)”是 ICMPv6 RFC 的基础。它被 RFC 2463 淘汰,而 RFC 2463 又被 RFC 4443 淘汰。ICMPv6 实现基于 IPv4,但它更复杂;本节将讨论新增的更改和内容。

根据 RFC 4443 第一部分的规定,ICMPv6 协议的下一个报头值为 58(第八章讨论了 IPv6 下一个报头)。ICMPv6 是 IPv6 不可或缺的一部分,必须由每个 IPv6 节点完全实现。除了错误处理和诊断之外,ICMPv6 还用于 IPv6 中的邻居发现(nd)协议,该协议取代并增强了 IPv4 中 ARP 的功能,还用于多播监听发现(MLD)协议,该协议是 IPv4 中 IGMP 协议的对等物,如图 3-2 中所示。

![9781430261964_Fig03-02.jpg](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/linux-kern-net/img/9781430261964_Fig03-02.jpg)

图 3-2 。IPv4 和 IPv6 中的 ICMP。IPv6 中 IGMP 协议的对应部分是 MLD 协议,IPv6 中 ARP 协议的对应部分是 nd 协议

本节介绍 ICMPv6 的实现。正如您将看到的,在处理和发送消息的方式上,它与 ICMPv4 实现有许多共同之处。甚至有在 ICMPv4 和 ICMPv6 中调用相同方法的情况(例如,`ping_rcv()`和`inet_peer_xrlim_allow()`)。有些不同,有些主题是 ICMPv6 特有的。`ping6`和`traceroute6`实用程序基于 ICMPv6,是 IPv4 的`ping`和`traceroute`实用程序的对应物(在本章开头的 ICMPv4 部分提到过)。ICMPv6 在`net/ipv6/icmp.c`和`net/ipv6/ip6_icmp.c`中实现。与 ICMPv4 一样,ICMPv6 不能作为内核模块构建。

ICMPv6 初始化

ICMPv6 初始化由`icmpv6_init()`方法和`icmpv6_sk_init()`方法完成。ICMPv6 协议的注册由`icmpv6_init()` ( `net/ipv6/icmp.c`)完成:

```
static const struct inet6_protocol icmpv6_protocol = {
         .handler        =       icmpv6_rcv,
         .err_handler    =       icmpv6_err,
         .flags          =       INET6_PROTO_NOPOLICY|INET6_PROTO_FINAL,
 };
```sh

`handler`回调为`icmpv6_rcv()`;这意味着对于协议字段等于 IPPROTO_ICMPV6 (58)的传入数据包,将调用`icmpv6_rcv()`。

当 INET6_PROTO_NOPOLICY 标志被设置时,这意味着不应该执行 IPsec 策略检查;例如,在`ip6_input_finish()`中没有调用`xfrm6_policy_check()`方法,因为 INET6_PROTO_NOPOLICY 标志被设置:

```
int __init icmpv6_init(void)
 {
         int err;
         ...
         if (inet6_add_protocol(&icmpv6_protocol, IPPROTO_ICMPV6) < 0)
                 goto fail;
         return 0;
 }

static int __net_init icmpv6_sk_init(struct net *net)
{
    struct sock *sk;
        ...
    for_each_possible_cpu(i) {
        err = inet_ctl_sock_create(&sk, PF_INET6,
                       SOCK_RAW, IPPROTO_ICMPV6, net);
        ...
        net->ipv6.icmp_sk[i] = sk;
        ...

}
```sh

与在 ICMPv4 中一样,为每个 CPU 创建一个原始的 ICMPv6 套接字,并保存在一个数组中。当前的`sk`可以通过`icmpv6_sk()`方法访问。

ICMPv6 标题

ICMPv6 报头由`type` (8 位)`code` (8 位)`checksum` (16 位)组成,如图图 3-3 所示。

![9781430261964_Fig03-03.jpg](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/linux-kern-net/img/9781430261964_Fig03-03.jpg)

图 3-3 。ICMPv6 标头

ICMPv6 报头由`struct icmp6hdr`表示:

```
struct icmp6hdr {
    __u8        icmp6_type;
    __u8        icmp6_code;
    __sum16    icmp6_cksum;
    ...
}
```sh

没有足够的空间显示`struct icmp6hdr`的所有字段,因为它太大了(它是在`include/uapi/linux/icmpv6.h`中定义的)。当类型字段的高位为 0(取值范围为 0-127)时,表示出错信息;当高位为 1(取值范围为 128 到 255)时,表示信息消息。表 3-1 根据消息的编号和内核符号显示了 ICMPv6 消息类型。

表 3-1 。ICMPv6 消息

![Tab03-01.jpg](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/linux-kern-net/img/Tab03-01.jpg)

分配的 ICMPv6 类型和代码的当前完整列表可在`www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xml`找到。

ICMPv6 执行一些 ICMPv4 没有执行的任务。例如,邻居发现是由 ICMPv6 完成的,而在 IPv4 中是由 ARP/RARP 协议完成的。多播组成员资格由 ICMPv6 结合 MLD(多播侦听程序发现)协议处理,而在 IPv4 中,这由 IGMP (Internet 组管理协议)执行。一些 ICMPv6 消息在含义上类似于 ICMPv4 消息;例如,ICMPv6 有这些消息:“无法到达目的地,”(icmp V6 _ DEST _ 未到达)、“超时”(icmp V6 _ 时间 _ 超出)、“参数问题”(ICMPV6_PARAMPROB)、“回显请求”(icmp V6 _ 回显请求),等等。另一方面,一些 ICMPv6 消息是 IPv6 独有的,例如 NDISC _ NEIGHBOUR _ SOLICITATION 消息。

接收 ICMPv6 消息

当获得一个 ICMPv6 包时,它被传递给`icmpv6_rcv()`方法,该方法只获得一个 SKB 作为参数。图 3-4 显示了接收到的 ICMPv6 消息的`Rx`路径。

![9781430261964_Fig03-04.jpg](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/linux-kern-net/img/9781430261964_Fig03-04.jpg)

图 3-4 。ICMPv6 消息的接收路径

在`icmpv6_rcv()`方法中,在一些健全性检查之后,`InMsgs` SNMP 计数器(ICMP6_MIB_INMSGS)递增。随后,检验校验和的正确性。如果校验和不正确,则`InErrors` SNMP 计数器(ICMP6_MIB_INERRORS)递增,SKB 被释放。在这种情况下,`icmpv6_rcv()`方法不会返回错误(事实上,它总是返回 0,很像它的 IPv4 对应方法`icmp_rcv()`)。然后,读取 ICMPv6 标头以找到其类型;相应的`procfs`消息类型计数器由 ICMP6MSGIN_INC_STATS_BH 宏递增(每个 ICMPv6 消息类型都有一个`procfs`计数器)。例如,当接收到 ICMPv6 回应请求(“pings”)时,`/proc/net/snmp6/Icmp6InEchos`计数器递增,当接收到 ICMPv6 邻居请求请求时,`/proc/net/snmp6/Icmp6InNeighborSolicits`计数器递增。

在 ICMPv6 中,没有像 ICMPv4 中的`icmp_pointers`表那样的调度表。根据 ICMPv6 消息类型,在一个长的`switch(type)`命令中调用处理程序:

*   “回应请求”(ICMPV6_ECHO_REQUEST)由`icmpv6_echo_reply()`方法处理。
*   “回显回复”(ICMPV6_ECHO_REPLY)由`ping_rcv()`方法处理。`ping_rcv()`方法在 IPv4 `ping`模块中(`net/ipv4/ping.c`);此方法是一种双栈方法(它处理 IPv4 和 IPv6—在本章开始时讨论过)。
*   数据包太大(ICMPV6_PKT_TOOBIG)。

*   首先检查数据块区(由`skb->data`指向)是否包含一个数据块,其大小至少与 ICMP 报头一样大。这是通过`pskb_may_pull()`方法完成的。如果不满足这个条件,数据包就会被丢弃。
*   然后调用`icmpv6_notify()`方法。这个方法最终调用`raw6_icmp_error()`方法,以便注册的原始套接字将处理 ICMP 消息。

*   “目的地不可达”、“超时”、“参数问题”(分别为 ICMPV6_DEST_UNREACH、ICMPV6_TIME_EXCEED、ICMPV6_PARAMPROB)也由`icmpv6_notify()`处理。
*   邻居发现(ND)消息 :

*   NDISC_ROUTER_SOLICITATION:通常发送到 FF02::2 的`all-routers`组播地址的消息,由路由器广告应答。(特殊 IPv6 组播地址在第八章的中讨论)。
*   NDISC_ROUTER_ADVERTISEMENT:路由器定期发送的消息,或者作为对路由器请求的即时响应。路由器通告包含用于链路确定和/或地址配置的前缀、建议的跳数限制值等等。
*   NDISC _ neighbor _ SOLICITATION:IP v4 中 ARP 请求的对应方。
*   NDISC _ neighbor _ ADVERTISEMENT:IP v4 中 ARP 回复的对应方。

*   NDISC_REDIRECT:由路由器用来通知主机到目的地的更好的第一跳。
*   所有邻居发现(ND)消息都由邻居发现方法`ndisc_rcv()` ( `net/ipv6/ndisc.c`)处理。在第七章的中讨论了`ndisc_rcv()`方法。

*   ICMPV6_MGM_QUERY(组播监听报告)由`igmp6_event_query()`处理。
*   ICMPV6_MGM_REPORT(组播监听报告)由`igmp6_event_report()`处理。注意:ICMPV6_MGM_QUERY 和 ICMPV6_MGM_REPORT 在第八章中有更详细的讨论。
*   未知类型的消息以及下面的消息都由`icmpv6_notify()`方法处理:

*   ICMPV6_MGM_REDUCTION:主机离开组播组时,发送 MLDv2 ICMPV6_MGM_REDUCTION 消息;参见`net/ipv6/mcast.c`中的`igmp6_leave_group()`方法。
*   ICMPV6_MLD2_REPORT: MLDv2 多播侦听器报告数据包;通常与所有支持 MLDv2 的路由器多播组地址(FF02::16)的目的地址一起发送。
*   ICMPV6_NI_QUERY- ICMP:节点信息查询。
*   ICMPV6_NI_REPLY: ICMP 节点信息响应。
*   ICMPV6_DHAAD_REQUEST: ICMP 家乡代理地址发现请求消息;请参见 RFC 6275 第 6.5 节“IPv6 中的移动性支持”
*   ICMPV6_DHAAD_REPLY: ICMP 家乡代理地址发现回复消息;参见 RFC 6275 第 6.6 节。
*   ICMPV6_MOBILE_PREFIX_SOL: ICMP 移动前缀请求消息格式;参见 RFC 6275 第 6.7 节。
*   ICMPV6_MOBILE_PREFIX_ADV: ICMP 移动前缀广告消息格式;参见 RFC 6275 第 6.8 节。

请注意,`switch(type)`命令是这样结束的:

```
    default:
        LIMIT_NETDEBUG(KERN_DEBUG "icmpv6: msg of unknown type\n");

        /* informational */
        if (type & ICMPV6_INFOMSG_MASK)
            break;

        /*
         * error of unknown type.
         * must pass to upper level
         */

        icmpv6_notify(skb, type, hdr->icmp6_code, hdr->icmp6_mtu);
    }
```sh

信息消息满足条件`(type & ICMPV6_INFOMSG_MASK)`,因此它们被丢弃,而不满足该条件的其他消息(因此应该是错误消息)被传递到上层。这是根据 RFC 4443 的第 2.4 节(“消息处理规则”)完成的。

发送 ICMPv6 消息

发送 ICMPv6 消息的主要方法是`icmpv6_send()`方法。当本地计算机在本节描述的条件下开始发送 ICMPv6 消息时,将调用方法。还有一个`icmpv6_echo_reply()`方法,它仅作为对 ICMPV6_ECHO_REQUEST ("ping ")消息的响应而被调用。从 IPv6 网络栈中的许多地方调用`icmp6_send()`方法。本节看几个例子。

示例:发送“超过跃点限制时间”ICMPv6 消息

转发数据包时,每台机器都将跳数限制计数器减 1。跃点限制计数器是 IPv6 标头的一个成员,它是 IPv6 中与 IPv4 中的生存时间相对应的部分。当跳数限制计数器头的值达到 0 时,通过调用`icmpv6_send()`方法发送带有 icmp V6 _ EXC _ 跳数限制代码的 ICMPV6_TIME_EXCEED 消息,然后更新统计数据并丢弃数据包:

```
int ip6_forward(struct sk_buff *skb)
{
    ...
        if (hdr->hop_limit <= 1) {
                 /* Force OUTPUT device used as source address */
                 skb->dev = dst->dev;
                 icmpv6_send(skb, ICMPV6_TIME_EXCEED, ICMPV6_EXC_HOPLIMIT, 0);
                 IP6_INC_STATS_BH(net,
                                  ip6_dst_idev(dst), IPSTATS_MIB_INHDRERRORS);

                 kfree_skb(skb);
                 return -ETIMEDOUT;
         }
    ...
}
```sh

```
(net/ipv6/ip6_output.c)
```sh

示例:发送“碎片重组时间超过”ICMPv6 消息

当一个片段超时时,通过调用`icmpv6_send()`方法:,发送回一个 ICMPV6_TIME_EXCEED 消息,消息中包含 icmp V6 _ EXC _ 片段时间代码

```
void ip6_expire_frag_queue(struct net *net, struct frag_queue *fq,
                            struct inet_frags *frags)
 {
        . . .
        icmpv6_send(fq->q.fragments, ICMPV6_TIME_EXCEED, ICMPV6_EXC_FRAGTIME, 0);
        . . .
 }
```sh

```
(net/ipv6/reassembly.c)
```sh

示例:发送“目的地不可达”/“端口不可达”ICMPv6 消息

接收 UDPv6 数据包时,会搜索匹配的 UDPv6 套接字。如果没有找到匹配的套接字,则检验校验和的正确性。如果它是错误的,数据包将被无声地丢弃。如果正确,统计信息(UDP_MIB_NOPORTS MIB 计数器,由`/proc/net/snmp6/Udp6NoPorts`输出到`procfs`)被更新,并且一个“目的地不可到达”/“端口不可到达”的 ICMPv6 消息与`icmpv6_send()`一起被发回:

```
int __udp6_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, int proto)
{
        ...
       sk = __udp6_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
        if (sk != NULL) {
        ...
        }
        ...
        if (udp_lib_checksum_complete(skb))
                goto discard;

        UDP6_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
        icmpv6_send(skb, ICMPV6_DEST_UNREACH, ICMPV6_PORT_UNREACH, 0);
        ...

}
```sh

这种情况与本章前面给出的 UDPv4 示例非常相似。

示例:发送“需要碎片”ICMPv6 消息

转发数据包时,如果其大小大于传出链路的 MTU,并且 SKB 中的`local_df`位未置位,则该数据包将被丢弃,ICMPV6_PKT_TOOBIG 消息将被发送回发送方。此消息中的信息用作路径 MTU (PMTU)发现过程的一部分。

请注意,与 IPv4 中发送带有 ICMP_FRAG_NEEDED 代码的 ICMP_DEST_UNREACH 消息的并行情况相反,在这种情况下,发送回的是 ICMPV6_PKT_TOOBIG 消息,而不是“目的地不可达”(ICMPV6_DEST_UNREACH)消息。ICMPV6_PKT_TOOBIG 消息在 ICMPv6: 中有自己的消息类型号

```
int ip6_forward(struct sk_buff *skb)
{
...
         if ((!skb->local_df && skb->len > mtu && !skb_is_gso(skb)) ||
             (IP6CB(skb)->frag_max_size && IP6CB(skb)->frag_max_size > mtu)) {
                 /* Again, force OUTPUT device used as source address */
                 skb->dev = dst->dev;
                 icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
                 IP6_INC_STATS_BH(net,
                                  ip6_dst_idev(dst), IPSTATS_MIB_INTOOBIGERRORS);
                 IP6_INC_STATS_BH(net,
                                  ip6_dst_idev(dst), IPSTATS_MIB_FRAGFAILS);
                 kfree_skb(skb);
                 return -EMSGSIZE;
         }
...
}
```sh

```
(net/ipv6/ip6_output.c)
```sh

示例:发送“参数问题”ICMPv6 消息

当在解析扩展标头时遇到问题时,会发回一条 ICMPV6_PARAMPROB 消息,其中包含 icmp V6 _ UNK _ 选项代码:

```
static bool ip6_tlvopt_unknown(struct sk_buff *skb, int optoff) {
        switch ((skb_network_header(skb)[optoff] & 0xC0) >> 6) {
        ...
        case 2: /* send ICMP PARM PROB regardless and drop packet */
                 icmpv6_param_prob(skb, ICMPV6_UNK_OPTION, optoff);
                 return false;
         }
```sh

```
(net/ipv6/exthdrs.c)
```sh

`icmpv6_send()`方法通过调用`icmpv6_xrlim_allow()`支持速率限制。这里我应该提到,与 ICMPv4 一样,ICMPv6 不会自动对所有类型的流量执行速率限制。以下是不执行速率限制检查的条件:

*   信息性消息
*   PMTU 发现
*   环回设备

如果所有这些条件都不匹配,则通过调用 ICMPv4 和 ICMPv6 共享的`inet_peer_xrlim_allow()`方法来执行速率限制。请注意,与 IPv4 不同,您不能在 IPv6 中设置速率掩码。ICMPv6 规范 RFC 4443 并没有禁止它,但它从未被实现。

让我们看看`icmp6_send()`方法的内部。首先,这是它的原型:

```
static void icmp6_send(struct sk_buff *skb, u8 type, u8 code, __u32 info)
```sh

参数和 IPv4 的`icmp_send()`方法类似,这里不再赘述。当进一步研究`icmp6_send()`代码时,您会发现一些健全性检查。通过调用`is_ineligible()`方法来检查触发消息是否为 ICMPv6 错误消息;如果是,则`icmp6_send()`方法终止。消息的长度不能超过 1280,这是 IPv6 的最小 MTU (IPV6_MIN_MTU,在`include/linux/ipv6.h`中定义)。这是根据 RFC 4443 第 2.4 (c)节完成的,该节规定每个 ICMPv6 错误消息必须包括尽可能多的 IPv6 违规(调用)数据包(导致错误的数据包),而不使错误消息数据包超过最小 IPv6 MTU。然后,通过`ip6_append_data()`方法和`icmpv6_push_pending_frame()`方法将消息传递到 IPv6 层,以释放 SKB。

现在我将转向`icmpv6_echo_reply()`方法;提醒一下,此方法是作为对 ICMPV6_ECHO 消息的响应而调用的。`icmpv6_echo_reply()`方法只获得一个参数,即 SKB。它构建了一个`icmpv6_msg`对象并将其类型设置为 ICMPV6_ECHO_REPLY。然后,它通过`ip6_append_data()`方法和`icmpv6_push_pending_frame()`方法将消息传递到 IPv6 层。如果`ip6_append_data()`方法失败,SNMP 计数器(ICMP6_MIB_OUTERRORS)递增,并且调用`ip6_flush_pending_frames()`来释放 SKB。

第七章和第八章也讨论了 ICMPv6。下一节将介绍 ICMP 套接字及其用途。

ICMP 套接字(" Ping 套接字")

Openwall GNU/*/Linux 发行版(Owl)的一个补丁添加了一种新的套接字类型(IPPROTO_ICMP ),它提供了优于其他发行版的安全性增强。ICMP 套接字启用一个`setuid-less`“ping”对于 Openwall GNU/*/Linux 来说,这是通往`setuid-less`发行版的最后一步。使用此修补程序,将使用以下内容创建一个新的 ICMPv4 ping 套接字(不是原始套接字):

```
socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP);
```sh

而不是用:

```
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
```sh

还支持 IPPROTO_ICMPV6 套接字,这是后来在`net/ipv6/icmp.c`中添加的。使用以下内容创建新的 ICMPv6 ping 套接字:

```
socket(PF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
```sh

而不是用:

```
socket(PF_INET6, SOCK_RAW, IPPROTO_ICMP6);
```sh

在 Mac OS X 中实现了类似的功能(非特权 ICMP );参见:`www.manpagez.com/man/4/icmp/`。

ICMP 套接字的大部分代码在`net/ipv4/ping.c`中;事实上,`net/ipv4/ping.c`中的大部分代码都是双栈的(IPv4 和 IPv6)。在 ??,只有很少的 IPv6 专用位。默认情况下,使用 ICMP 套接字是禁用的。您可以通过设置下面的`procfs`条目来启用 ICMP 套接字:`/proc/sys/net/ipv4/ping_group_range`。默认情况下,它是“1 0 ”,这意味着没有人(甚至是根用户)可以创建 ping 套接字。因此,如果您想允许一个拥有 1000 的`uid`和`gid`的用户使用 ICMP 套接字,您应该从命令行运行这个命令(具有 root 权限):`echo 1000 1000 > /proc/sys/net/ipv4/ping_group_range`,然后您可以从这个用户帐户使用 ICMP 套接字`ping`。如果您想为系统中的用户设置权限,您应该从命令行`echo 0  2147483647 > /proc/sys/net/ipv4/ping_group_range`运行。(2147483647 是 GID_T_MAX 的值;参见`include/net/ping.h`。)IPv4 和 IPv6 没有单独的安全设置;一切由`/proc/sys/net/ipv4/ping_group_range`控制。ICMP 套接字仅支持 IPv4 的 ICMP_ECHO 或 IPv6 的 ICMPV6_ECHO_REQUEST,在这两种情况下,ICMP 消息的代码都必须为 0。

`ping_supported()` helper 方法检查用于构建 ICMP 消息的参数(对于 IPv4 和 IPv6)是否有效。从`ping_sendmsg()`调用:

```
static inline int ping_supported(int family, int type, int code)
{
    return (family == AF_INET && type == ICMP_ECHO && code == 0) ||
           (family == AF_INET6 && type == ICMPV6_ECHO_REQUEST && code == 0);
}
```sh

```
(net/ipv4/ping.c)
```sh

ICMP 套接字将以下条目导出到 procfs:IP v4 的`/proc/net/icmp`和 IPv6 的`/proc/net/icmp6`。

有关 ICMP 套接字的更多信息,请参见`http://openwall.info/wiki/people/segoon/ping`和`http://lwn.net/Articles/420799/`。

摘要

本章讲述了 ICMPv4 和 ICMPv6 的实现。您了解了这两种协议的 ICMP 报头格式,以及使用这两种协议接收和发送消息。还讨论了 ICMPv6 的新特性,您将在接下来的章节中遇到这些新特性。使用 ICMPv6 消息的邻居发现协议在第七章中讨论,同样使用 ICMPv6 消息的 MLD 协议在第八章中讨论。下一章,第四章,讲述 IPv4 网络层的实现。

在接下来的“快速参考”一节中,我将介绍与本章中讨论的主题相关的主要方法,按照它们的上下文进行排序。本章中提到的两个表格、一些重要的相关`procfs`条目和一小段关于 ICMP 消息在`iptables reject`规则中的使用都被涵盖。

快速参考

我用一个 ICMPv4 和 ICMPv6 的重要方法的简短列表、6 个表格、一个关于`procfs`条目的部分,以及一个关于在`iptables`和`ip6tables`中使用拒绝目标来创建 ICMP“目的地不可达”消息的简短部分来结束本章。

方法

本章介绍了以下方法。

int icmp _ rcv(struct sk _ buf * skb):

此方法是处理传入 ICMPv4 数据包的主要处理程序。

extern void icmp _ send(struct sk _ buff * skb _ in,int type,int code,_ _ be32 info);

此方法发送 ICMPv4 消息。这些参数是启动 SKB、ICMPv4 消息类型、ICMPv4 消息代码和`info`(取决于类型)。

icmp6hdr *icmp6_hdr 结构(const struct sk _ buf * skb);

该方法返回指定的`skb`包含的 ICMPv6 头。

void icmp V6 _ send(struct sk _ buff * skb,u8 类型,u8 代码,_ _ u32 info);

此方法发送 ICMPv6 消息。这些参数是触发 SKB、ICMPv6 消息类型、ICMPv6 消息代码和`info`(取决于类型)。

参见 icmpv 6 _ param _ prob(struct sk _ buf * skb,u8 代码,int pos);

这个方法是`icmp6_send()`方法的一个方便版本,它所做的就是调用`icmp6_send()`,使用 ICMPV6_PARAMPROB 作为类型,使用其他指定的参数`skb`、`code`和`pos`,然后释放 SKB。

桌子

本章涵盖了以下表格。

表 3-2。ICMPv4“无法到达目的地”(ICMP _ DEST _ 无法到达)代码

| 

密码

| 

内核符号

| 

描述

|
| --- | --- | --- |
| Zero | ICMP_NET_UNREACH | 网络不可达 |
| one | ICMP_HOST_UNREACH | 主机无法访问 |
| Two | icmp _ prot _ unregish | 协议不可达 |
| three | ICMP_PORT_UNREACH | 端口不可达 |
| four | 需要 ICMP _ FRAG _ NEEDED | 需要分段,但设置了 DF 标志。 |
| five | ICMP _ SR _ 失败 | 源路由失败 |
| six | ICMP _ NET _ 未知 | 目标网络未知 |
| seven | ICMP _ 主机 _ 未知 | 目标主机未知 |
| eight | ICMP _ 主机 _ 隔离 | 源主机被隔离 |
| nine | icmp _ net _ 是 | 目标网络被管理性禁止。 |
| Ten | icmp _ host _ 是 | 目的主机被管理性禁止。 |
| Eleven | ICMP_NET_UNR_TOS | 对于服务类型,网络不可达。 |
| Twelve | ICMP_HOST_UNR_TOS | 对于服务类型,主机不可访问。 |
| Thirteen | ICMP _ PKT _ 已过滤 | 分组过滤 |
| Fourteen | ICMP _ PREC _ 违规 | 优先违规 |
| Fifteen | ICMP _ PREC _ 截止 | 优先截止 |
| Sixteen | 编号:ICMP_UNREACH | 不可达代码的数量 |

表 3-3。ICMPv4 重定向(ICMP_REDIRECT)代码

| 

密码

| 

内核符号

| 

描述

|
| --- | --- | --- |
| Zero | ICMP_REDIR_NET | 重定向网络 |
| one | ICMP _ REDIR _ 主机 | 重定向主机 |
| Two | icmp _ redir _ net | TOS 的重定向网络 |
| three | icmp _ redir _ hosttos | TOS 的重定向主机 |

表 3-4。ICMPv4 超时(ICMP_TIME_EXCEEDED)代码

| 

密码

| 

内核符号

| 

描述

|
| --- | --- | --- |
| Zero | ICMP_EXC_TTL | 超过 TTL 计数 |
| one | ICMP_EXC_FRAGTIME | 碎片重组时间超过限制 |

表 3-5。ICMPv6“无法到达目的地”(icmp V6 _ DEST _ 无法到达)代码

| 

密码

| 

内核符号

| 

描述

|
| --- | --- | --- |
| Zero | icmp V6 _ 北路由 | 没有到目的地的路线 |
| one | ICMPV6 _ ADM _ 禁止 | 与目的地的通信被行政禁止 |
| Two | ICMPV6 _ 非 _ 邻居 | 超出源地址的范围 |
| three | ICMPV6_ADDR_UNREACH | 地址无法到达 |
| four | ICMPV6_PORT_UNREACH | 端口不可达 |

注意,与 IP v4 ICMP _ DEST _ un reach/ICMP _ FRAG _ needs 对应的 ICMPV6_PKT_TOOBIG,并不是 ICMPV6_DEST_UNREACH 的代码,它本身就是一个 ICMPV6 类型。

表 3-6。ICMPv6 超时(ICMPV6_TIME_EXCEED)代码

| 

密码

| 

内核符号

| 

描述

|
| --- | --- | --- |
| Zero | ICMPV6_EXC_HOPLIMIT | 传输中超出跳数限制 |
| one | ICMPV6_EXC_FRAGTIME | 碎片重组时间超过限制 |

表 3-7。ICMPv6 参数问题(ICMPV6_PARAMPROB)代码

| 

密码

| 

内核符号

| 

描述

|
| --- | --- | --- |
| Zero | icmp V6 _ HDR _ 字段 | 遇到错误的标题字段 |
| one | ICMPV6_UNK_NEXTHDR | 遇到未知的下一个标头类型 |
| Two | icmp V6 _ UNK _ 选项 | 遇到未知的 IPv6 选项 |

procfs 条目

内核通过将值写入`/proc`下的条目,提供了一种从用户空间为各种子系统配置各种设置的方法。这些条目被称为`procfs`条目。所有的 ICMPv4 `procfs`条目都由`netns_ipv4`结构(`include/net/netns/ipv4.h`)中的变量表示,它是网络名称空间(`struct net`)中的一个对象。网络名称空间及其实现将在第十四章中讨论。以下是与 ICMPv4 `netns_ipv4`元素相对应的`sysctl`变量的名称、关于它们的用法的解释以及它们被初始化的默认值,还指定了在哪个方法中进行初始化。

sysctl_icmp_echo_ignore_all

当`icmp_echo_ignore_all`被设置时,回应请求(ICMP_ECHO)将不被回复。

`procfs`条目:`/proc/sys/net/ipv4/icmp_echo_ignore_all`

在`icmp_sk_init()`中初始化为 0

sysctl _ icmp _ echo _ ignore _ 广播

当接收到广播或多播回应(ICMP_ECHO)消息或时间戳(ICMP_TIMESTAMP)消息时,通过读取`sysctl_icmp_echo_ignore_broadcasts`检查广播/多播请求是否被允许。如果设置了该变量,则丢弃数据包并返回 0。

`procfs`条目:`/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts`

在`icmp_sk_init()`中初始化为 1

sysctl _ icmp _ ignore _ bogus _ error _ responses

一些路由器违反 RFC1122,向广播帧发送虚假响应。在`icmp_unreach()`方法中,你检查这个标志。如果此标志设置为 TRUE,内核将不会记录这些警告("<ipv4addr>)发送了无效的 ICMP 类型。。.").</ipv4addr>

`procfs`条目:`/proc/sys/net/ipv4/icmp_ignore_bogus_error_responses`

在`icmp_sk_init()`中初始化为 1

sysctl_icmp_ratelimit

限制向特定目标发送其类型与 icmp 速率掩码(`icmp_ratemask`,见本章下文)匹配的 ICMP 数据包的最大速率。

值 0 表示禁用任何限制;否则,它是响应之间的最小间隔,以毫秒为单位。

`procfs`条目:`/proc/sys/net/ipv4/icmp_ratelimit`

在`icmp_sk_init()`中初始化为 1 * HZ

sysctl _ icmp _ 成批分配掩码

由速率受限的 ICMP 类型构成的掩码。每个位都是 ICMPv4 类型。

`procfs`条目:`/proc/sys/net/ipv4/icmp_ratemask`

在`icmp_sk_init()`中初始化为 0x1818

sysctl _ icmp _ errors _ use _ inbound _ if addr

在`icmp_send()`中检查该变量的值。如果未设置,ICMP 错误消息将与数据包将发送到的接口的主地址一起发送。设置后,icmp 消息将与接收导致 ICMP 错误的数据包的接口的主地址一起发送。

`procfs`条目:`/proc/sys/net/ipv4/icmp_errors_use_inbound_ifaddr`

在`icmp_sk_init()`中初始化为 0

![image](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/linux-kern-net/img/sq.jpg) **注**更多关于 ICMP `sysctl`变量、它们的类型及其默认值,请参见

`Documentation/networking/ip-sysctl.txt`。

使用 iptables 创建“目的地不可达”信息

用户空间工具使我们能够设置规则,根据这些规则设置的过滤器来决定内核应该如何处理流量。处理`iptables`规则在 netfilter 子系统中完成,在第九章中讨论。`iptables`规则之一是`reject`规则,它丢弃数据包而不做进一步处理。当设置一个`iptables reject`目标时,用户可以使用`-j REJECT`和`--reject-with`限定符设置一个规则来发送带有各种代码的“目的地不可达”ICMPv4 消息。例如,下面的`iptables`规则将丢弃来自任何源的任何数据包,并发回“ICMP 主机被禁止”的 ICMP 消息:

```
iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
```sh

以下是用于设置 ICMPV4 消息的`--reject-with`限定符的可能值,该消息将被发送以回复发送主机:

```
icmp-net-unreachable   - ICMP_NET_UNREACH
icmp-host-unreachable  - ICMP_HOST_UNREACH
icmp-port-unreachable  - ICMP_PORT_UNREACH
icmp-proto-unreachable - ICMP_PROT_UNREACH
icmp-net-prohibited    - ICMP_NET_ANO
icmp-host-prohibited   - ICMP_HOST_ANO
icmp-admin-prohibited  - ICMP_PKT_FILTERED
```sh

您也可以使用`--reject-with tcp-reset`,它将发送一个 TCP RST 数据包来回复发送主机。

```
(net/ipv4/netfilter/ipt_REJECT.c)
```sh

与 IPv6 中的`ip6tables`一样,也有一个拒绝目标。例如:

```
ip6tables -A INPUT -s 2001::/64 -p ICMPv6  -j REJECT --reject-with icmp6-adm-prohibited
```sh

以下是用于设置 ICMPv6 消息的`--reject-with`限定符的可能值,该消息将被发送以回复发送主机:

```
no-route, icmp6-no-route              - ICMPV6_NOROUTE.
adm-prohibited, icmp6-adm-prohibited  - ICMPV6_ADM_PROHIBITED.
port-unreach, icmp6-port-unreachable  - ICMPV6_NOT_NEIGHBOUR.
addr-unreach, icmp6-addr-unreachable  - ICMPV6_ADDR_UNREACH.
```sh

```
(net/ipv6/netfilter/ip6t_REJECT.c)

四、IPv4

第三章讲述了 ICMP 协议在 IPv4 和 IPv6 中的实现。本章涉及 IPv4 协议,展示了在某些情况下如何使用 ICMP 消息来报告互联网协议错误。IPv4 协议(互联网协议版本 4)是当今基于标准的互联网的核心协议之一,并且路由互联网上的大部分流量。基本定义在 1981 年的 RFC 791“互联网协议”中。IPv4 协议提供任意两台主机之间的端到端连接。IP 层的另一个重要功能是转发数据包(也称为路由)和管理存储路由信息的表。第五章和第六章讨论 IPv4 路由。本章描述了 IPv4 Linux 实现:接收和发送 IPv4 数据包,包括多播数据包、IPv4 转发和处理 IPv4 选项。有些情况下,要发送的数据包大于传出接口的 MTU 在这种情况下,应该将数据包分割成更小的片段。当收到分段的数据包时,应该将它们组合成一个大的数据包,该数据包应该与分段前发送的数据包相同。这些也是本章讨论的 IPv4 协议的重要任务。

每个 IPv4 数据包都以至少 20 字节长的 IP 报头开始。如果使用 IP 选项,IPv4 报头最多可以有 60 个字节。在 IP 报头之后是传输报头(例如,TCP 报头或 UDP 报头),在它之后是有效载荷数据。要理解 IPv4 协议,您必须首先了解 IPv4 报头是如何构建的。在图 4-1 中,您可以看到 IPv4 报头,它由两部分组成:第一部分 20 个字节(直到 IPv4 报头中 options 字段的开头)是基本的 IPv4 报头,其后是 IP options 部分,其长度可以是 0 到 40 个字节。

9781430261964_Fig04-01.jpg

图 4-1 。IPv4 标头

IPv4 报头

IPv4 报头包含定义内核网络堆栈应该如何处理数据包的信息:正在使用的协议、源地址和目的地址、校验和、分段所需的数据包标识(id)、ttl有助于避免数据包因某些错误而被无休止地转发,等等。该信息存储在 IPv4 报头的 13 个成员中(第 14 个成员 IP Options 是 IPv4 报头的扩展,是可选的)。接下来描述 IPv4 的各种成员和各种 IP 选项。IPv4 报头由iphdr结构表示。其成员出现在图 4-1 中,将在下一节描述。本章后面的“IP 选项”一节将介绍 IP 选项及其用法。

图 4-1 显示了 IPv4 报头。所有成员始终存在,除了最后一个成员,即可选的 IP 选项。IPv4 成员的内容决定了它在 IPv4 网络堆栈中的处理方式:当出现问题时(例如,如果第一个成员的版本不是 4,或者校验和不正确),数据包将被丢弃。每个 IPv4 数据包都以 IPv4 报头开始,其后是有效载荷:

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 <asm/byteorder.h>"
#endif
    __u8          tos;
    __be16        tot_len;
    __be16        id;
    __be16        frag_off;
    __u8          ttl;
    __u8          protocol;
    __sum16       check;
    __be32        saddr;
    __be32        daddr;
    /*The options start here. */
};
(include/uapi/linux/ip.h)

以下是对 IPv4 标头成员的描述:

  • ihl:表示互联网头长度。IP v4 报头的长度,以 4 字节的倍数度量。IPv4 报头的长度是不固定的,而 IPv6 报头的长度是固定的(40 字节)。原因是 IPv4 报头可以包括可选的可变长度选项。当没有选项时,IPv4 报头的最小大小为 20 字节,最大大小为 60 字节。对应的ihl值对于最小 IPv4 报头大小是 5,对于最大大小是 15。IPv4 标头必须与 4 字节边界对齐。

  • version:应该是 4。

  • tos:IP v4 报头的tos字段最初用于服务质量(QoS)服务;tos代表服务类型。多年来,该字段具有不同的含义,如下所示:RFC 2474 定义了 IPv4 和 IPv6 报头中的区分服务字段(DS 字段),即tos的 0–5 位。它也被称为区分服务码点(DSCP)。2001 年的 RFC 3168 定义了 IP 报头的显式拥塞通知(ECN );它是tos字段的第 6 位和第 7 位。

  • tot_len:总长度,包括表头,以字节计量。因为tot_len是 16 位字段,最大可达 64KB。根据 RFC 791,最小大小为 576 字节。

  • id:IP v4 报头的标识。id字段对于分段很重要:当分段一个 SKB 时,该 SKB 的所有片段的id值应该是相同的。根据碎片的id重组碎片数据包。

  • frag_off:片段偏移量,16 位字段。低 13 位是片段的偏移量。在第一个片段中,偏移量为 0。偏移量以 8 字节为单位进行测量。高 3 位是标志:

  • 001 是 MF(碎片多)。它是为所有片段设置的,除了最后一个片段。

  • 010 是 DF(不要碎片化)。

  • 100 is CE (Congestion).

    参见include/net/ip.h中的 IP_MF、IP_DF 和 IP_CE 标志声明。

  • 生存时间:这是一个跳数计数器。每个转发节点将ttl减 1。当它达到 0 时,该数据包被丢弃,并且发送回超时 ICMPv4 消息;这样可以避免数据包因为这样或那样的原因被无休止地转发。

  • protocol:数据包的 L4 协议,例如, IPPROTO_TCP 用于 TCP 流量,IPPROTO_UDP 用于 UDP 流量(有关所有可用协议的列表,请参见include/linux/in.h)。

  • check:校验和(16 位字段)。校验和仅在 IPv4 报头字节上计算。

  • saddr:源 IPv4 地址,32 位。

  • daddr:目的 IPv4 地址,32 位。

在本节中,您已经了解了各种 IPv4 头成员及其用途。下一节将讨论 IPv4 协议的初始化,它设置在接收 IPv4 报头时调用的回调。

IPv4 初始化

IPv4 数据包是以太网类型为 0x0800 的数据包(以太网类型存储在 14 字节以太网报头的前两个字节中)。每个协议都应该定义一个协议处理程序,并且每个协议都应该初始化,以便网络堆栈可以处理属于该协议的数据包。为了让您了解是什么原因导致接收到的 IPv4 数据包被 IPv4 方法处理,本节描述了 IPv4 协议处理程序的注册:

static struct packet    _type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
};

static int __init inet_init(void)
{
  ...
  dev_add_pack(&ip_packet_type);
  ...
}
(net/ipv4/af_inet.c)

dev_add_pack()方法添加了ip_rcv()方法作为 IPv4 数据包的协议处理程序。这些数据包的以太网类型为 0x0800 (ETH_P_IP,在include/uapi/linux/if_ether.h中定义)。inet_init()方法执行各种 IPv4 初始化,并在引导阶段被调用。

IPv4 协议的主要功能分为 Rx(接收)路径和 Tx(发送)路径。现在您已经了解了 IPv4 协议处理程序的注册,您知道哪个协议处理程序管理 IPv4 数据包(ip_rcv回调)以及这个协议处理程序是如何注册的。现在,您可以开始了解 IPv4 Rx 路径以及如何处理接收到的 IPv4 数据包。Tx 路径将在后面的章节“发送 IPv4 数据包”中介绍

接收 IPv4 数据包

主要的 IPv4 接收方法是ip_rcv()方法,,它是所有 IPv4 数据包(包括多播和广播)的处理程序。事实上,这种方法主要由健全性检查组成。真正的工作是在它调用的ip_rcv_finish()方法中完成的。在ip_rcv()方法和ip_rcv_finish()方法之间是 NF_INET_PRE_ROUTING netfilter 钩子,通过调用 NF_HOOK 宏来调用(参见本节后面的代码片段)。在这一章中,你会遇到很多 NF_HOOK 宏的调用——这些是 netfilter 钩子。netfilter 子系统允许您在数据包在网络堆栈中的行程中的五个点注册回调。这些点将很快被提到他们的名字。添加 netfilter 挂钩的原因是为了能够在运行时加载 netfilter 内核模块。NF_HOOK 宏调用指定点的回调,如果这样的回调被注册的话。你也可能遇到 NF_HOOK 宏,叫做 NF_HOOK_COND,它是 NF_HOOK 宏的一个变种。在网络堆栈的某些地方,NF_HOOK_COND 宏包含一个布尔参数(最后一个参数),这个参数必须是true,钩子才能被执行(第九章讨论 netfilter 钩子)。请注意,netfilter 挂钩可以丢弃数据包,在这种情况下,它将不会继续沿其普通路径前进。图 4-2 显示了网络驱动程序接收到的数据包的接收路径(Rx) 。此数据包可以被传送到本地机器,也可以被转发到另一台主机。正是在路由表中的查找决定了这两个选项中的哪一个会发生。

9781430261964_Fig04-02.jpg

图 4-2 。接收 IPv4 数据包。为简单起见,该图不包括碎片/碎片整理/选项/IPsec 方法

图 4-2 显示了接收到的 IPv4 数据包的路径。IPv4 协议处理器ip_rcv()方法接收数据包(见图的左上侧)。首先,在调用ip_rcv_finish()方法之后,应该立即在路由子系统中执行查找。路由查找的结果决定了数据包是本地传送到本地主机还是被转发(路由查找在第五章的中解释)。如果数据包的目的地是本地主机,它将首先到达ip_local_deliver()方法,然后到达ip_local_deliver_finish()方法。当数据包要被转发时,将通过ip_forward()方法进行处理。图中出现了一些 netfilter 钩子,比如 NF_INET_PRE_ROUTING 和 NF_INET_LOCAL_IN。请注意,多播流量由ip_mr_input()方法处理,这将在本章后面的“接收 IPv4 多播数据包”一节中讨论。NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN、NF_INET_FORWARD 和 NF_INET_POST_ROUTING 是 netfilter 挂钩的五个入口点中的四个。第五个是 NF_INET_LOCAL_OUT,在本章后面的“发送 IPv4 数据包”一节中会提到。这五个入口点在include/uapi/linux/netfilter.h中定义。注意,这五个钩子的相同的enum也在 IPv6 中使用;例如,在ipv6_rcv()方法中,一个钩子正在 NF_INET_PRE_ROUTING ( net/ipv6/ip6_input.c)上注册。我们来看看ip_rcv()方法:

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{

首先执行一些健全性检查,我在本节中提到了其中的一些。IPv4 报头(ihl)的长度以 4 字节的倍数来度量。IP v4 报头的大小必须至少为 20 个字节,这意味着ihl的大小必须至少为 5。version应该是 4(对于 IPv4)。如果其中一个条件不满足,数据包将被丢弃,统计信息(IPSTATS _ MIB _ INHDRERRORS)将被更新。

        if (iph->ihl < 5 || iph->version != 4)
                goto inhdr_error;

根据 RFC 1122 的 3.2.1.2 部分,主机必须验证每个收到的数据报的 IPv4 报头校验和,并自动丢弃每个校验和不正确的数据报。这是通过调用ip_fast_csum()方法完成的,如果成功,该方法将返回 0。IPv4 报头校验和仅在 IPv4 报头字节上计算:

        if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
                goto inhdr_error;

然后调用 NF_HOOK 宏:

         return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
                        ip_rcv_finish);

当注册的 netfilter hook 方法返回 NF_DROP 时,表示应该丢弃数据包,数据包遍历不继续。当注册的 netfilter 挂钩返回 NF _ stopped 时,意味着该数据包被 netfilter 子系统接管,数据包遍历不再继续。当注册的 netfilter 钩子返回 NF_ACCEPT 时,数据包继续遍历。netfilter 钩子还有其他返回值(也称为判断),比如 NF_QUEUE、NF_REPEAT 和 NF_STOP,这在本章中没有讨论。(如前所述,netfilter 钩子在第九章中讨论过。)让我们暂时假设在 NF_INET_PRE_ROUTING 入口点中没有注册 netfilter 回调,因此 NF_HOOK 宏不会调用任何 netfilter 回调,而会调用ip_rcv_finish()方法。我们来看看ip_rcv_finish()的方法:

static int ip_rcv_finish(struct sk_buff *skb)
{
       const struct iphdr *iph = ip_hdr(skb);
       struct rtable *rt;

skb_dst()方法检查是否有dst对象附着在 SKB 上;dstdst_entry ( include/net/dst.h)的实例,代表路由子系统中的查找结果。查找是根据路由表和数据包报头完成的。路由子系统中的查找还设置了dstinput和/或output回调。例如,如果要转发数据包,路由子系统中的查找会将input回调设置为ip_forward()。当数据包的目的地是本地机器时,路由子系统中的查找会将input回调设置为ip_local_deliver()。对于多播包,在某些情况下可以是ip_mr_input()(我将在下一节讨论多播包)。dst对象的内容决定了数据包将如何继续它的旅程;例如,在转发数据包时,根据dst决定在调用dst_input()时应该调用哪个input回调,或者应该在哪个接口上传输。(我将在下一章深入讨论路由子系统)。

如果没有dst连接到 SKB,则通过ip_route_input_noref()方法在路由子系统中执行查找。如果查找失败,数据包将被丢弃。请注意,处理多播数据包不同于处理单播数据包(将在本章后面的“接收 IPv4 多播数据包”一节中讨论)。

       ...
       if (!skb_dst(skb)) {

在路由子系统中执行查找:

            int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                                           iph->tos, skb->dev);
            if (unlikely(err)) {
                if (err == -EXDEV)
                    NET_INC_STATS_BH(dev_net(skb->dev),
                                     LINUX_MIB_IPRPFILTER);
                goto drop;
            }
       }

image 注意当设置了反向路径滤波器(RPF) 时,在某些情况下__fib_validate_source()方法会返回-EXDEV(“跨设备链接”)错误。可以通过procfs中的一个条目设置 RPF。在这种情况下,数据包被丢弃,统计信息(LINUX_MIB_IPRPFILTER)被更新,该方法返回 NET_RX_DROP。注意,您可以通过查看cat /proc/net/netstat输出中的IPReversePathFilter列来显示 LINUX_MIB_IPRPFILTER 计数器。

现在执行检查以查看 IPv4 报头是否包括选项。因为 IPv4 报头的长度(ihl)是以 4 字节的倍数来度量的,如果它大于 5,这意味着它包括选项,所以应该调用ip_rcv_options()方法来处理这些选项。处理 IP 选项将在本章后面的“IP 选项”部分进行深入讨论。请注意,ip_rcv_options()方法可能会失败,您很快就会看到。如果是多播条目或广播条目,则分别更新 IPSTATS_MIB_INMCAST 统计信息或 IP stats _ MIB _ INM cast 统计信息。然后调用dst_input()方法。这个方法反过来简单地通过调用skb_dst(skb)->input(skb)来调用input回调方法:

    if (iph->ihl > 5 && ip_rcv_options(skb))
            goto drop;

    rt = skb_rtable(skb);
    if (rt->rt_type == RTN_MULTICAST) {
        IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INMCAST,
                skb->len);
    } else if (rt->rt_type == RTN_BROADCAST)
        IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INBCAST,
                skb->len);

    return dst_input(skb);

在本节中,您了解了接收 IPv4 数据包的各个阶段:执行的完整性检查、路由子系统中的查找、执行实际工作的ip_rcv_finish()方法。您还了解了当应该转发数据包时调用哪个方法,以及当数据包用于本地传递时调用哪个方法。IPv4 多播是一个特例。处理 IPv4 多播数据包的接收将在下一节讨论。

接收 IPv4 组播数据包

ip_rcv()方法也是多播数据包的处理程序。如前所述,在一些完整性检查之后,它调用ip_rcv_finish()方法,该方法通过调用ip_route_input_noref()在路由子系统中执行查找。在ip_route_input_noref()方法中,首先通过调用ip_check_mc_rcu()方法,检查本地机器是否属于目的多播地址的多播组。如果是,或者如果本地机器是多播路由器(CONFIG_IP_MROUTE被设置),则调用ip_route_input_mc()方法;让我们看一下代码:

int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
                         u8 tos, struct net_device *dev)
{
        int res;
        rcu_read_lock();
        . . .
        if (ipv4_is_multicast(daddr)) {
                struct in_device *in_dev = __in_dev_get_rcu(dev);
                if (in_dev) {
                        int our = ip_check_mc_rcu(in_dev, daddr, saddr,
                                                  ip_hdr(skb)->protocol);
                        if (our
#ifdef CONFIG_IP_MROUTE
                                ||
                            (!ipv4_is_local_multicast(daddr) &&
                             IN_DEV_MFORWARD(in_dev))
#endif
                           ) {
                                int res = ip_route_input_mc(skb, daddr, saddr,
                                                            tos, dev, our);
                                rcu_read_unlock();
                                return res;
                        }
                }
           . . .

        }
        . . .

让我们进一步研究一下ip_route_input_mc()方法。如果本机属于目的组播地址的组播组(变量our的值为 1),那么dstinput回调被设置为ip_local_deliver。如果本地主机是组播路由器并且IN_DEV_MFORWARD(in_dev)被设置,那么dstinput回调被设置为ip_mr_input。调用dst_input(skb)ip_rcv_finish()方法因此根据dstinput回调调用ip_local_deliver()方法或ip_mr_input()方法。IN_DEV_MFORWARD 宏检查procfs组播转发条目。请注意,procfs多播转发条目/proc/sys/net/ipv4/conf/all/mc_forwarding是一个只读条目(与 IPv4 单播procfs转发条目相反),因此您不能简单地通过从命令行运行来设置它:echo 1 > /proc/sys/net/ipv4/conf/all/mc_forwarding。例如,启动pimd守护进程会将其设置为 1,停止守护进程会将其设置为 0。pimd是一个轻量级的独立 PIM-SM v2 多播路由守护程序。如果您对学习多播路由守护进程的实现感兴趣,您可能想看看https://github.com/troglobit/pimd/中的pimd源代码:

static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,
                                 u8 tos, struct net_device *dev, int our)
 {
         struct rtable *rth;
         struct in_device *in_dev = __in_dev_get_rcu(dev);

        . . .

         if (our) {
                 rth->dst.input= ip_local_deliver;
                 rth->rt_flags |= RTCF_LOCAL;
         }

 #ifdef CONFIG_IP_MROUTE
         if (!ipv4_is_local_multicast(daddr) && IN_DEV_MFORWARD(in_dev))
                 rth->dst.input = ip_mr_input;
 #endif
        . . .

多播层保存一种称为多播转发缓存(MFC)的数据结构。我在这里不讨论 MFC 或ip_mr_input()方法的细节(我在第六章中讨论它们)。在这种情况下重要的是,如果在 MFC 中找到一个有效的条目,就调用ip_mr_forward()方法。ip_mr_forward()方法执行一些检查并最终调用ipmr_queue_xmit()方法。在ipmr_queue_xmit()方法中,ttl减少,通过调用ip_decrease_ttl()方法更新校验和(在ip_forward()方法中也是如此,您将在本章后面看到)。然后通过调用 NF_INET_FORWARD NF_HOOK 宏来调用ipmr_forward_finish()方法(假设 NF_INET_FORWARD 上没有注册的 IPv4 netfilter 钩子):

static void ipmr_queue_xmit(struct net *net, struct mr_table *mrt,
                             struct sk_buff *skb, struct mfc_cache *c, int vifi)
{
       . . .

       ip_decrease_ttl(ip_hdr(skb));
       ...
       NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev, dev,
                       ipmr_forward_finish);
       return;

}

ipmr_forward_finish()方法非常简短,在此完整展示。它所做的只是更新统计数据,如果 IPv4 报头中有选项,就调用ip_forward_options()方法(IP 选项将在下一节描述),并调用dst_output()方法:

static inline int ipmr_forward_finish(struct sk_buff *skb)
{
        struct ip_options *opt = &(IPCB(skb)->opt);

        IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTFORWDATAGRAMS);

IP_ADD_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTOCTETS, skb->len);


        if (unlikely(opt->optlen))
                ip_forward_options(skb);

        return dst_output(skb);
}

本节讨论了如何处理接收 IPv4 多播数据包。pimd是作为多播路由守护进程的一个例子提到的,它在多播数据包转发中与内核交互。下一节将描述各种 IP 选项,这些选项支持使用网络堆栈的特殊功能,例如跟踪数据包的路由、跟踪数据包的时间戳、指定数据包应该经过的网络节点。我还讨论了如何在网络堆栈中处理这些 IP 选项。

IP 选项

IPv4 报头的 IP 选项字段是可选的,并且由于安全原因和处理开销,不经常使用。哪些选项可能有帮助?例如,假设您的数据包被某个防火墙丢弃。您可以使用严格或宽松的源路由选项来指定不同的路由。或者如果你想找出数据包到一些目的地址的路径,你可以使用记录路由选项。

IPv4 标头可以包含零个、一个或多个选项。没有选项时,IPv4 报头大小为 20 字节。IP 选项字段的长度最多为 40 个字节。IPv4 最大长度为 60 字节的原因是因为 IPv4 报头长度是一个 4 位字段,它以 4 字节的倍数来表示长度。因此,该字段的最大值是 15,这给出了 60 字节的 IPv4 最大报头长度。当使用多个选项时,选项只是一个接一个地连接起来。IPv4 报头必须与 4 字节边界对齐,因此有时需要填充。以下 RFC 讨论了 IP 选项:781(时间戳选项)、791、1063、1108、1393(使用 IP 选项的 Traceroute)和 2113 (IP 路由器警报选项)。有两种形式的 IP 选项:

  • 单字节选项(选项类型) : T “选项列表结束”和“无操作”是仅有的单字节选项。

  • 多字节选项:当在选项类型 byte 后使用多字节选项时,有以下三个字段:

  • 长度(1 字节):选项的长度,以字节为单位。

  • 指针(1 字节):从选项开始的偏移量。

  • 选项数据:这是一个中间主机可以存储数据的空间,例如时间戳或 IP 地址。

在图 4-3 中显示了选项类型。

9781430261964_Fig04-03.jpg

图 4-3 。选项类型

置位时,copied标志表示该选项应在所有片段中复制。如果未设置该选项,则只应在第一个片段中复制该选项。IPOPT_COPIED 宏检查是否设置了指定 IP 选项的copied标志。它在ip_options_fragment()方法中用于检测不可复制的选项,并插入 IPOPT_NOOP。本节稍后将讨论ip_options_fragment()方法。

选项类可以是以下 4 个值之一:

  • 00:控制类(IPOPT_CONTROL)
  • 01:预留 1 (IPOPT_RESERVED1)
  • 10:调试和测量(IPOPT_MEASUREMENT)
  • 11: reserved2 (IPOPT_RESERVED2)

在 Linux 网络栈中,只有 IPOPT_TIMESTAMP 选项属于调试和测量类。所有其他选项都是控件类。

选项编号通过唯一编号指定一个选项;可能的值是 0–31,但不是所有的值都被 Linux 内核使用。

表 4-1 根据 Linux 符号、选项号、选项类别和copied标志显示了所有选项。

表 4-1 。选项表

Tab04-01.jpg

选项名(IPOPT_*)声明在include/uapi/linux/ip.h中。

Linux 网络堆栈不包括所有的 IP 选项。完整列表见www.iana.org/assignments/ip-parameters/ip-parameters.xml

我将简要描述这五个选项,然后深入描述时间戳选项和记录路径选项:

  • 选项列表结束(IPOPT_END) : 1 字节选项,用于表示选项字段的结束。这是一个单一的零字节选项(它的所有位都是“0”)。其后不能有 IP 选项。
  • 无操作(IPOPT_NOOP) : 1 字节选项用于内部填充,用于对齐。
  • Security (IPOPT_SEC) : 该选项为主机提供了一种发送安全性、处理限制和 TCC(封闭用户组)参数的方式。参见 RFC 791 和 RFC 1108。最初打算用于军事应用。
  • 松散源记录路由(IPOPT_LSRR) : 此选项指定数据包应该经过的路由器列表。在列表中的每两个相邻节点之间,可以有没有出现在列表中的中间路由器,但是应该保持顺序。
  • 商业互联网协议安全选项(IPOPT_CIPSO) : CIPSO 是 IETF 的草案,已经被多家厂商采用。它涉及网络标签标准。套接字的 CIPSO 标记意味着将 CIPSO IP 选项添加到通过该套接字离开系统的所有数据包中。该选项在收到数据包时生效。有关 CIPSO 选项的更多信息,请参见Documentation/netlabel/draft-ietf-cipso-ipsecurity-01.txtDocumentation/netlabel/cipso_ipv4.txt

时间戳选项

Timestamp (IPOPT_TIMESTAMP):时间戳选项在 RFC 781“互联网协议(IP)时间戳选项的规范”中指定此选项存储数据包路由上主机的时间戳。存储的时间戳是一个 32 位的时间戳,从 UTC 当天午夜开始,以毫秒为单位。此外,它还可以存储数据包路由中所有主机的地址,或者只存储沿路由选择的主机的时间戳。时间戳选项的最大长度是 40。不为片段复制时间戳选项;它只出现在第一个片段中。时间戳选项以三个字节的选项类型、长度和指针(偏移量)开始。第四个字节的高 4 位是溢出计数器,它在没有可用空间存储所需数据的每一跳中递增。当溢出计数器超过 15 时,返回一个参数问题的 ICMP 消息。低 4 位是标志。标志的值可以是下列值之一:

  • 0 :仅时间戳(IPOPT _ TS _ TSONLY)
  • 1 :时间戳和地址(IPOPT_TS_TSANDADDR)
  • 3 :仅指定跳数的时间戳(IPOPT_TS_PRESPEC)

image 注意您可以使用带有时间戳选项和前面提到的三个子类型的命令行ping实用程序:

ping -T tsonly     (IPOPT_TS_TSONLY)

ping -T tsandaddr  (IPOPT_TS_TSANDADDR)

ping -T tsprespec  (IPOPT_TS_PRESPEC)

图 4-4 显示了仅带有时间戳的时间戳选项(设置了 IPOPT_TS_TSONLY 标志)。路径上的每台路由器都会添加其 IPv4 地址。当没有更多空间时,溢出计数器递增。

9781430261964_Fig04-04.jpg

图 4-4 。时间戳选项(只有时间戳,标志= 0)

图 4-5 显示了带有时间戳和地址的时间戳选项(设置了 IPOPT_TS_TSANDADDR 标志)。路径上的每台路由器都会添加其 IPv4 地址和时间戳。同样,当没有更多空间时,溢出计数器递增。

9781430261964_Fig04-05.jpg

图 4-5 。时间戳选项(带时间戳和地址,标志= 1)

图 4-6 显示了带有时间戳的时间戳选项(设置了 IPOPT_TS_PRESPEC 标志)。路径上的每个路由器只有在预先指定的列表中才会添加其时间戳。同样,当没有更多空间时,溢出计数器递增。

9781430261964_Fig04-06.jpg

图 4-6 。时间戳选项(仅带有指定跳数的时间戳,标志= 3)

记录路线选项

记录路由(IPOPT_RR):记录一个数据包的路由。途中的每个路由器都会添加它的地址(见图 4-7 )。长度由发送设备设置。命令行实用程序ping –R使用记录路由 IP 选项。请注意,IPv4 报头仅够九个这样的路由使用(如果使用更多选项,甚至更少)。当报头已满并且没有空间插入额外的地址时,数据报被转发,而不将地址插入 IP 选项。参见 RFC 791 第 3.1 节。

9781430261964_Fig04-07.jpg

图 4-7 。记录路线选项

虽然ping –R使用记录路由 IP 选项,但在许多情况下,如果您尝试它,您将不会得到沿途所有网络节点的预期结果,因为出于安全原因,许多网络节点会忽略此 IP 选项。pingmanpage明确提到了这一点。从man ping开始:

. . .
-R
Includes the RECORD_ROUTE option in the ECHO_REQUEST packet and displays the route buffer on returned packets.
. . .
Many hosts ignore or discard this option.
. . .

  • 流 ID (IPOPT_SID) : 该选项提供了一种通过不支持流概念的网络携带 16 位 SATNET 流标识符的方式。
  • 严格源记录路由 【伊波特 _ SSRR】:该选项指定数据包应该经过的路由器列表。应该保持顺序,并且不允许在遍历中进行任何更改。出于安全原因,许多路由器会阻止宽松源记录路由(LSRR)和严格源记录路由(SSRR)选项。
  • 路由器警报(IPOPT _ RA):IP 路由器警报选项可用于通知中转路由器更仔细地检查 IP 数据包的内容。例如,这对于新协议是有用的,但是需要在路径上的路由器中进行相对复杂的处理。在 RFC 2113“IP 路由器警报选项”中指定

IP 选项在 Linux 中由ip_options结构表示:

struct ip_options {
        __be32          faddr;
        __be32          nexthop;
        unsigned char   optlen;
        unsigned char   srr;
        unsigned char   rr;
        unsigned char   ts;
        unsigned char   is_strictroute:1,
        srr_is_hit:1,
        is_changed:1,
        rr_needaddr:1,
        ts_needtime:1,
        ts_needaddr:1;
        unsigned char   router_alert;
        unsigned char   cipso;
        unsigned char   __pad2;
        unsigned char   __data[0];
};
(include/net/inet_sock.h)

以下是 IP 期权结构成员的简短描述:

  • faddr:保存的第一跳地址。当处理松散和严格路由时,在ip_options_compile()中设置,此时方法不是从 Rx 路径调用的(SKB 为空)。
  • nexthop:保存了 LSRR 和 SSRR 的 nexthop 地址。
  • optlen:选项长度,以字节为单位。不能超过 40 个字节。
  • is_strictroute:指定使用严格源路由的标志。解析严格路由选项类型(IPOPT_SSRR)时在ip_options_compile()方法中设置标志;请注意,它不是为松散路由(IPOPT_LSRR)设置的。
  • srr_is_hit:指定数据包目的地addr是本地主机的标志srr_is_hit标志在ip_options_rcv_srr()中设置。
  • is_changed: IP 校验和不再有效(当其中一个 IP 选项改变时,该标志被置位)。
  • rr_needaddr:需要记录传出设备的 IPv4 地址。为记录路由选项(IPOPT_RR)设置标志。
  • ts_needtime:需要记录时间戳。该标志是为时间戳 IP 选项的这些标志设置的:IPOPT_TS_TSONLY、IPOPT_TS_TSANDADDR 和 IPOPT_TS_PRESPEC(参见本节后面关于这些标志之间的差异的详细解释)。
  • ts_needaddr:需要记录传出设备的 IPv4 地址。仅当 IPOPT_TS_TSANDADDR 标志被置位时,该标志才被置位,并且它指示应该添加沿着分组路由的每个节点的 IPv4 地址。
  • router_alert:ip_options_compile()方法中设置解析路由器时的报警选项(IPOPT_RR)
  • __data[0]:一个缓冲区,用于存储由setsockopt()从用户空间接收的选项。

参见ip_options_get_from_user()ip_options_get_finish() ( net/ipv4/ip_options.c)。

我们来看看ip_rcv_options()方法:

static inline bool ip_rcv_options(struct sk_buff *skb)
{
         struct ip_options *opt;
         const struct iphdr *iph;
         struct net_device *dev = skb->dev;
       . . .

从 SKB 获取 IPv4 报头:

         iph = ip_hdr(skb);

从与 SKB 关联的inet_skb_parm对象中获取ip_options对象:

         opt = &(IPCB(skb)->opt);

计算预期期权长度:

         opt->optlen = iph->ihl*4 - sizeof(struct iphdr);

调用ip_options_compile()方法从 SKB 中构建一个ip_options对象:

         if (ip_options_compile(dev_net(dev), opt, skb)) {
                 IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
                 goto drop;
         }

当在 Rx 路径中调用ip_options_compile()方法(从ip_rcv_options()方法)时,它解析指定 SKB 的 IPv4 报头,并在验证选项的有效性后,根据 IPv4 报头内容,用它构建一个ip_options对象。当通过带有 IPPROTO_IP 和 IP_OPTIONS 的setsockopt()系统调用从用户空间获取选项时,也可以从ip_options_get_finish()方法调用ip_options_compile()方法。在这种情况下,数据从用户空间复制到opt->data,并且ip_options_compile()的第三个参数,即 SKB,为空;在这种情况下,ip_options_compile()方法从opt->__data构建ip_options对象。如果在解析选项时发现一些错误,并且是在 Rx 路径中(从ip_rcv_options()调用了ip_options_compile()方法),则发送回“参数问题”ICMPv4 消息(ICMP_PARAMETERPROB)。无论该方法是如何被调用的,如果出现错误,将返回代码为–EINVAL的错误。自然,使用ip_options对象比使用原始 IPv4 头更方便,因为这样访问 IP 选项字段要简单得多。在 Rx 路径中,ip_options_compile()方法构建的ip_options对象存储在 SKB 的控制缓冲区(cb)中;这通过将opt对象设置为&(IPCB(skb)->opt)来完成。IPCB(skb)宏是这样定义的:

#define IPCB(skb) ((struct inet_skb_parm*)((skb)->cb))

并且inet_skb_parm结构(包括一个ip_options对象)是这样定义的:

struct inet_skb_parm {
        struct ip_options       opt;            /* Compiled IP options          */
        unsigned char           flags;
        u16                     frag_max_size;
};
(include/net/ip.h)

所以&(IPCB(skb)->opt指向inet_skb_parm对象内部的ip_options对象。在本书中,我不会深入研究在ip_options_compile()方法中解析 IPv4 报头的所有小而繁琐的技术细节,因为有大量这样的细节,而且它们是不言自明的。我将简要讨论ip_options_compile()如何解析 Rx 路径中的一些单字节选项,如 IPOPT_END 和 IPOPT_NOOP,以及一些更复杂的选项,如 IPOPT_RR 和 IPOPT_TIMESTAMP,并展示一些在此方法中完成检查的示例,以及如何在下面的代码片段中实现它:

int ip_options_compile(struct net *net, struct ip_options *opt, struct sk_buff *skb)
{

         ...
         unsigned char *pp_ptr = NULL;
         struct rtable *rt = NULL;
         unsigned char *optptr;
         unsigned char *iph;
         int optlen, l;

为了开始解析过程,optptr指针应该指向 IP options 对象的开始,并在一个循环中迭代所有选项。对于 Rx 路径(当从ip_rcv_options()方法调用ip_options_compile()方法时),在ip_rcv()方法中接收到的 SKB 作为参数传递给ip_options_compile(),不用说,不能为空。在这种情况下,IP 选项在 IPv4 报头的初始固定大小(20 字节)之后立即开始。当从ip_options_get_finish()调用ip_options_compile()时,optptr指针被设置为opt->__data,因为ip_options_get_from_user()方法复制了从用户空间发送到opt->__data的选项。为了准确起见,我应该提到,如果需要对齐,ip_options_get_finish()方法也会写入opt->__data(它会在适当的位置写入 IPOPT_END)。

         if (skb != NULL) {
             rt = skb_rtable(skb);
             optptr = (unsigned char *)&(ip_hdr(skb)[1]);
         } else
             optptr = opt->__data;

在这种情况下,不能改为使用iph = ip_hdr(skb),因为要考虑 SKB 为空的情况。以下分配对于非 Rx 路径也是正确的:

        iph = optptr - sizeof(struct iphdr);

变量l初始化为选项长度(最多 40 字节)。在接下来的for循环的每次迭代中,它会减少当前选项的长度:

        for (l = opt->optlen; l > 0; ) {
            switch (*optptr) {

如果遇到一个 IPOPT_END 选项,则表明这是选项列表的结尾,后面不能有其他选项。在这种情况下,您为每个不同于 IPOPT_END 的字节写入 IPOPT_END,直到选项列表结束。还应设置is_changed布尔标志,因为它指示 IPv4 报头已更改(因此,校验和的重新计算待定—现在或在for循环内没有理由计算校验和,因为在循环期间 IPv4 报头可能有其他更改):

                case IPOPT_END:
                  for (optptr++, l--; l>0; optptr++, l--) {
                     if (*optptr != IPOPT_END) {
                         *optptr = IPOPT_END;
                         opt->is_changed = 1;
                     }
                  }
         goto eol;

如果遇到单字节选项的无操作(IPOPT_NOOP)选项类型,只需将l减 1,将optptr增 1,然后前进到下一个选项类型:

                case IPOPT_NOOP:
                  l--;
                  optptr++;
                  continue;
            }

Optlen被设置为被读取的选项的长度(因为optptr[1]保存选项长度):

            optlen = optptr[1];

无操作(IPOPT_NOOP)选项和选项列表结束(IPOPT_END)选项是仅有的单字节选项。所有其他选项都是多字节选项,必须至少有两个字节(选项类型和选项长度)。现在检查至少有两个选项字节,并且没有超过选项列表长度。如果有一些错误,pp_ptr指针被设置为指向问题的根源并退出循环。如果是在 Rx 路径,则回送一个“参数问题”的 ICMPv4 消息,将出现问题的偏移量作为参数传递,以便对方分析问题:

            if (optlen<2 || optlen>l) {
                pp_ptr = optptr;
                goto error;
            }
            switch (*optptr) {
                case IPOPT_SSRR:
                case IPOPT_LSRR:
                ...
                case IPOPT_RR:

记录路由选项的选项长度必须至少为 3 个字节:选项类型、选项长度和指针(偏移量):

                  if (optlen < 3) {
                      pp_ptr = optptr + 1;
                      goto error;
                  }

记录路由选项的选项指针偏移量必须至少为 4 个字节,因为为地址列表保留的空间必须在三个初始字节(选项类型、选项长度和指针)之后开始:

                  if (optptr[2] < 4) {
                             pp_ptr = optptr + 2;
                             goto error;
                  }
                  if (optptr[2] <= optlen) {

如果偏移量(optptr[2])加上三个初始字节超过了选项长度,则出现错误:

                      if (optptr[2]+3 > optlen) {
                           pp_ptr = optptr + 2;
                           goto error;
                      }
                      if (rt) {
                          spec_dst_fill(&spec_dst, skb);

将 IPv4 地址复制到记录路由缓冲区:

                          memcpy(&optptr[optptr[2]-1], &spec_dst, 4);

设置is_changed布尔标志,表示 IPv4 报头已更改(校验和的重新计算待定):

                          opt->is_changed = 1;
                      }

对于记录路由缓冲区中的下一个地址,将指针(偏移量)增加 4(每个 IPv4 地址为 4 个字节):

                      optptr[2] += 4;

设置rr_needaddr标志(该标志在ip_forward_options()方法中检查):

                      opt->rr_needaddr = 1;
                  }
                  opt->rr = optptr - iph;
                  break;

                       case IPOPT_TIMESTAMP:
                         ...

时间戳选项的选项长度必须至少为 4 个字节:选项类型、选项长度、指针(偏移量),第四个字节分为两个字段:较高的 4 位是溢出计数器,它在没有可用空间来存储所需数据的每一跳中递增,较低的 4 位是标志:仅时间戳、时间戳和地址以及指定跳的时间戳:

                         if (optlen < 4) {
                               pp_ptr = optptr + 1;
                               goto error;
                         }

optptr[2]是指针(偏移量)。因为,如前所述,每个时间戳选项以 4 个字节开始,这意味着指针(偏移量)必须至少为 5:

                         if (optptr[2] < 5) {
                                 pp_ptr = optptr + 2;
                                 goto error;
                         }
                         if (optptr[2] <= optlen) {
                                 unsigned char *timeptr = NULL;
                                 if (optptr[2]+3 > optptr[1]) {
                                         pp_ptr = optptr + 2;
                                         goto error;
                                 }

在切换命令中,检查optptr[3]&0xF的值。它是时间戳选项的标志(第四个字节的 4 个低位):

                                 switch (optptr[3]&0xF) {
                                       case IPOPT_TS_TSONLY:
                                         if (skb)
                                                 timeptr = &optptr[optptr[2]-1];
                                         opt->ts_needtime = 1;

对于带有仅时间戳标志(IPOPT_TS_TSONLY)的时间戳选项,需要 4 个字节;因此指针(偏移量)增加 4:

                                         optptr[2] += 4;
                                         break;

                                       case IPOPT_TS_TSANDADDR:
                                         if (optptr[2]+7 > optptr[1]) {
                                                 pp_ptr = optptr + 2;
                                                 goto error;
                                         }
                                         if (rt)  {
                                                  spec_dst_fill(&spec_dst, skb);
                                                  memcpy(&optptr[optptr[2]-1],
                                                         &spec_dst, 4);
                                                  timeptr = &optptr[optptr[2]+3];
                                         }
                                         opt->ts_needaddr = 1;
                                         opt->ts_needtime = 1;

对于带有时间戳和地址标志的时间戳选项(IPOPT_TS_TSANDADDR),需要 8 个字节;因此指针(偏移量)增加了 8:

                                         optptr[2] += 8;
                                         break;

                                       case IPOPT_TS_PRESPEC:
                                         if (optptr[2]+7 > optptr[1]) {
                                                 pp_ptr = optptr + 2;
                                                 goto error;
                                         }
                                         {
                                          __be32 addr;
                                          memcpy(&addr, &optptr[optptr[2]-1], 4);
                                             if (inet_addr_type(net,addr) == RTN_UNICAST)
                                                break;
                                          if (skb)
                                               timeptr = &optptr[optptr[2]+3];
                                         }
                                         opt->ts_needtime = 1;

对于带有时间戳和预先指定的跳数标志(IPOPT_TS_PRESPEC)的时间戳选项,需要 8 个字节,因此指针(偏移量)增加 8:

                                         optptr[2] += 8;
                                         break;
                                       default:
                                         ...
                                }
                          ...

ip_options_compile()方法构建了ip_options对象之后,严格的路由被处理。首先,检查设备是否支持源路由。这意味着/proc/sys/net/ipv4/conf/all/accept_source_route被设置,并且/proc/sys/net/ipv4/conf/<deviceName>/accept_source_route被设置。如果不满足这些条件,数据包将被丢弃:

             . . .
         if (unlikely(opt->srr)) {
             struct in_device *in_dev = __in_dev_get_rcu(dev);

             if (in_dev) {
                     if (!IN_DEV_SOURCE_ROUTE(in_dev)) {
                     . . .
                                 goto drop;
                     }
             }

             if (ip_options_rcv_srr(skb))
                     goto drop;
         }

我们来看看ip_options_rcv_srr()方法(还是那句话,我会把重点放在重要的点上,而不是小细节)。源路由地址列表被迭代。在解析过程中,会在循环中进行一些完整性检查,以查看是否有错误。当遇到第一个非本地地址时,循环退出,并执行以下操作:

  • 设置 IP 选项对象的srr_is_hit标志(opt->srr_is_hit = 1)。
  • opt->nexthop设置为找到的下一跳地址。
  • opt->is_changed标志设置为 1。

应该转发该数据包。当到达方法ip_forward_finish()时,调用ip_forward_options()方法。在此方法中,如果设置了 IP 选项对象的srr_is_hit标志,则 ipv4 报头的daddr被更改为opt->nexthop,偏移量增加 4(指向源路由地址列表中的下一个地址),并且—因为 IPv4 报头被更改—通过调用ip_send_check()方法重新计算校验和。

IP 选项和碎片化

在本节开始描述选项类型时,我提到了选项类型字节中的一个copied标志,它指示在转发分段数据包时是否复制选项。碎片中 IP 选项的处理由ip_options_fragment()方法完成,该方法从准备碎片的方法ip_fragment()中调用。只为第一个片段调用它。我们来看看ip_options_fragment()的方法,很简单:

void ip_options_fragment(struct sk_buff *skb)
{
        unsigned char *optptr = skb_network_header(skb) + sizeof(struct iphdr);
        struct ip_options *opt = &(IPCB(skb)->opt);
        int  l = opt->optlen;
        int  optlen;

e 循环简单地遍历选项,读取每个选项类型。optptr是指向选项列表的指针(从 IPv4 报头的前 20 个字节的末尾开始)。l是选项列表的大小,在每次循环迭代中递减 1:

        while (l > 0) {
                switch (*optptr) {

当选项类型为 IPOPT_END 时,它终止选项字符串,这意味着读取选项已完成:

                case IPOPT_END:
                        return;

                case IPOPT_NOOP:

option type为 IPOPT_NOOP,用于选项之间的填充时,optptr指针加 1,l递减,处理下一个选项:

                        l--;
                        optptr++;
                        continue;
                }

对选项长度执行健全性检查:

                optlen = optptr[1];
                if (optlen<2 || optlen>l)
                  return;

检查是否应复制该选项;如果没有,只需用memset()函数放一个或几个 IPOPT_NOOP 选项来代替它。memset()写入的 IPOPT_NOOP 字节数是被读取的选项的大小,即optlen:

                if (!IPOPT_COPIED(*optptr))
                        memset(optptr, IPOPT_NOOP, optlen);

现在进入下一个选项:

                l -= optlen;
                optptr += optlen;        }

IPOPT_TIMESTAMP 和 IPOPT_RR 是copied标志为 0 的选项(见表 4-1 )。在您之前看到的循环中,它们被替换为 IPOPT_NOOP,并且它们在 IP option 对象中的相关字段被重置为 0:

        opt->ts = 0;
        opt->rr = 0;
        opt->rr_needaddr = 0;
        opt->ts_needaddr = 0;
        opt->ts_needtime = 0;
}
(net/ipv4/ip_options.c)

在本节中,您已经了解了ip_rcv_options()如何处理带有 IP 选项的数据包的接收,以及ip_options_compile()方法如何解析 IP 选项。还讨论了知识产权方案的不成体系问题。下一节将介绍构建 IPv4 选项的过程,包括根据指定的ip_options对象设置 IPv4 报头的 IP 选项。

构建 IP 选项

ip_options_build()方法可以被认为是你在本章前面看到的ip_options_compile()方法的反向。它将一个ip_options对象作为参数,并将其内容写入 IPv4 报头。让我们来看看:

void ip_options_build(struct sk_buff *skb, struct ip_options *opt,
                      __be32 daddr, struct rtable *rt, int is_frag)
{
        unsigned char *iph = skb_network_header(skb);

        memcpy(&(IPCB(skb)->opt), opt, sizeof(struct ip_options));
        memcpy(iph+sizeof(struct iphdr), opt->__data, opt->optlen);
        opt = &(IPCB(skb)->opt);

        if (opt->srr)
                memcpy(iph+opt->srr+iph[opt->srr+1]-4, &daddr, 4);

        if (!is_frag) {
                if (opt->rr_needaddr)
                        ip_rt_get_source(iph+opt->rr+iph[opt->rr+2]-5, skb, rt);
                if (opt->ts_needaddr)
                        ip_rt_get_source(iph+opt->ts+iph[opt->ts+2]-9, skb, rt);
                if (opt->ts_needtime) {
                        struct timespec tv;
                        __be32 midtime;
                        getnstimeofday(&tv);
                        midtime = htonl((tv.tv_sec % 86400) *
                                         MSEC_PER_SEC + tv.tv_nsec / NSEC_PER_MSEC);
                        memcpy(iph+opt->ts+iph[opt->ts+2]-5, &midtime, 4);
                }
                return;
        }
        if (opt->rr) {
                memset(iph+opt->rr, IPOPT_NOP, iph[opt->rr+1]);
                opt->rr = 0;
                opt->rr_needaddr = 0;
        }
        if (opt->ts) {
                memset(iph+opt->ts, IPOPT_NOP, iph[opt->ts+1]);
                opt->ts = 0;
                opt->ts_needaddr = opt->ts_needtime = 0;
        }
}

ip_forward_options()方法处理转发分片包(net/ipv4/ip_options.c)。在该方法中,记录路由和严格记录路由选项被处理,并且ip_send_check()方法被调用以计算其 IPv4 报头被改变的分组的校验和(opt->is_changed标志被设置)并将opt->is_changed标志重置为 0。下一节将讨论 IPv4 Tx 路径,即数据包的发送方式。

我关于 Rx 路径的讨论到此结束。下一节将讨论 Tx 路径——发送 IPv4 数据包时会发生什么。

发送 IPv4 数据包

IPv4 层为其上一层(传输层(L4 ))提供了通过将数据包传递给链路层(L2)来发送数据包的方法。我将在本节中讨论这是如何实现的,您将看到在 IPv4 中处理 TCPv4 数据包传输和在 IPv4 中处理 UDPv4 数据包传输之间的一些差异。从第 4 层(传输层)发送 IPv4 数据包有两种主要方法:第一种是ip_queue_xmit()方法,由传输协议使用,它们自己处理碎片,如 TCPv4。ip_queue_xmit()方法并不是 TCPv4 使用的唯一传输方法,例如,它还使用ip_build_and_send_pkt()方法来发送 SYN ACK 消息(参见net/ipv4/tcp_ipv4.c)中的tcp_v4_send_synack()方法实现)。第二种方法是ip_append_data()方法,由不处理碎片的传输协议使用,如 UDPv4 协议或 ICMPv4 协议。ip_append_data()方法不发送任何包——它只准备包。ip_push_pending_frames()方法用于实际发送数据包,例如,它由 ICMPv4 或原始套接字使用。调用ip_push_pending_frames()实际上是通过调用ip_send_skb()方法启动传输过程,最终调用ip_local_out()方法。在内核 2.6.39 之前的 UDPv4 中使用了ip_push_pending_frames()方法进行传输;对于 2.6.39 中新的ip_finish_skb API,使用的是ip_send_skb()方法。两种方法都在net/ipv4/ip_output.c中实现。

有直接调用dst_output()方法的情况,没有使用ip_queue_xmit()方法或者ip_append_data()方法;例如,当使用使用 IP_HDRINCL 套接字选项的原始套接字发送时,不需要准备 IPv4 报头。自行构建 IPv4 的用户空间应用使用 IPv4 IP_HDRINCL 套接字选项。例如,众所周知的iputilspingnmapnping都允许用户这样设置 IPv4 报头的ttl:

ping –ttl ipDestAddress

或者:

nping –ttl ipDestAddress

通过设置了 IP_HDRINCL socket 选项的原始套接字发送数据包的过程如下:

static int raw_send_hdrinc(struct sock *sk, struct flowi4 *fl4,
               void *from, size_t length,
               struct rtable **rtp,
               unsigned int flags)
{
        ...
        err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
              rt->dst.dev, dst_output);
        ...
}

图 4-8 显示了从传输层发送 IPv4 数据包的路径。

9781430261964_Fig04-08.jpg

图 4-8 。发送 IPv4 数据包

在图 4-8 中,你可以看到来自传输层(L4)的传输数据包的不同路径;这些数据包由ip_queue_xmit()方法或ip_append_data()方法处理。

让我们从ip_queue_xmit()方法开始,这是两种方法中比较简单的一种:

int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
    . . .
    /* Make sure we can route this packet. */
    rt = (struct rtable *)__sk_dst_check(sk, 0);

rtable对象是在路由子系统中查找的结果。首先,我讨论了rtable实例为空的情况,您需要在路由子系统中执行查找。如果设置了严格路由选项标志,则目的地址被设置为 IP 选项的第一个地址:

    if (rt == NULL) {
        __be32 daddr;

        /* Use correct destination address if we have options. */
        daddr = inet->inet_daddr;
        if (inet_opt && inet_opt->opt.srr)
            daddr = inet_opt->opt.faddr;

现在,在路由子系统中使用ip_route_output_ports()方法执行查找:如果查找失败,数据包将被丢弃,并返回错误–EHOSTUNREACH:

        /* If this fails, retransmit mechanism of transport layer will
         * keep trying until route appears or the connection times
         * itself out.
         */
        rt = ip_route_output_ports(sock_net(sk), fl4, sk,
                       daddr, inet->inet_saddr,
                       inet->inet_dport,
                       inet->inet_sport,
                       sk->sk_protocol,
                       RT_CONN_FLAGS(sk),
                       sk->sk_bound_dev_if);
        if (IS_ERR(rt))
            goto no_route;
        sk_setup_caps(sk, &rt->dst);
    }
    skb_dst_set_noref(skb, &rt->dst);
    . . .

如果查找成功,但设置了选项中的is_strictroute标志和路由条目中的rt_uses_gateway标志,则数据包被丢弃,并返回错误–EHOSTUNREACH:

    if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
        goto no_route;

现在正在构建 IPv4 报头。您应该记得数据包来自第 4 层,这里的skb->data指向传输头。通过skb_push()方法将skb->data指针向后移动;将其移回所需的偏移量是 IPv4 报头的大小加上 IP 选项列表的大小(optlen),如果使用了 IP 选项:

    /* OK, we know where to send it, allocate and build IP header. */
    skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));

将 L3 标题(skb->network_header)设置为指向skb->data:

    skb_reset_network_header(skb);
    iph = ip_hdr(skb);
    *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
    if (ip_dont_fragment(sk, &rt->dst) && !skb->local_df)
        iph->frag_off = htons(IP_DF);
    else
        iph->frag_off = 0;
    iph->ttl      = ip_select_ttl(inet, &rt->dst);
    iph->protocol = sk->sk_protocol;
    ip_copy_addrs(iph, fl4);

选项长度(optlen)除以 4,并且结果被添加到 IPv4 报头长度(iph->ihl),因为 IPv4 报头是以 4 字节的倍数来测量的。然后调用ip_options_build()方法,根据指定 IP 选项的内容构建 IPv4 报头中的选项。ip_options_build()方法的最后一个参数is_frag指定没有碎片。本章前面的“IP 选项”部分讨论了ip_options_build()方法。

         if (inet_opt && inet_opt->opt.optlen) {
         iph->ihl += inet_opt->opt.optlen >> 2;
         ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
         }

设置 IPv4 报头中的id:

         ip_select_ident_more(iph, &rt->dst, sk,
                  (skb_shinfo(skb)->gso_segs ?: 1) - 1);

         skb->priority = sk->sk_priority;
         skb->mark = sk->sk_mark;

发送数据包:

         res = ip_local_out(skb);

在讨论ip_append_data()方法之前,我想提到一个回调,它是ip_append_data()方法的参数:getfrag()回调。getfrag()方法是将实际数据从用户空间复制到 SKB 的回调。在 UDPv4 中,getfrag()回调被设置为通用方法ip_generic_getfrag()。在 ICMPv4 中,getfrag()回调被设置为特定于协议的方法icmp_glue_bits()。这里我应该提到的另一个问题是 UDPv4 的软木塞特性。在内核 2.5.44 中增加了 UDP_CORK socket 选项;当启用此选项时,此套接字上的所有数据输出都累积到一个数据报中,该数据报在禁用此选项时传输。您可以通过setsockopt()系统调用来启用和禁用该套接字选项;参见man 7 udp。在内核 2.6.39 中,无锁传输快速路径被添加到 UDPv4 实现中。通过这种添加,当不使用软木塞功能时,不使用套接字锁。所以当 UDP_CORK socket 选项被设置时(通过setsockopt()系统调用),或者 MSG_MORE 标志被设置时,就会调用ip_append_data()方法。当 UDP_CORK socket 选项未设置时,使用udp_sendmsg()方法中的另一个路径,该路径不持有套接字锁,因此速度更快,并调用ip_make_skb()方法。调用ip_make_skb()方法类似于将ip_append_data()ip_push_pending_frames()方法合二为一,除了它不发送产生的 SKB。通过ip_send_skb()方法发送 SKB。

现在让我们来看看ip_append_data()方法:

int ip_append_data(struct sock *sk, struct flowi4 *fl4,
                   int getfrag(void *from, char *to, int offset, int len,
                               int odd, struct sk_buff *skb),
                   void *from, int length, int transhdrlen,
                   struct ipcm_cookie *ipc, struct rtable **rtp,
                   unsigned int flags)
{
        struct inet_sock *inet = inet_sk(sk);
        int err;

如果 MSG_PROBE 标志 us used,则表示调用者只对某些信息感兴趣(通常是 MTU,用于 PMTU 发现),所以不需要实际发送数据包,方法返回 0:

        if (flags&MSG_PROBE)
                return 0;

transhdrlen的值用于指示它是否是第一片段。ip_setup_cork()方法创建一个不存在的 cork IP options 对象,并将指定的ipc ( ipcm_cookie对象)的 IP 选项复制到 cork IP options:

        if (skb_queue_empty(&sk->sk_write_queue)) {
                err = ip_setup_cork(sk, &inet->cork.base, ipc, rtp);
                if (err)
                        return err;
        } else {
                transhdrlen = 0;
        }

真正的工作是由 __ ip_append_data()方法完成的;这是一个漫长而复杂的方法,我无法深入研究它的所有细节。我会提到,在这个方法中,根据网络设备是否支持分散/聚集(NETIF_F_SG),有两种不同的处理碎片的方式。当 NETIF_F_SG 标志置位时,使用skb_shinfo(skb)->frags,而当 NETIF_F_SG 标志未置位时,使用skb_shinfo(skb)->frag_list。当 MSG_MORE 标志被置位时,也有不同的内存分配。MSG_MORE 标志表示很快将发送另一个数据包。从 Linux 2.6 开始,UDP 套接字也支持这个标志。

        return __ip_append_data(sk, fl4, &sk->sk_write_queue, &inet->cork.base,
                                sk_page_frag(sk), getfrag,
                                from, length, transhdrlen, flags);
}

在本节中,您已经了解了 Tx 路径,即如何发送 IPv4 数据包。当数据包长度高于网络设备 MTU 时,数据包无法按原样发送。下一节将介绍 Tx 路径中的碎片以及如何处理碎片。

碎片化

网络接口对数据包的大小有限制。通常在 10/100/1000 Mb/s 以太网中,它是 1500 字节,尽管有网络接口允许使用高达 9K 的 MTU(称为巨型帧)。当发送的数据包大于传出网卡的 MTU 时,应该将其分成更小的片段。这在ip_fragment()方法 ( net/ipv4/ip_output.c)中完成。收到的分段数据包应重新组装成一个数据包。这是通过ip_defrag()方法(net/ipv4/ip_fragment.c)完成的,在下一节“碎片整理”中讨论

我们先来看看ip_fragment()方法。这是它的原型:

int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))

output回调是要使用的传输方法。当从ip_finish_output()调用ip_fragment()方法时,output回调就是ip_finish_output2()方法。ip_fragment()方法中有两条路径:快速路径和慢速路径。快速路径用于 SKB 的frag_list不为空的数据包,慢速路径用于不满足该条件的数据包。

首先执行检查以查看是否允许分段,如果不允许,则将带有所需分段代码的“Destination Unreachable”icmp v4 消息发送回发送方,更新统计信息(IPSTATS_MIB_FRAGFAILS ),丢弃数据包,并返回错误代码–EMSGSIZE:

int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
        {
        unsigned int mtu, hlen, left, len, ll_rs;
        . . .
        struct rtable *rt = skb_rtable(skb);
        int err = 0;

        dev = rt->dst.dev;

        . . .

        iph = ip_hdr(skb);

        if (unlikely(((iph->frag_off & htons(IP_DF)) && !skb->local_df) ||
              (IPCB(skb)->frag_max_size &&
               IPCB(skb)->frag_max_size > dst_mtu(&rt->dst)))) {
           IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
           icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
                     htonl(ip_skb_dst_mtu(skb)));
           kfree_skb(skb);
           return -EMSGSIZE;
        }
        . . .
        . . .

下一节将讨论分段中的快速路径及其实现。

快速路径

现在让我们来看看捷径。首先,通过调用skb_has_frag_list()方法,检查是否应该在快速路径中处理数据包,该方法简单地检查skb_shinfo(skb)->frag_list不为空;如果为空,则进行一些健全性检查,如果无效,则激活慢路径机制的回退(只需调用goto slow_path)。然后为第一个片段构建 IPv4 报头。该 IPv4 报头的frag_off被设置为htons(IP_MF),表示前面还有更多碎片。IPv4 报头的frag_off字段是 16 位字段;低 13 位是片段偏移量,高 3 位是标志。对于第一个片段,偏移量应该为 0,标志应该为 IP_MF(更多片段)。对于除最后一个片段之外的所有其他片段,应设置 IP_MF 标志,低 13 位应为片段偏移量(以 8 字节为单位测量)。对于最后一个片段,不应设置 IP_MF 标志,但低 13 位仍将保存片段偏移量。

以下是如何将hlen设置为以字节为单位的 IPv4 报头大小:

   hlen = iph->ihl * 4;
   . . .
   if (skb_has_frag_list(skb)) {
        struct sk_buff *frag, *frag2;
        int first_len = skb_pagelen(skb);
        . . .
        err     = 0;
        offset = 0;
        frag = skb_shinfo(skb)->frag_list;

通过skb_frag_list_init(skb)skb_shinfo(skb)->frag_list设置为空:

        skb_frag_list_init(skb);
        skb->data_len = first_len - skb_headlen(skb);
        skb->len = first_len;
        iph->tot_len = htons(first_len);

为第一个片段设置 IP_MF(更多片段)标志:

        iph->frag_off = htons(IP_MF);

因为某些 IPv4 报头字段的值已更改,所以需要重新计算校验和:

        ip_send_check(iph);

现在看看遍历frag_list并构建片段的循环:

        for (;;) {
           /* Prepare header of the next frame,
            * before previous one went down. */
           if (frag) {
             frag->ip_summed = CHECKSUM_NONE;
             skb_reset_transport_header(frag);

ip_fragment()是从传输层(L4)调用的,所以skb->data指向传输头。skb->data指针应该向后移动hlen字节,这样它将指向 IPv4 报头(hlen是 IPv4 报头的大小,以字节为单位):

             __skb_push(frag, hlen);

将 L3 标头(skb->network _header)设置为指向skb->data:

             skb_reset_network_header(frag);

将创建的 IPv4 报头复制到 L3 网络报头中;在这个for循环的第一次迭代中,它是在循环之外为第一个片段创建的头:

             memcpy(skb_network_header(frag), iph, hlen);

现在,下一个片段的 IPv4 报头及其tot_len被初始化:

             iph = ip_hdr(frag);
             iph->tot_len = htons(frag->len);

将 SKB 的各种 SKB 字段(如pkt_typepriorityprotocol)复制到frag:

             ip_copy_metadata(frag, skb);

只有对于第一个片段(偏移量为 0 ),才应该调用ip_options_fragment()方法:

             if (offset == 0)
                 ip_options_fragment(frag);
             offset += skb->len - hlen;

IPv4 报头的frag_off字段是以 8 字节的倍数来测量的,因此将偏移量除以 8:

             iph->frag_off = htons(offset>>3);

除了最后一个片段,每个片段都应该设置 IP_MF 标志:

             if (frag->next != NULL)
                 iph->frag_off |= htons(IP_MF);

某些 IPv4 标头字段的值已更改,因此应重新计算校验和:

             /* Ready, complete checksum */
             ip_send_check(iph);
           }

现在发送带有output回调的片段。如果发送成功,递增 IPSTATS_MIB_FRAGCREATES。如果有错误,退出循环:

           err = output(skb);

           if (!err)
              IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
           if (err || !frag)
              break;

获取下一个 SKB:

           skb = frag;
           frag = skb->next;
           skb->next = NULL;

下面的右括号是for循环的结尾:

        }

for循环终止,应该检查最后一次调用output(skb)的返回值。如果成功,则更新统计信息(IPSTATS_MIB_FRAGOKS ),该方法返回 0:

    if (err == 0) {
         IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
         return 0;
     }

如果对output(skb)的最后一次调用在一次循环迭代中失败,包括最后一次,skb 被释放,统计数据(IPSTATS_MIB_FRAGFAILS)被更新,并且返回错误代码(err):

    while (frag) {
         skb = frag->next;
         kfree_skb(frag);
         frag = skb;
     }
     IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
     return err;

您现在应该对碎片化中的快速路径以及它是如何实现的有了很好的理解。

慢速路径

现在让我们来看看如何在碎片化中实现慢速路径:

        . . .

        iph = ip_hdr(skb);

        left = skb->len - hlen;         /* Space per frame */
        . . .

        while (left > 0) {
                len = left;
                /* IF: it doesn't fit, use 'mtu' - the data space left */
                if (len > mtu)
                        len = mtu;

每个片段(除了最后一个)应该在一个 8 字节的边界上对齐:

                if (len < left) {
                        len &= ∼7;
                }

分配一个 SKB:

                if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {
                        NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");
                        err = -ENOMEM;
                        goto fail;
                }

                /*
                 *      Set up data on packet
                 */

将各种 SKB 字段(如pkt_typepriorityprotocol)从skb复制到skb2:

                ip_copy_metadata(skb2, skb);
                skb_reserve(skb2, ll_rs);
                skb_put(skb2, len + hlen);
                skb_reset_network_header(skb2);
                skb2->transport_header = skb2->network_header + hlen;

                /*
                 *      Charge the memory for the fragment to any owner
                 *      it might possess
                 */

                if (skb->sk)
                        skb_set_owner_w(skb2, skb->sk);

                /*
                 *      Copy the packet header into the new buffer.
                 */

                skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);

                /*
                 *      Copy a block of the IP datagram.
                 */
                if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
                        BUG();
                left -= len;

                /*
                 *      Fill in the new header fields.
                 */
                iph = ip_hdr(skb2);

frag_off是 8 字节的倍数,因此将偏移量除以 8:

                iph->frag_off = htons((offset >> 3));
                . . .

对于第一个片段,仅处理选项一次:

                if (offset == 0)
                        ip_options_fragment(skb);

MF 标志(更多片段)应设置在除最后一个片段之外的任何片段上:

                if (left > 0 || not_last_frag)
                        iph->frag_off |= htons(IP_MF);
                ptr += len;
                offset += len;

                /*
                 *      Put this fragment into the sending queue.
                 */
                iph->tot_len = htons(len + hlen);

因为某些 IPv4 报头字段的值已更改,所以应该重新计算校验和:

                ip_send_check(iph);

现在发送带有output回调的片段。如果发送成功,递增 IPSTATS_MIB_FRAGCREATES。如果有错误,则释放数据包,更新统计信息(IPSTATS_MIB_FRAGFAILS),并返回错误代码:

                err = output(skb2);
                if (err)
                        goto fail;

                IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
        }

现在while (left > 0)循环已经终止,调用consume_skb()方法释放 SKB,统计信息(IPSTATS_MIB_FRAGOKS)被更新,返回err的值:

        consume_skb(skb);
        IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
        return err;

本节讨论了分段中慢速路径的实现,Tx 路径中分段的讨论到此结束。请记住,在主机上收到的分段数据包应该重新构建,以便应用可以处理原始数据包。下一节讨论碎片整理——碎片整理的反义词。

碎片整理

碎片整理是将数据包的所有碎片重新组合到一个缓冲区中的过程,这些碎片在 IPv4 报头中都具有相同的id。Rx 路径中处理碎片整理的主要方法是ip_defrag() ( net/ipv4/ip_fragment.c,从ip_local_deliver()调用。还有一些地方可能需要碎片整理,比如在防火墙中,为了能够检查数据包,应该知道数据包的内容。在ip_local_deliver()方法中,调用ip_is_fragment()方法检查数据包是否有碎片;如果是,则调用ip_defrag()方法。ip_defrag()方法有两个参数:第一个是 SKB,第二个是一个 32 位字段,表示方法被调用的点。它的值可以是以下值:

  • 调用from ip_local_deliver()时的 IP_DEFRAG_LOCAL_DELIVER。
  • ip_call_ra_chain()调用时的 IP_DEFRAG_CALL_RA_CHAIN。
  • 从 IPVS 调用时的 IP_DEFRAG_VS_IN 或 IP_DEFRAG_VS_FWD 或 IP_DEFRAG_VS_OUT。

关于第二个参数ip_defrag()的可能值的完整列表,请查看include/net/ip.h中的ip_defrag_users enum定义。

让我们看看ip_local_deliver()中的ip_defrag()调用:

int ip_local_deliver(struct sk_buff *skb)
{
    /*
     *    Reassemble IP fragments.
     */

    if (ip_is_fragment(ip_hdr(skb))) {
        if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }

    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
               ip_local_deliver_finish);
}
(net/ipv4/ip_input.c)

ip_is_fragment()是一个简单的 helper 方法,它将 IPv4 报头作为唯一的参数,并在它是一个片段时返回true,如下所示:

static inline bool ip_is_fragment(const struct iphdr *iph)
{
         return (iph->frag_off & htons(IP_MF | IP_OFFSET)) != 0;
}
(include/net/ip.h)

在以下两种情况下,ip_is_fragment()方法返回true:

  • IP_MF 标志被设置。
  • 片段偏移量不为 0。

因此,它将在所有片段上返回true:

  • 在第一个片段上,其中frag_off为 0,但设置了 IP_MF 标志。
  • 在最后一个片段上,其中frag_off不是 0,但是 IP_MF 标志没有被设置。
  • 在所有其他片段上,其中frag_off不为 0 并且设置了 IP_MF 标志。

碎片整理的实现基于ipq对象的哈希表。哈希函数 ( ipqhashfn)有四个参数:片段 id、源地址、目的地址和协议:

struct ipq {
        struct inet_frag_queue q;

        u32                 user;
        __be32              saddr;
        __be32              daddr;
        __be16              id;
        u8                  protocol;
        u8                  ecn; /* RFC3168 support */
        int                 iif;
        unsigned int        rid;
        struct inet_peer    *peer;
};

请注意,IPv4 碎片整理的逻辑是与其 IPv6 副本共享的。因此,举例来说,inet_frag_queue结构和像inet_frag_find()方法和inet_frag_evictor()方法这样的方法并不特定于 IPv4 它们也用于 IPv6(参见net/ipv6/reassembly.cnet/ipv6/nf_conntrack_reasm.c)。

ip_defrag()方法很短。首先,它通过调用ip_evictor()方法来确保有足够的内存。然后它试图通过调用ip_find()方法为 SKB 找到一个ipq;如果没有找到,它就创建一个ipq对象。ip_find()方法返回的ipq对象被分配给一个名为qp的变量(一个指向ipq对象的指针)。然后它调用ip_frag_queue()方法将片段添加到片段链表中(qp->q.fragments)。对列表的添加是根据片段偏移量完成的,因为列表是按片段偏移量排序的。在添加了 SKB 的所有片段后,ip_frag_queue() 方法调用ip_frag_reasm()方法从其所有片段构建一个新的包。ip_frag_reasm()方法还通过调用ipq_kill()方法来停止ip_expire()的定时器。如果有错误,并且新数据包的大小超过了最大允许大小(65535),ip_frag_reasm()方法更新统计信息(IPSTATS_MIB_REASMFAILS)并返回-E2BIG。如果调用ip_frag_reasm()中的skb_clone()方法失败,则返回–ENOMEM。在这种情况下,IPSTATS_MIB_REASMFAILS 统计信息也会更新。应该在指定的时间间隔内从其所有片段构造分组。如果未在该时间间隔内完成,方法ip_expire()将发送一条“超时”的 ICMPv4 消息,并带有“片段重组时间超时”代码。碎片整理时间间隔可以通过以下procfs条目设置:/proc/sys/net/ipv4/ipfrag_time。默认情况下是 30 秒。

我们来看看ip_defrag()方法:

int ip_defrag(struct sk_buff *skb, u32 user)
{
        struct ipq *qp;
        struct net *net;

        net = skb->dev ? dev_net(skb->dev) : dev_net(skb_dst(skb)->dev);
        IP_INC_STATS_BH(net, IPSTATS_MIB_REASMREQDS);

        /* Start by cleaning up the memory. */
        ip_evictor(net);

        /* Lookup (or create) queue header */
        if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {
                int ret;

                spin_lock(&qp->q.lock);
                ret = ip_frag_queue(qp, skb);
                spin_unlock(&qp->q.lock);
                ipq_put(qp);
                return ret;
        }

        IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);
        kfree_skb(skb);
        return -ENOMEM;
}

在查看ip_frag_queue()方法之前,考虑下面的宏,它只是返回与指定的 SKB 相关联的ipfrag_skb_cb对象:

#define FRAG_CB(skb)    ((struct ipfrag_skb_cb *)((skb)->cb))

现在我们来看一下ip_frag_queue()方法。我不会描述所有的细节,因为该方法非常复杂,并且考虑了可能由重叠引起的问题(由于重传可能出现重叠片段)。在下面的代码片段中,qp->q.len被设置为包的总长度,包括它的所有片段;当未设置 IP_MF 标志时,这意味着这是最后一个片段:

static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
        struct sk_buff *prev, *next;
        . . .
        /* Determine the position of this fragment. */
        end = offset + skb->len - ihl;
        err = -EINVAL;

        /* Is this the final fragment? */
        if ((flags & IP_MF) == 0) {
                /* If we already have some bits beyond end
                 * or have different end, the segment is corrupted.
                 */
                if (end < qp->q.len ||
                    ((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))
                        goto err;
                qp->q.last_in |= INET_FRAG_LAST_IN;
                qp->q.len = end;
        } else {
           . . .
        }

现在,通过查找片段偏移量之后的第一个位置,找到了添加片段的位置(片段的链表是按偏移量排序的):

        . . .
        prev = NULL;
        for (next = qp->q.fragments; next != NULL; next = next->next) {
               if (FRAG_CB(next)->offset >= offset)
                       break;  /* bingo! */
               prev = next;
        }

现在,prev指向新片段的添加位置,如果它不为空。跳过处理重叠和其他一些检查,让我们继续将片段插入到列表中:

        FRAG_CB(skb)->offset = offset;
        /* Insert this fragment in the chain of fragments. */
        skb->next = next;
        if (!next)
            qp->q.fragments_tail = skb;
        if (prev)
            prev->next = skb;
        else
            qp->q.fragments = skb;
        . . .
        qp->q.meat += skb->len;

注意,对于每个片段,qp->q.meat增加了skb->len。如前所述,qp->q.len是所有片段的总长度,当它等于qp->q.meat时,意味着所有的片段都被添加,应该用ip_frag_reasm()的方法重新组装成一个包。

现在您可以看到重组是如何发生的以及在哪里发生的:(重组是通过调用ip_frag_reasm()方法来完成的):

    if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&
        qp->q.meat == qp->q.len) {
        unsigned long orefdst = skb->_skb_refdst;

        skb->_skb_refdst = 0UL;
        err = ip_frag_reasm(qp, prev, dev);
        skb->_skb_refdst = orefdst;
        return err;
    }

我们来看看ip_frag_reasm()方法:

static int ip_frag_reasm(struct ipq *qp, struct sk_buff *prev,
                         struct net_device *dev)
{
        struct net *net = container_of(qp->q.net, struct net, ipv4.frags);
        struct iphdr *iph;
        struct sk_buff *fp, *head = qp->q.fragments;
        int len;
      ...

         /* Allocate a new buffer for the datagram. */
         ihlen = ip_hdrlen(head);
         len = ihlen + qp->q.len;

         err = -E2BIG;
         if (len > 65535)
                goto out_oversize;
         ...
         skb_push(head, head->data - skb_network_header(head));

促进

转发数据包的主要处理程序是ip_forward()方法:

int ip_forward(struct sk_buff *skb)
{
    struct iphdr          *iph;    /* Our header */
    struct rtable         *rt;     /* Route we use */
    struct ip_options     *opt    = &(IPCB(skb)->opt);

我应该描述一下为什么大的接收卸载(LRO)数据包在转发中被丢弃。LRO 是一种性能优化技术,它将数据包合并在一起,创建一个大型 SKB,然后将它们传递到更高的网络层。这减少了 CPU 开销,从而提高了性能。转发由 LRO 建造的大型 SKB 是不可接受的,因为它将比传出的 MTU 更大。因此,当启用 LRO 时,SKB 被释放,并且该方法返回 NET_RX_DROP。通用接收卸载(GRO) 设计包括转发能力,但 LRO 没有:

    if (skb_warn_if_lro(skb))
        goto drop;

如果设置了router_alert选项,应该调用ip_call_ra_chain()方法来处理数据包。当在原始套接字上用 IP_ROUTER_ALERT 调用setsockopt()时,该套接字被添加到名为ip_ra_chain的全局列表中(参见include/net/ip.h)。ip_call_ra_chain()方法将数据包传递给所有的原始套接字。您可能想知道为什么数据包被发送到所有的原始套接字,而不是单个原始套接字?与 TCP 或 UDP 相反,在原始套接字中没有套接字侦听的端口。

如果pkt_type——由eth_type_trans()方法确定,应该从网络驱动程序调用,并在附录 A 中讨论——不是 PACKET_HOST,则数据包被丢弃:

    if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb))
        return NET_RX_SUCCESS;

    if (skb->pkt_type != PACKET_HOST)
        goto drop;

IPv4 报头的ttl(生存时间)字段是在每个转发设备中减 1 的计数器。如果ttl达到 0,则表明应该丢弃该数据包,并且应该发送带有“超过 TTL 计数”代码的相应超时 ICMPv4 消息:

    if (ip_hdr(skb)->ttl <= 1)
        goto too_many_hops;. . .
        . . .

too_many_hops:
    /* Tell the sender its packet died... */
    IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_INHDRERRORS);
    icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
    . . .

现在,检查严格路线标志(is_strictroute)和rt_uses_gateway标志是否都被设置;在这种情况下,严格路由不能被应用,并且带有“严格路由失败”代码的“目的地不可达”ICMPv4 消息被发回:

    rt = skb_rtable(skb);

    if (opt->is_strictroute && rt->rt_uses_gateway)
        goto sr_failed;
    . . .
sr_failed:
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
    goto drop;
    . . .

现在,执行检查以查看分组的长度是否大于输出设备 MTU。如果是,这意味着不允许按原样发送该数据包。执行另一个检查以查看 IPv4 报头中的 DF(不分段)字段是否被设置,以及 SKB 中的local_df标志是否未被设置。如果满足这些条件,这意味着当数据包到达ip_output()方法时,它不会被ip_fragment()方法分段。这意味着数据包不能按原样发送,也不能被分段;因此,带有“需要分段”代码的目的地不可达 ICMPv4 消息被发回,数据包被丢弃,统计信息(IPSTATS_MIB_FRAGFAILS)被更新:

      if (unlikely(skb->len > dst_mtu(&rt->dst) &&
          !skb_is_gso(skb) && (ip_hdr(skb)->frag_off & htons(IP_DF)))
            && !skb->local_df) {
      IP_INC_STATS(dev_net(rt->dst.dev), IPSTATS_MIB_FRAGFAILS);
      icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
              htonl(dst_mtu(&rt->dst)));
      goto drop;    }

因为 IPv4 报头的ttl和校验和将要被改变,所以应该保留 SKB 的副本:

        /* We are about to mangle packet. Copy it! */
         if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev)+rt->dst.header_len))
                 goto drop;
         iph = ip_hdr(skb);

如前所述,转发数据包的每个节点都应该减少ttl。由于ttl的改变,校验和也在ip_decrease_ttl()方法中相应更新:

         /* Decrease ttl after skb cow done */
         ip_decrease_ttl(iph);

现在,重定向 ICMPv4 消息被发回。如果路由条目的 RTCF _ 多尔直接标志被设置,那么“重定向到主机”代码被用于该消息(我在第五章的中讨论 ICMPv4 重定向消息)。

         /*
          *      We now generate an ICMP HOST REDIRECT giving the route
          *      we calculated.
          */
         if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
                 ip_rt_send_redirect(skb);

Tx 路径中的skb->priority被设置为套接字优先级(sk->sk_priority)—例如,参见ip_queue_xmit()方法。接下来,可以通过调用带有 SOL_SOCKET 和 SO_PRIORITY 的setsockopt()系统调用来设置套接字优先级。但是,在转发数据包时,没有套接字连接到 SKB。因此,在ip_forward()方法中,skb->priority是根据一个名为ip_tos2prio的特殊表格设置的。该表有 16 个条目(见include/net/route.h)。

         skb->priority = rt_tos2priority(iph->tos);

现在,假设没有 netfilter NF_INET_FORWARD 钩子,调用ip_forward_finish()方法:

         return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
                        rt->dst.dev, ip_forward_finish);

ip_forward_finish()中,统计数据被更新,我们检查 IPv4 包是否包含 IP 选项。如果是,就调用ip_forward_options()方法来处理选项。如果它没有选项,就调用dst_output()方法。这个方法唯一做的事情就是调用skb_dst(skb)->output(skb):

static int ip_forward_finish(struct sk_buff *skb)
     {
     struct ip_options *opt  = &(IPCB(skb)->opt);

     IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTFORWDATAGRAMS);

     IP_ADD_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTOCTETS, skb->len);

     if (unlikely(opt->optlen))
             ip_forward_options(skb);

     return dst_output(skb);
     }

在本节中,您了解了转发数据包的方法(ip_forward()ip_forward_finish()))、数据包在转发过程中被丢弃的情况、发送 ICMP 重定向的情况等等。

摘要

本章讨论了 IPv4 协议——如何构建 IPv4 数据包、IPv4 报头结构和 IP 选项,以及如何处理它们。您了解了 IPv4 协议处理程序是如何注册的。您还了解了 IPv4 中的 Rx 路径(如何处理 IPv4 数据包的接收)和 Tx 路径(如何处理 IPv4 数据包的传输)。有些情况下,数据包大于网络接口 MTU,因此如果不在发送方进行分段,然后在接收方进行碎片整理,就无法发送数据包。您了解了 IPv4 中碎片的实现(包括慢速路径和快速路径如何实现以及何时使用)以及 IPv4 中碎片整理的实现。本章还讲述了 IPv4 转发——在不同的网络接口上发送传入的数据包,而不将其传递到上层。您还看到了一些在转发过程中丢弃数据包和发送 ICMP 重定向的例子。下一章将讨论 IPv4 路由子系统。接下来的“快速参考”部分涵盖了与本章中讨论的主题相关的主要方法,按其上下文排序。

快速参考

我以本章中提到的 IPv4 子系统的重要方法和宏的简短列表来结束本章。

方法

以下是本章中提到的 IPv4 层的重要方法的简短列表。

int IP _ queue _ xmit(struct sk _ buf * skb,struct flow * fl);

这种方法将数据包从 L4(传输层)移动到 L3(网络层),例如从 TCPv4 调用。

int IP _ append _ data(struct sock * sk,struct flowi4 *fl4,int getfrag(void *from,char *to,int offset,int len,int odd,struct sk_buff *skb),void *from,int length,int transhdrlen,struct ipcm_cookie *ipc,struct rtable **rtp,unsigned int flags);

这种方法将数据包从 L4(传输层)移动到 L3(网络层);例如,在使用 corked UDP 套接字时从 UDPv4 调用,以及从 ICMPv4 调用。

struct sk _ buff * IP _ make _ skb(struct sock * sk,struct flowi4 *fl4,int getfrag(void *from,char *to,int offset,int len,int odd,struct sk_buff *skb),void *from,int length,int transhdrlen,struct ipcm_cookie *ipc,struct rtable **rtp,unsigned int flags);

这个方法是在内核 2.6.39 中添加的,用于启用 UDPv4 实现的无锁传输快速路径;不使用 UDP_CORK 套接字选项时调用。

int IP _ generic _ get frag(void * from,char *to,int offset,int len,int odd,struct sk _ buff * skb);

这个方法是将数据从用户空间复制到指定的skb的通用方法。

static int icmp _ glue _ bits(void * from,char *to,int offset,int len,int odd,struct sk _ buff * skb);

这个方法就是 ICMPv4 getfrag回调。ICMPv4 模块用icmp_glue_bits()作为getfrag回调来调用ip_append_data()方法。

int IP _ options _ compile(struct net * net,struct ip_options *opt,struct sk _ buff * skb);

该方法通过解析 IP 选项构建一个ip_options对象。

void IP _ options _ fragment(struct sk _ buff * skb);

此方法使用 NOOPs 填充其复制标志未设置的选项,并重置这些 IP 选项的相应字段。仅针对第一个片段调用。

void IP _ options _ build(struct sk _ buff * skb,struct ip_options *opt,__be32 daddr,struct rtable *rt,int is _ frag);

该方法获取指定的ip_options对象,并将其内容写入 IPv4 头。最后一个参数is_frag,在所有对ip_options_build()方法的调用中实际上都是 0。

void IP _ forward _ options(struct sk _ buff * skb);

此方法处理 IP 选项转发。

int ip_rcv(struct sk_buff *skb,struct net_device *dev,struct packet_type *pt,struct net _ device * orig _ dev);

此方法是 IPv4 数据包的主要接收处理程序。

IP _ rcv _ options(struct sk _ buf * skb):

此方法是处理接收带有选项的数据包的主要方法。

int IP _ options _ rcv _ SRR(struct sk _ buf * skb):

此方法使用严格路由选项处理数据包的接收。

int IP _ forward(struct sk _ buff * skb);

此方法是转发 IPv4 数据包的主要处理程序。

静态 void ipmr _ queue _ xmit(struct net * net,struct mr_table *mrt,struct sk_buff *skb,struct mfc_cache *c,int vifi);

这种方法是多播传输方法。

static int raw _ send _ HDR Inc(struct sock * sk,struct flowi4 *fl4,void *from,size_t length,struct rtable **rtp,unsigned int flags);

当设置了 IPHDRINC 套接字选项时,原始套接字使用此方法进行传输。它直接调用dst_output()方法。

int IP _ fragment(struct sk _ buff * skb,int(* output)(struct sk _ buff *));

这种方法是主要的分段方法。

int ip_defrag(struct sk_buff *skb,u32 user);

这种方法是主要的碎片整理方法。它处理传入的 IP 片段。第二个参数user表示从哪里调用这个方法。关于第二个参数可能值的完整列表,请查看include/net/ip.h中的ip_defrag_users enum定义。

bool skb _ has _ frag _ list(const struct sk _ buff * skb);

如果skb_shinfo(skb)->frag_list不为空,该方法返回true。方法skb_has_frag_list()过去被命名为skb_has_frags(),在内核 2.6.37 中被重命名为skb_has_frag_list()。(原因是这个名字令人困惑。)skb 可以以两种方式分段:通过页面数组(称为skb_shinfo(skb)->frags[])和通过 skb 列表(称为skb_shinfo(skb)->frag_list)。因为skb_has_frags()测试的是后者,所以它的名字很混乱,因为听起来更像是在测试前者。

int IP _ local _ deliver(struct sk _ buff * skb);

此方法处理向第 4 层传送数据包。

int IP _ options _ get _ from _ user(struct net * net,struct ip_options_rcu **optp,unsigned char __user *data,int optlen);

该方法通过使用 IP_OPTIONS 的setsockopt()系统调用处理来自用户空间的设置选项。

bool IP _ is _ fragment(const struct ipdr * IPF):

如果包是一个片段,这个方法返回true

int IP _ decrease _ TTL(struct iphdr * iph);

此方法将指定 IPv4 标头的ttl减 1,并且由于其中一个 IPv4 标头字段(ttl)已更改,因此会重新计算 IPv4 标头校验和。

int IP _ build _ and _ send _ PKT(struct sk _ buff * skb,struct sock *sk,__be32 saddr,__be32 daddr,struct IP _ options _ rcu * opt);

TCPv4 使用此方法发送 SYN ACK。参见net/ipv4/tcp_ipv4.c中的tcp_v4_send_synack()方法。

int IP _ Mr _ input(struct sk _ buff * skb);

此方法处理传入的多播数据包。

int ip_mr_forward(struct net *net,struct mr_table *mrt,struct sk_buff *skb,struct mfc_cache *cache,int local);

此方法转发多播数据包。

bool IP _ call _ ra _ chain(struct sk _ buff * skb);

此方法处理路由器警报 IP 选项。

宏指令

本节提到了本章中的一些宏,它们处理 IPv4 堆栈中遇到的机制,如分段、netfilter 挂钩和 IP 选项。

断续器

这个宏返回skb->cb指向的inet_skb_parm对象。它用于访问存储在inet_skb_parm对象(include/net/ip.h)中的ip_options对象。

问 _CB(skb)

这个宏返回skb->cb指向的ipfrag_skb_cb对象(net/ipv4/ip_fragment.c)。

int NF_HOOK(uint8_t pf,unsigned int hook,struct sk_buff *skb,struct net_device *in,struct net_device out,int (okfn)(struct sk_buff *))

这个宏是 netilter 钩子;第一个参数pf是协议族;对于 IPv4,它是 NFPROTO_IPV4,对于 IPv6,它是 NFPROTO_IPV6。第二个参数是网络堆栈中五个 netfilter 挂钩点之一;这五点在include/uapi/linux/netfilter.h中定义,IPv4 和 IPv6 都可以使用。如果没有注册钩子或者如果注册的 netfilter 钩子没有丢弃或拒绝数据包,将调用okfn回调。

int NF_HOOK_COND(uint8_t pf,unsigned int hook,struct sk_buff *skb,struct net_device *in,struct net_device out,int (okfn)(struct sk_buff *),bool cond)

这个宏与NF_HOOK()宏相同,但是增加了一个布尔参数cond,它必须是true,以便调用 netfilter 钩子。

IPOPT_COPIED()

该宏返回选项类型的复制标志。

五、IPv4 路由子系统

第四章讨论了 IPv4 子系统。在本章和下一章,我将讨论最重要的 Linux 子系统之一,路由子系统,以及它在 Linux 中的实现。Linux 路由子系统被广泛用于路由器——从家庭和小型办公室路由器,到企业路由器(连接组织或 ISP)和互联网主干网上的核心高速路由器。无法想象没有这些设备的现代世界。这两章中的讨论仅限于 IPv4 路由子系统,它与 IPv6 的实现非常相似。本章主要介绍 IPv4 路由子系统使用的主要数据结构,如路由表、转发信息库(FIB)信息和 FIB 别名、FIB TRIE 等。(顺便说一下,TRIE 不是首字母缩略词,但它来源于单词 retrieval )。TRIE 是一种数据结构,一种取代 FIB 哈希表的特殊树。您将了解如何在路由子系统中执行查找,如何以及何时生成 ICMP 重定向消息,以及如何删除路由缓存代码。请注意,本章中的讨论和代码示例都与内核 3.9 相关,只有两个部分明确提到了不同的内核版本。

转发和 FIB

Linux 网络栈的一个重要目标是转发流量。这一点在讨论在互联网主干网上运行的核心路由器时尤为重要。负责转发数据包和维护转发数据库的 Linux IP 堆栈层称为路由子系统。对于小型网络,FIB 的管理可以由系统管理员完成,因为大多数网络拓扑是静态的。当讨论核心路由器时,情况有点不同,因为拓扑是动态的,并且有大量不断变化的信息。在这种情况下,FIB 的管理通常由用户空间路由守护进程来完成,有时与特殊的硬件增强功能一起完成。这些用户空间守护进程通常维护自己的路由表,有时会与内核路由表交互。

让我们从基础开始:什么是路由?看一个非常简单的转发例子:你有两个以太局域网,LAN1 和 LAN2。在 LAN1 上有一个子网 192.168.1.0/24,在 LAN2 上有一个子网 192.168.2.0/24。这两个局域网之间有一台机器,将被称为“转发路由器”转发路由器中有两个以太网网卡(NIC)。连接到 LAN1 的网络接口是eth0,IP 地址为 192.168.1.200,连接到 LAN2 的网络接口是eth1,IP 地址为 192.168.2.200,如图图 5-1 所示。为了简单起见,我们假设转发路由器上没有运行防火墙守护程序。您开始从 LAN1 发送流量,目的地是 LAN2。根据称为路由表的数据结构,转发从 LAN1 发送到 LAN2(反之亦然)的输入数据包的过程称为路由。我将在本章和下一章讨论这个过程和路由表数据结构。

9781430261964_Fig05-01.jpg

图 5-1 。在两个局域网之间转发数据包

在图 5-1 中,从 LAN1 在eth0到达目的地为 LAN2 的数据包通过eth1作为输出设备转发。在此过程中,传入的数据包从内核网络堆栈的第 2 层(链路层)移动到转发路由器的第 3 层(网络层)。然而,与流量被指定到转发路由器机器(“流量到我”)的情况相反,不需要将分组移动到第 4 层(传输层),因为该流量不打算由任何第 4 层传输套接字处理。应该转发该流量。移动到第 4 层有性能成本,最好尽可能避免。该流量在第 3 层处理,根据转发路由器上配置的路由表,数据包在作为输出接口的eth1上转发(或被拒绝)。

图 5-2 显示了前面提到的内核处理的三个网络层。

9781430261964_Fig05-02.jpg

图 5-2 。由网络内核堆栈处理的三层

我在这里应该提到的另外两个术语是路由中常用的默认网关默认路由。当您在路由表中定义默认网关条目时,其它路由条目(如果有)未处理的每个数据包都必须转发给它,而不管该数据包 IP 报头中的目的地址。在无类域间路由(CIDR)表示法中,默认路由被指定为 0.0.0.0/0。举个简单的例子,您可以添加一台 IPv4 地址为 192.168.2.1 的机器作为默认网关,如下所示:

ip route add default via 192.168.2.1

或者,当使用route命令时,像这样:

route add default gateway 192.168.2.1

在本节中,您学习了什么是转发,并看到了一个简单的示例,说明了数据包如何在两个局域网之间转发。您还学习了什么是默认网关,什么是默认路由,以及如何添加它们。现在您已经知道了基本术语和转发是什么,让我们继续看一看路由子系统中的查找是如何执行的。

在路由子系统中执行查找

对于 Rx 路径和 Tx 路径中的每个分组,在路由子系统中进行查找。在 3.6 之前的内核中,Rx 路径和 Tx 路径中的每次查找都包括两个阶段:在路由缓存中查找,以及在缓存未命中的情况下,在路由表中查找(我将在本章末尾的“IPv4 路由缓存”一节中讨论路由缓存)。通过fib_lookup()方法进行查找。当fib_lookup()方法在路由子系统中找到合适的条目时,它构建一个由各种路由参数组成的fib_result对象,并返回 0。我将在本节和本章的其他部分讨论fib_result对象。这里是fib_lookup()的原型:

int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res)

flowi4对象由对 IPv4 路由查找过程很重要的字段组成,包括目的地址、源地址、服务类型(TOS)等等。事实上,flowi4对象定义了路由表中查找的关键字,应该在用fib_lookup()方法执行查找之前初始化。对于 IPv6,有一个名为flowi6的并行对象;两者都在include/net/flow.h中定义。fib_result对象构建在 IPv4 查找过程中。fib_lookup()方法首先搜索本地 FIB 表。如果查找失败,它将在主 FIB 表中执行查找(我将在下一节“FIB 表”中描述这两个表)。成功完成查找后,在 Rx 路径或 Tx 路径中,构建一个dst对象(在include/net/dst.h中定义的dst_entry结构的一个实例,目的缓存)。dst对象嵌入在一个名为rtable的结构中,您很快就会看到。实际上,rtable对象代表一个可以与 SKB 相关联的路由条目。dst_entry对象最重要的成员是两个名为inputoutput的回调。在路由查找过程中,根据路由查找结果,这些回调被分配为适当的处理程序。这两个回调只得到一个 SKB 作为参数:

struct dst_entry {
    ...
    int  (*input)(struct sk_buff *);
    int  (*output)(struct sk_buff *);
    ...
}

下面是rtable结构;如您所见,dst对象是这个结构中的第一个对象:

struct rtable {
    struct dst_entry  dst;

    int               rt_genid;
    unsigned int      rt_flags;
    __u16             rt_type;
    __u8              rt_is_input;
    __u8              rt_uses_gateway;

    int               rt_iif;

    /* Info on neighbour */
    __be32            rt_gateway;

    /* Miscellaneous cached information */
    u32               rt_pmtu;

    struct list_head  rt_uncached;
};
(include/net/route.h)

以下是对rtable结构成员的描述:

  • rt_flags:rtable对象标志;这里提到了一些重要的标志:

  • RTCF _ 广播:该位置位时,目的地址是一个广播地址。该标志在__mkroute_output()方法和ip_route_input_slow()方法中设置。

  • RTCF 多播:当该位置位时,目的地址是一个多播地址。该标志在ip_route_input_mc()方法和__mkroute_output()方法中设置。

  • RTCF _ 多尔直接:当设置时,应该发送一个 ICMPv4 重定向消息作为对传入数据包的响应。设置该标志需要满足几个条件,包括输入设备和输出设备相同,并且设置了相应的procfs send_redirects条目。还有更多条件,你将在本章后面看到。该标志在__mkroute_input()方法中设置。

  • RTCF _ 本地:该位置位时,目的地址是本地的。该标志有以下几种设置方法:ip_route_input_slow()__mkroute_output()ip_route_input_mc()__ip_route_output_key()。一些 RTCF_XXX 标志可以同时设置。例如,当设置了 RTCF 广播或 RTCF 多播时,可以设置 RTCF 本地。关于 RTCF_ XXX 旗帜的完整列表,请查阅include/uapi/linux/in_route.h。注意,其中有一些是未使用的。

  • rt_is_input:当这是输入路径时被设置为 1 的标志。

  • rt_uses_gateway:根据下式得到一个值:

  • 当下一跳是网关时,rt_uses_gateway为 1。

  • 当下一跳是直接路由时,rt_uses_gateway为 0。

  • rt_iif:呼入接口的ifindex。(注意在内核 3.6 中,rt_oif成员被从rtable结构中移除;它被设置为指定流键的oif,但实际上只在一个方法中使用过)。

  • rt_pmtu: The Path MTU (the smallest MTU along the route).

    请注意,在内核 3.6 中,添加了fib_compute_spec_dst()方法,它获取 SKB 作为参数。这个方法使得rtable结构中的rt_spec_dst成员变得不需要,结果rt_spec_dstrtable结构中被移除。在特殊情况下需要使用fib_compute_spec_dst()方法,例如在icmp_reply()方法中,当使用发送方的源地址作为回复目的地来回复发送方时。

对于目的地为本地主机的输入单播包,dst对象的input回调被设置为ip_local_deliver(),对于应该被转发的输入单播包,这个input回调被设置为ip_forward()。对于在本地机器上生成并发送出去的数据包,output回调被设置为ip_output()。对于一个组播包,input回调可以设置为ip_mr_input()(在本章没有详细描述的一些条件下)。有些情况下input回调被设置为ip_error(),你将在本章后面的禁止规则示例中看到。让我们看看fib_result对象:

struct fib_result {
         unsigned char    prefixlen;
         unsigned char    nh_sel;
         unsigned char    type;
         unsigned char    scope;
         u32              tclassid;
         struct fib_info  *fi;
         struct fib_table *table;
         struct list_head *fa_head;
};
(include/net/ip_fib.h)
  • prefixlen:前缀长度,代表网络掩码。其值在 0 到 32 的范围内。使用默认路由时为 0。例如,当通过ip route add 192.168.2.0/24 dev eth0添加路由条目时,根据添加条目时指定的网络掩码,prefixlen是 24。在check_leaf()方法(net/ipv4/fib_trie.c)中设置prefixlen
  • nh_sel:下一跳号。当只使用一个下一跳时,它是 0。使用多路径路由时,可以有多个下一跳。nexthop 对象存储在路由条目的数组中(在fib_info对象中),这将在下一节中讨论。
  • type:fib_result对象的type是最重要的字段,因为它实际上决定了如何处理数据包:是否将它转发到不同的机器,本地传送,无声地丢弃它,用 ICMPv4 消息回复丢弃它,等等。fib_result对象的类型是根据数据包内容(尤其是目的地址)和管理员设置的路由规则、路由守护程序或重定向消息来确定的。在本章后面和下一章中,你将看到fib_result对象的type是如何在查找过程中确定的。两种最常见的类型的fib_result对象是 RTN_UNICAST 类型和 RTN_LOCAL 类型,前者在数据包通过网关或直接路由转发时设置,后者在数据包发往本地主机时设置。您将在本书中遇到的其他类型是 RTN_BROADCAST 类型,用于应该作为广播在本地接受的数据包 RTN _ MULTICAST 类型,用于多播路由 RTN _ UNREACHABLE 类型,用于触发发送回 icmp v4“Destination UNREACHABLE”消息的数据包,等等。总共有 12 种路线类型。有关所有可用路线类型的完整列表,请参见include/uapi/linux/rtnetlink.h
  • fi:指向fib_info对象的指针,表示一个路由条目。fib_info对象保存了对下一跳(fib_nh)的引用。我将在本章后面的“FIB 信息”一节中讨论 FIB 信息结构。
  • table:指向在其上进行查找的 FIB 表的指针。在check_leaf()方法(net/ipv4/fib_trie.c)中设置。
  • fa_head:指向fib_alias列表的指针(与该路线相关联的fib_alias对象的列表);路由条目的优化是在使用fib_alias对象时完成的,这避免了为每个路由条目创建一个单独的fib_info对象,尽管事实上还有其他fib_info对象非常相似。所有 FIB 别名按fa_tos降序和fib_priority(公制)升序排序。fa_tos为 0 的别名是最后一个,可以匹配任何 TOS。我将在本章后面的“FIB 别名”一节中讨论fib_alias结构。

在本节中,您学习了如何在路由子系统中执行查找。您还了解了与路由查找过程相关的重要数据结构,如fib_resultrtable。下一节讨论 FIB 表是如何组织的。

纤维表

路由子系统的主要数据结构是路由表,用fib_table结构表示。路由表可以以某种简化的方式描述为条目表,其中每个条目确定应该为去往子网(或特定 IPv4 目的地地址)的流量选择哪个下一跳。当然,这个条目还有其他参数,将在本章后面讨论。每个路由条目包含一个fib_info对象(include/net/ip_fib.h,它存储最重要的路由条目参数(但不是全部,您将在本章后面看到)。fib_info对象由fib_create_info()方法(net/ipv4/fib_semantics.c)创建,并存储在一个名为fib_info_hash的散列表中。当路由使用prefsrc时,fib_info对象也被添加到一个名为fib_info_laddrhash的散列表中。

有一个名为fib_info_cntfib_info对象的全局计数器,它在通过fib_create_info()方法创建一个fib_info对象时递增,在通过free_fib_info()方法释放一个fib_info对象时递减。当哈希表增长超过某个阈值时,它会动态调整大小。在fib_info_hash散列表中的查找由fib_find_info()方法完成(当没有找到条目时返回 NULL)。序列化对fib_info成员的访问是由名为fib_info_lock的自旋锁完成的。下面是fib_table的结构:

struct fib_table {
        struct hlist_node       tb_hlist;
        u32                     tb_id;
        int                     tb_default;
        int                     tb_num_default;
        unsigned long           tb_data[0];
};
(include/net/ip_fib.h)
  • tb_id:表格标识符。对于主表,tb_id是 254 (RT_TABLE_MAIN),对于本地表,tb_id是 255 (RT_TABLE_LOCAL)。我很快就会谈到主表和本地表——现在,只需注意在没有策略路由的情况下工作时,只有这两个 FIB 表,即主表和本地表,是在 boot 中创建的。
  • tb_num_default:表中默认路线的数量。创建表格的fib_trie_table()方法将tb_num_default初始化为 0。通过fib_table_insert()方法,添加默认路由会使tb_num_default增加 1。通过fib_table_delete()方法,删除默认路由会使tb_num_default递减 1。
  • tb_data[0]:路由条目(trie)对象的占位符。

本节讲述了 FIB 表是如何实现的。接下来,您将了解 FIB 信息,它表示单个路由条目。

纤维信息

路由条目由一个fib_info结构表示。它由重要的路由条目参数组成,例如传出网络设备(fib_dev)、优先级(fib_priority)、该路由的路由协议标识符(fib_protocol)等等。我们来看看fib_info的结构:

struct fib_info {
    struct hlist_node    fib_hash;
    struct hlist_node    fib_lhash;
    struct net        *fib_net;
    int               fib_treeref;
    atomic_t          fib_clntref;
    unsigned int      fib_flags;
    unsigned char     fib_dead;
    unsigned char     fib_protocol;
    unsigned char     fib_scope;
    unsigned char     fib_type;
    __be32            fib_prefsrc;
    u32               fib_priority;
    u32               *fib_metrics;
#define fib_mtu fib_metrics[RTAX_MTU-1]
#define fib_window fib_metrics[RTAX_WINDOW-1]
#define fib_rtt fib_metrics[RTAX_RTT-1]
#define fib_advmss fib_metrics[RTAX_ADVMSS-1]
    int               fib_nhs;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
    int               fib_power;
#endif
    struct rcu_head   rcu;
    struct fib_nh     fib_nh[0];
#define fib_dev       fib_nh[0].nh_dev
};
(include/net/ip_fib.h)
  • fib_net:fib_info对象所属的网络名称空间。

  • fib_treeref:一个引用计数器,表示保存对这个fib_info对象的引用的fib_alias对象的数量。该参考计数器在fib_create_info()方法中递增,在fib_release_info()方法中递减。两种方法都在net/ipv4/fib_semantics.c

  • fib_clntref:参考计数器,通过fib_create_info()方法(net/ipv4/fib_semantics.c)递增,通过fib_info_put()方法(include/net/ip_fib.h)递减。如果在fib_info_put()方法中将它减 1 后,它达到零,那么相关联的fib_info对象被free_fib_info()方法释放。

  • fib_dead:表示是否允许用free_fib_info()方法释放fib_info对象的标志;在调用free_fib_info()方法之前,必须将fib_dead设置为 1。如果没有设置fib_dead标志(其值为 0),那么它被认为是活动的,并且试图用free_fib_info()方法释放它将会失败。

  • fib_protocol:该路由的路由协议标识。当在没有指定路由协议 ID 的情况下从用户空间添加路由规则时,fib_protocol被指定为 RTPROT_BOOT。管理员可以添加带有“proto static”修饰符的路由,这表示该路由是由管理员添加的;这可以这样做,例如,像这样:ip route add proto static 192.168.5.3 via 192.168.2.1。可以给fib_protocol分配这些标志中的一个:

  • RTPROT_UNSPEC:一个错误值。

  • RTPROT_REDIRECT:设置时,路由条目是由于接收到 ICMP 重定向消息而创建的。RTPROT_REDIRECT 协议标识符仅在 IPv6 中使用。

  • RTPROT_KERNEL:该位置位时,路由条目由内核创建(例如,在创建本地 IPv4 路由表时,简要说明)。

  • RTPROT_BOOT:设置时,管理员添加了一个路由,但没有指定“proto static”修饰符。

  • RTPROT_STATIC:系统管理员安装的路由。

  • RTPROT_RA:不要误读这个——这个协议标识符不是用于路由器告警的;它用于 RDISC/ND 路由器广告,并且仅由 IPv6 子系统在内核中使用;参见:net/ipv6/route.c。我在第八章中讨论了它。

路由条目也可以由用户空间路由守护进程添加,比如 ZEBRA、XORP、MROUTED 等等。然后,将从协议标识符列表中为其分配相应的值(参见include/uapi/linux/rtnetlink.h中的 RTPROT_XXX 定义)。例如,对于 XORP 守护进程,它将是 RTPROT_XORP。注意,这些标志(如 RTPROT_KERNEL 或 RTPROT_STATIC)也被 IPv6 用于并行字段(rt6_info结构中的rt6i_protocol字段);rt6_info对象是与rtable对象平行的 IPv6。

  • fib_scope:目的地址的范围。简而言之,作用域被分配给地址和路由。Scope 表示主机与其他节点之间的距离。ip address show命令显示主机上所有已配置 IP 地址的范围。ip route show命令显示主表所有路由表项的范围。范围可以是下列之一:

  • 主机(RT_SCOPE_HOST):该节点无法与其他网络节点通信。环回地址的作用域是主机。

  • global (RT_SCOPE_UNIVERSE):地址可以在任何地方使用。这是最常见的情况。

  • link (RT_SCOPE_LINK):该地址只能从直接连接的主机访问。

  • site (RT_SCOPE_SITE):这个只在 IPv6 中使用(我在第八章中讨论)。

  • nowhere (RT_SCOPE_NOWHERE):目的地不存在。

当管理员在未指定范围的情况下添加路由时,会根据以下规则为fib_scope字段分配一个值:

  • 全局范围(RT_SCOPE_UNIVERSE):用于所有网关单播路由。

  • scope link (RT_SCOPE_LINK):用于直接单播和广播路由。

  • scope host (RT_SCOPE_HOST):用于本地路由。

  • fib_type:路线的类型。fib_type字段被添加到了fib_info结构中,作为一个键来确保fib_info对象的类型是不同的。在内核 3.7 中,fib_type字段被添加到了fib_info struct中。最初,这个类型只存储在 FIB alias 对象(fib_alias)的fa_type字段中。您可以根据指定的类别添加规则来阻止流量,例如通过:ip route add prohibit 192.168.1.17 from 192.168.2.103.

  • 生成的fib_info对象的fib_type为 RTN_PROHIBIT。

  • 从 192.168.2.103 向 192.168.1.17 发送流量会导致 ICMPv4 消息“数据包过滤”(ICMP_PKT_FILTERED)。

  • fib_prefsrc:有时候你想给查找键提供一个特定的源地址。这是通过设置fib_prefsrc.来完成的

  • fib_priority:该路径的优先级默认为 0,优先级最高。优先级值越高,优先级越低。例如,优先级 3 低于优先级 0,优先级 0 是最高优先级。例如,您可以通过以下方式之一使用ip命令对其进行配置:

  • ip route add 192.168.1.10 via 192.168.2.1 metric 5

  • ip route add 192.168.1.10 via 192.168.2.1 priority 5

  • ip route add 192.168.1.10 via 192.168.2.1 preference 5

这三个命令中的每一个都将fib_priority设置为 5;他们之间没有任何区别。此外,ip route命令的metric参数与fib_info结构的fib_metrics字段没有任何关系。

  • fib_mtu, fib_window, fib_rtt, and fib_advmss simply give more convenient names to commonly used elements of the fib_metrics array.

    fib_metrics是由各种度量组成的 15 (RTAX_MAX)个元素的数组。它在net/core/dst.c中被初始化为dst_default_metrics。很多指标都与 TCP 协议有关,比如初始拥塞窗口(initcwnd)指标。本章末尾的表 5-1 显示了所有可用的度量,并显示每个度量是否是 TCP 相关的度量。

    从用户空间,可以这样设置 TCPv4 initcwnd指标,例如:

    ip route add 192.168.1.0/24 initcwnd 35
    

    有些指标不是特定于 TCP 的——例如,mtu指标,可以从用户空间像这样设置:

    ip route add 192.168.1.0/24 mtu 800
    

    或者像这样:

    ip route add 192.168.1.0/24 mtu lock 800
    

    这两个命令的区别在于,当指定修饰符lock时,不会尝试任何路径 MTU 发现。当没有指定修饰符lock时,由于路径 MTU 发现,MTU 可能被内核更新。有关如何实现的更多信息,请参见net/ipv4/route.c中的__ip_rt_update_pmtu()方法:

    static void __ip_rt_update_pmtu(struct rtable *rt, struct flowi4 *fl4, u32 mtu)
    {
    

    指定mtu lock修饰符时避免路径 MTU 更新是通过调用dst_metric_locked()方法实现的:

    . . .
    if (dst_metric_locked(dst, RTAX_MTU))
          return;
    . . .
    }
    
    
  • fib_nhs:下一跳的次数。当未设置多路径路由(CONFIG_IP_ROUTE_MULTIPATH)时,它不能大于 1。多路径路由功能为一条路由设置多条备选路径,可能会为这些路径分配不同的权重。这个特性提供了一些好处,比如容错、增加带宽或提高安全性(我将在第六章中讨论)。

  • fib_dev:将数据包传输到下一跳的网络设备。

  • fib_nh[0]:fib_nh[0]成员代表下一跳。使用多路径路由时,您可以在一个路由中定义多个下一跳,在这种情况下,有一个下一跳数组。定义两个 nexthop 节点可以这样做,例如:ip route add default scope global nexthop dev eth0 nexthop dev eth1

如前所述,当fib_type为 RTN_PROHIBIT 时,发送一条“包过滤”(ICMP_PKT_FILTERED)的 ICMPv4 消息。是如何实现的?名为fib_props的数组由 12 (RTN_MAX)个元素组成(在net/ipv4/fib_semantics.c中定义)。这个数组的索引是路由类型。可用的路由类型,如 RTN_PROHIBIT 或 RTN_UNICAST,可在include/uapi/linux/rtnetlink.h中找到。数组中的每个元素都是struct fib_prop的一个实例;fib_prop结构是一个非常简单的结构:

struct fib_prop {
          int     error;
          u8      scope;
  };
(net/ipv4/fib_lookup.h)

对于每个路线类型,对应的fib_prop对象包含该路线的errorscope。例如,对于 RTN_UNICAST 路由类型(网关或直接路由),这是一种非常常见的路由,错误值为 0,表示没有错误,范围为 RT_SCOPE_UNIVERSE。对于 RTN_PROHIBIT 路由类型(系统管理员为阻止流量而配置的规则),错误为–EACCES,范围为 RT_SCOPE_UNIVERSE:

const struct fib_prop fib_props[RTN_MAX + 1] = {
 . . .
         [RTN_PROHIBIT] = {
                 .error  = -EACCES,
                 .scope  = RT_SCOPE_UNIVERSE,
         },

. . .

本章末尾的表 5-2 显示了所有可用的路由类型、错误代码和范围。

当您通过ip route add prohibit 192.168.1.17 from 192.168.2.103配置前面提到的规则时,当数据包从 192.168.2.103 发送到 192.168.1.17 时,会发生以下情况:在 Rx 路径中执行路由表查找。当找到相应的条目时,实际上是 FIB TRIE 中的一个叶子,调用check_leaf()方法。该方法以数据包的路由类型作为索引来访问fib_props数组(fa->fa_type):

static int check_leaf(struct fib_table *tb, struct trie *t, struct leaf *l,
                      t_key key,  const struct flowi4 *flp,
                      struct fib_result *res, int fib_flags)
{
    . . .
       fib_alias_accessed(fa);
       err = fib_props[fa->fa_type].error;
       if (err) {
                . . .
                return err;
                 }
    . . .

最后,在 IPv4 路由子系统中启动查找的fib_lookup()方法返回一个错误–EACCES(在我们的例子中)。它从check_leaf()通过fib_table_lookup()一路传播回来,直到它返回到触发这个链的方法,即fib_lookup()方法。当fib_lookup()方法在接收路径中返回一个错误时,它由ip_error()方法处理。根据错误,采取行动。在–EACCES 的情况下,会发回一个代码为 Packet Filtered(ICMP _ PKT _ Filtered)的目的地不可达的 ICMPv4,并丢弃该数据包。

本节介绍了 FIB 信息,它代表一个路由条目。下一节讨论 IPv4 路由子系统中的缓存(不要与 IPv4 路由缓存混淆,后者已从网络堆栈中删除,将在本章末尾的“IPv4 路由缓存”一节中讨论)。

贮藏

缓存路由查找的结果是一种优化技术,可以提高路由子系统的性能。路由查找的结果通常缓存在 nexthop ( fib_nh)对象中;当数据包不是单播数据包或使用了realms(数据包itag不为 0)时,结果不会缓存在下一跳中。原因是,如果所有类型的数据包都被缓存,那么不同类型的路由可以使用相同的下一跳,这是应该避免的。有一些小的例外,我不在本章讨论。Rx 和 Tx 路径中的缓存执行如下:

  • 在 Rx 路径中,缓存 nexthop ( fib_nh)对象中的fib_result对象是通过设置 nexthop ( fib_nh)对象的nh_rth_input字段来完成的。
  • 在 Tx 路径中,缓存 nexthop ( fib_nh)对象中的fib_result对象是通过设置 nexthop ( fib_nh)对象的nh_pcpu_rth_output字段来完成的。
  • nh_rth_inputnh_pcpu_rth_output都是rtable结构的实例。
  • 缓存fib_result是通过 Rx 和 Tx 路径中的rt_cache_route()方法完成的(net/ipv4/route.c)。
  • 路径 MTU 和 ICMPv4 重定向的缓存是通过 FIB 异常完成的。

为了提高性能,nh_pcpu_rth_output是每个 CPU 的变量,这意味着每个 CPU 都有一个输出dst条目的副本。几乎总是使用缓存。少数例外情况是当发送了 ICMPv4 重定向消息,或者设置了itag ( tclassid,或者没有足够的内存。

在本节中,您已经学习了如何使用 nexthop 对象进行缓存。下一节讨论代表下一跳的fib_nh结构,以及 FIB 下一跳异常。

下一跳(fib_nh)

fib_nh结构表示下一跳。它包括诸如传出下一跳网络设备(nh_dev)、传出下一跳接口索引(nh_oif)、范围(nh_scope等信息。我们来看看:

struct fib_nh {
    struct net_device       *nh_dev;
    struct hlist_node       nh_hash;
    struct fib_info         *nh_parent;
    unsigned int            nh_flags;
    unsigned char           nh_scope;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
    int                     nh_weight;
    int                     nh_power;
#endif
#ifdef CONFIG_IP_ROUTE_CLASSID
    __u32                   nh_tclassid;
#endif
    int                     nh_oif;
    __be32                  nh_gw;
    __be32                  nh_saddr;
    int                     nh_saddr_genid;
    struct rtable __rcu * __percpu *nh_pcpu_rth_output;
    struct rtable __rcu     *nh_rth_input;
    struct fnhe_hash_bucket *nh_exceptions;
};
(include/net/ip_fib.h)

nh_dev字段表示网络设备(net_device对象),去往下一跳的流量将在该网络设备上传输。当与一个或多个路由相关联的网络设备被禁用时,会发送 NETDEV_DOWN 通知。处理这个事件的 FIB 回调是fib_netdev_event()方法;它是fib_netdev_notifier通知对象的回调,通过调用register_netdevice_notifier()方法在ip_fib_init()方法中注册(通知链在第十四章中讨论)。fib_netdev_event()方法在收到 NETDEV_DOWN 通知时调用fib_disable_ip()方法。在fib_disable_ip()方法中,执行以下步骤:

  • 首先调用fib_sync_down_dev()方法(net/ipv4/fib_semantics.c)。在fib_sync_down_dev()方法中,设置下一跳标志(nh_flags)的 RTNH_F_DEAD 标志,并且设置 FIB 信息标志(fib_flags)。
  • 通过fib_flush()方法刷新路径。
  • 调用rt_cache_flush()方法和arp_ifdown()方法。arp_ifdown()方法不在任何通知链上。

FIB 下一跳异常

在内核 3.6 中添加了 FIB nexthop 异常,以处理不是由于用户空间操作而是由于 ICMPv4 重定向消息或路径 MTU 发现而导致路由条目更改的情况。哈希键是目的地址。FIB 下一跳异常基于 2048 条目哈希表;回收(释放散列条目)从链深度 5 开始。每个 nexthop 对象(fib_nh)都有一个 FIB nexthop 异常哈希表,nh_exceptions(fnhe_hash_bucket结构的一个实例)。我们来看看fib_nh_exception的结构:

struct fib_nh_exception {
    struct fib_nh_exception __rcu    *fnhe_next;
    __be32                           fnhe_daddr;
    u32                              fnhe_pmtu;
    __be32                           fnhe_gw;
    unsigned long                    fnhe_expires;
    struct rtable __rcu              *fnhe_rth;
    unsigned long                    fnhe_stamp;
};
(include/net/ip_fib.h)

通过update_or_create_fnhe()方法(net/ipv4/route.c)创建fib_nh_exception对象。FIB 下一跳异常在哪里生成?第一种情况是在__ip_do_redirect()方法中接收到 ICMPv4 重定向消息(“重定向到主机”)时。“重定向到主机”消息包括一个新网关。fib_nh_exceptionfnhe_gw字段在创建 FIB nexthop 异常对象时被设置为新网关(在update_or_create_fnhe()方法中):

static void __ip_do_redirect(struct rtable *rt, struct sk_buff *skb, struct flowi4 *fl4,
                 bool kill_route)
{
  ...
  __be32 new_gw = icmp_hdr(skb)->un.gateway;
  ...
  update_or_create_fnhe(nh, fl4->daddr, new_gw, 0, 0);
  ...

}

生成 FIB nexthop 异常的第二种情况是在路径 MTU 已经改变时,在__ip_rt_update_pmtu()方法中。在这种情况下,当创建 FIB nexthop 异常对象(在update_or_create_fnhe()方法中)时,fib_nh_exception对象的fnhe_pmtu字段被设置为新的 MTU。如果 PMTU 值在过去 10 分钟内没有更新,则该值过期(ip_rt_mtu_expires)。通过ipv4_mtu()方法(一个dst->ops->mtu处理程序)在每次dst_mtu()调用时检查这个时间段。默认为 600 秒的ip_rt_mtu_expires可通过procfs条目/proc/sys/net/ipv4/route/mtu_expires进行配置:

static void __ip_rt_update_pmtu(struct rtable *rt, struct flowi4 *fl4, u32 mtu)
{
    . . .
    if (fib_lookup(dev_net(dst->dev), fl4, &res) == 0) {
        struct fib_nh *nh = &FIB_RES_NH(res);

        update_or_create_fnhe(nh, fl4->daddr, 0, mtu,
                      jiffies + ip_rt_mtu_expires);
    }
    . . .
}

image 注意 FIB nexthop 异常用于 Tx 路径。从 Linux 3.11 开始,它们也用于 Rx 路径。结果,没有了fnhe_rth,有了fnhe_rth_inputfnhe_rth_output

从内核 2.4 开始,支持策略路由。使用策略路由,数据包的路由不仅取决于目的地址,还取决于其他几个因素,如源地址或 TOS。系统管理员最多可以添加 255 个路由表。

策略路由

在没有策略路由的情况下工作时(未设置 CONFIG_IP_MULTIPLE_TABLES),会创建两个路由表:本地表和主表。主表 id 是 254 (RT_TABLE_MAIN),本地表 id 是 255 (RT_TABLE_LOCAL)。本地表包含本地地址的路由条目。这些路由条目只能由内核添加到本地表中。向主表(RT_TABLE_MAIN)添加路由条目由系统管理员完成(例如,通过ip route add)。这些表格是由net/ipv4/fib_frontend.cfib4_rules_init()方法创建的。在 2.6.25 之前的内核中,这些表被称为ip_fib_local_tableip_fib_main_table,但是它们被删除了,以便使用带有适当参数的fib_get_table()方法来统一访问路由表。通过统一访问,我的意思是,当策略路由支持启用和禁用时,对路由表的访问都是以相同的方式完成的,使用fib_get_table()方法。fib_get_table()方法只获得两个参数:网络名称空间和表 id。请注意,net/ipv4/fib_rules.c中的策略路由案例有一个不同的同名方法fib4_rules_init(),该方法在使用策略路由支持时被调用。当使用策略路由支持(设置了 CONFIG_IP_MULTIPLE_TABLES)时,有三个初始表(本地、主和默认),最多可以有 255 个路由表。我将在第六章的中详细讨论策略路由。访问主路由表的方法如下:

  • 通过系统管理员命令(使用ip routeroute):

  • 通过ip route add添加路由是通过从用户空间发送 RTM_NEWROUTE 消息实现的,该消息由inet_rtm_newroute()方法处理。请注意,路由不一定总是允许流量的规则。您还可以添加一个阻止流量的路由,例如通过ip route add prohibit 192.168.1.17 from 192.168.2.103.应用此规则的结果是,从 192.168.2.103 发送到 192.168.1.17 的所有数据包都将被阻止。

  • ip route del删除路由是通过从用户空间发送 RTM_DELROUTE 消息实现的,该消息由inet_rtm_delroute()方法处理。

  • ip route show转储路由表是通过从用户空间发送 RTM_GETROUTE 消息实现的,该消息由inet_dump_fib()方法处理。

注意ip route show显示主表。为了显示本地表,您应该运行ip route show table local

  • 通过route add添加路由是通过发送 SIOCADDRT IOCTL 实现的,它由ip_rt_ioctl()方法(net/ipv4/fib_frontend.c)处理。
  • route del删除路由是通过发送 SIOCDELRT IOCTL 实现的,它由ip_rt_ioctl()方法(net/ipv4/fib_frontend.c)处理。
  • 由用户空间路由守护进程执行路由协议,如 BGP(边界网关协议)、EGP(外部网关协议)、OSPF(开放最短路径优先)等。这些路由守护程序运行在核心路由器上,核心路由器在互联网主干上运行,可以处理成千上万的路由。

这里我应该提到,由于 ICMPv4 重定向消息或路径 MTU 发现而改变的路由缓存在下一跳异常表中,稍后将讨论。下一节描述 FIB 别名,它有助于路由优化。

光纤别名(fib_alias)

有时会创建几个指向同一目的地址或同一子网的路由条目。这些路由条目的不同之处仅在于它们的 TOS 值。不是为每个这样的路线创建一个fib_info,而是创建一个fib_alias对象。一个fib_alias更小,减少了内存消耗。下面是一个创建 3 个fib_alias对象的简单例子:

ip route add 192.168.1.10 via 192.168.2.1 tos 0x2
ip route add 192.168.1.10 via 192.168.2.1 tos 0x4
ip route add 192.168.1.10 via 192.168.2.1 tos 0x6

让我们来看看fib_alias的结构定义:

struct fib_alias {
        struct list_head        fa_list;
        struct fib_info         *fa_info;
        u8                      fa_tos;
        u8                      fa_type;
        u8                      fa_state;
        struct rcu_head         rcu;
};
(net/ipv4/fib_lookup.h)

注意在fib_alias结构(fa_scope)中也有一个作用域,但是在内核 2.6.39 中它被移到了fib_info结构中。

fib_alias对象存储到相同子网的路由,但参数不同。你可以拥有一个被许多fib_alias对象共享的fib_info对象。在这种情况下,所有这些fib_alias对象中的fa_info指针将指向同一个共享的fib_info对象。在图 5-3 中,你可以看到一个fib_info对象被三个fib_alias对象共享,每个对象有不同的fa_tos。注意,fib_info对象的参考计数器值是 3 ( fib_treeref)。

9781430261964_Fig05-03.jpg

图 5-3 。由三个 fib_alias 对象共享的 fib_info。每个 fib_alias 对象都有不同的 fa_tos 值

让我们来看看当您试图添加一个之前已经添加了一个fib_node的键时会发生什么(就像前面的例子中的三个 TOS 值 0x2、0x4 和 0x 6);假设您已经创建了 TOS 为 0x2 的第一个规则,现在您创建了 TOS 为 0x4 的第二个规则。

fib_alias对象由fib_table_insert()方法创建,该方法处理添加路由条目:

int fib_table_insert(struct fib_table *tb, struct fib_config *cfg)
 {
         struct trie *t = (struct trie *) tb->tb_data;
         struct fib_alias *fa, *new_fa;
         struct list_head *fa_head = NULL;
         struct fib_info *fi;
      . . .

首先,创建一个fib_info对象。注意,在fib_create_info()方法中,在分配和创建了一个fib_info对象之后,通过调用fib_find_info()方法执行查找来检查是否已经存在一个类似的对象。如果这样的对象存在,它将被释放,并且被发现的对象的引用计数器(您很快就会看到代码片段中的ofi)将增加 1:

fi = fib_create_info(cfg);

我们来看看前面提到的fib_create_info()方法中的代码片段;为了创建第二个 TOS 规则,第一个规则的fib_info对象和第二个规则的fib_info对象是相同的。你应该记得 TOS 字段存在于fib_alias对象中,而不存在于fib_info对象中:

struct fib_info *fib_create_info(struct fib_config *cfg)
{
    struct fib_info *fi = NULL;
    struct fib_info *ofi;
    . . .
    fi = kzalloc(sizeof(*fi)+nhs*sizeof(struct fib_nh), GFP_KERNEL);
    if (fi == NULL)
            goto failure;
    . . .
link_it:
        ofi = fib_find_info(fi);

如果发现类似的对象,释放fib_info对象并增加fib_treeref引用计数:

        if (ofi) {
                fi->fib_dead = 1;
                free_fib_info(fi);
                ofi->fib_treeref++;
                return ofi;
        }
    . . .
}

现在执行一个检查来找出是否有一个fib_info对象的别名;在这种情况下,将没有别名,因为第二个规则的 TOS 不同于第一个规则的 TOS:

     l = fib_find_node(t, key);
     fa = NULL;

     if (l) {
             fa_head = get_fa_head(l, plen);
             fa = fib_find_alias(fa_head, tos, fi->fib_priority);
     }

if (fa && fa->fa_tos == tos &&
    fa->fa_info->fib_priority == fi->fib_priority) {
    . . .
       }

现在一个fib_alias被创建,它的fa_info指针被指定指向被创建的第一个规则的fib_info:

new_fa = kmem_cache_alloc(fn_alias_kmem, GFP_KERNEL);
if (new_fa == NULL)
    goto out;

new_fa->fa_info = fi;
    . . .

现在我已经介绍了 FIB 别名,您已经准备好查看 ICMPv4 重定向消息,该消息是在存在次优路由时发送的。

ICMPv4 重定向消息

有时路由条目不是最佳的。在这种情况下,会发送一条 ICMPv4 重定向消息。次优条目的主要标准是输入设备和输出设备相同。但是,正如您将在本节中看到的,还需要满足更多的条件才能发送一个 ICMPv4 重定向消息。ICMPv4 重定向消息有四个代码:

  • ICMP_REDIR_NET:重定向网络
  • ICMP_REDIR_HOST:重定向主机
  • ICMP_REDIR_NETTOS:为 TOS 重定向网络
  • ICMP_REDIR_HOSTTOS:为 TOS 重定向主机

图 5-4 显示了一个次优路线的设置。此设置中有三台机器,都位于同一子网(192.168.2.0/24)中,并且都通过网关(192.168.2.1)连接。AMD 服务器(192.168.2.200)增加了 Windows 服务器(192.168.2.10)作为ip route add 192.168.2.7 via 192.168.2.10访问 192.168.2.7(笔记本电脑)的网关。例如,AMD 服务器通过ping 192.168.2.7向笔记本电脑发送流量。因为默认网关是192.168.2.10,流量被发送到192.168.2.10。Windows 服务器检测到这是一个次优路由,因为 AMD 服务器可以直接发送到 192.168.2.7,并向 AMD 服务器发回一个带有 ICMP_REDIR_HOST 代码的 ICMPv4 重定向消息。

9781430261964_Fig05-04.jpg

图 5-4 。重定向到主机(ICMP_REDIR_HOST),一个简单的设置

现在您对重定向有了更好的理解,让我们看看 ICMPv4 消息是如何生成的。

生成 ICMPv4 重定向消息

当存在一些次优路由时,发送 ICMPv4 重定向消息。次优路由最显著的条件是输入设备和输出设备相同,但还需要满足更多条件。生成 ICMPv4 重定向消息分两个阶段完成:

  • __mkroute_input()方法 : 中,如果需要,这里会设置 RTCF _ 多尔直接标志。

  • In the ip_forward() method: Here the ICMPv4 Redirect message is actually sent by calling the ip_rt_send_redirect() method.

    static int __mkroute_input(struct sk_buff *skb,
                       const struct fib_result *res,
                       struct in_device *in_dev,
                       __be32 daddr, __be32 saddr, u32 tos)
    {
        struct rtable *rth;
        int err;
        struct in_device *out_dev;
        unsigned int flags = 0;
        bool do_cache;
    

    应满足以下所有条件,以便设置 RTCF _ 多尔直接标志:

  • 输入设备和输出设备是相同的。

  • 设置procfs条目/proc/sys/net/ipv4/conf/<deviceName>/send_redirects

  • 该传出设备是共享媒体,或者源地址(saddr)和下一跳网关地址(nh_gw)在同一个子网:

    if (out_dev == in_dev && err && IN_DEV_TX_REDIRECTS(out_dev) &&
      (IN_DEV_SHARED_MEDIA(out_dev) ||
       inet_addr_onlink(out_dev, saddr, FIB_RES_GW(*res)))) {
    
      flags |= RTCF_DOREDIRECT;
      do_cache = false;
    }
      . . .
    

通过以下方式设置rtable对象标志:

    rth->rt_flags = flags;
    . . .

}

发送 ICMPv4 重定向消息是在第二阶段通过ip_forward()方法完成的:

int ip_forward(struct sk_buff *skb)
{
    struct iphdr          *iph;    /* Our header */
    struct rtable         *rt;     /* Route we use */
    struct ip_options     *opt     = &(IPCB(skb)->opt);

接下来,执行检查以查看 RTCF _ 多尔直接标志是否被设置,严格路由的 IP 选项是否不存在(参见第四章),以及它是否不是 IPsec 分组。(对于 IPsec 隧道,隧道化分组的输入设备可以与解封装的分组输出设备相同;参见http://lists.openwall.net/netdev/2007/08/24/29):

if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
    ip_rt_send_redirect(skb);

ip_rt_send_redirect()方法中,实际发送 ICMPv4 重定向消息。第三个参数是建议的新网关的 IP 地址,在本例中为 192.168.2.7(笔记本电脑的地址):

void ip_rt_send_redirect(struct sk_buff *skb)
  {
      . . .
      icmp_send(skb, ICMP_REDIRECT, ICMP_REDIR_HOST,
            rt_nexthop(rt, ip_hdr(skb)->daddr))
      . . .
  }
(net/ipv4/route.c)

接收 ICMPv4 重定向消息

对于要处理的 ICMPv4 重定向消息,它应该通过一些健全性检查。通过__ip_do_redirect()方法 : 处理 ICMPv4 重定向消息

static void __ip_do_redirect(struct rtable *rt, struct sk_buff *skb, struct flowi4
    *fl4,bool kill_route)
{
    __be32 new_gw = icmp_hdr(skb)->un.gateway;
    __be32 old_gw = ip_hdr(skb)->saddr;
    struct net_device *dev = skb->dev;
    struct in_device *in_dev;
    struct fib_result res;
    struct neighbour *n;
    struct net *net;
      . . .

执行各种检查,例如网络设备被设置为接受重定向。如有必要,重定向会被拒绝:

if (rt->rt_gateway != old_gw)
    return;

in_dev = __in_dev_get_rcu(dev);
if (!in_dev)
    return;

net = dev_net(dev);
if (new_gw == old_gw || !IN_DEV_RX_REDIRECTS(in_dev) ||
    ipv4_is_multicast(new_gw) || ipv4_is_lbcast(new_gw) ||
    ipv4_is_zeronet(new_gw))
    goto reject_redirect;

if (!IN_DEV_SHARED_MEDIA(in_dev)) {
    if (!inet_addr_onlink(in_dev, new_gw, old_gw))
        goto reject_redirect;
    if (IN_DEV_SEC_REDIRECTS(in_dev) && ip_fib_check_default(new_gw, dev))
        goto reject_redirect;
} else {
    if (inet_addr_type(net, new_gw) != RTN_UNICAST)
        goto reject_redirect;
}

在相邻子系统中执行查找;查找的关键是建议网关的地址new_gw,它是在本方法开始时从 ICMPv4 消息中提取的:

n = ipv4_neigh_lookup(&rt->dst, NULL, &new_gw);
if (n) {
    if (!(n->nud_state & NUD_VALID)) {
        neigh_event_send(n, NULL);
    } else {
           if (fib_lookup(net, fl4, &res) == 0) {
              struct fib_nh *nh = &FIB_RES_NH(res);

创建/更新 FIB 下一跳异常,指定建议网关的 IP 地址(new_gw):

                update_or_create_fnhe(nh, fl4->daddr, new_gw,
                              0, 0);
            }
            if (kill_route)
                rt->dst.obsolete = DST_OBSOLETE_KILL;
            call_netevent_notifiers(NETEVENT_NEIGH_UPDATE, n);
        }
        neigh_release(n);
    }
    return;

reject_redirect:
      . . .
(net/ipv4/route.c)

既然我们已经介绍了如何处理接收到的 ICMPv4 消息,我们接下来可以处理 IPv4 路由缓存以及删除它的原因。

IPv4 路由缓存

在 3.6 之前的内核中,有一个带有垃圾收集器的 IPv4 路由缓存。内核 3.6 中移除了 IPv4 路由缓存(大约在 2012 年 7 月)。FIB TRIE / FIB hash 是内核多年来的选择,但不是默认的。使用 FIB TRIE 可以删除 IPv4 路由缓存,因为它存在拒绝服务(DoS)问题。FIB TRIE(也称为 LC-trie)是最长的匹配前缀查找算法,对于大型路由表,其性能优于 FIB hash。它消耗更多的内存,也更复杂,但是由于它的性能更好,所以它使得删除路由缓存变得可行。FIB TRIE 代码在被合并之前在内核中存在了很长时间,但它不是默认的。移除 IPv4 路由缓存的主要原因是对其发起 DoS 攻击很容易,因为 IPv4 路由缓存为每个唯一的流创建了一个缓存条目。基本上,这意味着通过向随机目的地发送数据包,您可以生成无限数量的路由缓存条目。

合并 FIB TRIE 需要移除路由缓存、一些麻烦的 FIB 哈希表以及路由缓存垃圾收集器方法。本章简要讨论了路由缓存。因为新手读者可能不知道它有什么用,请注意,在基于 Linux 的软件行业中,在像 RedHat Enterprise 这样的商业发行版中,内核在很长一段时间内都是完全维护和完全支持的(例如,RedHat 对其发行版的支持长达 7 年)。因此,很有可能一些读者会参与基于 3.6 之前内核的项目,在那里你会找到路由缓存和基于 FIB 散列的路由表。深入研究 FIB TRIE 数据结构的理论和实现细节超出了本书的范围。要了解更多信息,我推荐 Robert Olsson 和 Stefan Nilsson 的文章“TRASH-A dynamic LC-trie and hash data structure”,

请注意,对于 IPv4 路由缓存实施,无论使用多少个路由表,都只有一个缓存(使用策略路由时,最多可以有 255 个路由表)。请注意,它也支持 IPv4 多路径路由缓存,但在 2007 年的内核 2.6.23 中被删除了。事实上,它从未很好地工作过,也从未脱离过实验状态。

对于 3.6 内核之前的内核,FIB TRIE 尚未合并,在 IPv4 路由子系统中的查找是不同的:对路由表的访问先于对路由缓存的访问,这些表以不同的方式组织,并且有一个路由缓存垃圾收集器,它既是异步的(周期性定时器)又是同步的(在特定条件下激活,例如当缓存条目的数量超过某个阈值时)。缓存基本上是一个很大的散列,以 IP 流源地址、目的地址和 TOS 作为关键字,与所有特定于流的信息相关联,如邻居条目、PMTU、重定向、TCPMSS 信息等等。这样做的好处是,缓存的条目查找起来很快,并且包含了更高层所需的所有信息。

image 以下两段(“Rx 路径”和“Tx 路径”)指的是 2.6.38 内核。

Rx 路径

在 Rx 路径中,首先调用ip_route_input_common()方法。此方法在 IPv4 路由缓存中执行查找,这比在 IPv4 路由表中查找要快得多。在这些路由表中的查找基于最长前缀匹配(LPM)搜索算法。使用 LPM 搜索,最具体的表条目(具有最高子网掩码的条目)称为最长前缀匹配。如果在路由缓存中查找失败(“缓存未命中”),则通过调用ip_route_input_slow()方法在路由表中进行查找。这个方法调用fib_lookup()方法来执行实际的查找。成功后,它调用ip_mkroute_input()方法,该方法通过调用rt_intern_hash()方法将路由条目插入到路由缓存中。

Tx 路径

在 Tx 路径中,首先调用ip_route_output_key()方法。该方法在 IPv4 路由缓存中执行查找。在缓存未命中的情况下,它调用ip_route_output_slow()方法,后者调用fib_lookup()方法在路由子系统中执行查找。随后,一旦成功,它就调用ip_mkroute_output()方法,该方法(以及其他动作)通过调用rt_intern_hash()方法将路由条目插入路由缓存。

摘要

本章讲述了 IPv4 路由子系统的各种主题。路由子系统对于处理传入和传出的数据包至关重要。您了解了各种主题,如转发、路由子系统中的查找、FIB 表的组织、策略路由和路由子系统以及 ICMPv4 重定向消息。您还了解了 FIB 别名带来的优化,以及路由缓存被移除的事实,以及原因。下一章将介绍 IPv4 路由子系统的高级主题。

快速参考

我用 IPv4 路由子系统的重要方法、宏和表格的简短列表,以及关于路由标志的简短解释来结束本章。

image IP v4 路由子系统在net/ipv4 : fib_frontend.cfib_trie.cfib_semantics.croute.c下的这些模块中实现。

fib_rules.c模块实现策略路由,仅在 CONFIG_IP_MULTIPLE_TABLES 设置时编译。其中最重要的头文件是fib_lookup.hinclude/net/ip_fib.hinclude/net/route.h

目标缓存(dst)的实现在net/core/dst.cinclude/net/dst.h中。

应该为多路径路由支持设置 CONFIG_IP_ROUTE_MULTIPATH。

方法

本节列出了本章中提到的方法。

int fib _ table _ insert(struct fib _ table * TB,struct fib _ config * CFG);

该方法基于指定的fib_config对象将 IPv4 路由条目插入到指定的 FIB 表(fib_table对象)中。

int fib _ table _ delete(struct fib _ table * TB,struct fib _ config * CFG);

该方法基于指定的fib_config对象,从指定的 FIB 表(fib_table对象)中删除 IPv4 路由条目。

struct fib _ info * fib _ create _ info(struct fib _ config * CFG);

该方法创建一个从指定的fib_config对象派生的fib_info对象。

请参阅 free _ fib _ info(struct fib _ info * fi):

该方法释放一个处于非活动状态的fib_info对象(fib_dead标志不为 0),并递减全局fib_info对象计数器(fib_info_cnt)。

void fib _ alias _ accessed(struct fib _ alias * fa);

该方法将指定的fib_aliasfa_state标志设置为 FA _ S _ ACCESSED。注意,唯一的fa_state标志是 FA _ S _ ACCESSED。

void IP _ rt _ send _ redirect(struct sk _ buff * skb);

此方法发送 ICMPV4 重定向消息,作为对次优路径的响应。

void _ _ IP _ do _ redirect(struct rtable * rt,struct sk_buff skb,struct flowi4fl4,bool kill _ route);

此方法处理接收 ICMPv4 重定向消息。

void update _ or _ create _ fnhe(struct fib _ NH * NH,__be32 daddr,__be32 gw,u32 pmtu,unsigned long expires);

该方法在指定的下一跳对象(fib_nh)中创建 FIB 下一跳异常表(fib_nh_exception),如果它还不存在,并初始化它。当由于 ICMPv4 重定向或由于 PMTU 发现而应该有路由更新时,将调用该命令。

u32 dst _ metric(const struct dst _ entry * dst,int metric);

该方法返回指定的dst对象的度量。

struct fib _ table * fib _ trie _ table(u32 id);

该方法分配并初始化 FIB TRIE 表。

struct leaf * fib _ find _ node(struct trie * t,u32 key);

此方法使用指定的键执行 TRIE 查找。成功时返回一个leaf对象,失败时返回 NULL。

宏指令

本节列出了 IPv4 路由子系统的宏,其中一些宏在本章中提到过。

FIB_RES_GW()

这个宏返回与指定的fib_result对象相关联的nh_gw字段(下一跳网关地址)。

FIB_RES_DEV()

该宏返回与指定的fib_result对象相关的nh_dev字段(下一跳net_device对象)。

OIF 纤维网()

这个宏返回与指定的fib_result对象相关联的nh_oif字段(下一跳输出接口索引)。

FIB_RES_NH()

该宏返回指定的fib_result对象的fib_info的下一跳(fib_nh对象)。设置多路径路由时,可以有多个 nexthops 在这种情况下,考虑指定的fib_result对象的nh_sel字段的值,作为嵌入在fib_info对象中的下一跳数组的索引。

(include/net/ip_fib.h)

IN_DEV_FORWARD()

此宏检查指定的网络设备(in_device对象)是否支持 IPv4 转发。

IN _ DEV _ RX _ 重定向()

此宏检查指定的网络设备(in_device对象)是否支持接受 ICMPv4 重定向。

IN_DEV_TX_REDIRECTS()

此宏检查指定的网络设备(in_device对象)是否支持发送 ICMPv4 重定向。

IS_LEAF()

该宏检查指定的树节点是否是叶节点。

IS_TNODE()

该宏检查指定的树节点是否是内部节点(trie节点或tnode)。

change_nexthops()

该宏迭代指定的fib_info对象(net/ipv4/fib_semantics.c)的下一跳。

桌子

路由有 15 个(RTAX_MAX)度量。有些是 TCP 相关的,有些是通用的。表 5-1 显示了这些指标中哪些与 TCP 相关。

表 5-1。路线指标

|

Linux 符号

|

TCP 度量(是/否)

|
| --- | --- |
| RTAX_UNSPEC | 普通 |
| RTAX_LOCK | 普通 |
| S7-1200 可编程控制器 | 普通 |
| RTAX _ 窗口 | Y |
| S7-1200 可编程控制器 | Y |
| rtax _ rttvar(消歧义) | Y |
| rtax _ ssthresh(消歧义) | Y |
| RTAX_CWND | Y |
| RTAX_ADVMSS | Y |
| RTAX _ 重新排序 | Y |
| RTX _ hope 限制 | 普通 |
| rtax _ initcwnd(虚拟专用网络) | Y |
| RTAX _ 功能 | 普通 |
| RTAX_RTO_MIN | Y |
| RTAX_INITRWND | Y |

(include/uapi/linux/rtnetlink.h)

表 5-2 显示了所有路线类型的误差值和范围。

表 5-2。路线类型

|

Linux 符号

|

错误

|

范围

|
| --- | --- | --- |
| RTN_UNSPEC | Zero | RT_SCOPE_NOWHERE |
| RTN _ 单播 | Zero | RT _ SCOPE _ 宇宙 |
| RTN _ 本地 | Zero | RT _ SCOPE _ 主机 |
| RTN _ 广播 | Zero | RT_SCOPE_LINK |
| RTN _ 任播 | Zero | RT_SCOPE_LINK |
| RTN _ 多播 | Zero | RT _ SCOPE _ 宇宙 |
| rtn _ 黑洞 | -埃因瓦尔 | RT _ SCOPE _ 宇宙 |
| RTN _ 不可达 | -EHOSTUNREACH | RT _ SCOPE _ 宇宙 |
| RTN _ 禁止 | -电子会议 | RT _ SCOPE _ 宇宙 |
| RTN_THROW | -伊根 | RT _ SCOPE _ 宇宙 |
| RTN_NAT | -埃因瓦尔 | RT_SCOPE_NOWHERE |
| RTN_XRESOLVE | -埃因瓦尔 | RT_SCOPE_NOWHERE |

路线标志

当运行route –n命令时,您会得到一个显示路由标志的输出。下面是标志值和一个简短的route –n输出示例:

u(航路打开)

h(目标是主机)

g(使用网关)

r(为动态路由恢复路由)

d(由守护程序或重定向动态安装)

m(从路由守护程序或重定向修改)

答(由 addrconf 安装)

!(拒绝路线)

表 5-3 显示了运行route –n的输出示例(结果组织成表格形式):

表 5-3。内核 IP 路由表

Tab05-03.jpg

六、高级路由

第五章讲述了 IPv4 路由子系统。本章继续讲述路由子系统,并讨论高级 IPv4 路由主题,如多播路由、多路径路由、策略路由等。这本书讨论的是 Linux 内核网络实现——它没有深入研究用户空间多播路由守护进程实现的内部,这些实现非常复杂,超出了本书的范围。然而,我确实在某种程度上讨论了用户空间多播路由守护进程和内核中多播层之间的交互。我还简要讨论了互联网组管理协议(IGMP)协议,这是组播组成员管理的基础;添加和删除多播组成员是由 IGMP 协议完成的。要理解多播主机和多播路由器之间的交互,需要一些 IGMP 的基本知识。

多路径路由能够在一条路由中添加多个下一跳。策略路由支持配置不仅仅基于目的地址的路由策略。我从描述多播路由开始。

多播路由

第四章在“接收 IPv4 组播数据包”一节中简要提到了组播路由。我现在将更深入地讨论它。发送多播流量意味着向多个接收者发送相同的数据包。此功能在流媒体、音频/视频会议等方面非常有用。在节省网络带宽方面,它比单播流量有明显的优势。多播地址被定义为 D 类地址。该组的无类域间路由(CIDR)前缀是 224.0.0.0/4。IPv4 多播地址的范围是从 224.0.0.0 到 239.255.255.255。处理多播路由必须结合与内核交互的用户空间路由守护进程来完成。根据 Linux 实现,与单播路由相反,如果没有这个用户空间路由守护进程,多播路由不能仅由内核代码处理。有各种各样的多播守护进程:例如:mrouted,它基于距离矢量多播路由协议(DVMRP)的实现,或者pimd,它基于与协议无关的多播协议(PIM)。RFC 1075 中定义了 DVMRP 协议,它是第一个多播路由协议。它基于路由信息协议(RIP)协议。

PIM 协议有两个版本,,内核都支持(配置 _IP_PIMSM_V1 和配置 _IP_PIMSM_V2)。PIM 有四种不同的模式:PIM-SM (PIM 稀疏模式)、PIM-DM (PIM 密集模式)、PIM 源特定多播(PIM-SSM)和双向 PIM。该协议被称为协议独立,因为它不依赖于任何特定的路由协议进行拓扑发现。本节讨论用户空间守护进程和内核多播路由层之间的交互。深入研究 PIM 协议或 DVMRP 协议(或任何其他多播路由协议)的内部已经超出了本书的范围。通常,多播路由查找基于源地址和目的地址。有一个“多播策略路由”内核特性,它与第五章中提到的单播策略路由内核特性类似,也将在本章中讨论。多播策略路由协议是使用策略路由 API 实现的(例如,它调用fib_rules_lookup()方法来执行查找,创建fib_rules_ops对象,并用fib_rules_register()方法注册它,等等)。使用多播策略路由,路由可以基于附加标准,如入口网络接口。此外,您可以使用多个多播路由表。为了使用多播策略路由,必须设置 IP_MROUTE_MULTIPLE_TABLES。

图 6-1 显示了一个简单的 IPv4 组播路由设置。拓扑结构非常简单:左边的笔记本电脑通过发送一个 IGMP 数据包(IP_ADD_MEMBERSHIP)加入一个组播组(224.225.0.1)。IGMP 协议将在下一节“IGMP 协议”中讨论中间的 AMD 服务器被配置为组播路由器,用户空间组播路由守护进程(如pimdmrouted)在上面运行。右边的 Windows 服务器的 IP 地址为 192.168.2.10,它向 224.225.0.1 发送组播流量;该流量通过多播路由器转发到笔记本电脑。请注意,Windows 服务器本身没有加入 224.225.0.1 多播组。运行ip route add 224.0.0.0/4 dev <networkDeviceName>告诉内核通过指定的网络设备发送所有多播流量。

9781430261964_Fig06-01.jpg

图 6-1 。简单多播路由设置

下一节讨论 IGMP 协议,它用于管理多播组成员。

《IGMP 议定书》

IGMP 协议是 IPv4 多播不可分割的一部分。它必须在支持 IPv4 多播的每个节点上实现。在 IPv6 中,组播管理由 MLD(组播监听发现)协议处理,该协议使用 ICMPv6 消息,在第八章的中讨论。使用 IGMP 协议,可以建立和管理多播组成员。IGMP 有三个版本:

  1. igmp v1(RFC 1112)**:有两种类型的消息—主机成员报告和主机成员查询。当主机想要加入多播组时,它发送成员报告消息。多播路由器发送成员资格查询,以发现哪些主机多播组在其连接的本地网络上有成员。查询被发送到所有主机组地址(224.0.0.1,IGMP 所有主机)并携带 TTL 1,以便成员资格查询不会传播到 LAN 之外。

  2. IGMPv2 (RFC 2236)**: This is an extension of IGMPv1. The IGMPv2 protocol adds three new messages:

    1. 成员资格查询(0x11):有两个子类型的成员资格查询消息:一般查询,用于了解哪些组在所连接的网络上具有成员,以及特定于组的查询,用于了解特定组在所连接的网络上是否具有任何成员。
    2. 版本 2 成员报告(0x16)。
    3. 离开组(0x17)。

    image 注意 IGMPv2 也支持版本 1 成员报告消息,以向后兼容 IGMPv1。参见 RFC 2236 第 2.1 节。

  3. IGMPv3 (RFC 3376,由 RFC 4604 更新) : 此次协议重大修订增加了一项名为源过滤的功能。这意味着当主机加入多播组时,它可以指定一组源地址,从这些地址接收多播流量。源过滤器也可以排除源地址。为了支持源过滤特性,socket API 被扩展;请参见 RFC 3678,“多播源过滤器的套接字接口扩展”我还应该提到,多播路由器定期(大约每两分钟)向所有主机多播组地址 224.0.0.1 发送成员查询。接收成员资格查询的主机用成员资格报告来响应。这是在内核中由igmp_rcv()方法实现的:获取 IGMP _ 主机 _ 成员资格 _ 查询消息由igmp_heard_query()方法处理。

image IP v4 IGMP 的内核实现在net/core/igmp.cinclude/linux/igmp.hinclude/uapi/linux/igmp.h

下一节研究 IPv4 多播路由的基本数据结构、多播路由表及其 Linux 实现。

多播路由表

多播路由表由名为mr_table的结构表示。我们来看看:

struct mr_table {
    struct list_head     list;
#ifdef CONFIG_NET_NS
    struct net           *net;
#endif
    u32                  id;
    struct sock __rcu    *mroute_sk;
    struct timer_list    ipmr_expire_timer;
    struct list_head     mfc_unres_queue;
    struct list_head     mfc_cache_array[MFC_LINES];
    struct vif_device    vif_table[MAXVIFS];
    . . .
};
(net/ipv4/ipmr.c)

以下是对mr_table结构中一些成员的描述:

  • net:组播路由表关联的网络名称空间;默认情况下,它是初始网络名称空间init_net。网络名称空间在第十四章中讨论。
  • id:组播路由表 id;当使用单个表时,它是 RT_TABLE_DEFAULT (253)。
  • mroute_sk :这个指针代表内核保存的用户空间套接字的引用。通过使用MRT_INIT套接字选项从用户空间调用setsockopt()来初始化mroute_sk指针,并通过使用 MRT_DONE 套接字选项调用setsockopt()来使其无效。用户空间和内核之间的交互是基于调用setsockopt()方法、从用户空间发送 IOCTLs、构建 IGMP 包并通过从内核调用sock_queue_rcv_skb()方法将它们传递给多播路由守护进程。
  • ipmr_expire_timer:清理未解析组播路由条目的定时器。这个定时器在用ipmr_new_table()方法创建组播路由表时被初始化,在用ipmr_free_table()方法删除组播路由表时被删除。
  • mfc_unres_queue:未解析的路由条目队列。
  • mfc_cache_array:一个路由条目的缓存,有 64 个(MFC_LINES)条目,将在下一节中简要讨论。
  • vif_table[MAXVIFS]:32 个(MAXVIFS) vif_device对象的数组。通过vif_add()方法添加条目,通过vif_delete()方法删除条目。vif_device结构代表一个虚拟组播路由网络接口;它可以基于物理设备或 IPIP (IP over IP)隧道。vif_device结构将在后面的“Vif 装置”章节中讨论。

我已经介绍了组播路由表,提到了它的重要成员,比如组播转发缓存(MFC) 和未解析路由条目的队列。接下来我将看看 MFC,它嵌入在多播路由表对象中,在多播路由中起着重要的作用。

组播转发缓存(MFC)

多播路由表中最重要的数据结构是 MFC,它实际上是缓存条目(mfc_cache对象)的数组。这个名为mfc_cache_array的数组嵌入在组播路由表(mr_table)对象中。它有 64 个(MFC_LINES)元素。这个数组的索引是散列值(散列函数接受两个参数—多播组地址和源 IP 地址;请参见本章末尾“快速参考”一节中对 MFC_HASH 宏的描述)。

通常只有一个多播路由表,它是mr_table结构的一个实例,对它的引用保存在 IPv4 网络名称空间(net->ipv4.mrt)中。该表是由ipmr_rules_init()方法创建的,该方法还指定net->ipv4.mrt指向创建的组播路由表。当使用前面提到的多播策略路由功能时,可以有多个多播策略路由表。在这两种情况下,你用同样的方法得到路由表,ipmr_fib_lookup()ipmr_fib_lookup()方法获得三个参数作为输入:网络名称空间、流和指向它应该填充的mr_table对象的指针。正常情况下,它只是将指定的mr_table指针设置为net->ipv4.mrt;当处理多个表时(设置了 IP_MROUTE_MULTIPLE_TABLES),实现更加复杂。让我们来看看mfc_cache的结构:

struct mfc_cache {
    struct list_head list;
    __be32 mfc_mcastgrp;
    __be32 mfc_origin;
    vifi_t mfc_parent;
    int mfc_flags;
    union {
            struct {
                    unsigned long expires;
                    struct sk_buff_head unresolved; /* Unresolved buffers */
            } unres;
            struct {
                    unsigned long last_assert;
                    int minvif;
                    int maxvif;
                    unsigned long bytes;
                    unsigned long pkt;
                    unsigned long wrong_if;
                    unsigned char ttls[MAXVIFS];    /* TTL thresholds */
            } res;
    } mfc_un;
    struct rcu_head rcu;
 };
(include/linux/mroute.h)

以下是对mfc_cache结构中一些成员的描述:

  • mfc_mcastgrp:该条目所属组播组的地址。

  • mfc_origin:路由的源地址。

  • mfc_parent:源接口。

  • mfc_flags:条目的标志。可以有下列值之一:

  • MFC_STATIC:当路由是静态添加的,而不是由多播路由守护进程添加的。

  • MFC_NOTIFY:路由条目的 RTM_F_NOTIFY 标志设置的时间。更多细节见rt_fill_info()方法和ipmr_get_route()方法。

  • mfc_un联合由两部分组成:

  • unres:未解析的缓存条目。

  • res:解析的缓存条目。

某个流的 SKB 第一次到达内核时,它被添加到未解析条目的队列中(mfc_un.unres.unresolved),其中最多可以保存三个 skb。如果队列中有三个 skb,那么数据包不会被添加到队列中,而是被释放,ipmr_cache_unresolved()方法返回-ENOBUFS("没有可用的缓冲区空间"):

static int ipmr_cache_unresolved(struct mr_table *mrt, vifi_t vifi, struct sk_buff *skb)
{
         . . .
         if (c->mfc_un.unres.unresolved.qlen > 3) {
                 kfree_skb(skb);
                 err = -ENOBUFS;
        } else {
            . . .

}
(net/ipv4/ipmr.c)

本节描述了 MFC 及其重要成员,包括已解析条目队列和未解析条目队列。下一节将简要描述什么是多播路由器,以及它在 Linux 中是如何配置的。

多播路由器

为了将机器配置为多播路由器,您应该设置 CONFIG_IP_MROUTE 内核配置选项。您还应该运行一些路由守护进程,如前面提到的pimdmrouted。这些路由守护进程创建一个套接字来与内核通信。例如,在pimd中,您通过调用socket(AF_INET, SOCK_RAW, IPPROTO_IGMP)创建一个原始的 IGMP 套接字。在这个套接字上调用setsockopt()会触发向内核发送命令,这些命令由ip_mroute_setsockopt()方法处理。当使用 MRT_INIT 从路由守护进程调用这个套接字上的setsockopt()时,内核被设置为在所使用的mr_table对象的mroute_sk字段中保存对用户空间套接字的引用,并且通过调用 IPV4_DEVCONF_ALL(net,MC_FORWARDING)++来设置mc_forwarding procfs条目(/proc/sys/net/ipv4/conf/all/mc_forwarding)。注意,mc_forwarding procfs条目是一个只读条目,不能从用户空间设置。您不能创建多播路由守护进程的另一个实例:当处理 MRT_INIT 选项时,ip_mroute_setsockopt()方法检查mr_table对象的mroute_sk字段是否已初始化,如果是,则返回-EADDRINUSE。添加网络接口是通过在这个套接字上用 MRT_ADD_VIF 调用setsockopt()完成的,删除网络接口是通过在这个套接字上用 MRT_DEL_VIF 调用setsockopt()完成的。您可以通过传递一个vifctl对象作为setsockopt()系统调用的optval参数,将网络接口的参数传递给这些setsockopt()调用。让我们来看看vifctl的结构:

struct vifctl {
    vifi_t    vifc_vifi;                /* Index of VIF */
    unsigned char vifc_flags;            /* VIFF_ flags */
    unsigned char vifc_threshold;        /* ttl limit */
    unsigned int vifc_rate_limit;        /* Rate limiter values (NI) */
    union {
        struct in_addr vifc_lcl_addr;     /* Local interface address */
        int            vifc_lcl_ifindex;  /* Local interface index   */
    };
    struct in_addr vifc_rmt_addr;    /* IPIP tunnel addr */
};
(include/uapi/linux/mroute.h)

以下是对vifctl结构中一些成员的描述:

  • vifc_flags可以是:

  • 当你想使用 IPIP 隧道时。

  • VIFF_REGISTER:当你想注册接口的时候。

  • VIFF_USE_IFINDEX:当你想使用本地接口索引而不是本地接口 IP 地址时;在这种情况下,您将把vifc_lcl_ifindex设置为本地接口索引。VIFF_USE_IFINDEX 标志适用于 2.6.33 及更高版本的内核。

  • vifc_lcl_addr:本地接口 IP 地址。(这是默认设置,不应该为使用它设置任何标志)。

  • vifc_lcl_ifindex:本地接口索引。当vifc_flags中的 VIFF_USE_IFINDEX 标志置位时,应置位该位。

  • vifc_rmt_addr:隧道的远程节点的地址。

当多播路由守护进程关闭时,使用 MRT_DONE 选项调用setsockopt()方法 。这将触发调用mrtsock_destruct()方法来使所使用的mr_table对象的mroute_sk字段无效,并执行各种清理。

本节讲述了什么是多播路由器,以及在 Linux 中如何配置它。我还检查了vifctl结构。接下来,我看一下 Vif 设备,它代表一个多播网络接口。

Vif 设备

多播路由支持两种模式:直接多播和封装在隧道上的单播包中的多播。在这两种情况下,使用相同的对象(vif_device结构的一个实例)来表示网络接口。在隧道上工作时,将设置 VIFF_TUNNEL 标志。添加和删除多播接口分别由vif_add()方法和vif_delete()方法完成。vif_add()方法还通过调用dev_set_allmulti(dev, 1)方法将设备设置为支持组播,该方法递增指定网络设备(net_device对象)的allmulti计数器。vif_delete()方法调用dev_set_allmulti(dev, -1)来递减指定网络设备的allmulti计数器(net_device对象)。关于dev_set_allmulti()方法的更多细节,参见附录 A 。我们来看看vif_device的结构;它的成员是不言自明的:

struct vif_device {
        struct net_device       *dev;       /* Device we are using */
        unsigned long   bytes_in,bytes_out;
        unsigned long   pkt_in,pkt_out;     /* Statistics                   */
        unsigned long   rate_limit;         /* Traffic shaping (NI)         */
        unsigned char   threshold;          /* TTL threshold                */
        unsigned short  flags;              /* Control flags                */
        __be32          local,remote;       /* Addresses(remote for tunnels)*/
        int             link;               /* Physical interface index     */
};
(include/linux/mroute.h)

为了接收多播流量,主机必须加入多播组。这是通过在用户空间中创建一个套接字,并使用 IPPROTO_IP 和 IP_ADD_MEMBERSHIP 套接字选项调用setsockopt()来完成的。用户空间应用还创建了一个ip_mreq对象,在这里它初始化请求参数,比如所需的组多播地址和主机的源 IP 地址(参见netinet/in.h用户空间头)。在net/ipv4/igmp.c中,setsockopt()调用由ip_mc_join_group()方法在内核中处理。最终,ip_mc_join_group()方法将组播地址添加到组播地址列表(mc_list)中,该列表是in_device对象的成员。主机可以通过使用 IPPROTO_IP 和 IP_DROP_MEMBERSHIP 套接字选项调用setsockopt()来离开多播组。这是在内核中由net/ipv4/igmp.c中的ip_mc_leave_group()方法处理的。单个套接字可以加入多达 20 个多播组(sysctl_igmp_max_memberships)。试图通过同一个套接字加入 20 个以上的多播组将会失败,并出现-ENOBUFS 错误(“没有可用的缓冲区空间”))参见net/ipv4/igmp.c中的ip_mc_join_group()方法实现。

IPv4 组播接收路径

第四章的“接收 IPv4 多播数据包”一节简要讨论了如何处理多播数据包。我现在将更深入地描述这一点。我的讨论假设我们的机器被配置为多播路由器;正如前面提到的,这意味着 CONFIG_IP_MROUTE 被设置,并且像pimdmrouted这样的路由守护进程在这个主机上运行。多播数据包由ip_route_input_mc()方法处理,其中分配并初始化路由表条目(一个rtable对象),并且在设置 CONFIG_IP_MROUTE 的情况下,将dst对象的input回调设置为ip_mr_input()。我们来看看ip_mr_input()的方法:

int ip_mr_input(struct sk_buff *skb)
{
        struct mfc_cache *cache;
        struct net *net = dev_net(skb->dev);

首先,如果数据包打算用于本地传送,则将local标志设置为true,因为ip_mr_input()方法也处理本地多播数据包。

int local = skb_rtable(skb)->rt_flags & RTCF_LOCAL;
struct mr_table *mrt;

/* Packet is looped back after forward, it should not be
* forwarded second time, but still can be delivered locally.
*/
if (IPCB(skb)->flags & IPSKB_FORWARDED)
goto dont_forward;

通常,当使用单个多播路由表时,ipmr_rt_fib_lookup()方法简单地返回net->ipv4.mrt对象:

mrt = ipmr_rt_fib_lookup(net, skb);
if (IS_ERR(mrt)) {
        kfree_skb(skb);
        return PTR_ERR(mrt);
}
if (!local) {

当发送加入或离开数据包时,IGMPv3 和一些 IGMPv2 实现在 IPv4 报头中设置路由器警报选项(IPOPT_RA)。参见net/ipv4/igmp.c中的igmpv3_newpack()方法:

if (IPCB(skb)->opt.router_alert) {

ip_call_ra_chain()方法 ( net/ipv4/ip_input.c)调用raw_rcv()方法将包传递给用户空间原始套接字,该套接字进行监听。ip_ra_chain对象包含对多播路由套接字的引用,该引用作为参数传递给raw_rcv()方法。更多细节,请看net/ipv4/ip_input.c中的ip_call_ra_chain()方法实现:

if (ip_call_ra_chain(skb))
        return 0;

存在未设置路由器警报选项的实现,如以下注释中所解释的;这些情况也必须通过直接调用raw_rcv()方法来处理:

} else if (ip_hdr(skb)->protocol == IPPROTO_IGMP) {
        /* IGMPv1 (and broken IGMPv2 implementations sort of
        * Cisco IOS <= 11.2(8)) do not put router alert
        * option to IGMP packets destined to routable
        * groups. It is very bad, because it means
        * that we can forward NO IGMP messages.
        */
        struct sock *mroute_sk;

mrt->mroute_sk套接字是组播路由用户空间应用创建的套接字内核中的一个副本:

mroute_sk = rcu_dereference(mrt->mroute_sk);
        if (mroute_sk) {
        nf_reset(skb);
        raw_rcv(mroute_sk, skb);
        return 0;
        }
     }
}

首先,通过调用ipmr_cache_find()方法在多播路由缓存mfc_cache_array中执行查找。哈希键是数据包的目的多播组地址和源 IP 地址,取自 IPv4 报头:

cache = ipmr_cache_find(mrt, ip_hdr(skb)->saddr, ip_hdr(skb)->daddr);
if (cache == NULL) {

在虚拟设备阵列中执行查找(vif_table)以查看是否存在与输入网络设备匹配的对应条目(skb->dev):

int vif = ipmr_find_vif(mrt, skb->dev);

ipmr_cache_find_any()方法处理多播代理支持的高级特性(本书不讨论):

        if (vif >= 0)
                cache = ipmr_cache_find_any(mrt, ip_hdr(skb)->daddr,
                                            vif);
}

/*
*      No usable cache entry
*/
if (cache == NULL) {
        int vif;

如果数据包的目的地是本地主机,则传送它:

if (local) {
        struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
        ip_local_deliver(skb);
        if (skb2 == NULL)
                return -ENOBUFS;
        skb = skb2;
}

read_lock(&mrt_lock);
vif = ipmr_find_vif(mrt, skb->dev);
if (vif >= 0) {

ipmr_cache_unresolved()方法通过调用ipmr_cache_alloc_unres()方法创建一个多播路由条目(mfc_cache对象)。该方法创建一个缓存条目(mfc_cache对象),并初始化其到期时间间隔(通过设置mfc_un.unres.expires)。让我们来看看这个非常短的方法,ipmr_cache_alloc_unres() :

static struct mfc_cache *ipmr_cache_alloc_unres(void)
{
    struct mfc_cache *c = kmem_cache_zalloc(mrt_cachep, GFP_ATOMIC);

    if (c) {
        skb_queue_head_init(&c->mfc_un.unres.unresolved);

设置到期时间间隔:

        c->mfc_un.unres.expires = jiffies + 10*HZ;
    }
    return c;
}

如果路由守护程序未在其到期时间间隔内解析路由条目,则该条目将从未解析条目的队列中删除。当创建多播路由表时(通过ipmr_new_table()方法),其定时器(ipmr_expire_timer)被设置。这个定时器周期性地调用ipmr_expire_process()方法。ipmr_expire_process()方法遍历未解析条目队列中的所有未解析缓存条目(mrtable对象的mfc_unres_queue,并移除过期的未解析缓存条目。

在创建了未解析的高速缓存条目之后,ipmr_cache_unresolved()方法将其添加到未解析条目的队列中(多播表的mfc_unres_queuemrtable,并且将未解析队列长度加 1(多播表的cache_resolve_queue_lenmrtable)。它还调用ipmr_cache_report()方法,该方法构建 IGMP 消息(IGMPMSG_NOCACHE ),并通过最终调用sock_queue_rcv_skb()方法将其交付给用户空间多播路由守护进程。

我提到过用户空间路由守护进程应该在某个时间间隔内解析路由。我不会深究这是如何在用户空间中实现的。但是,请注意,一旦路由守护程序决定它应该解析一个未解析的条目,它就构建缓存条目参数(在一个mfcctl对象中)并使用 MRT_ADD_MFC 套接字选项调用setsockopt(),然后它传递嵌入在setsockopt()系统调用的optval参数中的mfcctl对象;这在内核中由ipmr_mfc_add()方法处理:

                int err2 = ipmr_cache_unresolved(mrt, vif, skb);
                read_unlock(&mrt_lock);

                return err2;
        }
        read_unlock(&mrt_lock);
        kfree_skb(skb);
        return -ENODEV;
}

read_lock(&mrt_lock);

如果在 MFC 中找到缓存条目,调用ip_mr_forward()方法继续包遍历:

        ip_mr_forward(net, mrt, skb, cache, local);
        read_unlock(&mrt_lock);

        if (local)
                return ip_local_deliver(skb);

        return 0;

dont_forward:
        if (local)
                return ip_local_deliver(skb);
        kfree_skb(skb);
        return 0;
}

本节详细介绍了 IPv4 多播接收路径以及与该路径中的路由守护程序的交互。下一节描述多播路由转发方法,ip_mr_forward()

ip_mr_forward()方法

我们来看看ip_mr_forward()的方法:

static int ip_mr_forward(struct net *net, struct mr_table *mrt,
             struct sk_buff *skb, struct mfc_cache *cache,
             int local)
{
    int psend = -1;
    int vif, ct;
    int true_vifi = ipmr_find_vif(mrt, skb->dev);

    vif = cache->mfc_parent;

在这里,您可以看到已解析的缓存对象(mfc_un.res)的更新统计信息:

cache->mfc_un.res.pkt++;
cache->mfc_un.res.bytes += skb->len;

if (cache->mfc_origin == htonl(INADDR_ANY) && true_vifi >= 0) {
    struct mfc_cache *cache_proxy;

表达式(*, G)表示从任何源发送到组 G 的业务:

    /* For an (*,G) entry, we only check that the incomming
    * interface is part of the static tree.
    */
    cache_proxy = ipmr_cache_find_any_parent(mrt, vif);
    if (cache_proxy &&
        cache_proxy->mfc_un.res.ttls[true_vifi] < 255)
        goto forward;
}
/*
* Wrong interface: drop packet and (maybe) send PIM assert.
*/
if (mrt->vif_table[vif].dev != skb->dev) {
    if (rt_is_output_route(skb_rtable(skb))) {
        /* It is our own packet, looped back.
         * Very complicated situation...
         *
         * The best workaround until routing daemons will be
         * fixed is not to redistribute packet, if it was
         * send through wrong interface. It means, that
         * multicast applications WILL NOT work for
         * (S,G), which have default multicast route pointing
         * to wrong oif. In any case, it is not a good
         * idea to use multicasting applications on router.
         */
        goto dont_forward;
    }

    cache->mfc_un.res.wrong_if++;

    if (true_vifi >= 0 && mrt->mroute_do_assert &&
        /* pimsm uses asserts, when switching from RPT to SPT,
         * so that we cannot check that packet arrived on an oif.
         * It is bad, but otherwise we would need to move pretty
         * large chunk of pimd to kernel. Ough... --ANK
         */
        (mrt->mroute_do_pim ||
        cache->mfc_un.res.ttls[true_vifi] < 255) &&
        time_after(jiffies,
               cache->mfc_un.res.last_assert + MFC_ASSERT_THRESH)) {
        cache->mfc_un.res.last_assert = jiffies;

调用ipmr_cache_report()方法构建 IGMP 消息(IGMPMSG_WRONGVIF ),并通过调用sock_queue_rcv_skb()方法将其交付给用户空间多播路由守护进程:

        ipmr_cache_report(mrt, skb, true_vifi, IGMPMSG_WRONGVIF);
    }
    goto dont_forward;
}

该帧现在可以转发了:

forward:
    mrt->vif_table[vif].pkt_in++;
    mrt->vif_table[vif].bytes_in += skb->len;

    /*
     *    Forward the frame
     */
    if (cache->mfc_origin == htonl(INADDR_ANY) &&
        cache->mfc_mcastgrp == htonl(INADDR_ANY)) {
        if (true_vifi >= 0 &&
            true_vifi != cache->mfc_parent &&
            ip_hdr(skb)->ttl >
                cache->mfc_un.res.ttls[cache->mfc_parent]) {
            /* It's an (*,*) entry and the packet is not coming from
             * the upstream: forward the packet to the upstream
             * only.
             */
            psend = cache->mfc_parent;
            goto last_forward;
        }
        goto dont_forward;
    }
    for (ct = cache->mfc_un.res.maxvif - 1;
         ct >= cache->mfc_un.res.minvif; ct--) {
        /* For (*,G) entry, don't forward to the incoming interface */
        if ((cache->mfc_origin != htonl(INADDR_ANY) ||
             ct != true_vifi) &&
            ip_hdr(skb)->ttl > cache->mfc_un.res.ttls[ct]) {
            if (psend != -1) {
                struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);

调用ipmr_queue_xmit()方法继续转发数据包:

                if (skb2)
                    ipmr_queue_xmit(net, mrt, skb2, cache,
                            psend);
            }
            psend = ct;
        }
    }
last_forward:
    if (psend != -1) {
        if (local) {
            struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);

            if (skb2)
                ipmr_queue_xmit(net, mrt, skb2, cache, psend);
        } else {
            ipmr_queue_xmit(net, mrt, skb, cache, psend);
            return 0;
        }
    }

dont_forward:
    if (!local)
        kfree_skb(skb);
    return 0;
}

既然我已经介绍了多播路由转发方法ip_mr_forward(),那么是时候检查一下ipmr_queue_xmit()方法了。

ipmr_queue_xmit()方法

我们来看看ipmr_queue_xmit()的方法:

static void ipmr_queue_xmit(struct net *net, struct mr_table *mrt,
                            struct sk_buff *skb, struct mfc_cache *c, int vifi)
{
        const struct iphdr *iph = ip_hdr(skb);
        struct vif_device *vif = &mrt->vif_table[vifi];
        struct net_device *dev;
        struct rtable *rt;
        struct flowi4 fl4;

使用隧道时使用encap字段:

        int encap = 0;

        if (vif->dev == NULL)
                goto out_free;

#ifdef CONFIG_IP_PIMSM
        if (vif->flags & VIFF_REGISTER) {
                vif->pkt_out++;
                vif->bytes_out += skb->len;
                vif->dev->stats.tx_bytes += skb->len;
                vif->dev->stats.tx_packets++;
                ipmr_cache_report(mrt, skb, vifi, IGMPMSG_WHOLEPKT);
                goto out_free;
        }
#endif

使用隧道时,使用分别代表目的地址和本地地址的vif->remotevif->local进行路由查找。这些地址是隧道的端点。当使用代表物理设备的vif_device对象时,使用 IPv4 报头的目的地和作为源地址的 0 来执行路由查找:

if (vif->flags & VIFF_TUNNEL) {
        rt = ip_route_output_ports(net, &fl4, NULL,
                                   vif->remote, vif->local,
                                   0, 0,
                                   IPPROTO_IPIP,
                                   RT_TOS(iph->tos), vif->link);
        if (IS_ERR(rt))
                goto out_free;
        encap = sizeof(struct iphdr);
} else {
       rt = ip_route_output_ports(net, &fl4, NULL, iph->daddr, 0,
                                  0, 0,
                                  IPPROTO_IPIP,
                                  RT_TOS(iph->tos), vif->link);
       if (IS_ERR(rt))
               goto out_free;
}

dev = rt->dst.dev;

注意,如果分组大小高于 MTU,则不发送 ICMPv4 消息(如在单播转发的这种情况下所做的);只有统计数据被更新,数据包被丢弃:

if (skb->len+encap > dst_mtu(&rt->dst) && (ntohs(iph->frag_off) & IP_DF)) {
        /* Do not fragment multicasts. Alas, IPv4 does not
         * allow to send ICMP, so that packets will disappear
         * to blackhole.
         */

        IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
        ip_rt_put(rt);
        goto out_free;
}

encap += LL_RESERVED_SPACE(dev) + rt->dst.header_len;

if (skb_cow(skb, encap)) {
        ip_rt_put(rt);
        goto out_free;
}

vif->pkt_out++;
vif->bytes_out += skb->len;

skb_dst_drop(skb);
skb_dst_set(skb, &rt->dst);

TTL 减小,转发数据包时重新计算 IPv4 报头校验和(因为 TTL 是 IPv4 字段之一);单播数据包的ip_forward()方法也是如此:

ip_decrease_ttl(ip_hdr(skb));

/* FIXME: forward and output firewalls used to be called here.
 * What do we do with netfilter? -- RR
 */
if (vif->flags & VIFF_TUNNEL) {
        ip_encap(skb, vif->local, vif->remote);
        /* FIXME: extra output firewall step used to be here. --RR */
        vif->dev->stats.tx_packets++;
        vif->dev->stats.tx_bytes += skb->len;
}

IPCB(skb)->flags |= IPSKB_FORWARDED;

/*
* RFC1584 teaches, that DVMRP/PIM router must deliver packets locally
* not only before forwarding, but after forwarding on all output
* interfaces. It is clear, if mrouter runs a multicasting
* program, it should receive packets not depending to what interface
* program is joined.
* If we will not make it, the program will have to join on all
* interfaces. On the other hand, multihoming host (or router, but
* not mrouter) cannot join to more than one interface - it will
* result in receiving multiple packets.
 */

调用 NF_INET_FORWARD 钩子:

        NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev, dev,
                ipmr_forward_finish);
        return;

out_free:
        kfree_skb(skb);
}

ipmr_forward_finish()方法

让我们来看看ipmr_forward_finish()方法,,这是一个非常简短的方法——它实际上与ip_forward()方法相同:

static inline int ipmr_forward_finish(struct sk_buff *skb)
{
        struct ip_options *opt = &(IPCB(skb)->opt);

        IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTFORWDATAGRAMS);
        IP_ADD_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTOCTETS, skb->len);

处理 IPv4 选项,如果设置的话(见第四章):

        if (unlikely(opt->optlen))
                ip_forward_options(skb);

        return dst_output(skb);
}

最终,dst_output()通过调用ip_finish_output()方法的ip_mc_output()方法发送数据包(两个方法都在net/ipv4/route.c中)。

现在我已经介绍了这些多播方法,让我们更好地理解 TTL 字段的值是如何在多播流量中使用的。

多播流量中的 TTL

在讨论多播流量时,IPv4 报头的 TTL 字段具有双重含义。第一个与单播 IPV4 流量中的相同:TTL 表示一个跳计数器,在转发数据包的每个设备上该计数器减 1。当它达到 0 时,数据包被丢弃。这样做是为了避免由于某些错误而导致数据包的无休止传输。TTL 的第二个含义是阈值,这是多播流量所特有的。TTL 值分为几个范围。路由器的每个接口都有一个 TTL 阈值,只有 TTL 大于接口阈值的数据包才会被转发。以下是这些阈值的值:

  • 0: 限于同一主机(不能通过任何接口发出)
  • 1: 限制在同一个子网(不会被路由器转发)
  • 32: 限制在同一个地点
  • 64: 局限于同一地区
  • 128: 限于同一个大陆
  • 范围不受限制(全局)

参见史蒂夫·迪林的“4.3BSD UNIX 和相关系统的 IP 多播扩展”,可在www.kohala.com/start/mcast.api.txt获得。

image IPv4 组播路由在net/ipv4/ipmr.cinclude/linux/mroute.hinclude/uapi/linux/mroute.h实现。

我对多播路由的讨论到此结束。本章现在转到策略路由,使您能够配置不仅仅基于目的地址的路由策略。

策略路由

使用策略路由, 一个系统管理员最多可以定义 255 个路由表。本节讨论 IPv4 策略路由;IPv6 策略路由在第八章中讨论。在本节中,我使用术语策略规则来表示由策略路由创建的条目,以避免将普通路由条目(在第五章中讨论)与策略规则混淆。

策略路由管理

策略路由管理是通过iproute2包的ip rule命令完成的(策略路由管理不能与route命令并行)。让我们看看如何添加、删除和转储所有策略路由规则:

  • 你用ip rule add命令添加一条规则;比如:ip rule add tos 0x04 table 252。插入此规则后,将根据表 252 的路由规则处理 IPv4 TOS 字段匹配 0x04 的每个数据包。在添加路由时,您可以通过指定表的编号将路由条目添加到这个表中;例如:ip route add default via 192.168.2.10 table 252。这个命令在内核中由net/core/fib_rules.c中的fib_nl_newrule()方法处理。先前ip rule命令中的tos修饰符是ip rule命令可用的选择器修饰符之一;参见man 8 ip rule,以及本章末尾“快速参考”部分的表 6-1 。
  • 使用ip rule del命令删除一个规则;比如:ip rule del tos 0x04 table 252。这个命令在内核中由net/core/fib_rules.c中的fib_nl_delrule()方法处理。
  • 使用ip rule list命令或ip rule show命令转储所有规则。这两个命令都在内核中由net/core/fib_rules.c中的fib_nl_dumprule()方法处理。

现在,您已经对策略路由管理的基础有了很好的了解,所以让我们研究一下策略路由的 Linux 实现。

策略路由实现

策略路由的核心基础设施是fib_rules模块、 net/core/fib_rules.c。它由内核网络堆栈的三个协议使用:IPv4(包括多播模块,它具有多播策略路由功能,如本章前面的“多播路由”一节所述)、IPv6 和 DECnet。IPv4 策略路由也在名为fib_rules.c的文件中实现。不要被相同的名字(net/ipv4/fib_rules.c)迷惑。在 IPv6 中,策略路由在net/ipv6/fib6_rules.c中实现。头文件include/net/fib_rules.h包含策略路由核心的数据结构和方法。下面是fib4_rule结构的定义,它是 IPv4 策略路由的基础:

struct fib4_rule {
    struct fib_rule    common;
    u8            dst_len;
    u8            src_len;
    u8            tos;
    __be32            src;
    __be32            srcmask;
    __be32            dst;
    __be32            dstmask;
#ifdef CONFIG_IP_ROUTE_CLASSID
    u32            tclassid;
#endif
};
(net/ipv4/fib_rules.c)

缺省情况下,在引导时通过调用fib_default_rules_init()方法创建三个策略: 本地(RT_TABLE_LOCAL)表、主(RT_TABLE_MAIN)表和缺省(RT_TABLE_DEFAULT)表。查找是通过fib_lookup()方法完成的。注意在include/net/ip_fib.h中有两种不同的fib_lookup()方法的实现。第一个封装在#ifndef CONFIG_IP_MULTIPLE_TABLES 块中,用于非策略路由,第二个用于策略路由。使用策略路由时,查找是这样执行的:如果初始策略路由规则没有变化(net->ipv4.fib_has_custom_rules未设置),这意味着规则必须在三个初始路由表之一中。因此,首先在本地表中进行查找,然后在主表中查找,最后在默认表中查找。如果没有相应的条目,则返回网络不可达(-ENETUNREACH)错误。如果在初始策略路由规则中有一些变化(net->ipv4.fib_has_custom_rules被设置),那么 _ fib_lookup()方法 被调用,这是一个更重的方法,因为它遍历规则列表并为每个规则调用fib_rule_match()以决定它是否匹配。参见net/core/fib_rules.cfib_rules_lookup()方法的实现。(从__fib_lookup()方法调用fib_rules_lookup()方法)。这里我应该提到的是,net->ipv4.fib_has_custom_rules变量在初始化阶段由fib4_rules_init()方法设置为false,在fib4_rule_configure()方法和fib4_rule_delete()方法中设置为true。请注意,应该设置 CONFIG_IP_MULTIPLE_TABLES 来使用策略路由。

我的多播路由讨论到此结束。下一节将讨论多路径路由,即在一条路由中添加多个下一跳的能力。

多路径路由

多路径路由 提供了向路由添加多个下一跳的能力。定义两个 nexthop 节点可以这样做,比如:ip route add default scope global nexthop dev eth0 nexthop dev eth1。系统管理员还可以为每个下一跳分配权重,例如:ip route add 192.168.1.10 nexthop via 192.168.2.1 weight 3 nexthop via 192.168.2.10 weight 5fib_info结构表示一个 IPv4 路由条目,它可以有多个 FIB 下一跳。fib_info对象的fib_nhs成员代表 FIB nexthop 对象的数量;fib_info对象包含一个名为fib_nh的 FIB nexthop 对象数组。所以在这种情况下,创建了一个单独的fib_info对象,带有两个 FIB nexthop 对象的数组。内核将每个下一跳的权重保存在 FIB nexthop 对象的nh_weight字段中(fib_nh)。如果在添加多路径路径时未指定权重,则在fib_create_info()方法中,权重默认设置为 1。使用多路径路由时,调用fib_select_multipath()方法来确定下一跳。该方法从两个地方调用:Tx 路径中的__ip_route_output_key()方法和 Rx 路径中的ip_mkroute_input()方法。注意,当在流程中设置输出设备时,不会调用fib_select_multipath()方法,因为输出设备是已知的:

struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {
. . .
#ifdef CONFIG_IP_ROUTE_MULTIPATH
    if (res.fi->fib_nhs > 1 && fl4->flowi4_oif == 0)
        fib_select_multipath(&res);
    else
#endif
. . .

}

在 Rx 路径中,不需要检查fl4->flowi4_oif是否为 0,因为它在该方法的开始被设置为 0。fib_select_multipath()方法的细节我就不深究了。我只想提一下,这种使用jiffies的方法有一定的随机性,有助于创建公平的加权路由分布,并且每个下一跳的权重都被考虑在内。通过设置指定fib_result对象的 FIB nexthop 选择器(nh_sel)来分配要使用的 FIB nexthop。与由专用模块(net/ipv4/ipmr.c)处理的多播路由相反,多路径路由的代码分散在现有的路由代码中,包含在#ifdef CONFIG_IP_ROUTE_MULTIPATH 条件中,并且没有在源代码中添加单独的模块来支持它。在第五章中提到,曾经有对 IPv4 多路径路由缓存的支持,但是在 2007 年的内核 2.6.23 中被移除了;事实上,它从未很好地工作过,也从未脱离过实验状态。不要将多路径路由缓存的删除与路由缓存的删除相混淆;这是两个不同的缓存。路由缓存的移除发生在五年后的内核 3.6 (2012)中。

image 注意多路径路由支持需要设置 CONFIG_IP_ROUTE_MULTIPATH。

摘要

本章讲述了高级 IPv4 路由主题,如组播路由、IGMP 协议、策略路由和多路径路由。您了解了多播路由的基本结构,比如多播表(mr_table))、多播转发缓存(MFC)、Vif 设备等等。您还了解了如何将主机设置为多播路由器,以及如何在多播路由中使用ttl字段。第七章处理 Linux 相邻子系统。接下来的“快速参考”部分涵盖了与本章讨论的主题相关的主要方法,按其上下文排序。

快速参考

我用一个重要路由子系统方法的简短列表(其中一些在本章中提到过)、一个宏列表和procfs多播条目和表来结束本章。

方法

让我们从方法开始:

int IP _ mrroute _ setsockopt(struct sock * sk,int optname,char __user *optval,signed int opt len);

这个方法处理来自多播路由守护进程的setsockopt()调用。支持的套接字选项有:MRT_INIT、MRT_DONE、MRT_ADD_VIF、MRT_DEL_VIF、MRT_ADD_MFC、MRT_DEL_MFC、MRT_ADD_MFC_PROXY、MRT_DEL_MFC_PROXY、MRT_ASSERT、MRT_PIM(设置 PIM 支持时)和 MRT_TABLE(设置多播策略路由时)。

int IP _ mrroute _ getsockopt(struct sock * sk、int optname、char __user *optval、int _ _ user * opt len);

这个方法处理来自多播路由守护进程的getsockopt()调用。支持的套接字选项有 MRT_VERSION、MRT_ASSERT 和 MRT_PIM。

struct Mr _ table * ipmr _ new _ table(struct net * net,u32 id);

此方法创建一个新的多播路由表。表格的id将是指定的id.

void ipmr _ free _ table(struct Mr _ table * mrt);

这个方法释放指定的多播路由表和附属于它的资源。

int IP _ MC _ join _ group(struct sock * sk,struct IP _ Mr eqn * IMR);

此方法用于加入多播组。要加入的多播组的地址在给定的ip_mreqn对象中指定。如果成功,该方法返回 0。

静态 struct MFC _ cache * ipmr _ cache _ find(struct Mr _ table * mrt,_be32 origin, _ be32 mcastgrp);

此方法在 IPv4 多播路由缓存中执行查找。当没有找到条目时,它返回 NULL。

bool IP v4 _ is _ multicast(_ _ be32 addr);

如果地址是组播地址,这个方法返回true

int IP _ Mr _ input(struct sk _ buff * skb);

该方法是主要的 IPv4 组播 Rx 方法(net/ipv4/ipmr.c)。

struct MFC _ cache * IPM _ cache _ alloc(请参阅);

该方法分配多播转发高速缓存(mfc_cache)条目。

静态结构 MFC _ cache * ipmr _ cache _ alloc _ unres(void);

该方法为未解析的高速缓存分配多播路由高速缓存(mfc_cache)条目,并设置未解析条目队列的expires字段。

void fib _ select _ multipath(struct fib _ result * RES);

使用多路径路由时,调用此方法来确定下一跳。

int dev_set_allmulti(结构网络设备*dev,int Inc);

该方法根据指定的增量(增量可以是正数,也可以是负数)递增/递减指定网络设备的allmulti计数器。

int igmp _ rcv(struct sk _ buf * skb);

此方法是 IGMP 数据包的接收处理程序。

static int ipmr _ MFC _ add(struct net * net,struct mr_table *mrt,struct mfcctl *mfc,int mrtsock,int parent);

此方法添加一个多播缓存条目;通过用 MRT_ADD_MFC 从用户空间调用setsockopt()来调用它。

static int ipmr _ MFC _ delete(struct Mr _ table * mrt,struct mfcctl *mfc,int parent);

此方法删除多播缓存条目;通过用 MRT_DEL_MFC 从用户空间调用setsockopt()来调用它。

static int vif_add(struct net *net,struct mr_table *mrt,struct vifctl *vifc,int mrt sock);

此方法添加一个多播虚拟接口;通过使用 MRT_ADD_VIF 从用户空间调用setsockopt()来调用它。

静态 int Vif _ delete(struct Mr _ table * mrt,int vifi,int notify,struct list _ head * head);

此方法删除多播虚拟接口;通过使用 MRT_DEL_VIF 从用户空间调用setsockopt()来调用它。

静态 void ipmr_expire_process(无符号长整型 arg);

此方法从未解析条目队列中移除过期条目。

static int ipmr _ cache _ report(struct Mr _ table * mrt,struct sk_buff *pkt,vifi_t vifi,int assert);

该方法构建一个 IGMP 包,将 IGMP 报头中的类型设置为指定的assert值,并将代码设置为 0。通过调用sock_queue_rcv_skb()方法,这个 IGMP 包被传递给用户空间多播路由守护进程。当一个未解析的缓存条目被添加到未解析条目的队列中,并且想要通知用户空间路由守护进程它应该解析它时,可以给assert参数分配以下值之一:IGMPMSG_NOCACHE、IGMPMSG_WRONGVIF 和 IGMPMSG_WHOLEPKT。

static int ipmr _ device _ event(struct notifier _ block * this,unsigned long event,void * ptr);

此方法是由register_netdevice_notifier()方法注册的通知程序回调;当某个网络设备未注册时,会生成一个 NETDEV_UNREGISTER 事件;这个回调接收这个事件并删除vif_table中的vif_device对象,其设备是未注册的设备。

静态 void mrt sock _ destruct(struct sock * sk);

当用户空间路由守护进程用 MRT_DONE 调用setsockopt()时,这个方法被调用。这个方法使多播路由套接字(多播路由表的mroute_sk)无效,递减mc_forwarding procfs条目,并调用mroute_clean_tables()方法来释放资源。

宏指令

本节描述我们的宏。

MFC_HASH(a,b)

这个宏计算用于向 MFC 缓存添加条目的哈希值。它将组多播地址和源 IPv4 地址作为参数。

VIF_EXISTS(_mrt,_idx)

该宏检查vif_table中条目的存在;如果指定组播路由表(mrt)的组播虚拟设备(vif_table)的数组中有指定索引(_idx)的条目,则返回true

Procfs 多播条目

以下是对两个重要的procfs多播条目的描述:

S7-1200 可编程控制器

列出所有多播虚拟接口;它显示多播虚拟设备表(vif_table)中的所有vif_device对象。显示/proc/net/ip_mr_vif entryipmr_vif_seq_show()方法处理。

/proc/net/ip_mr_cache

多播转发缓存(MFC)的状态。该条目显示了所有缓存条目的以下字段:组组播地址(mfc_mcastgrp)、源 IP 地址(mfc_origin)、输入接口索引(mfc_parent)、转发数据包(mfc_un.res.pkt)、转发字节(mfc_un.res.bytes)、错误接口索引(mfc_un.res.wrong_if)、转发接口索引(vif_table中的一个索引)以及该索引对应的mfc_un.res.ttls数组中的条目。显示/proc/net/ip_mr_cache条目由ipmr_mfc_seq_show()方法处理。

桌子

最后,在表格 6-1 中,是规则选择器的表格。

表 6-1 。IP 规则选择器

Tab06-01.jpg

七、Linux 相邻子系统

本章讨论了 Linux 相邻子系统及其在 Linux 中的实现。相邻子系统负责发现同一链路上节点的存在,并负责将 L3(网络层)地址转换为 L2(链路层)地址。正如下一节所述,需要 L2 地址来构建外发数据包的 L2 报头。实现这种转换的协议在 IPv4 中称为地址解析协议(ARP ),在 IPv6 中称为邻居发现协议(ndISC 或 ND)。相邻子系统提供了用于执行 L3 到 L2 映射的独立于协议的基础设施。然而,本章的讨论仅限于最常见的情况,即 IPv4 和 IPv6 中相邻子系统的使用。请记住,ARP 协议就像在第三章中讨论的 ICMP 协议一样,容易受到安全威胁——例如 ARP 中毒攻击和 ARP 欺骗攻击(ARP 协议的安全方面超出了本书的范围)。

在这一章中,我首先讨论了常见的相邻数据结构和一些重要的 API 方法,它们在 IPv4 和 IPv6 中都有使用。然后讨论了 ARP 协议和 NDISC 协议的具体实现。您将看到邻居是如何创建和释放的,并且您将了解用户空间和相邻子系统之间的交互。您还将了解 ARP 请求和 ARP 回复、NDISC 邻居请求和 NDISC 邻居通告,以及 NDISC 协议用来避免重复 IPv6 地址的重复地址检测(DAD)机制。

相邻子系统核心

相邻子系统的用途是什么?当数据包通过 L2 层发送时,需要 L2 目的地址来构建 L2 报头。使用相邻子系统请求和请求回复,给定主机的 L3 地址(或者这种 L3 地址不存在的事实),可以找出主机的 L2 地址。在最常用的链路层(L2)以太网中,主机的 L2 地址就是其 MAC 地址。在 IPv4 中,ARP 是相邻协议,请求请求和请求回复分别称为 ARP 请求和 ARP 回复。在 IPv6 中,邻居协议是 NDISC,请求请求和请求回复分别称为邻居请求和邻居通告,。

有些情况下,不需要相邻子系统的任何帮助就可以找到目的地址,例如发送广播时。在这种情况下,目的 L2 地址是固定的(例如,在以太网中是 FF:FF:FF:FF:FF:FF)。或者当目的地址是多播地址时,L3 多播地址与其 L2 地址之间存在固定的映射。我将在本章中讨论这种情况。

Linux 相邻子系统的基本数据结构是邻居。邻居代表连接到同一链路(L2)的网络节点。它由neighbour结构表示。这种表示对于特定的协议不是唯一的。然而,如上所述,neighbour结构的讨论将限于其在 IPv4 和 IPv6 协议中的使用。让我们看看neighbour的结构:

struct neighbour {
        struct neighbour __rcu  *next;
        struct neigh_table      *tbl;
        struct neigh_parms      *parms;
        unsigned long           confirmed;
        unsigned long           updated;
        rwlock_t                lock;
        atomic_t                refcnt;
        struct sk_buff_head     arp_queue;
        unsigned int            arp_queue_len_bytes;
        struct timer_list       timer;
        unsigned long           used;
        atomic_t                probes;
        __u8                    flags;
        __u8                    nud_state;
        __u8                    type;
        __u8                    dead;
        seqlock_t               ha_lock;
        unsigned char           ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
        struct hh_cache         hh;
        int                     (*output)(struct neighbour *, struct sk_buff *);
        const struct neigh_ops  *ops;
        struct rcu_head         rcu;
        struct net_device       *dev;
        u8                      primary_key[0];
};
(include/net/neighbour.h)

以下是对neighbour结构中一些重要成员的描述:

  • next:指向哈希表中同一桶上的下一个邻居的指针。

  • tbl:与该邻居关联的邻居表。

  • parms:与此neighbour关联的neigh_parms对象 。它由相关邻表的constructor方法初始化。例如,在 IPv4 中,arp_constructor()方法将parms初始化为相关网络设备的arp_parms。不要将其与邻桌的neigh_parms对象混淆。

  • confirmed:确认时间戳(本章稍后讨论)。

  • refcnt:参考计数器。 通过neigh_hold()宏递增,通过neigh_release()方法递减。只有在引用计数器递减后,其值为 0 时,neigh_release()方法才通过调用neigh_destroy()方法来释放邻居对象。

  • arp_queue:未解析 skb 的队列。尽管有这个名字,这个成员并不是 ARP 所独有的,而是被其他协议所使用,例如 NDISC 协议。

  • timer:每个neighbour对象都有一个定时器;计时器回调是neigh_timer_handler()方法。neigh_timer_handler()方法可以改变邻居的网络不可达检测(NUD)状态。发送邀约请求时,邻居状态为 NUD _ 未完成或 NUD _ 探测,且邀约请求探测数大于等于neigh_max_probes(),则邻居状态设置为 NUD _ 失败,调用neigh_invalidate()方法。

  • ha_lock:提供对邻居硬件地址的访问保护(ha)。

  • ha:邻居对象的硬件地址;在以太网的情况下,它是邻居的 MAC 地址。

  • hh:L2 头的硬件头缓存(一个hh_cache对象)。

  • output: A pointer to a transmit method, like the neigh_resolve_output() method or the neigh_direct_output() method. It is dependent on the NUD state and as a result can be assigned to different methods during a neighbour lifetime. When initializing the neighbour object in the neigh_alloc() method, it is set to be the neigh_blackhole() method, which discards the packet and returns -ENETDOWN.

    下面是助手方法(设置output回调的方法):

  • void neigh_connect(struct neighbour *neigh)

    将指定邻居的output()方法设置为neigh->ops->connected_output

  • void neigh_suspect(struct neighbour *neigh)

    将指定邻居的output()方法设置为neigh->ops->output

  • nud_state:NUD 州的邻居。nud_state值可以在邻居对象的生命周期内动态改变。本章末尾“快速参考”部分的表 7-1 描述了基本的 NUD 状态及其 Linux 符号。NUD 国家机器非常复杂;在本书中,我没有深入探究它的所有细微差别。

  • dead:neighbour对象活着时,设置标志 t hat。当创建一个neighbour对象时,在__neigh_create()方法结束时,它被初始化为 0。对于没有设置dead标志的邻居对象,neigh_destroy()方法将失败。neigh_flush_dev()方法将dead标志设置为 1,但是还没有删除邻居条目。被标记为死亡的邻居(它们的dead标志被置位)的移除稍后由垃圾收集器完成。

  • 邻居的 IP 地址(L3)。使用primary_key在相邻表中进行查找。primary_key长度 基于所使用的协议。例如,对于 IPv4,它应该是 4 个字节。对于 IPv6,它应该是sizeof(struct in6_addr),因为in6_addr结构代表一个 IPv6 地址。因此,primary_key被定义为 0 字节的数组,在分配邻居时,应考虑使用哪种协议。参见本章后面关于entry_sizekey_len的解释,在neigh_table结构成员的描述中。

为了避免为每个传输的新数据包发送请求,内核将 L3 地址和 L2 地址之间的映射保存在一个称为邻表的数据结构中;在 IPv4 的情况下,它是 ARP 表(有时也称为 ARP 缓存,尽管它们是相同的)——与您在第五章中看到的 IPv4 路由子系统相反:路由缓存在被删除之前,和路由表是两个不同的实体,由两种不同的数据结构表示。在 IPv6 的情况下,相邻表是 NDISC 表(也称为 NDISC 缓存)。ARP 表(arp_tbl)和 NDISC 表(nd_tbl)都是neigh_table结构的实例。我们来看看neigh_table的结构:

struct neigh_table {
        struct neigh_table      *next;
        int                     family;
        int                     entry_size;
        int                     key_len;
        __u32                   (*hash)(const void *pkey,
                                        const struct net_device *dev,
                                        __u32 *hash_rnd);
        int                     (*constructor)(struct neighbour *);
        int                     (*pconstructor)(struct pneigh_entry *);
        void                    (*pdestructor)(struct pneigh_entry *);
        void                    (*proxy_redo)(struct sk_buff *skb);
        char                    *id;
        struct neigh_parms      parms;
        /* HACK. gc_* should follow parms without a gap! */
        int                     gc_interval;
        int                     gc_thresh1;
        int                     gc_thresh2;
        int                     gc_thresh3;
        unsigned long           last_flush;
        struct delayed_work     gc_work;
        struct timer_list       proxy_timer;
        struct sk_buff_head     proxy_queue;
        atomic_t                entries;
        rwlock_t                lock;
        unsigned long           last_rand;
        struct neigh_statistics __percpu *stats;
        struct neigh_hash_table __rcu *nht;
        struct pneigh_entry     **phash_buckets;
};
(include/net/neighbour.h)

以下是neigh_table结构中的一些重要成员:

  • next:每个协议创建自己的neigh_table实例。系统中有一个所有相邻表的链表。neigh_tables全局变量是一个指向列表开头的指针。next变量指向列表中的下一项。

  • family:协议族:IPv4 邻居表的 AF _ INET(arp_tbl),IPv6 邻居表的 AF _ INET 6(nd_tbl)。

  • entry_size:通过neigh_alloc()方法分配邻居条目时,分配的大小为tbl->entry_size + dev->neigh_priv_len。通常neigh_priv_len值为 0。在内核 3.3 之前,entry_size被显式初始化为 ARP 的sizeof(struct neighbour) + 4,NDISC 的sizeof(struct neighbour) + sizeof(struct in6_addr)。这种初始化的原因是,在分配邻居时,您还想为primary_key[0]成员分配空间。从内核 3.3 开始,enrty_size被从arp_tblndisc_tbl的静态初始化中移除,entry_size初始化是基于核心相邻层中的key_len通过neigh_table_init_no_netlink()方法完成的。

  • key_len:查找键的大小;对于 IPv4 是 4 字节,因为 IPv4 地址的长度是 4 字节,对于 IPv6 是sizeof (struct in6_addr)in6_addr结构代表一个 IPv6 地址。

  • hash:用于将关键字(L3 地址)映射到特定散列值的散列函数;对于 ARP,它是arp_hash()方法。对于 NDISC,这是一种ndisc_hash()方法。

  • constructor:该方法在创建neighbour对象时执行 协议特定的初始化。比如 IPv4 中 ARP 的arp_constructor(),IPv6 中 NDISC 的ndisc_constructor()constructor回调由__neigh_create()方法调用。如果成功,它返回 0。

  • pconstructor:用于创建邻居代理条目的方法;ARP 不用,NDISC 用的是pndisc_constructor。此方法应该在成功时返回 0。如果查找失败,则从pneigh_lookup()方法中调用pconstructor方法,条件是用creat = 1调用了pneigh_lookup()

  • pdestructor:销毁邻居代理条目的方法。和pconstructor回调一样,pdestructor不是 ARP 用的,是 NDISC 用的pndisc_destructorpdestructor方法是从pneigh_delete()方法和pneigh_ifdown()方法中调用的。

  • id:表格的名称;IPv4 是arp_cache,IPv6 是ndisc_cache

  • parms:一个neigh_parms对象:每个相邻的表都有一个关联的neigh_parms对象,它由各种配置设置组成,比如可达性信息、各种超时等等。ARP 表和 NDISC 表中的neigh_parms初始化不同。

  • gc_interval:不被相邻核心直接使用。

  • gc_thresh1gc_thresh2gc_thresh3:邻表条目数的阈值。用作激活同步垃圾收集器(neigh_forced_gc)的标准,并在neigh_periodic_work()异步垃圾收集器处理器中使用。请参阅本章后面的“创建和释放邻居”一节中关于分配邻居对象的解释。在 ARP 表中,默认值为:gc_thresh1是 128,gc_thresh2是 512,gc_thresh3是 1024。这些值可以通过procfs设置。IPv6 中的 NDISC 表也使用相同的默认值。IPv4 procfs条目是:

  • /proc/sys/net/ipv4/neigh/default/gc_thresh1

  • /proc/sys/net/ipv4/neigh/default/gc_thresh2

  • /proc/sys/net/ipv4/neigh/default/gc_thresh3

对于 IPv6,这些是procfs条目:

  • /proc/sys/net/ipv6/neigh/default/gc_thresh1
  • /proc/sys/net/ipv6/neigh/default/gc_thresh2
  • /proc/sys/net/ipv6/neigh/default/gc_thresh3
  • last_flush:最近一次运行neigh_forced_gc()方法的时间。在neigh_table_init_no_netlink ()方法中被初始化为当前时间(jiffies)。
  • gc_work:异步垃圾收集器处理程序。通过neigh_table_init_no_netlink()方法设置为neigh_periodic_work()定时器。delayed_work struct是一种工作队列。在内核 2.6.32 之前,neigh_periodic_timer()方法是异步垃圾收集器处理程序;它只处理一个桶,而不是整个相邻哈希表。neigh_periodic_work()方法首先检查表中的条目数是否小于gc_thresh1,如果是,则不做任何事情就退出;然后它重新计算可到达时间(parmsreachable_time字段,它是与邻表关联的neigh_parms对象)。然后,它扫描相邻哈希表,并删除其状态不是 NUD _ 永久或 NUD _ 计时器,并且其引用计数为 1 的条目,如果满足这些条件之一:它们处于 NUD _ 失败状态,或者当前时间在它们的used时间戳+ gc_staletime之后(gc_staletimeneighbour parms对象的成员)。通过将dead标志设置为 1 并调用neigh_cleanup_and_release()方法来移除邻居条目。
  • proxy_timer:当一台主机被配置为 ARP 代理时,可以避免立即处理请求,而是延迟处理。这是因为对于 ARP 代理主机,可能会有大量的请求(与主机不是 ARP 代理的情况相反,在这种情况下,您通常会有少量的 ARP 请求)。有时,您可能希望延迟对此类广播的回复,以便让拥有此类 IP 地址的主机优先获得请求。该延迟是达到proxy_delay参数的随机值。ARP 代理定时器处理程序是neigh_proxy_process()方法。proxy_timer是由neigh_table_init_no_netlink()方法初始化的。
  • proxy_queue:SKBs 的代理 ARP 队列。skb 是用pneigh_enqueue()的方法添加的。
  • stats:邻居统计(neigh_statistics)对象;由每 CPU 计数器组成,如allocs,它是由neigh_alloc()方法分配的邻居对象的数量,或destroys,它是由neigh_destroy()方法释放的邻居对象的数量,等等。邻居统计计数器由 NEIGH_CACHE_STAT_INC 宏递增。请注意,因为统计是针对每个 CPU 计数器的,所以这个宏使用了宏this_cpu_inc()。您可以分别用cat /proc/net/stat/arp_cachecat/proc/net/stat/ndisc_cache显示 ARP 统计和 NDISC 统计。在本章末尾的“快速参考”部分,有一个对neigh_statistics结构的描述,说明了每个计数器递增的方法。
  • nht:邻居哈希表(neigh_hash_table对象)。
  • phash_buckets : 邻居代理哈希表;在neigh_table_init_no_netlink()方法中分配。

邻表的初始化通过neigh_table_init()方法完成:

  • 在 IPv4 中,ARP 模块定义 ARP 表(名为arp_tblneigh_table结构的一个实例)并将其作为参数传递给neigh_table_init()方法(参见net/ipv4/arp.c中的arp_init()方法)。
  • 在 IPv6 中,NDISC 模块定义了 NDSIC 表(也是名为nd_tblneigh_table结构的一个实例),并将其作为参数传递给neigh_table_init()方法(参见net/ipv6/ndisc.c中的ndisc_init()方法)。

neigh_table_init()方法还通过调用neigh_table_init_no_netlink()、方法中的neigh_hash_alloc()方法为八个散列条目分配空间来创建相邻的散列表(nht对象):

static void neigh_table_init_no_netlink(struct neigh_table *tbl)
{
    . . .
    RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3));
    . . .
}

static struct neigh_hash_table *neigh_hash_alloc(unsigned int shift)
{

哈希表的大小是1<< shift (when size <= PAGE_SIZE):

    size_t size = (1 << shift) * sizeof(struct neighbour *);
    struct neigh_hash_table *ret;
    struct neighbour __rcu **buckets;
    int i;

    ret = kmalloc(sizeof(*ret), GFP_ATOMIC);
    if (!ret)
        return NULL;
    if (size <= PAGE_SIZE)
        buckets = kzalloc(size, GFP_ATOMIC);
    else
        buckets = (struct neighbour __rcu **)
              __get_free_pages(GFP_ATOMIC | __GFP_ZERO,
                       get_order(size));
    . . .

}

您可能想知道为什么需要neigh_table_init_no_netlink()方法——为什么不在neigh_table_init()方法中执行所有的初始化?neigh_table_init_no_netlink()方法执行相邻表的所有初始化,除了将它链接到相邻表的全局链表neigh_tables。本来这样的初始化,没有链接到neigh_tables链表,是 ATM 需要的,结果neigh_table_init()方法被拆分,ATM clip 模块调用了neigh_table_init_no_netlink()方法,而不是调用neigh_table_init()方法;然而,随着时间的推移,在 ATM 中发现了不同的解决方案。虽然 ATM clip 模块不再调用neigh_table_init_no_netlink()方法,但是这些方法的分离仍然存在,也许将来会需要。

我应该提到,使用相邻子系统的每个 L3 协议也注册了一个协议处理程序:对于 IPv4,ARP 包(其以太网报头中的类型是 0x0806 的包)的处理程序是arp_rcv()方法:

static struct packet_type arp_packet_type __read_mostly = {
         .type = cpu_to_be16(ETH_P_ARP),
         .func = arp_rcv,
 };

 void __init arp_init(void)
 {
     . . .
         dev_add_pack(&arp_packet_type);
     . . .
}
(net/ipv4/arp.c)

对于 IPv6,相邻消息是 ICMPv6 消息,因此它们由 ICMPv6 处理程序icmpv6_rcv()方法处理。有五个 ICMPv6 相邻消息;当(通过icmpv6_rcv()方法)收到它们中的每一个时,调用ndisc_rcv()方法来处理它们(见net/ipv6/icmp.c)。ndisc_rcv()方法将在本章后面的章节中讨论。每个邻居对象通过neigh_ops结构定义了一组方法。这是通过它的constructor方法完成的。neigh_ops结构包含一个协议族成员和四个函数指针:

struct neigh_ops {
        int      family;
        void     (*solicit)(struct neighbour *, struct sk_buff *);
        void     (*error_report)(struct neighbour *, struct sk_buff *);
        int      (*output)(struct neighbour *, struct sk_buff *);
        int      (*connected_output)(struct neighbour *, struct sk_buff *);
};
(include/net/neighbour.h)
  • family:IP v4 的 AF_INET,IPv6 的 AF_INET6。
  • solicit:这个方法负责发送邻居请求:在 ARP 中是arp_solicit()方法,在 NDISC 中是ndisc_solicit()方法。
  • error_report:当邻居状态为 NUD _ 失败时,从neigh_invalidate()方法调用该方法。例如,当请求请求未被回复时,在某个超时之后会发生这种情况。
  • output:下一跳 L3 地址已知,但 L2 地址未解析时,output回调应为neigh_resolve_output()
  • connected_output:邻居状态为 NUD _ 可达或 NUD _ 已连接时,邻居的输出方式设置为connected_output()。参见neigh_update()方法和neigh_timer_handler()方法中的neigh_connect()调用。

创建和释放邻居

通过__neigh_create()方法创建邻居:

struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct                    net_device *dev, bool want_ref)

首先,__neigh_create()方法通过调用neigh_alloc()方法分配一个邻居对象,该方法也执行各种初始化。有些情况下,neigh_alloc()方法调用同步垃圾收集器(也就是neigh_forced_gc()方法):

static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
        struct neighbour *n = NULL;
        unsigned long now = jiffies;
        int entries;

        entries = atomic_inc_return(&tbl->entries) - 1;

如果表条目的数量大于gc_thresh3(默认为 1024)或者如果表条目的数量大于gc_thresh2(默认为 512),并且自上次刷新以来经过的时间大于 5 Hz,则调用同步垃圾收集器方法(neigh_forced_gc()方法)。如果在运行了neigh_forced_gc()方法之后,表条目的数量大于gc_thresh3 (1024),你不分配一个邻居对象并返回 NULL:

        if (entries >= tbl->gc_thresh3 ||
            (entries >= tbl->gc_thresh2 &&
            time_after(now, tbl->last_flush + 5 * HZ))) {
               if (!neigh_forced_gc(tbl) &&
                   entries >= tbl->gc_thresh3)
                       goto out_entries;
}

然后,__neigh_create()方法通过调用指定邻居表的constructor方法来执行特定于协议的设置(ARP 使用arp_constructor(),NDISC 使用ndisc_constructor())。在构造器方法中,处理特殊情况,如多播或回送地址。在arp_constructor()方法中,比如你调用arp_mc_map()方法根据邻居 IPv4 primary_key地址设置邻居的硬件地址(ha),你设置nud_state为 NUD _ 诺阿普,因为组播地址不需要 ARP。例如,在ndisc_constructor()方法中,当处理多播地址时,您做了一些非常类似的事情:您调用ndisc_mc_map()来根据邻居 IPv6 primary_key地址设置邻居的硬件地址ha,并且您再次将nud_state设置为 NUD _ 诺阿普。对于广播地址也有特殊的处理:在arp_constructor()方法中,例如,当邻居类型是 RTN_BROADCAST 时,你设置邻居硬件地址(ha)为网络设备广播地址(net_device对象的broadcast字段),你设置nud_state为 NUD_NOARP。注意,IPv6 协议不实现传统的 IP 广播,因此广播地址的概念是不相关的(尽管在地址ff02::1有一个链路本地所有节点多播组)。有两种特殊情况需要进行额外的设置:

  • netdev_opsndo_neigh_construct()回调被定义时,它被调用。事实上,这仅在经典的 IP over ATM 代码中完成(clip);参见net/atm/clip.c
  • neigh_parms对象的neigh_setup()回调被定义时,它被调用。例如,这用于绑定驱动程序中;参见drivers/net/bonding/bond_main.c

当试图通过__neigh_create()方法创建一个neighbour对象时,如果邻居条目的数量超过了哈希表的大小,则必须将其扩大。这是通过调用neigh_hash_grow()方法来完成的,就像这样:

struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey,
                 struct net_device *dev, bool want_ref)
{
     . . .

哈希表大小为1 << nht->hash_shift;如果超过,哈希表必须扩大:

     if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
        nht = neigh_hash_grow(tbl, nht->hash_shift + 1);
     . . .
}

want_ref参数为真时,您将在该方法中增加邻居引用计数。您还初始化了neighbour对象的confirmed字段:

n->confirmed = jiffies - (n->parms->base_reachable_time << 1);

它被初始化为略小于当前时间jiffies(原因很简单,您希望更快地要求可达性确认)。在__neigh_create()方法结束时,dead标志被初始化为 0,并且neighbour对象被添加到邻居散列表中。

neigh_release()方法通过调用neigh_destroy()方法,递减邻居的引用计数器,并在到达零时释放它。neigh_destroy()方法将验证邻居被标记为dead:其dead标志为 0 的邻居不会被移除。

在本节中,您了解了创建和释放邻居的内核方法。接下来,您将学习如何从用户空间触发添加和删除邻居条目,以及如何显示邻居表,对于 IPv4 使用arp命令,对于 IPv4/IPv6 使用ip命令。

用户空间和相邻子系统之间的交互

ARP 表的管理是通过iproute2包的ip neigh命令或者net-tools包的arp命令来完成的。因此,您可以通过从命令行运行以下命令之一来显示 ARP 表:

  • arp:通过net/ipv4/arp.c中的arp_seq_show()方式处理。
  • ip neigh show(或ip neighbour show):通过net/core/neighbour.c中的neigh_dump_info()方式处理。

注意,ip neigh show命令显示了相邻表条目的 NUD 状态(如 NUD 可达或 NUD 失效)。还要注意,arp命令只能显示 IPv4 邻居表(ARP 表),而使用ip命令可以显示 IPv4 ARP 表和 IPv6 邻居表。如果您想只显示 IPv6 邻居表,您应该运行ip -6 neigh show

ARP 和 NDISC 模块也通过procfs导出数据。这意味着您可以通过运行cat /proc/net/arp来显示 ARP 表(这个procfs条目由arp_seq_show()方法处理,正如前面提到的,该方法与处理arp命令的方法相同)。或者可以通过cat /proc/net/stat/arp_cache显示 ARP 统计,通过cat /proc/net/stat/ndisc_cache显示 NDISC 统计(两者都是通过neigh_stat_seq_show()方法处理的)。

可以用ip neigh add添加一个条目,由neigh_add()方法处理。运行ip neigh add时,您可以指定正在添加的条目的状态(比如 NUD _ 永久、NUD _ 陈旧、NUD _ 可达等等)。例如:

ip neigh add 192.168.0.121 dev eth0 lladdr 00:30:48:5b:cc:45 nud permanent

删除一个条目可以通过ip neigh del完成,并由neigh_delete()方法处理。例如:

ip neigh del 192.168.0.121 dev eth0

可以使用ip neigh add proxy向代理 ARP 表添加条目。例如:

ip neigh add proxy 192.168.2.11 dev eth0

加法由neigh_add()方法再次处理。在这种情况下,在从用户空间传递的数据中设置 NTF _ 代理标志(参见ndm对象的ndm_flags字段),因此调用pneigh_lookup()方法在代理邻居哈希表(phash_buckets)中执行查找。在查找失败的情况下,pneigh_lookup()方法向代理邻居哈希表添加一个条目。

可以用ip neigh del proxy从代理 ARP 表中删除一个条目。例如:

ip neigh del proxy 192.168.2.11 dev eth0

删除由neigh_delete()方法处理。同样,在这种情况下,在从用户空间传递的数据中设置 NTF _ 代理标志(参见ndm对象的ndm_flags字段),因此调用pneigh_delete()方法从代理邻居表中删除条目。

使用ip ntable命令,您可以控制相邻表的参数。例如:

  • ip ntable show:显示所有相邻表的参数。
  • ip ntable change:改变邻表的参数值。由neightbl_set()方法处理。例如:ip ntable change name arp_cache queue 20 dev eth0

您也可以通过arp add向 ARP 表添加条目。并且可以手动向 ARP 表添加静态条目,就像这样:arp –s <IPAddress> <MacAddress>。相邻子系统垃圾收集器不会删除静态 ARP 条目,但是它们不会在重新启动后保持不变。

下一节将简要描述相邻子系统如何处理网络事件。

处理网络事件

相邻内核不会用register_netdevice_notifier()方法注册任何事件。另一方面,ARP 模块和 NDISC 模块会注册网络事件。在 ARP 中,arp_netdev_event()方法被注册为netdev事件的回调。它通过调用通用的neigh_changeaddr()方法和调用rt_cache_flush()方法来处理 MAC 地址事件的变化。从内核 3.11 开始,当 IFF_NOARP 标志发生变化时,可以通过调用neigh_changeaddr()方法来处理 NETDEV_CHANGE 事件。当设备通过__dev_notify_flags()方法改变其标志,或者当设备通过netdev_state_change()方法改变其状态时,NETDEV_CHANGE 事件被触发。在 NDISC 中,ndisc_netdev_event()方法被注册为 netdev 事件的回调;它处理 NETDEV_CHANGEADDR、NETDEV_DOWN 和 NETDEV_NOTIFY_PEERS 事件。

在描述了 IPv4 和 IPv6 共有的基本数据结构,如邻居表(neigh_table)和neighbour结构,并讨论了如何创建和释放neighbour对象之后,是时候描述第一个邻居协议 ARP 协议的实现了。

ARP 协议(IPv4)

RFC 826 中定义了 ARP 协议。使用以太网时,这些地址被称为 MAC 地址,是 48 位值。MAC 地址应该是唯一的,但是您必须考虑到您可能会遇到不唯一的 MAC 地址。一个常见的原因是,在大多数网络接口上,系统管理员可以使用像ifconfigip这样的用户空间工具来配置 MAC 地址。

发送 IPv4 数据包时,您知道目的 IPv4 地址。您应该构建一个以太网报头,其中应该包含目的 MAC 地址。根据给定的 IPv4 地址查找 MAC 地址是由 ARP 协议完成的,您很快就会看到这一点。如果 MAC 地址未知,您可以通过广播发送 ARP 请求。这个 ARP 请求包含您正在寻找的 IPv4 地址。如果存在具有此类 IPv4 地址的主机,该主机将发送单播 ARP 响应作为回复。ARP 表(arp_tbl)是neigh_table结构的一个实例。ARP 报头由arphdr结构表示:

struct arphdr {
    __be16          ar_hrd;         /* format of hardware address   */
    __be16          ar_pro;         /* format of protocol address   */
    unsigned char   ar_hln;         /* length of hardware address   */
    unsigned char   ar_pln;         /* length of protocol address   */
    __be16          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
};
(include/uapi/linux/if_arp.h)

以下是对arphdr结构中一些重要成员的描述:

  • ar_hrd是硬件类型;对于以太网,它是 0x01。有关可用 ARP 报头硬件标识符的完整列表,请参见include/uapi/linux/if_arp.h中的 ARPHRD_XXX 定义。
  • ar_pro是协议 ID;对于 IPv4,它是 0x80。有关可用协议 id 的完整列表,请参见include/uapi/linux/if_ether.h中的 ETH_P_XXX。
  • ar_hln是以字节为单位的硬件地址长度,以太网地址为 6 字节。
  • ar_pln是协议地址的长度,以字节为单位,IPv4 地址为 4 字节。
  • ar_op是操作码,ARP 请求的 ARPOP_REQUEST,ARP 回复的 ARPOP_REPLY。有关可用 ARP 报头操作码的完整列表,请查看include/uapi/linux/if_arp.h

紧跟在ar_op之后的是发送方硬件(MAC)地址和 IPv4 地址,以及目标硬件(MAC)地址和 IPv4 地址。这些地址不是 ARP 报头(arphdr)结构的一部分。在arp_process()方法、中,它们是通过读取 ARP 报头的相应偏移量来提取的,您可以在本章后面的“ARP:接收请求和回复”一节中看到关于arp_process()方法的解释。图 7-1 显示了一个 ARP 以太网数据包的 ARP 报头。

9781430261964_Fig07-01.jpg

图 7-1 。ARP 报头(用于以太网)

在 ARP 中,定义了四个neigh_ops对象:arp_direct_opsarp_generic_opsarp_hh_opsarp_broken_ops。ARP 表neigh_ops对象的初始化由arp_constructor()方法完成,基于网络设备特性:

  • 如果net_device对象的header_ops为空,则neigh_ops对象将被设置为arp_direct_ops。在这种情况下,将使用neigh_direct_output()方法发送数据包,这实际上是对dev_queue_xmit()的包装。然而,在大多数以太网设备中,net_device对象的header_ops被通用的ether_setup()方法初始化为eth_header_ops;参见net/ethernet/eth.c
  • 如果net_device对象的header_ops包含一个空的cache()回调,那么neigh_ops对象将被设置为arp_generic_ops
  • 如果net_device对象的header_ops包含一个非空的cache()回调,那么neigh_ops对象将被设置为arp_hh_ops。在使用通用eth_header_ops对象的情况下,cache()回调就是eth_header_cache()回调。
  • 对于三种类型的设备,neigh_ops对象将被设置为arp_broken_ops(当net_device对象的类型为 ARPHRD_ROSE、ARPHRD_AX25 或 ARPHRD_NETROM 时)。

现在我已经介绍了 ARP 协议和 ARP 头(arphdr)对象,让我们看看 ARP 请求是如何发送的。

ARP:发送征求请求

征集请求发送到哪里?最常见的情况是在 Tx 路径中,在实际离开网络层(L3)并移动到链路层(L2)之前。在ip_finish_output2()方法中,首先通过调用__ipv4_neigh_lookup_noref()方法在 ARP 表中查找下一跳 IPv4 地址,如果没有找到任何匹配的邻居条目,则通过调用__neigh_create()方法创建一个条目:

static inline int ip_finish_output2(struct sk_buff *skb)
{
        struct dst_entry *dst = skb_dst(skb);
        struct rtable *rt = (struct rtable *)dst;
        struct net_device *dev = dst->dev;
        unsigned int hh_len = LL_RESERVED_SPACE(dev);
        struct neighbour *neigh;
        u32 nexthop;
        . . .
        . . .
        nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
        neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
        if (unlikely(!neigh))
                neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
        if (!IS_ERR(neigh)) {
                int res = dst_neigh_output(dst, neigh, skb);
     . . .
}

让我们来看看dst_neigh_output()法 :

static inline int dst_neigh_output(struct dst_entry *dst, struct neighbour *n,
                                   struct sk_buff *skb)
{
        const struct hh_cache *hh;

        if (dst->pending_confirm) {
                unsigned long now = jiffies;

                dst->pending_confirm = 0;
                /* avoid dirtying neighbour */
                if (n->confirmed != now)
                        n->confirmed = now;
        }

当你用这个流程第一次到达这个方法的时候,nud_state不是 NUD _ 连接的,输出回调是neigh_resolve_output()方法 :

        hh = &n->hh;
        if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
                return neigh_hh_output(hh, skb);
        else
                return n->output(n, skb);
}
(include/net/dst.h)

neigh_resolve_output()方法中,你调用neigh_event_send()方法,最终通过__skb_queue_tail(&neigh->arp_queue, skb)将 SKB 放入邻居的arp_queue;稍后,从邻居定时器处理程序neigh_timer_handler()调用的neigh_probe()方法将通过调用solicit()方法 ( neigh->ops->solicit在我们的例子中是arp_solicit()方法):来发送数据包

static void neigh_probe(struct neighbour *neigh)
        __releases(neigh->lock)
{
        struct sk_buff *skb = skb_peek(&neigh->arp_queue);
        . . .
        neigh->ops->solicit(neigh, skb);
        atomic_inc(&neigh->probes);
        kfree_skb(skb);
}

让我们来看看arp_solicit()方法,它实际上发送了 ARP 请求:

static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb)
{
        __be32 saddr = 0;
        u8 dst_ha[MAX_ADDR_LEN], *dst_hw = NULL;
        struct net_device *dev = neigh->dev;
        __be32 target = *(__be32 *)neigh->primary_key;
        int probes = atomic_read(&neigh->probes);
        struct in_device *in_dev;

        rcu_read_lock();
        in_dev = __in_dev_get_rcu(dev);
        if (!in_dev) {
                rcu_read_unlock();
                return;
        }

使用arp_announce procfs条目,您可以为要发送的 ARP 数据包设置使用本地源 IP 地址的限制:

  • 0: 使用在任何接口上配置的任何本地地址。这是默认值。
  • 首先尝试使用目标子网上的地址。如果没有这样的地址,请使用第 2 级。
  • 2: 使用主 IP 地址。

请注意,使用了这两个条目的最大值:

/proc/sys/net/ipv4/conf/all/arp_announce
/proc/sys/net/ipv4/conf/<netdeviceName>/arp_announce

另请参见本章末尾“快速参考”一节中对 IN_DEV_ARP_ANNOUNCE 宏的描述。

switch (IN_DEV_ARP_ANNOUNCE(in_dev)) {
default:
case 0:         /* By default announce any local IP */
                 if (skb && inet_addr_type(dev_net(dev),
                                           ip_hdr(skb)->saddr) == RTN_LOCAL)
                         saddr = ip_hdr(skb)->saddr;
                 break;
case 1:         /* Restrict announcements of saddr in same subnet */
                 if (!skb)
                 break;
                 saddr = ip_hdr(skb)->saddr;
                 if (inet_addr_type(dev_net(dev), saddr) == RTN_LOCAL) {

inet_addr_onlink()方法 检查指定的目标地址和指定的源地址是否在同一个子网内:

                 /* saddr should be known to target */
                 if (inet_addr_onlink(in_dev, target, saddr))
                         break;
         }
         saddr = 0;
         break;
case 2:         /* Avoid secondary IPs, get a primary/preferred one */
         break;
}
rcu_read_unlock();

if (!saddr)

inet_select_addr()方法返回指定设备的第一个主接口的地址,该设备的作用域小于指定的作用域(在本例中为 RT_SCOPE_LINK),并且与目标位于同一个子网:

        saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);

probes -= neigh->parms->ucast_probes;
if (probes < 0) {
                if (!(neigh->nud_state & NUD_VALID))
                        pr_debug("trying to ucast probe in NUD_INVALID\n");
                neigh_ha_snapshot(dst_ha, neigh, dev);
                dst_hw = dst_ha;
} else {
                probes -= neigh->parms->app_probes;
                if (probes < 0) {

在使用用户空间 ARP 守护进程时设置配置 ARP 有像 OpenNHRP 这样的项目,它们是基于 ARPD 的。下一跳解析协议(NHRP) 用于提高通过非广播多路访问(NBMA) 网络路由计算机网络流量的效率(我在本书中不讨论 ARPD 用户空间守护进程):

#ifdef CONFIG_ARPD
                        neigh_app_ns(neigh);
#endif
                        return;
                }
        }

现在您调用arp_send()方法来发送一个 ARP 请求。注意,最后一个参数target_hw为空。您还不知道目标硬件(MAC)地址。当target_hw为空呼叫arp_send()时,发送广播 ARP 请求:

        arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,
                 dst_hw, dev->dev_addr, NULL);
}

我们来看看arp_send()法,挺短的:

void arp_send(int type, int ptype, __be32 dest_ip,
              struct net_device *dev, __be32 src_ip,
              const unsigned char *dest_hw, const unsigned char *src_hw,
              const unsigned char *target_hw)
{
        struct sk_buff *skb;

        /*
         *      No arp on this interface.
         */

您必须检查该网络设备是否支持 IFF_NOARP。存在 ARP 被禁用的情况:例如,管理员可以通过ifconfig eth1 –arpip link set eth1 arp off禁用 ARP。一些网络设备在创建时设置 IFF_NOARP 标志,例如,IPv4 隧道设备或 PPP 设备,它们不需要 ARP。参见net/ipv4/ipip.c中的ipip_tunnel_setup()方法或drivers/net/ppp_generic.c中的ppp_setup()方法。

        if (dev->flags&IFF_NOARP)
                return;

arp_create()方法 创建一个带有 ARP 头的 SKB,并根据指定的参数对其进行初始化:

skb = arp_create(type, ptype, dest_ip, dev, src_ip,
                         dest_hw, src_hw, target_hw);
if (skb == NULL)
                return;

arp_xmit()方法唯一做的事情是通过 NF_HOOK()宏 : 调用dev_queue_xmit()

        arp_xmit(skb);
}

现在是时候了解如何处理这些 ARP 请求以及如何处理 ARP 回复了。

ARP:接收请求和回复

在 IPv4 中,arp_rcv()方法负责处理 ARP 数据包,如前所述。我们来看看arp_rcv()的方法:

static int arp_rcv(struct sk_buff *skb, struct net_device *dev,
                   struct packet_type *pt, struct net_device *orig_dev)
{
        const struct arphdr *arp;

如果接收 ARP 数据包的网络设备设置了 IFF_NOARP 标志,或者如果数据包的目的地不是本地机器,或者是环回设备,那么应该丢弃数据包。您继续进行一些更全面的检查,如果一切正常,您将继续执行arp_process()方法,该方法执行处理 ARP 数据包的实际工作:

if (dev->flags & IFF_NOARP ||
            skb->pkt_type == PACKET_OTHERHOST ||
            skb->pkt_type == PACKET_LOOPBACK)
                goto freeskb;

如果 SKB 是共享的,您必须克隆它,因为它可能在被arp_rcv()方法处理时被其他人更改。如果共享的话,skb_share_check()方法创建 SKB 的克隆(参见附录 A )。

skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
                goto out_of_mem;
        /* ARP header, plus 2 device addresses, plus 2 IP addresses.  */
        if (!pskb_may_pull(skb, arp_hdr_len(dev)))
                goto freeskb;
        arp = arp_hdr(skb);

ARP 头的ar_hln代表硬件地址的长度,以太网头应该是 6 个字节,应该等于net_device对象的addr_len。ARP 头的ar_pln代表协议地址的长度,应该等于 IPv4 地址的长度,为 4 个字节:

        if (arp->ar_hln != dev->addr_len || arp->ar_pln != 4)
                goto freeskb;

        memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));
        return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);

freeskb:
        kfree_skb(skb);
out_of_mem:
        return 0;
}

处理 ARP 请求不限于以本地主机为目的地的数据包。当本地主机被配置为代理 ARP 或专用 VLAN 代理 ARP(参见 RFC 3069)时,您还可以处理目的地不是本地主机的数据包。内核 2.6.34 增加了对私有 VLAN 代理 ARP 的支持。

arp_process()方法中,您只处理 ARP 请求或 ARP 响应。对于 ARP 请求,您通过ip_route_input_noref()方法在路由子系统中执行查找。如果 ARP 数据包是发送给本地主机的(路由条目的rt_type是 RTN_LOCAL),那么您就要检查一些条件(稍后会介绍)。如果所有这些检查都通过了,一个 ARP 回复就会用arp_send()方法发送回来。如果 ARP 包不是给本地主机的,而是应该被转发的(路由条目的rt_type是 RTN_UNICAST),那么您检查一些条件(也将简要描述),如果它们被满足,您通过调用pneigh_lookup()方法在代理 ARP 表中执行查找。

现在您将看到处理 ARP 请求的主要 ARP 方法的实现细节,即arp_process()方法。

arp_process()方法

让我们来看看arp_process()方法,真正的工作是在这里完成的:

static int arp_process(struct sk_buff *skb)
{
        struct net_device *dev = skb->dev;
        struct in_device *in_dev = __in_dev_get_rcu(dev);
        struct arphdr *arp;
        unsigned char *arp_ptr;
        struct rtable *rt;
        unsigned char *sha;
        __be32 sip, tip;
        u16 dev_type = dev->type;
        int addr_type;
        struct neighbour *n;
        struct net *net = dev_net(dev);

        /* arp_rcv below verifies the ARP header and verifies the device
         * is ARP'able.
         */

        if (in_dev == NULL)
                goto out;

从 SKB 获取 ARP 头(是网络头,见arp_hdr()方法):

arp = arp_hdr(skb);

switch (dev_type) {
default:
                if (arp->ar_pro != htons(ETH_P_IP) ||
                        htons(dev_type) != arp->ar_hrd)
                            goto out;
                break;
case ARPHRD_ETHER:
                . . .
                if ((arp->ar_hrd != htons(ARPHRD_ETHER) &&
                     arp->ar_hrd != htons(ARPHRD_IEEE802)) ||
            arp->ar_pro != htons(ETH_P_IP))
                        goto out;
                break;
                . . .

您希望在arp_process()方法中只处理 ARP 请求或 ARP 响应,并丢弃所有其他数据包:

        /* Understand only these message types */

        if (arp->ar_op != htons(ARPOP_REPLY) &&
            arp->ar_op != htons(ARPOP_REQUEST))
                goto out;

/*
 *      Extract fields
 */
        arp_ptr = (unsigned char *)(arp + 1);

arp_process()方法—提取报头:

紧接在 ARP 报头之后,有以下字段(参见上面的 ARP 报头定义):

  • sha:源硬件地址(MAC 地址,6 字节)。
  • sip:源 IPv4 地址(4 字节)。
  • tha:目标硬件地址(MAC 地址,6 字节)。
  • tip:目标 IPv4 地址(4 字节)。

提取siptip地址:

sha     = arp_ptr;
arp_ptr += dev->addr_len;

arp_ptr前移相应的偏移量后,将sip设置为源 IPv4 地址:

memcpy(&sip, arp_ptr, 4);
arp_ptr += 4;
switch (dev_type) {
. . .
default:
                arp_ptr += dev->addr_len;
        }

arp_ptr前移相应的偏移量后,将tip设置为目标 IPv4 地址;

memcpy(&tip, arp_ptr, 4);

丢弃这两种类型的数据包:

  • 多播数据包
  • 如果禁用了带有环回地址的本地路由,则为环回设备发送数据包;另请参见本章末尾“快速参考”一节中对 IN_DEV_ROUTE_LOCALNET 宏的描述。
/*
 *      Check for bad requests for 127.x.x.x and requests for multicast
 *      addresses.  If this is one such, delete it.
 */
        if (ipv4_is_multicast(tip) ||
            (!IN_DEV_ROUTE_LOCALNET(in_dev) && ipv4_is_loopback(tip)))
                goto out;

        . . .

使用重复地址检测(DAD)时,源 IP ( sip)为 0。DAD 允许您检测 LAN 上不同主机上是否存在双 L3 地址。DAD 在 IPv6 中作为地址配置过程中不可或缺的一部分实施,但在 IPv4 中则不是。但是,在 IPv4 中支持正确处理 DAD 请求,您很快就会看到这一点。iputils包的arping实用程序是在 IPv4 中使用 DAD 的一个例子。当使用arping –D发送 ARP 请求时,您发送了一个 ARP 请求,其中 ARP 报头的sip为 0。(–D修饰符告诉arping处于 DAD 模式);tip通常是发送方 IPv4 地址(因为你要检查同一个局域网上是否有另一台主机和你的 IPv4 地址相同);如果存在与 DAD ARP 请求的tip具有相同 IP 地址的主机,它将发回一个 ARP 回复(不将发送方添加到其邻居表中):

/* Special case: IPv4 duplicate address detection packet (RFC2131) */
if (sip == 0) {
                if (arp->ar_op == htons(ARPOP_REQUEST) &&

arp_process()方法—arp_ignore()和 arp_filter()方法

arp_ignore procfs条目支持发送 ARP 回复作为对 ARP 请求的响应的不同模式。使用的值是/proc/sys/net/ipv4/conf/all/arp_ignore/proc/sys/net/ipv4/conf/<netDeviceName>/arp_ignore的最大值。默认情况下,arp_ignore procfs条目的值是 0,在这种情况下,arp_ignore() 方法返回 0。您用arp_send()回复 ARP 请求,在下一段代码中可以看到(假设inet_addr_type(net, tip)返回 RTN_LOCAL)。arp_ignore()方法检查 IN_DEV_ARP_IGNORE(in_dev)的值;有关更多详细信息,请参见net/ipv4/arp.c中的arp_ignore() 实现以及本章末尾“快速参考”一节中对 IN_DEV_ARP_IGNORE 宏的描述:

                 inet_addr_type(net, tip) == RTN_LOCAL &&
                !arp_ignore(in_dev, sip, tip))
                arp_send(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha,
                         dev->dev_addr, sha);
        goto out;
}

if (arp->ar_op == htons(ARPOP_REQUEST) &&
    ip_route_input_noref(skb, tip, sip, 0, dev) == 0) {

        rt = skb_rtable(skb);
        addr_type = rt->rt_type;

addr_type等于 RTN_LOCAL 时,数据包用于本地传送:

        if (addr_type == RTN_LOCAL) {
                int dont_send;

                dont_send = arp_ignore(in_dev, sip, tip);

arp_filter()方法在两种情况下失败(返回 1):

  • 当使用ip_route_output()方法在路由表中查找失败时。
  • 当路由条目的输出网络设备不同于接收 ARP 请求的网络设备时。

如果成功,arp_filter()方法返回 0(另请参见本章末尾“快速参考”一节中对 IN_DEV_ARPFILTER 宏的描述):

                     if (!dont_send && IN_DEV_ARPFILTER(in_dev))
                        dont_send = arp_filter(sip, tip, dev);
                if (!dont_send) {

在发送 ARP 回复之前,您希望将发送方添加到您的邻表中或更新它;这是通过neigh_event_ns()方法完成的。neigh_event_ns()方法创建一个新的邻居表条目,并将其状态设置为 NUD 陈旧。如果已经有这样一个条目,它用neigh_update()方法将其状态更新为 NUD 陈旧。以这种方式添加条目被称为被动学习:

                        n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
                        if (n) {
                                arp_send(ARPOP_REPLY, ETH_P_ARP, sip,
                                         dev, tip, sha, dev->dev_addr,
                                         sha);
                                neigh_release(n);
                        }
                }
                goto out;
        } else if (IN_DEV_FORWARD(in_dev)) {

当设备可以用作 ARP 代理时,arp_fwd_proxy()方法返回 1;当设备可以用作 ARP VLAN 代理时,arp_fwd_pvlan()方法返回 1:

               if (addr_type == RTN_UNICAST  &&
                    (arp_fwd_proxy(in_dev, dev, rt) ||
                     arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
                     (rt->dst.dev != dev &&
                      pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))) {

再次调用neigh_event_ns()方法,用 NUD _ 陈旧创建发送方的邻居条目,或者如果这样的条目存在,将该条目状态更新为 NUD _ 陈旧:

                        n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
                        if (n)
                                neigh_release(n);

                       if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED ||
                            skb->pkt_type == PACKET_HOST ||
                            in_dev->arp_parms->proxy_delay == 0) {
                                arp_send(ARPOP_REPLY, ETH_P_ARP, sip,
                                         dev, tip, sha, dev->dev_addr,
                                         sha);
                        } else {

通过将 SKB 放在proxy_queue的尾部,调用pneigh_enqueue()方法,延迟发送 ARP 回复。注意,延迟是随机的,是一个介于 0 和in_dev->arp_parms->proxy_delay之间的数字:

                                pneigh_enqueue(&arp_tbl,
                                               in_dev->arp_parms, skb);
                                return 0;
                        }
                        goto out;
                }
        }
}

        /* Update our ARP tables */

注意,调用__neigh_lookup() 方法的最后一个参数是 0,这意味着您只在邻居表中执行查找(如果查找失败,不创建新的邻居):

n = __neigh_lookup(&arp_tbl, &sip, dev, 0);

IN_DEV_ARP_ACCEPT 宏告诉您网络设备是否设置为接受 ARP 请求(另请参见本章末尾的“快速参考”部分中对 IN_DEV_ARP_ACCEPT 宏的描述):

if (IN_DEV_ARP_ACCEPT(in_dev)) {
                /* Unsolicited ARP is not accepted by default.
                   It is possible, that this option should be enabled for some
                   devices (strip is candidate)
                */

未经请求的 ARP 请求仅用于更新邻居表。在这样的请求中,tip等于sip(arping实用程序支持通过arping –U发送未经请求的 ARP 请求):

        if (n == NULL &&
            (arp->ar_op == htons(ARPOP_REPLY) ||
             (arp->ar_op == htons(ARPOP_REQUEST) && tip == sip)) &&
           inet_addr_type(net, sip) == RTN_UNICAST)
               n = __neigh_lookup(&arp_tbl, &sip, dev, 1);
}

if (n) {
        int state = NUD_REACHABLE;
        int override;

        /* If several different ARP replies follows back-to-back,
           use the FIRST one. It is possible, if several proxy
           agents are active. Taking the first reply prevents
           arp trashing and chooses the fastest router.
        */
        override = time_after(jiffies, n->updated + n->parms->locktime);

        /* Broadcast replies and request packets
           do not assert neighbour reachability.
         */
         if (arp->ar_op != htons(ARPOP_REPLY) ||
             skb->pkt_type != PACKET_HOST)
                 state = NUD_STALE;

调用neigh_update()更新邻表:

                neigh_update(n, sha, state,
                             override ? NEIGH_UPDATE_F_OVERRIDE : 0);
                neigh_release(n);
        }

out:
        consume_skb(skb);
        return 0;
}

既然您已经了解了 IPv4 ARP 协议的实现,那么是时候转向 IPv6 NDISC 协议的实现了。您将很快注意到 IPv4 和 IPv6 中相邻子系统实现之间的一些差异。

NDISC 协议(IPv6)

邻居发现(NDISC)协议基于 RFC 2461,“IP 版本 6 (IPv6)的邻居发现”,该协议后来在 2007 年被 RFC 4861 废弃。同一链路上的 IPv6 节点(主机或路由器)使用邻居发现协议来发现彼此的存在、发现路由器、确定彼此的 L2 地址以及维护邻居可达性信息。添加了重复地址检测(DAD ),以避免在同一个 LAN 上出现两个 L3 地址。我将讨论 DAD 和处理 NDISC 邻居请求和邻居广告。

接下来,您将了解 IPv6 邻居发现协议如何避免创建重复的 IPv6 地址。

重复地址检测

您如何确定局域网上没有其他相同的 IPv6 地址?这种可能性很低,但如果这样的地址确实存在,它可能会引起麻烦。爸爸是一个解决办法。当主机尝试配置地址时,它首先创建一个链路本地地址(链路本地地址以 FE80 开头)。这个地址是暂定的(IFA _ F _ 暂定),这意味着主机只能与 ND 消息通信。然后主机通过调用addrconf_dad_start()方法 ( net/ipv6/addrconf.c)启动 DAD 进程。主机发送邻居请求 DAD 消息。目标是它的暂定地址,源是全零(未指定的地址)。如果在指定的时间间隔内没有应答,状态将变为永久(IFA _ F _ 永久)。当设置乐观 DAD (CONFIG_IPV6_OPTIMISTIC_DAD)时,您不会等到 DAD 完成,而是允许主机在 DAD 成功完成之前与对等方通信。参见 RFC 4429,“IPv6 的乐观重复地址检测(DAD)”,2006 年。

IPv6 的邻居表称为nd_tbl:

struct neigh_table nd_tbl = {
        .family =       AF_INET6,
        .key_len =      sizeof(struct in6_addr),
        .hash =         ndisc_hash,
        .constructor =  ndisc_constructor,
        .pconstructor = pndisc_constructor,
        .pdestructor =  pndisc_destructor,
        .proxy_redo =   pndisc_redo,
        .id =           "ndisc_cache",
        .parms = {
                .tbl                    = &nd_tbl,
                .base_reachable_time    = ND_REACHABLE_TIME,
                .retrans_time           = ND_RETRANS_TIMER,
                .gc_staletime           = 60 * HZ,
                .reachable_time         = ND_REACHABLE_TIME,
                .delay_probe_time       = 5 * HZ,
                .queue_len_bytes        = 64*1024,
                .ucast_probes           = 3,
                .mcast_probes           = 3,
                .anycast_delay          = 1 * HZ,
                .proxy_delay            = (8 * HZ) / 10,
                .proxy_qlen             = 64,
        },
        .gc_interval =    30 * HZ,
        .gc_thresh1 =    128,
        .gc_thresh2 =    512,
        .gc_thresh3 =   1024,
};
(net/ipv6/ndisc.c)

注意,NDISC 表中的一些成员等于 ARP 表中的并行成员,例如,垃圾收集器阈值的值(gc_thresh1gc_thresh2gc_thresh3)。

Linux IPv6 邻居发现实现基于 ICMPv6 消息来管理相邻节点之间的交互。邻居发现协议定义了以下五种 ICMPv6 消息类型:

#define NDISC_ROUTER_SOLICITATION       133
#define NDISC_ROUTER_ADVERTISEMENT      134
#define NDISC_NEIGHBOUR_SOLICITATION    135
#define NDISC_NEIGHBOUR_ADVERTISEMENT   136
#define NDISC_REDIRECT                  137
(include/net/ndisc.h)

请注意,这五种 ICMPv6 消息类型是信息性消息。值在 0 到 127 范围内的 ICMPv6 消息类型是错误消息,值在 128 到 255 范围内的 ICMPv6 消息类型是信息性消息。关于这方面的更多信息,请参见第三章,其中讨论了 ICMP 协议。本章仅讨论邻居请求和邻居发现消息。

正如本章开头所提到的,因为邻居发现消息是 ICMPv6 消息,所以它们由icmpv6_rcv()方法处理,该方法又为消息类型是前面提到的五种类型之一的 ICMPv6 数据包调用ndisc_rcv()方法(参见net/ipv6/icmp.c)。

在 NDISC 中,有三个neigh_ops对象:ndisc_generic_opsndisc_hh_opsndisc_direct_ops:

  • 如果net_device对象的header_ops为空,则neigh_ops对象将被设置为ndisc_direct_ops。就像在arp_direct_ops的情况下一样,发送数据包是用neigh_direct_output()方法完成的,这实际上是一个对dev_queue_xmit()的包装。注意,正如前面 ARP 部分提到的,在大多数以太网设备中,net_device对象的header_ops不为空。
  • 如果net_device对象的header_ops包含一个空的cache()回调,那么neigh_ops对象被设置为ndisc_generic_ops
  • 如果net_device对象的header_ops包含一个非空的cache()回调,那么neigh_ops对象被设置为ndisc_hh_ops

本节讨论了 DAD 机制以及它如何帮助避免重复地址。下一节描述如何发送征求请求。

NIDSC:发送招标请求

与您在 IPv6 中看到的类似,如果没有找到任何匹配项,您也会执行查找并创建一个条目:

static int ip6_finish_output2(struct sk_buff *skb)
{
        struct dst_entry *dst = skb_dst(skb);
        struct net_device *dev = dst->dev;
        struct neighbour *neigh;
        struct in6_addr *nexthop;
        int ret;
               . . .

               . . .

        nexthop = rt6_nexthop((struct rt6_info *)dst, &ipv6_hdr(skb)->daddr);
        neigh = __ipv6_neigh_lookup_noref(dst->dev, nexthop);
        if (unlikely(!neigh))
                neigh = __neigh_create(&nd_tbl, nexthop, dst->dev, false);
        if (!IS_ERR(neigh)) {
                ret = dst_neigh_output(dst, neigh, skb);
               . . .

最终,就像在 IPv4 Tx 路径中一样,您从neigh_probe()方法中调用 solicit 方法neigh->ops->solicit(neigh, skb)。这种情况下的neigh->ops->solicit就是ndisc_solicit()方法。ndisc_solicit() 是一种很短的方法;事实上,它是对ndisc_send_ns()方法的包装:

static void ndisc_solicit(struct neighbour *neigh, struct sk_buff *skb)
{
        struct in6_addr *saddr = NULL;
        struct in6_addr mcaddr;
        struct net_device *dev = neigh->dev;
        struct in6_addr *target = (struct in6_addr *)&neigh->primary_key;
        int probes = atomic_read(&neigh->probes);

        if (skb && ipv6_chk_addr(dev_net(dev), &ipv6_hdr(skb)->saddr, dev, 1))
                saddr = &ipv6_hdr(skb)->saddr;

        if ((probes -= neigh->parms->ucast_probes) < 0) {
                if (!(neigh->nud_state & NUD_VALID)) {
                        ND_PRINTK(1, dbg,
                                  "%s: trying to ucast probe in NUD_INVALID: %pI6\n",
                                  __func__, target);
                }
                ndisc_send_ns(dev, neigh, target, target, saddr);
        } else if ((probes -= neigh->parms->app_probes) < 0) {
#ifdef CONFIG_ARPD
                neigh_app_ns(neigh);
#endif
        } else {
                addrconf_addr_solict_mult(target, &mcaddr);
                ndisc_send_ns(dev, NULL, target, &mcaddr, saddr);
        }
}

为了发送征集请求,我们需要构建一个nd_msg对象:

struct nd_msg {
        struct icmp6hdr icmph;
        struct in6_addr target;
        __u8            opt[0];
};
(include/net/ndisc.h)

对于请求请求,ICMPv6 标头类型应设置为 NDISC _ NEIGHBOUR _ SOLICITATION,对于请求回复,ICMPv6 标头类型应设置为 NDISC _ NEIGHBOUR _ ADVERTISEMENT。请注意,对于邻居广告消息,有时需要在 ICMPv6 标头中设置标志。ICMPv6 报头包括一个名为icmpv6_nd_advt的结构,该结构包括覆盖、请求和路由器标志:

struct icmp6hdr {
        __u8            icmp6_type;
        __u8            icmp6_code;
        __sum16         icmp6_cksum;
        union {
                . . .
                . . .
                struct icmpv6_nd_advt {
#if defined(__LITTLE_ENDIAN_BITFIELD)
                        __u32           reserved:5,
                                        override:1,
                                        solicited:1,
                                        router:1,
                                        reserved2:24;
. . .
#endif
                } u_nd_advt;
        } icmp6_dataun;
. . .
#define icmp6_router            icmp6_dataun.u_nd_advt.router
#define icmp6_solicited         icmp6_dataun.u_nd_advt.solicited
#define icmp6_override          icmp6_dataun.u_nd_advt.override
. . .
(include/uapi/linux/icmpv6.h)
  • 当响应邻居请求发送消息时,设置solicited标志(icmp6_solicited)。
  • 当你想要覆盖一个相邻的高速缓存条目(更新 L2 地址)时,你设置override标志(icmp6_override)。
  • 当发送邻居通告消息的主机是路由器时,您设置router标志(icmp6_router)。

您可以在下面的ndisc_send_na()方法中看到这三个标志的用法。我们来看看ndisc_send_ns()法:

void ndisc_send_ns(struct net_device *dev, struct neighbour *neigh,
                   const struct in6_addr *solicit,
                   const struct in6_addr *daddr, const struct in6_addr *saddr)
{
        struct sk_buff *skb;
        struct in6_addr addr_buf;
        int inc_opt = dev->addr_len;
        int optlen = 0;
        struct nd_msg *msg;

        if (saddr == NULL) {
                if (ipv6_get_lladdr(dev, &addr_buf,
                                   (IFA_F_TENTATIVE|IFA_F_OPTIMISTIC)))
                        return;
                saddr = &addr_buf;
        }

        if (ipv6_addr_any(saddr))
                inc_opt = 0;
        if (inc_opt)
                optlen += ndisc_opt_addr_space(dev);

        skb = ndisc_alloc_skb(dev, sizeof(*msg) + optlen);
        if (!skb)
                return;

构建嵌入在nd_msg对象中的 ICMPv6 头:

        msg = (struct nd_msg *)skb_put(skb, sizeof(*msg));
        *msg = (struct nd_msg) {
                .icmph = {
                        .icmp6_type = NDISC_NEIGHBOUR_SOLICITATION,
                },
                .target = *solicit,
        };

        if (inc_opt)
                ndisc_fill_addr_option(skb, ND_OPT_SOURCE_LL_ADDR,
                                       dev->dev_addr);

        ndisc_send_skb(skb, daddr, saddr);
}

我们来看看ndisc_send_na()的方法:

static void ndisc_send_na(struct net_device *dev, struct neighbour *neigh,
                          const struct in6_addr *daddr,
                          const struct in6_addr *solicited_addr,
                          bool router, bool solicited, bool override, bool inc_opt)
{
        struct sk_buff *skb;
        struct in6_addr tmpaddr;
        struct inet6_ifaddr *ifp;
        const struct in6_addr *src_addr;
        struct nd_msg *msg;
        int optlen = 0;

        . . .

        skb = ndisc_alloc_skb(dev, sizeof(*msg) + optlen);
        if (!skb)
                return;

构建嵌入在nd_msg对象中的 ICMPv6 头:

        msg = (struct nd_msg *)skb_put(skb, sizeof(*msg));
        *msg = (struct nd_msg) {
                .icmph = {
                        .icmp6_type = NDISC_NEIGHBOUR_ADVERTISEMENT,
                        .icmp6_router = router,
                        .icmp6_solicited = solicited,
                        .icmp6_override = override,
                },
                .target = *solicited_addr,
        };

        if (inc_opt)
                ndisc_fill_addr_option(skb, ND_OPT_TARGET_LL_ADDR,
                                       dev->dev_addr);

        ndisc_send_skb(skb, daddr, src_addr);
}

本节描述了如何发送招标请求。下一节将讨论如何处理邻居招揽和广告。

NDISC:接收邻居请求和广告

如上所述,ndisc_rcv()方法处理所有五种邻居发现消息类型;让我们来看看这个方法:

int ndisc_rcv(struct sk_buff *skb)
{
        struct nd_msg *msg;

        if (skb_linearize(skb))
                return 0;

        msg = (struct nd_msg *)skb_transport_header(skb);

        __skb_push(skb, skb->data - skb_transport_header(skb));

根据 RFC 4861,邻居消息的跳数限制应该是 255;跳数限制长度为 8 位,因此最大跳数限制为 255。值 255 确保数据包没有被转发,这保证您不会受到某种安全攻击。不满足此要求的数据包将被丢弃:

if (ipv6_hdr(skb)->hop_limit != 255) {
                ND_PRINTK(2, warn, "NDISC: invalid hop-limit: %d\n",
                          ipv6_hdr(skb)->hop_limit);
                return 0;
}

根据 RFC 4861,邻居消息的 ICMPv6 代码应该为 0,因此丢弃不满足此要求的数据包:

if (msg->icmph.icmp6_code != 0) {
                ND_PRINTK(2, warn, "NDISC: invalid ICMPv6 code: %d\n",
                          msg->icmph.icmp6_code);
return 0;
        }

memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));

switch (msg->icmph.icmp6_type) {
case NDISC_NEIGHBOUR_SOLICITATION:
                ndisc_recv_ns(skb);
                break;

        case NDISC_NEIGHBOUR_ADVERTISEMENT:
                ndisc_recv_na(skb);
                break;

        case NDISC_ROUTER_SOLICITATION:
                ndisc_recv_rs(skb);
                break;

        case NDISC_ROUTER_ADVERTISEMENT:
                ndisc_router_discovery(skb);
                break;

        case NDISC_REDIRECT:
                ndisc_redirect_rcv(skb);
                break;
        }

        return 0;
}

我不在本章讨论路由器请求和路由器广告,因为它们在第八章中讨论过。我们来看看ndisc_recv_ns()法:

static void ndisc_recv_ns(struct sk_buff *skb)
{
        struct nd_msg *msg = (struct nd_msg *)skb_transport_header(skb);
        const struct in6_addr *saddr = &ipv6_hdr(skb)->saddr;
        const struct in6_addr *daddr = &ipv6_hdr(skb)->daddr;
        u8 *lladdr = NULL;
        u32 ndoptlen = skb->tail - (skb->transport_header +
                                    offsetof(struct nd_msg, opt));
        struct ndisc_options ndopts;
        struct net_device *dev = skb->dev;
        struct inet6_ifaddr *ifp;
        struct inet6_dev *idev = NULL;
        struct neighbour *neigh;

saddr是全零的未指定地址(IPV6_ADDR_ANY)时,ipv6_addr_any()方法返回 1。当源地址是未指定的地址(全零)时,这意味着请求是 DAD:

        int dad = ipv6_addr_any(saddr);
bool inc;
int is_router = -1;

执行一些有效性检查:

if (skb->len < sizeof(struct nd_msg)) {
                ND_PRINTK(2, warn, "NS: packet too short\n");
                return;
}

if (ipv6_addr_is_multicast(&msg->target)) {
        ND_PRINTK(2, warn, "NS: multicast target address\n");
        return;
}

/*
 * RFC2461 7.1.1:
 * DAD has to be destined for solicited node multicast address.
 */
if (dad && !ipv6_addr_is_solict_mult(daddr)) {
        ND_PRINTK(2, warn, "NS: bad DAD packet (wrong destination)\n");
        return;
}

if (!ndisc_parse_options(msg->opt, ndoptlen, &ndopts)) {
        ND_PRINTK(2, warn, "NS: invalid ND options\n");
        return;
}

if (ndopts.nd_opts_src_lladdr) {
        lladdr = ndisc_opt_addr_data(ndopts.nd_opts_src_lladdr, dev);
        if (!lladdr) {
                ND_PRINTK(2, warn,
                          "NS: invalid link-layer address length\n");
                return;
        }

        /* RFC2461 7.1.1:
         *      If the IP source address is the unspecified address,
         *      there MUST NOT be source link-layer address option
         *      in the message.
         */
        if (dad) {
                ND_PRINTK(2, warn,
                          "NS: bad DAD packet (link-layer address option)\n");
                return;
        }
}

inc = ipv6_addr_is_multicast(daddr);

ifp = ipv6_get_ifaddr(dev_net(dev), &msg->target, dev, 1);
if (ifp) {

        if (ifp->flags & (IFA_F_TENTATIVE|IFA_F_OPTIMISTIC)) {
                if (dad) {
                        /*
                         * We are colliding with another node
                         * who is doing DAD
                         * so fail our DAD process
                        */
                        addrconf_dad_failure(ifp);
                        return;
                } else {
                        /*
                         * This is not a dad solicitation.
                         * If we are an optimistic node,
                         * we should respond.
                         * Otherwise, we should ignore it.
                        */
                        if (!(ifp->flags & IFA_F_OPTIMISTIC))
                                goto out;
                }
        }

        idev = ifp->idev;
} else {
        struct net *net = dev_net(dev);

        idev = in6_dev_get(dev);
        if (!idev) {
                /* XXX: count this drop? */
                return;
        }

        if (ipv6_chk_acast_addr(net, dev, &msg->target) ||
            (idev->cnf.forwarding &&
             (net->ipv6.devconf_all->proxy_ndp || idev->cnf.proxy_ndp) &&
             (is_router = pndisc_is_router(&msg->target, dev)) >= 0)) {
                if (!(NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED) &&
                    skb->pkt_type != PACKET_HOST &&
                    inc != 0 &&
                    idev->nd_parms->proxy_delay != 0) {
                        /*
                         * for anycast or proxy,
                         * sender should delay its response
                         * by a random time between 0 and
                         * MAX_ANYCAST_DELAY_TIME seconds.
                         * (RFC2461) -- yoshfuji
                        */
                        struct sk_buff *n = skb_clone(skb, GFP_ATOMIC);
                        if (n)
                                pneigh_enqueue(&nd_tbl, idev->nd_parms, n);
                        goto out;
                }
        } else
                goto out;
}

if (is_router < 0)
        is_router = idev->cnf.forwarding;

if (dad) {

发送邻居广告消息:

        ndisc_send_na(dev, NULL, &in6addr_linklocal_allnodes, &msg->target,
                      !!is_router, false, (ifp != NULL), true);
        goto out;
}

if (inc)
        NEIGH_CACHE_STAT_INC(&nd_tbl, rcv_probes_mcast);
else
        NEIGH_CACHE_STAT_INC(&nd_tbl, rcv_probes_ucast);

/*
 *      update / create cache entry
 *      for the source address
*/
neigh = __neigh_lookup(&nd_tbl, saddr, dev,
                       !inc || lladdr || !dev->addr_len);
if (neigh)

用发件人的 L2 地址更新你的邻表;nud_state将被设置为 NUD 陈旧:

        neigh_update(neigh, lladdr, NUD_STALE,
                     NEIGH_UPDATE_F_WEAK_OVERRIDE|
                     NEIGH_UPDATE_F_OVERRIDE);
if (neigh || !dev->header_ops) {

发送邻居广告消息:

                ndisc_send_na(dev, neigh, saddr, &msg->target,
                              !!is_router,
                              true, (ifp != NULL && inc), inc);
                if (neigh)
                        neigh_release(neigh);
        }

out:
        if (ifp)
                in6_ifa_put(ifp);
        else
                in6_dev_put(idev);
}

让我们来看看处理邻居广告的方法,ndisc_recv_na() :

static void ndisc_recv_na(struct sk_buff *skb)
{
        struct nd_msg *msg = (struct nd_msg *)skb_transport_header(skb);
        const struct in6_addr *saddr = &ipv6_hdr(skb)->saddr;
        const struct in6_addr *daddr = &ipv6_hdr(skb)->daddr;
        u8 *lladdr = NULL;
        u32 ndoptlen = skb->tail - (skb->transport_header +
                                    offsetof(struct nd_msg, opt));
        struct ndisc_options ndopts;
        struct net_device *dev = skb->dev;
        struct inet6_ifaddr *ifp;
        struct neighbour *neigh;

        if (skb->len < sizeof(struct nd_msg)) {
                ND_PRINTK(2, warn, "NA: packet too short\n");
                return;
        }

        if (ipv6_addr_is_multicast(&msg->target)) {
                ND_PRINTK(2, warn, "NA: target address is multicast\n");
                return;
        }

        if (ipv6_addr_is_multicast(daddr) &&
            msg->icmph.icmp6_solicited) {
                ND_PRINTK(2, warn, "NA: solicited NA is multicasted\n");
                return;
        }

        if (!ndisc_parse_options(msg->opt, ndoptlen, &ndopts)) {
                ND_PRINTK(2, warn, "NS: invalid ND option\n");
                return;
        }
        if (ndopts.nd_opts_tgt_lladdr) {
                lladdr = ndisc_opt_addr_data(ndopts.nd_opts_tgt_lladdr, dev);
                if (!lladdr) {
                        ND_PRINTK(2, warn,
                                  "NA: invalid link-layer address length\n");
                        return;
                }
        }
        ifp = ipv6_get_ifaddr(dev_net(dev), &msg->target, dev, 1);
        if (ifp) {
                if (skb->pkt_type != PACKET_LOOPBACK
                    && (ifp->flags & IFA_F_TENTATIVE)) {
                                addrconf_dad_failure(ifp);
                                return;
                }
                /* What should we make now? The advertisement
                   is invalid, but ndisc specs say nothing
                   about it. It could be misconfiguration, or
                   an smart proxy agent tries to help us :-)

                   We should not print the error if NA has been
                   received from loopback - it is just our own
                   unsolicited advertisement.
                 */
                if (skb->pkt_type != PACKET_LOOPBACK)
                        ND_PRINTK(1, warn,
                                  "NA: someone advertises our address %pI6 on %s!\n",
                                  &ifp->addr, ifp->idev->dev->name);
                in6_ifa_put(ifp);
                return;
        }
        neigh = neigh_lookup(&nd_tbl, &msg->target, dev);

        if (neigh) {
                u8 old_flags = neigh->flags;
                struct net *net = dev_net(dev);

                if (neigh->nud_state & NUD_FAILED)
                        goto out;

                /*
                 * Don't update the neighbour cache entry on a proxy NA from
                 * ourselves because either the proxied node is off link or it
                 * has already sent a NA to us.
                 */
                if (lladdr && !memcmp(lladdr, dev->dev_addr, dev->addr_len) &&
                    net->ipv6.devconf_all->forwarding &&
                    net->ipv6.devconf_all->proxy_ndp &&
                    pneigh_lookup(&nd_tbl, net, &msg->target, dev, 0)) {
                        /* XXX: idev->cnf.proxy_ndp */
                        goto out;
                }

更新邻表。当接收到的消息是邻居请求时,icmp6_solicited被设置,因此您想要将状态设置为 NUD _ 可达。当设置了icmp6_override标志时,您希望设置override标志(这意味着用指定的lladdr更新 L2 地址,如果不同的话):

                neigh_update(neigh, lladdr,
                             msg->icmph.icmp6_solicited ? NUD_REACHABLE : NUD_STALE,
                             NEIGH_UPDATE_F_WEAK_OVERRIDE|
                             (msg->icmph.icmp6_override ? NEIGH_UPDATE_F_OVERRIDE : 0)|
                             NEIGH_UPDATE_F_OVERRIDE_ISROUTER|
                             (msg->icmph.icmp6_router ? NEIGH_UPDATE_F_ISROUTER : 0));

                if ((old_flags & ∼neigh->flags) & NTF_ROUTER) {
                        /*
                         * Change: router to host
                         */
                        struct rt6_info *rt;
                        rt = rt6_get_dflt_router(saddr, dev);
                        if (rt)
                                ip6_del_rt(rt);
                }

out:
                neigh_release(neigh);
        }
}

摘要

本章描述了 IPv4 和 IPv6 中的相邻子系统。首先,您了解了相邻子系统的目标。然后,您了解了 IPv4 中的 ARP 请求和 ARP 回复,以及 IPv6 中的 NDISC 邻居请求和 NDISC 邻居通告。您还了解了 DAD 实现如何避免重复的 IPv6 地址,并且看到了处理相邻子系统请求和回复的各种方法。第八章讨论了 IPv6 子系统的实现。接下来的“快速参考”部分涵盖了与本章中讨论的主题相关的主要方法和宏,按其上下文排序。我还展示了neigh_statistics结构,它表示相邻子系统收集的统计数据。

快速参考

以下是相邻子系统的一些重要方法和宏,以及对neigh_statistics结构的描述。

image 核心邻码在net/core/neighbour.cinclude/net/neighbour.hinclude/uapi/linux/neighbour.h

ARP 代码(IPv4)在net/ipv4/arp.cinclude/net/arp.hinclude/uapi/linux/if_arp.h中。

NDISC 代码(IPv6)在net/ipv6/ndisc.cinclude/net/ndisc.h中。

方法

让我们从介绍方法开始。

void neigh_table_init(结构 neigh_table *tbl)

该方法调用neigh_table_init_no_netlink()方法来执行邻表的初始化,并将该表链接到全局邻表链表(neigh_tables)。

void neigh _ table _ init _ no _ netlink(struct neigh _ table * TBL)

这个方法执行所有的邻居初始化,除了链接到全局邻居表链表,这是由neigh_table_init()完成的,如前所述。

int neigh_table_clear(结构 neigh_table *tbl)

该方法释放指定邻表的资源。

struct neighbor * neigh _ alloc(struct neigh _ table * TBL,struct net_device *dev)

这个方法分配一个邻居对象。

struct neigh _ hash _ table * neigh _ hash _ alloc(无符号整数移位)

这个方法分配一个相邻的哈希表。

struct neighbor * _ _ neigh _ create(struct neigh _ table * TBL,const void *pkey,struct net_device *dev,bool want_ref)

这个方法创建一个邻居对象。

int neigh_add(struct sk_buff *skb,struct nlmsghdr *nlh,void *arg)

此方法添加一个邻居条目;它是 netlink RTM_NEWNEIGH 消息的处理程序。

int neigh _ delete(struct sk _ buff * skb,struct nlmsghdr *nlh,void *arg)

此方法删除邻居条目;它是 netlink RTM_DELNEIGH 消息的处理程序。

void neigh _ probe(struct neighbor * neigh)

这个方法从邻居arp_queue获取一个 SKB,并调用相应的solicit()方法来发送它。在 ARP 的情况下,它将是arp_solicit()。它递增邻居probes计数器并释放数据包。

int neigh _ forced _ GC(struct neigh _ table * TBL)

此方法是同步垃圾收集方法。它移除不处于永久状态(NUD _ 永久)并且其引用计数等于 1 的邻居条目。邻居的移除和清理是通过首先将邻居的失效标志设置为 1,然后调用neigh_cleanup_and_release()方法来完成的,该方法获取一个邻居对象作为参数。在某些情况下,从neigh_alloc()方法调用neigh_forced_gc()方法,如本章前面的“创建和释放邻居”一节所述。如果至少移除了一个邻居对象,则neigh_forced_gc()方法返回 1,否则返回 0。

void neigh _ periodic _ work(struct work _ struct * work)

这个方法是异步垃圾收集器处理程序。

静态 void neigh_timer_handler(无符号长整型参数)

此方法是每邻居定期计时器垃圾收集器处理程序。

struct neighbor * _ _ neigh _ lookup(struct neigh _ table * TBL,const void *pkey,struct net_device *dev,int creat)

此方法通过给定的键在指定的相邻表中执行查找。如果creat参数为 1,并且查找失败,调用neigh_create()方法在指定的邻居表中创建一个邻居条目并返回它。

neigh_hh_init(结构邻居*n,结构 dst_entry *dst)

此方法根据指定的路由缓存条目初始化指定邻居的 L2 缓存(hh_cache对象)。

void __init arp_init(void)

该方法执行 ARP 协议的设置:初始化 ARP 表,将arp_rcv()注册为接收 ARP 数据包的处理程序,初始化procfs条目,注册sysctl条目,注册 ARP netdev通知回调,arp_netdev_event()

int arp_rcv(结构 sk_buff *skb,结构 net_device *dev,结构 packet_type *pt,结构 net_device *orig_dev)

此方法是 ARP 数据包(类型为 0x0806 的以太网数据包)的 Rx 处理程序。

int ARP _ constructor(struct neighbor * neigh)

此方法执行 ARP 邻居初始化。

int ARP _ process(struct sk _ buff * skb)

这个方法由arp_rcv()方法调用,处理 ARP 请求和 ARP 响应的主要处理。

void ARP _ solicit(struct neighbor * neigh,struct sk_buff *skb)

这个方法通过调用arp_send()方法,在一些检查和初始化之后发送请求(ARPOP_REQUEST)。

请参见 arp_send(int type,int ptype,_ _ _ _ _ 32 dest _ IP,struct net_device *dev,_ _ _ _ _ 32 src _ IP,const unsigned char * dest _ hw,const unsigned char * src _ hw,const unsigned char * target _ hw)

这个方法创建一个 ARP 包,通过调用arp_create()方法用指定的参数初始化它,并通过调用arp_xmit()方法发送它。

请参阅 ARP _ xmit(struct sk _ buf * skb)

这个方法实际上是通过用dev_queue_xmit()调用 NF_HOOK 宏来发送包的。

arphdr *arp_hdr 结构(const struct sk _ buff * skb)

这个方法获取指定 SKB 的 ARP 头。

int arp_mc_map(__be32 addr,u8 *haddr,struct net_device *dev,int dir)

该方法根据网络设备类型将 IPv4 地址转换为 L2(链路层)地址。例如,当设备是以太网设备时,这是通过ip_eth_mc_map()方法完成的;当设备是 Infiniband 设备时,这是通过ip_ib_mc_map()方法完成的。

静态内联 int ARP _ FWD _ proxy(struct in _ device * in _ dev,struct net_device *dev,struct rtable *rt)

如果指定的设备可以对指定的路由条目使用代理 ARP,则此方法返回 1。

静态内联 int ARP _ FWD _ pvlan(struct in _ device * in _ dev,struct net_device *dev,struct rtable *rt,__be32 sip,__be32 tip)

如果指定的设备可以对指定的路由条目和指定的 IPv4 源地址和目的地址使用代理 ARP VLAN,则此方法返回 1。

int ARP _ net dev _ event(struct notifier _ block * this,unsigned long event,void *ptr)

这个方法是用于netdev通知事件的 ARP 处理程序。

int ndisc _ net dev _ event(struct notifier _ block * this,unsigned long event,void *ptr)

这个方法是用于netdev通知事件的 NDISC 处理程序。

int ndis _ rcv(struct sk _ buf * skb)

该方法是接收五种类型请求包之一的主要 NDISC 处理程序。

静态 int neigh _ black hole(struct neighbor * neigh,struct sk_buff *skb)

此方法会丢弃数据包并返回–enet down 错误(网络中断)。

静态 void ndisc _ recv _ ns(struct sk _ buff * skb)和静态 void ndisc _ recv _ na(struct sk _ buff * skb)

这些方法分别处理接收邻居请求和邻居广告。

静态空 ndisc _ recv _ RS(struct sk _ buff * skb)和静态空 ndisc _ router _ discovery(struct sk _ buff * skb)

这些方法分别处理接收路由器请求和路由器广告。

int ndisc _ MC _ map(const struct in 6 _ addr * addr,char *buf,struct net_device *dev,int dir)

该方法根据网络设备类型将 IPv4 地址转换为 L2(链路层)地址。在 IPv6 下的以太网中,这是通过ipv6_eth_mc_map()方法完成的。

int ndisc _ constructor(struct neighbor * neigh)

此方法执行 NDISC 邻居初始化。

void ndisc _ solicit(struct neighbor * neigh,struct sk_buff *skb)

这个方法通过调用ndisc_send_ns()方法,在一些检查和初始化之后发送请求。

int icmpv 6 _ rcv(struct sk _ buf * skb)

此方法是接收 ICMPv6 消息的处理程序。

bool IPv6 _ addr _ any(const struct in 6 _ addr * a)

当给定的 IPv6 地址是全零的未指定地址(IPv6 _ ADDR _ 任意)时,此方法返回 1。

int inet _ addr _ onlink(struct in _ device * in _ dev,__be32 a,__be32 b)

此方法检查两个指定的地址是否在同一子网上。

宏指令

现在,让我们看看宏。

开发者代理地址解析器

如果/proc/sys/net/ipv4/conf/<netDevice>/proxy_arp被置位或/proc/sys/net/ipv4/conf/all/proxy_arp is set,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。

开发者代理地址解析协议 PVLAN

如果/proc/sys/net/ipv4/conf/<netDevice>/proxy_arp_pvlan被设置,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。

开发者地址过滤器(开发者地址)

如果/proc/sys/net/ipv4/conf/<netDevice>/arp_filter被置位或者/proc/sys/net/ipv4/conf/all/arp_filter被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。

输入设备地址解析接受(输入设备)

如果/proc/sys/net/ipv4/conf/<netDevice>/arp_accept被置位或者/proc/sys/net/ipv4/conf/all/arp_accept被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。

开发者地址解析通告

该宏返回/proc/sys/net/ipv4/conf/<netDevice>/arp_announce/proc/sys/net/ipv4/conf/all/arp_announce的最大值,其中netDevice是与指定的in_dev关联的网络设备。

内部开发地址解析忽略(内部开发)

该宏返回/proc/sys/net/ipv4/conf/<netDevice>/arp_ignore/proc/sys/net/ipv4/conf/all/arp_ignore的最大值,其中netDevice是与指定的in_dev关联的网络设备。

设备地址解析通知(设备地址)

该宏返回/proc/sys/net/ipv4/conf/<netDevice>/arp_notify/proc/sys/net/ipv4/conf/all/arp_notify的最大值,其中netDevice是与指定的in_dev关联的网络设备。

开发共享媒体(开发中)

如果/proc/sys/net/ipv4/conf/<netDevice>/shared_media被置位或者/proc/sys/net/ipv4/conf/all/shared_media被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。

开发中路由本地网

如果/proc/sys/net/ipv4/conf/<netDevice>/route_localnet被置位或者/proc/sys/net/ipv4/conf/all/route_localnet被置位,该宏返回true,其中netDevice是与指定的in_dev相关联的网络设备。

neigh_hold()

此宏递增指定邻居的引用计数。

邻居统计结构

neigh_statistics结构对于监控相邻子系统很重要;正如本章开头提到的,ARP 和 NDISC 都通过procfs(分别是/proc/net/stat/arp_cache/proc/net/stat/ndisc_cache,)导出这个结构成员。以下是对其成员的描述,并指出它们的增量:

struct neigh_statistics {
        unsigned long allocs;           /* number of allocated neighs     */
        unsigned long destroys;         /* number of destroyed neighs     */
        unsigned long hash_grows;       /* number of hash resizes         */
        unsigned long res_failed;       /* number of failed resolutions   */
        unsigned long lookups;          /* number of lookups              */
        unsigned long hits;             /* number of hits (among lookups) */
        unsigned long rcv_probes_mcast; /* number of received mcast ipv6  */
        unsigned long rcv_probes_ucast; /* number of received ucast ipv6  */
        unsigned long periodic_gc_runs; /* number of periodic GC runs     */
        unsigned long forced_gc_runs;   /* number of forced GC runs       */
        unsigned long unres_discards;   /* number of unresolved drops     */
};

下面是对neigh_statistics结构成员的描述:

  • allocs:分配的邻居数量;通过neigh_alloc()方法递增。
  • destroys:被摧毁的邻居的数量;通过neigh_destroy()方法递增。
  • hash_grows:哈希调整大小的次数;通过neigh_hash_grow()方法递增。
  • res_failed:解析失败的次数;通过neigh_invalidate()方法递增。
  • lookups:已完成的邻居查找的数量;通过neigh_lookup()方法和neigh_lookup_nodev()方法递增。
  • hits:执行邻居查找时的命中次数;当你命中时,通过neigh_lookup()方法和neigh_lookup_nodev()方法递增。
  • rcv_probes_mcast:接收到的组播探测数(仅限 IPv6 通过ndisc_recv_ns()方法递增。
  • rcv_probes_ucast:接收到的单播探测数(仅限 IPv6 通过ndisc_recv_ns()方法递增。
  • periodic_gc_runs:周期性 GC 调用的次数;通过neigh_periodic_work()方法递增。
  • forced_gc_runs:强制 GC 调用的次数;通过neigh_forced_gc()方法递增。
  • unres_discards:未解决滴数;当丢弃未解析的数据包时,通过__neigh_event_send()方法递增。

桌子

这是被盖住的桌子。

表 7-1。网络不可达检测状态

|

Linux 操作系统

|

标志

|
| --- | --- |
| NUD_INCOMPLETE | 地址解析正在进行中,邻居的链路层地址尚未确定。这意味着已发送请求,您正在等待请求回复或超时。 |
| NUD 可到达 | 已知最近可以联系到该邻居。 |
| NUD 过时了 | 自从收到前向路径运行正常的最后一个肯定确认后,已过去了超过 ReachableTime 毫秒。 |
| NUD 延迟 | 不再知道该邻居是可到达的。暂时延迟发送探测,以便上层协议有机会提供可达性确认。 |
| NUD 探针 | 不再知道邻居是可到达的,并且正在发送单播邻居请求探测以验证可达性。 |
| NUD _ 失败 | 将邻居设置为不可达。删除邻居时,会将其设置为 NUD 失败状态。 |

八、IPv6

在第七章中,我讨论了 Linux 相邻子系统及其实现。在这一章中,我将讨论 IPv6 协议及其在 Linux 中的实现。IPv6 是 TCP/IP 协议栈的下一代网络层协议。它是由互联网工程任务组(IETF)开发的,旨在取代 IPv4,后者仍然承载着绝大多数的互联网流量。

在 90 年代早期,由于预期的互联网增长,IETF 开始努力开发下一代 IP 协议。第一个 IPv6 RFC 来自 1995 年:RFC 1883,“互联网协议,第 6 版(IPv6)规范。”后来在 1998 年,RFC 2460 取代了它。IPv6 解决的主要问题是地址短缺:IPv6 地址的长度是 128 位。IPv6 设置了更大的地址空间。我们在 IPv6 中使用 2¹²⁸ 地址,而不是 IPv4 中的 2³² 地址。这确实大大增加了地址空间,可能远远超过未来几十年的需要。但是扩展地址空间并不是 IPv6 的唯一优势,有些人可能会这么想。基于从 IPv4 中获得的经验,IPv6 做了许多改变来改进 IP 协议。我们将在本章中讨论这些变化。

作为一种改进的网络层协议,IPv6 协议正获得越来越多的支持。互联网在全球的日益普及,以及智能移动设备和平板电脑市场的不断增长,无疑使 IPv4 地址的枯竭成为一个更明显的问题。这就需要过渡到 IPv4 的继任者 IPv6 协议。

IPv6–简短介绍

IPv6 子系统无疑是一个非常广泛的主题,它正在稳步发展。在过去的十年中,令人兴奋的功能被添加进来。这些新功能中有一些是基于 IPv4 的,比如 ICMPv6 套接字、IPv6 组播路由和 IPv6 NAT。IPsec 在 IPv6 中是强制的,在 IPv4 中是可选的,尽管大多数操作系统也在 IPv4 中实现了 IPsec。当我们深入研究 IPv6 内核内部时,我们发现了许多相似之处。有时方法的名字甚至一些变量的名字都是相似的,除了增加了“v6”或“6”然而,在一些地方的实施中有一些变化。

我们选择在本章中讨论 IPv6 的重要新功能,展示它与 IPv4 的一些不同之处,并解释为什么要做出改变。扩展头、多播监听器发现(MLD)协议和自动配置过程是我们讨论的一些新特性,并通过一些用户空间示例进行演示。我们还将讨论接收 IPv6 数据包的工作原理、IPv6 转发的工作原理,以及与 IPv4 的一些不同之处。总的来说,IPv6 的开发者似乎在过去使用 IPv4 的经验的基础上做了很多改进,IPv6 的实现带来了很多 IPv4 没有的好处和很多优于 IPv4 的优点。我们将在下一节讨论 IPv6 地址,包括多播地址和特殊地址。

IPv6 地址

学习 IPv6 的第一步是熟悉 RFC 4291 中定义的 IPv6 寻址架构。IPv6 地址有三种类型:

  • 单播: 该地址唯一标识一个接口。发送到单播地址的数据包会被传送到该地址标识的接口。
  • 任播: 这个地址可以分配给一组接口(通常在不同的节点上)。IPv4 中不存在这种类型的地址。事实上,它是单播地址和组播地址的混合。发送到任播地址的数据包被传送到该地址所标识的接口之一(根据路由协议,是“最近”的接口)。
  • 组播: 这个地址可以分配给一组接口(通常在不同的节点上)。发送到多播地址的数据包会被传送到该地址标识的所有接口。一个接口可以属于任意数量的多播组。

IPv6 中没有广播地址。在 IPv6 中,为了获得与广播相同的结果,可以向所有节点的组组播地址发送数据包(ff02::1))。在 IPv4 中,地址解析协议(ARP)协议的很大一部分功能是基于广播的。IPv6 子系统使用邻居发现而不是 ARP 来将 L3 地址映射到 L2 地址。IPv6 邻居发现协议基于 ICMPv6,它使用组播地址而不是广播地址,正如您在上一章中看到的。在本章的后面,你会看到更多使用多播流量的例子。

IPv6 地址由 8 个 16 位的块组成,总共 128 位。IPv6 地址看起来像这样:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx(其中 x 是十六进制数字。)有时候你会在一个 IPv6 地址里面遇到“::”;这是前导零的快捷方式。

在 IPv6 中,使用地址前缀。事实上,前缀相当于 IPv4 子网掩码。RFC 4291“IP 版本 6 寻址架构”中描述了 IPv6 前缀 IPv6 地址前缀由以下符号表示:ipv6-address/prefix-length

前缀长度是一个十进制值,指定地址中有多少个最左边的连续位构成前缀。我们使用“/n”来表示前缀 n 位长。例如,对于所有以 32 位2001:0da7开头的 IPv6 地址,使用以下前缀:2001:da7::/32

现在您已经了解了 IPv6 地址的类型,接下来您将了解一些特殊的 IPv6 地址及其用法。

特殊地址

在本节中,我将描述一些特殊的 IPv6 地址及其用法。建议您熟悉这些特殊地址,因为您将在本章后面的内容中以及浏览代码时遇到其中的一些地址(如 DAD 中使用的全零的未指定地址,或重复地址检测)。以下列表包含特殊的 IPv6 地址及其用法说明:

  • 每个接口上应该至少有一个链路本地单播地址。链路本地地址允许与同一物理网络中的其他节点进行通信;邻居发现、自动地址配置等等都需要它。路由器不得转发任何带有本地链路源地址或目的地址的数据包。本地链路地址分配有前缀fe80::/64
  • 全球单播地址一般格式如下:前 n 位为global routing prefix,后 m 位为subnet ID,其余 128- n - m 位为interface ID
  • global routing prefix:分配给站点的值。它代表网络 ID 或地址前缀。
  • subnet ID:站点内子网的标识符。
  • interface ID:一个id;其值在子网内必须是唯一的。RFC 3513 第 2.5.1 节对此进行了定义。

RFC 3587“IPv6 全球单播地址格式”中描述了全球单播地址 RFC 4291 中定义了可分配的全局单播地址空间。

  • IPv6 环回地址是0:0:0:0:0:0:0:1,简称为::1
  • 全零(0:0:0:0:0:0:0:0)的地址称为未指定地址。它用于 DAD(重复地址检测),就像你在前一章看到的那样。它不应用作目的地址。您不能通过使用用户空间工具(如ip命令或ifconfig命令)将未指定的地址分配给接口。
  • IPv4 映射的 IPv6 地址是以 80 位零开始的地址。接下来的 16 位是 1,剩下的 32 位是 IPv4 地址。例如,::ffff:192.0.2.128代表 192.0.2.128 的 IPv4 地址。有关这些地址的用法,请参见 RFC 4038,“IPv6 过渡的应用方面”
  • 不推荐使用与 IPv4 兼容的格式;在这种格式中,IPv4 地址位于 IPv6 地址的低 32 位,所有剩余的位都是 0;前面提到的地址应该是这个格式的::192.0.2.128。参见 RFC 4291,2.5.5.1 部分。
  • 站点本地地址 最初设计用于站点内部寻址,无需全局前缀,但在 2004 年的 RFC 3879“不认可的站点本地地址”中被弃用。

IPv6 地址在 Linux 中由in6_addr结构表示;在in6_addr结构中使用三个数组(8、16 和 32 位元素)的并集有助于位操作操作:

struct in6_addr {
        union {
                __u8            u6_addr8[16];
                __be16          u6_addr16[8];
                __be32          u6_addr32[4];
        } in6_u;
#define s6_addr                 in6_u.u6_addr8
#define s6_addr16               in6_u.u6_addr16
#define s6_addr32               in6_u.u6_addr32
};
(include/uapi/linux/in6.h)

多播在 IPv6 中扮演着重要的角色,尤其是对于基于 ICMPv6 的协议,如 NDISC(我在第七章中讨论过,它涉及 Linux 的相邻子系统)和 MLD(将在本章后面讨论)。我将在下一节讨论 IPv6 中的组播地址。

多播地址

多播地址提供了一种定义多播组的方法;一个节点可以属于一个或多个多播组。目的地是多播地址的分组应该被传送到属于该多播组的每个节点。在 IPv6 中,所有组播地址都以 FF 开头(前 8 位)。接下来的 4 位用于标志,4 位用于作用域。最后,最后 112 位是组 ID。标志字段的 4 位含义如下:

  • 位 0: 保留供将来使用。
  • 位 1: 值 1 表示集合点嵌入在地址中。集合点的讨论更多地与用户空间守护进程相关,不在本书的范围之内。有关更多详细信息,请参见 RFC 3956,“在 IPv6 多播地址中嵌入集合点(RP)地址”这个位有时被称为 R 标志(R 代表集合点。)
  • 位 2: 值 1 表示基于网络前缀分配的多播地址。(参见 RFC 3306。)这个位有时被称为 P 标志(P 表示前缀信息。)
  • 位 3: 值 0 表示永久分配的(“众所周知的”)多播地址,由互联网号码分配机构(IANA)分配。值 1 表示非永久分配的(“瞬时”)多播地址。该位有时被称为 T 标志(T 代表临时。)

作用域可以是表 8-1 中的一个条目,该表通过 Linux 符号和值显示了各种 IPv6 作用域。

表 8-1 。IPv6 作用域

|

十六进制值

|

描述

|

Linux 符号

|
| --- | --- | --- |
| 0x01 | 本地节点 | IPV6 _ ADDR _ 范围 _ 节点本地 |
| 0x02 | 链接本地 | IPV6 _ ADDR _ 范围 _ 链接本地 |
| 0x05 | 本地站点 | IPV6 _ ADDR _ 范围 _ 站点本地 |
| 0x08 | 组织 | IPV6 _ ADDR _ 范围 _ 组织本地 |
| 0x0e | 全球的 | IPV6 _ ADDR _ 范围 _ 全球 |

既然您已经了解了 IPv6 组播地址,您将在下一节了解一些特殊的组播地址。

特殊多播地址

我将在本章中提到一些特殊的多播地址。RFC 4291 的第 2.7.1 节定义了这些特殊的多播地址:

  • 所有节点组播地址组:ff01::1ff02::1
  • 所有路由器组播地址组:ff01::2ff02::2ff05::2

根据 RFC 3810,有这个特殊的地址:所有支持 MLDv2 的路由器组播组,也就是ff02::16。版本 2 多播侦听器报告将被发送到此特殊地址;我将在本章后面的“多播侦听程序发现(MLD)”一节中讨论它。

要求节点计算并加入(在适当的接口上)已经为该节点的接口(手动或自动)配置的所有单播和任播地址的相关请求节点多播地址。请求节点多播地址是基于节点的单播和任播地址计算的。被请求节点多播地址是通过获取地址的低 24 位(单播或任播)并将这些位附加到前缀ff02:0:0:0:0:1:ff00::/104形成的,从而得到范围ff02:0:0:0:0:1:ff00:0000ff02:0:0:0:0:1:ffff:ffff内的多播地址。参见 RFC 4291。

方法addrconf_addr_solict_mult()计算链路本地的请求节点多播地址(include/net/addrconf.h)。方法addrconf_join_solict()加入请求地址多播组(net/ipv6/addrconf.c)。

在前一章中,您看到了邻居通告消息通过ndisc_send_na()方法发送到本地链路的所有节点地址(ff02::1)。在本章后面的小节中,您将看到更多使用特殊地址的例子,如所有节点多播组地址或所有路由器多播组地址。在本节中,您已经看到了一些组播地址,您将在本章后面以及浏览 IPv6 源代码时遇到这些地址。现在,我将在下一节讨论 IPv6 报头。

IPv6 报头

每个 IPv6 数据包都以 IPv6 报头开始,了解其结构对于全面理解 IPv6 Linux 实现非常重要。IPv6 报头具有 40 字节的固定长度;因此,没有指定 IPv6 报头长度的字段(与 IPv4 相反,IPv4 报头的ihl成员表示报头长度)。请注意,IPv6 报头中也没有校验和字段,这将在本章稍后解释。在 IPv6 中,没有像在 IPv4 中那样的 IP 选项机制。IPv4 中的 IP 选项处理机制具有性能成本。相反,IPV6 具有更有效的扩展头机制,这将在下一节“扩展头”中讨论图 8-1 显示了 IPv6 报头及其字段。

9781430261964_Fig08-01.jpg

图 8-1 。IPv6 报头

请注意,在最初的 IPv6 标准 RFC 2460 中,优先级(流量类别)是 8 位,流标签是 20 位。在ipv6hdr结构的定义中,priority(业务类别)字段大小是 4 比特。事实上,在 Linux IPv6 实现中,flow_lbl的前 4 位被粘在priority(流量类)字段上,以便形成一个“类”图 8-1 反映了 Linux 对ipv6hdr结构的定义,如下所示:

struct ipv6hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8                    priority:4,
                                version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u8                    version:4,
                                priority:4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif
        __u8                    flow_lbl[3];

        __be16                  payload_len;
        __u8                    nexthdr;
        __u8                    hop_limit;

        struct  in6_addr        saddr;
        struct  in6_addr        daddr;
};
(include/uapi/linux/ipv6.h)

以下是对ipv6hdr结构成员的描述:

  • version:4 位字段。应该设置为 6。
  • priority : 表示 IPv6 数据包的流量类别或优先级。RFC 2460 是 IPv6 的基础,它没有定义特定的流量类别或优先级值。
  • flow_lbl : 在编写基础 IPv6 标准(RFC 2460)时,流标签字段被认为是实验性的。它提供了一种标记特定流的数据包序列的方法;上层可以出于各种目的使用这种标记。2011 年的 RFC 6437“IPv6 流标签规范”建议使用流标签来检测地址欺骗。
  • payload_len:16 位字段。不含 IPv6 报头的数据包最大可达 65,535 字节。我将在下一节介绍逐跳选项报头时讨论更大的数据包(“巨型帧”)。
  • nexthdr : 当没有扩展头时,这将是上层协议号,如 UDP 的 IPPROTO_UDP (17)或 TCP 的 IPPROTO_TCP (6)。可用协议列表在include/uapi/linux/in.h中。使用扩展标头时,这将是紧跟在 IPv6 标头之后的下一个标头的类型。我将在下一节讨论扩展头。
  • hop_limit : 一个字节字段。每个转发设备将hop_limit计数器减 1。当它达到零时,一条 ICMPv6 消息被发回,数据包被丢弃。这类似于 IPv4 报头中的 TTL 成员。参见net/ipv6/ip6_output.c中的ip6_forward()方法。
  • saddr : IPv6 源地址(128 位)。
  • daddr : IPv6 目的地址(128 位)。如果使用了路由报头,这可能不是最终的数据包目的地。

请注意,与 IPv4 报头不同,IPv6 报头中没有校验和。假设校验和由第 2 层和第 4 层保证。IPv4 中的 UDP 允许校验和为 0,表示没有校验和;IPV6 中的 UDP 通常需要有自己的校验和。在 IPv6 中有一些特殊情况,IPv6 UDP 隧道允许零 UDP 校验和;请参见 RFC 6935,“隧道数据包的 IPv6 和 UDP 校验和”在处理 IPv4 子系统的第四章中,您会看到在转发数据包时会调用ip_decrease_ttl()方法。此方法重新计算 IPv4 报头的校验和,因为ttl的值已更改。在 IPv6 中,转发数据包时不需要重新计算校验和,因为 IPv6 报头中根本没有校验和。这导致基于软件的路由器的性能提高。

在本节中,您已经看到了 IPv6 报头是如何构建的。您看到了 IPv4 报头和 IPv6 报头之间的一些差异,例如,在 IPv6 报头中没有校验和,也没有报头长度。下一节讨论 IPv6 扩展头,它是 IPv4 选项的对应部分。

扩展标题

IPv4 报头可以包括 IP 选项,其可以将 IPv4 报头的最小大小从 20 字节扩展到 60 字节。在 IPv6 中,我们有可选的扩展头。除了一个例外(逐跳选项报头),在分组到达其最终目的地之前,扩展报头不被分组的传递路径上的任何节点处理;这大大提高了转发过程的性能。基础 IPv6 标准定义了扩展报头。IPv6 数据包可以包括 0 个、1 个或多个扩展标头。这些报头可以放在 IPv6 报头和数据包的上层报头之间。IPv6 报头的nexthdr字段是紧跟在 IPv6 报头之后的下一个报头的编号。这些扩展标头是链接的;每个扩展标头都有一个 Next 标头字段。在最后一个扩展头中,Next 头表示上层协议(如 TCP、UDP 或 ICMPv6)。扩展标头的另一个优点是,将来添加新的扩展标头很容易,并且不需要对 IPv6 标头进行任何更改。

扩展头必须严格按照它们在包中出现的顺序进行处理。每个扩展标头最多应出现一次,但目标选项标头除外,它最多应出现两次。(详见本节下文目的地选项标题的说明。)逐跳选项标头必须紧接在 IPv6 标头之后出现;所有其他选项可以以任何顺序出现。RFC 2460 的第 4.1 节(“扩展报头顺序”)陈述了扩展报头应该出现的推荐顺序,但是这不是强制性的。当处理数据包时遇到未知的下一个报头号时,将通过调用icmpv6_param_prob()方法向发送方发回一条 ICMPv6“参数问题”消息,代码为“未知的下一个报头”(ICMPV6_UNK_NEXTHDR)。本章末尾“快速参考”部分的表 8-4 中描述了可用的 ICMPv6“参数问题代码”。

每个扩展头必须在 8 字节边界上对齐。对于可变大小的扩展头,有一个头扩展长度字段,如果需要,它们使用填充来确保它们在 8 字节边界上对齐。所有 Linux IPv6 扩展头的编号及其 Linux 内核符号表示显示在本章末尾“快速参考”部分的表 8-2“IPv6 扩展头”中。

inet6_add_protocol()方法为每个扩展报头(除了逐跳选项报头)注册一个协议处理程序。不为逐跳选项报头注册协议处理程序的原因是有一种特殊的方法来解析逐跳选项报头,即ipv6_parse_hopopts()方法。在调用协议处理程序之前调用此方法。(参见ipv6_rcv()方法,net/ipv6/ip6_input.c)。如前所述,逐跳选项报头必须是第一个报头,紧跟在 IPv6 报头之后。以这种方式,例如,用于片段扩展报头的协议处理程序被注册:

static const struct inet6_protocol frag_protocol =
{
    .handler    =    ipv6_frag_rcv,
    .flags      =    INET6_PROTO_NOPOLICY,
};

int __init ipv6_frag_init(void)
{
    int ret;

    ret = inet6_add_protocol(&frag_protocol, IPPROTO_FRAGMENT);

(net/ipv6/reassembly.c)

以下是对所有 IPv6 扩展标头的描述:

  • 逐跳选项头:必须在每个节点上处理逐跳选项头。它由ipv6_parse_hopopts()方法(net/ipv6/exthdrs.c)解析。

  • 逐跳选项报头必须紧跟在 IPv6 报头之后。例如,它被多播侦听程序发现协议使用,您将在本章后面的“多播侦听程序发现(MLD)”一节中看到。逐跳选项报头包括可变长度选项字段。它的第一个字节是它的类型,可以是下列之一:

  • 路由器警报(Linux 内核符号:IPV6 _ TLV _ 路由器警报,值:5)。请参阅 RFC 6398,“IP 路由器警报注意事项和用法”

  • Jumbo (Linux 内核符号:IPV6_TLV_JUMBO,值:194)。IPv6 数据包有效负载通常最长可达 65,535 字节。使用 jumbo 选项,最高可达 2³² 字节。请参见 RFC 2675,“IPv6 图片”

  • Pad1 (Linux 内核符号:IPV6_TLV_PAD1,值:0)。Pad1 选项用于插入一个字节的填充。当需要多个填充字节时,应使用 PadN 选项(见下一步)(而不是多个 Pad1 选项)。参见 RFC 2460 的第 4.2 节。

  • PadN (Linux 内核符号:IPV6_TLV_PADN,值:1)。PadN 选项用于在报头的选项区域插入两个或多个八位字节的填充。

  • 路由选项头: 这与 IPv4 松散源记录路由(IPOPT_LSRR)平行,在第四章的“IP 选项”一节中讨论。它提供了指定一个或多个路由器的能力,这些路由器应该沿着数据包到其最终目的地的遍历路由被访问。

  • 分片选项头: 与 IPv4 相反,IPv6 中的分片只能发生在发送数据包的主机上,而不能发生在任何中间节点上。碎片由从ip6_finish_output()方法调用的ip6_fragment()方法实现。在ip6_fragment()方法中,有一条慢速路径和一条快速路径,这与 IPv4 分段非常相似。IPv6 分片的实现在net/ipv6/ip6_output.c,IPv6 碎片整理的实现在net/ipv6/reassembly.c

  • 认证头: 认证头(AH)提供数据认证、数据完整性和防重放保护。它在 RFC 4302“IP 认证头”中有描述,这使得 RFC 2402 过时了。

  • 封装安全有效载荷选项头:RFC 4303“IP 封装安全有效载荷(ESP)”中有描述,使得 RFC 2406 过时。注意:封装安全有效载荷(ESP)协议在第十章的中讨论,其中讨论了 IPsec 子系统。

  • 目的地选项头: 目的地选项头在一个数据包中可以出现两次;在路由选项报头之前和之后。当它在路由选项报头之前时,它包括应该由路由器选项报头指定的路由器处理的信息。当它在路由器选项报头之后时,它包括应该由最终目的地处理的信息。

在下一节中,您将看到 IPv6 协议处理程序,即ipv6_rcv()方法,是如何与 IPv6 数据包相关联的。

IPv6 初始化

inet6_init()方法执行各种 IPv6 初始化(如procfs初始化、TCPv6、UDPv6 和其他协议的协议处理程序注册)、IPv6 子系统初始化(如 IPv6 邻居发现、IPv6 多播路由和 IPv6 路由子系统)等。更多细节,请看net/ipv6/af_inet6.c。通过为 IPv6 定义一个packet_type对象并用dev_add_pack()方法注册它,将ipv6_rcv()方法注册为 IPv6 数据包的协议处理程序,这与 IPv4 中的做法非常相似:

static struct packet_type ipv6_packet_type __read_mostly = {
        .type = cpu_to_be16(ETH_P_IPV6),
        .func = ipv6_rcv,
};

static int __init ipv6_packet_init(void)
{
        dev_add_pack(&ipv6_packet_type);
        return 0;
}
(net/ipv6/af_inet6.c)

作为刚才显示的注册的结果,以太网类型为 ETH_P_IPV6 (0x86DD)的每个以太网数据包将由ipv6_rcv()方法处理。接下来,我将讨论用于设置 IPv6 地址的 IPv6 自动配置机制。

自动配置

自动配置是一种允许主机为其每个接口获取或创建唯一地址的机制。IPv6 自动配置过程在系统启动时启动;节点(主机和路由器)为其接口生成一个链路本地地址。该地址被视为“暂定”(设置接口标志 IFA _ F _ 暂定);这意味着它只能与邻居发现消息通信。应该验证该地址没有被链路上的另一个节点使用。这是通过 DAD(重复地址检测)机制来完成的,在前一章讨论 Linux 相邻子系统时已经描述过了。如果节点不是唯一的,自动配置过程将停止,需要手动配置。如果地址是唯一的,自动配置过程将继续。主机自动配置的下一阶段包括向所有路由器多播组地址发送一个或多个路由器请求(ff02::2)。这是通过从addrconf_dad_completed()方法中调用ndisc_send_rs()方法来完成的。路由器回复路由器通告消息,该消息被发送到所有主机地址ff02::1。路由器请求和路由器广告都通过 ICMPv6 消息使用邻居发现协议。路由器请求 ICMPv6 类型是 NDISC_ROUTER_SOLICITATION (133),路由器通告 ICMPv6 类型是 NDISC_ROUTER_ADVERTISEMENT (134)。

radvd守护进程是一个开源路由器广告守护进程的例子,用于无状态自动配置(http://www.litech.org/radvd/)。您可以在radvd配置文件中设置一个前缀,它将在路由器广告消息中发送。radvd守护进程定期发送路由器广告。除此之外,它还监听路由器请求(RS)请求,并用路由器广告(RA) 回复消息进行应答。这些路由器广告(RA)消息包括一个前缀字段,它在自动配置过程中起着重要的作用,您马上就会看到这一点。前缀长度必须为 64 位。当主机收到路由器广告(RA)消息时,它会根据该前缀和自己的 MAC 地址来配置其 IP 地址。如果设置了隐私扩展功能(CONFIG_IPV6_PRIVACY ),那么在 IPV6 地址创建中还会增加一些随机性。隐私扩展机制通过添加前面提到的随机性,避免了从 IPv6 地址获取关于机器身份的细节,IPv6 地址通常是使用其 MAC 地址和前缀生成的。有关隐私扩展的更多详细信息,请参见 RFC 4941,“IPv6 中无状态地址自动配置的隐私扩展”

当主机收到路由器通告消息时,它可以自动配置自己的地址和其它一些参数。它还可以根据这些广告选择默认路由器。也可以为主机上自动配置的地址设置优选寿命有效寿命。preferred lifetime 值指定通过无状态地址自动配置从前缀生成的地址保持在首选状态的时间长度(秒)。当优选时间结束时,该地址将停止通信(不会应答ping6等)。).valid lifetime 值以秒为单位指定地址有效的时间长度(即,已经使用它的应用可以继续使用它);当这个时间结束时,地址被删除。首选生存期和有效生存期在内核中分别由inet6_ifaddr对象的prefered_lftvalid_lft字段表示(include/net/if_inet6.h)。

重新编号是用新前缀替换旧前缀,并根据新前缀更改主机 IPv6 地址的过程。使用radvd也可以很容易地完成重新编号,方法是在它的配置设置中添加一个新的前缀,设置一个首选生存期和一个有效生存期,然后重新启动radvd守护进程。另请参见 RFC 4192,“无标志日的 IPv6 网络重新编号程序”,以及 RFCs 5887、6866 和 6879。

动态主机配置协议版本 6 (DHCPv6) 是有状态地址配置的一个例子;在有状态自动配置模型中,主机从服务器获取接口地址和/或配置信息和参数。服务器维护一个数据库,该数据库记录哪些地址分配给了哪些主机。在这本书里我不会深究 DHCPv6 协议的细节。DHCPv6 协议由 RFC 3315“IPv6 的动态主机配置协议(DHCPv6)”指定 RFC 4862“IPv6 无状态地址自动配置”中描述了 IPv6 无状态自动配置标准

在本节中,您已经了解了自动配置过程,并且看到了通过配置和重启radvd用新前缀替换旧前缀是多么容易。下一节将讨论作为 IPv6 协议处理程序的ipv6_rcv()方法如何处理 IPv6 数据包的接收,其方式与您在 IPv4 中看到的方式有些相似。

接收 IPv6 数据包

主要的 IPv6 接收方法是ipv6_rcv()方法,它是所有 IPv6 数据包(包括多播;如前所述,IPv6 中没有广播)。IPv4 和 IPv6 中的 Rx 路径有许多相似之处。与在 IPv4 中一样,我们首先进行一些完整性检查,比如检查 IPv6 报头的版本是否为 6,以及源地址是否为多播地址。(根据 RFC 4291 的第 2.7 节,这是禁止的。)如果有逐跳选项头,一定是第一个。如果 IPV6 报头的nexthdr的值为 0,这表示逐跳选项报头,并且通过调用ipv6_parse_hopopts()方法对其进行解析。真正的工作由ip6_rcv_finish()方法完成,该方法通过调用 NF_HOOK()宏来调用。如果此时注册了一个 netfilter 回调函数(NF_INET_PRE_ROUTING),它将被调用。我将在下一章讨论 netfilter 钩子。让我们来看看ipv6_rcv()的方法:

int ipv6_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
             struct net_device *orig_dev)
{
        const struct ipv6hdr *hdr;
        u32             pkt_len;
        struct inet6_dev *idev;

从与套接字缓冲区(SKB)相关联的网络设备获取网络名称空间 :

struct net *net = dev_net(skb->dev);

        . . .

从 SKB 获取 IPv6 报头:

hdr = ipv6_hdr(skb);

执行一些健全性检查,并在必要时丢弃 SKB:

        if (hdr->version != 6)
                goto err;

        /*
         * RFC4291 2.5.3
         * A packet received on an interface with a destination address
         * of loopback must be dropped.
         */
        if (!(dev->flags & IFF_LOOPBACK) &&
            ipv6_addr_loopback(&hdr->daddr))
                goto err;

        . . .

        /*
         * RFC4291 2.7
         * Multicast addresses must not be used as source addresses in IPv6
         * packets or appear in any Routing header.
         */
        if (ipv6_addr_is_multicast(&hdr->saddr))
                goto err;

        . . .
        if (hdr->nexthdr == NEXTHDR_HOP) {
                if (ipv6_parse_hopopts(skb) < 0) {
                        IP6_INC_STATS_BH(net, idev, IPSTATS_MIB_INHDRERRORS);
                        rcu_read_unlock();
                        return NET_RX_DROP;
                }
        }
        . . .

        return NF_HOOK(NFPROTO_IPV6, NF_INET_PRE_ROUTING, skb, dev, NULL,
                       ip6_rcv_finish);
err:
        IP6_INC_STATS_BH(net, idev, IPSTATS_MIB_INHDRERRORS);
drop:
        rcu_read_unlock();
        kfree_skb(skb);
        return NET_RX_DROP;
}
(net/ipv6/ip6_input.c)

ip6_rcv_finish()方法首先通过调用ip6_route_input()方法在路由子系统中执行查找,以防没有dst连接到 SKB。ip6_route_input()方法最终会调用fib6_rule_lookup()

int ip6_rcv_finish(struct sk_buff *skb)
 {
      . . .
      if (!skb_dst(skb))
                 ip6_route_input(skb);

调用附属于 SKB 的dstinput回调;

         return dst_input(skb);
 }

(net/ipv6/ip6_input.c)

image 注意fib6_rule_lookup()方法有两种不同的实现:一种是在net/ipv6/fib6_rules.c中设置策略路由(CONFIG_IPV6_MULTIPLE_TABLES)时,一种是在net/ipv6/ip6_fib.c中未设置策略路由时。

正如您在第五章中看到的,该章讨论了 IPv4 路由子系统的高级主题,路由子系统中的查找构建了一个dst对象,并设置了它的inputoutput回调;在 IPv6 中,执行类似的任务。在ip6_rcv_finish()方法在路由子系统中执行查找之后,它调用dst_input()方法,该方法实际上调用了与数据包相关联的dst对象的input回调。

图 8-2 显示了网络驱动程序接收到的数据包的接收路径(Rx) 。此数据包可以被传送到本地机器,也可以被转发到另一台主机。路由表中的查找结果决定了这两个选项中的哪一个会发生。

9781430261964_Fig08-02.jpg

图 8-2 。接收 IPv6 数据包

image 为简单起见,该图不包括扩展头/IPsec 方法的分段/碎片整理/解析。

IPv6 路由子系统中的查找将目的缓存(dst)的input回调设置为:

  • ip6_input()当数据包的目的地是本地机器时。
  • ip6_forward()何时转发数据包。
  • ip6_mc_input()当数据包的目的地是组播地址时。
  • ip6_pkt_discard()数据包将被丢弃的时间。ip6_pkt_discard()方法丢弃数据包,并用目的地不可达(icmp V6 _ DEST _ 未到达)消息回复发送方。

传入的 IPv6 数据包可以在本地传递或转发;在下一节中,您将了解 IPv6 数据包的本地传送。

本地交付

让我们首先看看本地交付案例:ip6_input()方法是一个非常短的方法:

int ip6_input(struct sk_buff *skb)
{
        return NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
                       ip6_input_finish);
}
(net/ipv6/ip6_input.c)

如果在这个点(NF_INET_LOCAL_IN)注册了一个 netfilter 钩子,它将被调用。否则,我们将继续使用ip6_input_finish()方法:

static int ip6_input_finish(struct sk_buff *skb)
{
        struct net *net = dev_net(skb_dst(skb)->dev);
        const struct inet6_protocol *ipprot;

inet6_dev结构(include/net/if_inet6.h)是 IPv4 in_device结构的 IPv6 并行。它包含与 IPv6 相关的配置,例如网络接口单播地址列表(addr_list)和网络接口组播地址列表(mc_list)。该 IPv6 相关配置可由用户使用ip命令或ifconfig命令进行设置。

        struct inet6_dev *idev;
        unsigned int nhoff;
        int nexthdr;
        bool raw;

        /*
         *      Parse extension headers
         */

        rcu_read_lock();
resubmit:
        idev = ip6_dst_idev(skb_dst(skb));
        if (!pskb_pull(skb, skb_transport_offset(skb)))
                goto discard;
        nhoff = IP6CB(skb)->nhoff;

从 SKB 获取下一个标题号:

nexthdr = skb_network_header(skb)[nhoff];

首先,在原始套接字包的情况下,我们尝试将其传递到原始套接字:

raw = raw6_local_deliver(skb, nexthdr);

每个扩展头(除了逐跳扩展头)都有一个协议处理程序,该程序由inet6_add_protocol()方法注册;这个方法实际上在全局inet6_protos数组中添加了一个条目(参见net/ipv6/protocol.c)。

if ((ipprot = rcu_dereference(inet6_protos[nexthdr])) != NULL) {
        int ret;

        if (ipprot->flags & INET6_PROTO_FINAL) {
                const struct ipv6hdr *hdr;

                /* Free reference early: we don't need it any more,
                   and it may hold ip_conntrack module loaded
                   indefinitely. */
                nf_reset(skb);

                skb_postpull_rcsum(skb, skb_network_header(skb),
                                   skb_network_header_len(skb));
                hdr = ipv6_hdr(skb);

RFC 3810 是 MLDv2 规范,它说:“请注意,MLDv2 消息不受源过滤的限制,必须始终由主机和路由器进行处理。”我们不希望由于源过滤而丢弃 MLD 多播分组,因为这些 MLD 分组应该总是根据 RFC 来处理。因此,在丢弃该分组之前,我们确保如果该分组的目的地地址是多播地址,则该分组不是 MLD 分组。这是通过在丢弃之前调用ipv6_is_mld()方法来完成的。如果该方法指示该分组是 MLD 分组,则它不会被丢弃。您还可以在本章后面的“多播侦听器发现(MLD)”一节中了解更多信息。

        if (ipv6_addr_is_multicast(&hdr->daddr) &&
            !ipv6_chk_mcast_addr(skb->dev, &hdr->daddr,
            &hdr->saddr) &&
            !ipv6_is_mld(skb, nexthdr, skb_network_header_len(skb)))
        goto discard;
}

当 INET6_PROTO_NOPOLICY 标志被设置时,这表示不需要对该协议执行 IPsec 策略检查:

        if (!(ipprot->flags & INET6_PROTO_NOPOLICY) &&
            !xfrm6_policy_check(NULL, XFRM_POLICY_IN, skb))
                goto discard;
        ret = ipprot->handler(skb);
        if (ret > 0)
                goto resubmit;
        else if (ret == 0)
                IP6_INC_STATS_BH(net, idev, IPSTATS_MIB_INDELIVERS);
} else {
        if (!raw) {
                if (xfrm6_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                        IP6_INC_STATS_BH(net, idev,
                                         IPSTATS_MIB_INUNKNOWNPROTOS);
                        icmpv6_send(skb, ICMPV6_PARAMPROB,
                                    ICMPV6_UNK_NEXTHDR, nhoff);
                }
                kfree_skb(skb);
        } else {

一切都很顺利,所以增加 INDELIVERS SNMP MIB 计数器(/proc/net/snmp6/Ip6InDelivers)并使用consume_skb()方法释放数据包:

                        IP6_INC_STATS_BH(net, idev, IPSTATS_MIB_INDELIVERS);
                        consume_skb(skb);
                }
        }
        rcu_read_unlock();
        return 0;

discard:
        IP6_INC_STATS_BH(net, idev, IPSTATS_MIB_INDISCARDS);
        rcu_read_unlock();
        kfree_skb(skb);
        return 0;
}
(net/ipv6/ip6_input.c)

您已经看到了本地交付的实现细节,这是由ip6_input()ip6_input_finish()方法执行的。现在是时候转向 IPv6 中转发的实现细节了。同样在这里,IPv4 中的转发和 IPv6 中的转发有许多相似之处。

促进

IPv6 中的转发与 IPv4 中的转发非常相似。不过,还是有一些细微的变化。例如,在 IPv6 中,转发数据包时不计算校验和。(如前所述,IPv6 报头中根本没有校验和字段。)我们来看看ip6_forward()法:

int ip6_forward(struct sk_buff *skb)
{
        struct dst_entry *dst = skb_dst(skb);
        struct ipv6hdr *hdr = ipv6_hdr(skb);
        struct inet6_skb_parm *opt = IP6CB(skb);
        struct net *net = dev_net(dst->dev);
        u32 mtu;

应该设置 IPv6 procfs转发条目(/proc/sys/net/ipv6/conf/all/forwarding):

if (net->ipv6.devconf_all->forwarding == 0)
        goto error;

使用大型接收卸载(LRO)时,数据包长度将超过最大传输单位(MTU)。与在 IPv4 中一样,当启用 LRO 时,SKB 被释放,并返回错误–EINVAL:

if (skb_warn_if_lro(skb))
        goto drop;

if (!xfrm6_policy_check(NULL, XFRM_POLICY_FWD, skb)) {
        IP6_INC_STATS(net, ip6_dst_idev(dst), IPSTATS_MIB_INDISCARDS);
        goto drop;
}

丢弃不是发往本地主机的数据包。与 SKB 相关联的pkt_type是根据传入分组的以太网报头中的目的地 MAC 地址来确定的。这是通过eth_type_trans()方法完成的,该方法通常在网络设备驱动程序中处理传入的数据包时调用。见eth_type_trans()法,net/ethernet/eth.c

if (skb->pkt_type != PACKET_HOST)
        goto drop;

skb_forward_csum(skb);

/*
 *      We DO NOT make any processing on
 *      RA packets, pushing them to user level AS IS
 *      without any WARRANTY that application will be able
 *      to interpret them. The reason is that we
 *      cannot make anything clever here.
 *
 *      We are not end-node, so that if packet contains
 *      AH/ESP, we cannot make anything.
 *      Defragmentation also would be mistake, RA packets
 *      cannot be fragmented, because there is no warranty
 *      that different fragments will go along one path. --ANK
 */
if (opt->ra) {
        u8 *ptr = skb_network_header(skb) + opt->ra;

我们应该尝试将数据包传送到由setsockopt()设置了 IPV6_ROUTER_ALERT 套接字选项的套接字。这是通过调用ip6_call_ra_chain()方法来完成的;如果ip6_call_ra_chain()中的传递成功,ip6_forward()方法返回 0,数据包不被转发。参见net/ipv6/ip6_output.cip6_call_ra_chain()方法的实现。

        if (ip6_call_ra_chain(skb, (ptr[2]<<8) + ptr[3]))
                return 0;
}

/*
 *      check and decrement ttl
 */
if (hdr->hop_limit <= 1) {
        /* Force OUTPUT device used as source address */
        skb->dev = dst->dev;

当跳数限制为 1(或更少)时,发送回 ICMP 错误消息,这与我们在 IPv4 中转发数据包且 TTL 达到 0 时的情况非常相似。在这种情况下,数据包将被丢弃:

        icmpv6_send(skb, ICMPV6_TIME_EXCEED, ICMPV6_EXC_HOPLIMIT, 0);
        IP6_INC_STATS_BH(net,
                         ip6_dst_idev(dst), IPSTATS_MIB_INHDRERRORS);

        kfree_skb(skb);
        return -ETIMEDOUT;
}
/* XXX: idev->cnf.proxy_ndp? */
if (net->ipv6.devconf_all->proxy_ndp &&
    pneigh_lookup(&nd_tbl, net, &hdr->daddr, skb->dev, 0)) {
        int proxied = ip6_forward_proxy_check(skb);
        if (proxied > 0)
                return ip6_input(skb);
        else if (proxied < 0) {
                IP6_INC_STATS(net, ip6_dst_idev(dst),
                              IPSTATS_MIB_INDISCARDS);
                goto drop;
        }
}

if (!xfrm6_route_forward(skb)) {
        IP6_INC_STATS(net, ip6_dst_idev(dst), IPSTATS_MIB_INDISCARDS);
        goto drop;
}
dst = skb_dst(skb);

/* IPv6 specs say nothing about it, but it is clear that we cannot
   send redirects to source routed frames.
   We don't send redirects to frames decapsulated from IPsec.
 */
if (skb->dev == dst->dev && opt->srcrt == 0 && !skb_sec_path(skb)) {
        struct in6_addr *target = NULL;
        struct inet_peer *peer;
        struct rt6_info *rt;

        /*
         *      incoming and outgoing devices are the same
         *      send a redirect.
         */

        rt = (struct rt6_info *) dst;
        if (rt->rt6i_flags & RTF_GATEWAY)
                target = &rt->rt6i_gateway;
        else
                target = &hdr->daddr;

        peer = inet_getpeer_v6(net->ipv6.peers, &rt->rt6i_dst.addr, 1);

        /* Limit redirects both by destination (here)
           and by source (inside ndisc_send_redirect)
         */
        if (inet_peer_xrlim_allow(peer, 1*HZ))
        ndisc_send_redirect(skb, target);
        if (peer)
        inet_putpeer(peer);
} else {
        int addrtype = ipv6_addr_type(&hdr->saddr);

        /* This check is security critical. */
        if (addrtype == IPV6_ADDR_ANY ||
            addrtype & (IPV6_ADDR_MULTICAST | IPV6_ADDR_LOOPBACK))
        goto error;
        if (addrtype & IPV6_ADDR_LINKLOCAL) {
                icmpv6_send(skb, ICMPV6_DEST_UNREACH,
                            ICMPV6_NOT_NEIGHBOUR, 0);
                goto error;
        }
}

请注意,根据基础 IPV6 标准 RFC 2460 的第五部分“数据包大小问题”,IPv6 的 IPV6_MIN_MTU 为 1280 字节。

mtu = dst_mtu(dst);
if (mtu < IPV6_MIN_MTU)
        mtu = IPV6_MIN_MTU;

if ((!skb->local_df && skb->len > mtu && !skb_is_gso(skb)) ||
    (IP6CB(skb)->frag_max_size && IP6CB(skb)->frag_max_size > mtu)) {
        /* Again, force OUTPUT device used as source address */
        skb->dev = dst->dev;

用“数据包太大”的 ICMPv6 消息回复发送者,并释放 SKB;在这种情况下,ip6_forward()方法返回–EMSGSIZ:

        icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
        IP6_INC_STATS_BH(net,
                         ip6_dst_idev(dst), IPSTATS_MIB_INTOOBIGERRORS);
        IP6_INC_STATS_BH(net,
                         ip6_dst_idev(dst), IPSTATS_MIB_FRAGFAILS);
        kfree_skb(skb);
        return -EMSGSIZE;
}
if (skb_cow(skb, dst->dev->hard_header_len)) {
        IP6_INC_STATS(net, ip6_dst_idev(dst), IPSTATS_MIB_OUTDISCARDS);
        goto drop;
}

hdr = ipv6_hdr(skb);

数据包将被转发,因此减少 IPv6 报头的hop_limit

/* Mangling hops number delayed to point after skb COW */
hdr->hop_limit--;

IP6_INC_STATS_BH(net, ip6_dst_idev(dst), IPSTATS_MIB_OUTFORWDATAGRAMS);
IP6_ADD_STATS_BH(net, ip6_dst_idev(dst), IPSTATS_MIB_OUTOCTETS, skb->len);
return NF_HOOK(NFPROTO_IPV6, NF_INET_FORWARD, skb, skb->dev, dst->dev,
               ip6_forward_finish);

error:
IP6_INC_STATS_BH(net, ip6_dst_idev(dst), IPSTATS_MIB_INADDRERRORS);
drop:
kfree_skb(skb);
return -EINVAL;
}
(net/ipv6/ip6_output.c)

ip6_forward_finish()方法是一个单行方法,它简单地调用目的缓存(dst ) output回调:

static inline int ip6_forward_finish(struct sk_buff *skb)
{
return dst_output(skb);
}
(net/ipv6/ip6_output.c)

在本节中,您已经看到了 IPv6 数据包的接收是如何处理的,是通过本地传送还是通过转发。您还看到了接收 IPv6 数据包和接收 IPv4 数据包之间的一些差异。在下一节中,我将讨论多播流量的 Rx 路径。

接收 IPv6 组播数据包

ipv6_rcv()方法是单播包和组播包的 IPv6 处理程序。如上所述,在一些完整性检查之后,它调用ip6_rcv_finish()方法,该方法通过调用ip6_route_input()方法在路由子系统中执行查找。在ip6_route_input()方法中,在接收多播包的情况下,input回调被设置为ip6_mc_input方法。我们来看看ip6_mc_input()的方法:

int ip6_mc_input(struct sk_buff *skb)
{
        const struct ipv6hdr *hdr;
        bool deliver;

        IP6_UPD_PO_STATS_BH(dev_net(skb_dst(skb)->dev),
                         ip6_dst_idev(skb_dst(skb)), IPSTATS_MIB_INMCAST,
                         skb->len);

        hdr = ipv6_hdr(skb);

ipv6_chk_mcast_addr()方法 ( net/ipv6/mcast.c)检查指定网络设备的组播地址列表(mc_list)中是否包含指定的组播地址(本例中为 IPv6 头中的目的地址,hdr->daddr)。注意,因为第三个参数为空,所以我们在这个调用中不检查是否有任何源地址的源过滤器;本章稍后将讨论如何处理源过滤。

deliver = ipv6_chk_mcast_addr(skb->dev, &hdr->daddr, NULL);

如果本地机器是一个多播路由器(也就是说,CONFIG_IPV6_MROUTE 被设置),我们在对ip6_mr_input()方法进行一些检查之后继续。IPv6 组播路由实现与 IPv4 组播路由实现非常相似,在第六章中已经讨论过,所以在本书中不再讨论。IPv6 组播路由实现在net/ipv6/ip6mr.c中。基于 Mickael Hoerdt 的补丁,在内核 2.6.26 (2008)中增加了对 IPv6 多播路由的支持。

#ifdef CONFIG_IPV6_MROUTE
. . .
         if (dev_net(skb->dev)->ipv6.devconf_all->mc_forwarding &&
             !(ipv6_addr_type(&hdr->daddr) &
               (IPV6_ADDR_LOOPBACK|IPV6_ADDR_LINKLOCAL)) &&
             likely(!(IP6CB(skb)->flags & IP6SKB_FORWARDED))) {
                 /*
                  * Okay, we try to forward - split and duplicate
                  * packets.
                  */
                 struct sk_buff *skb2;

                 if (deliver)
                         skb2 = skb_clone(skb, GFP_ATOMIC);
                 else {
                         skb2 = skb;
                         skb = NULL;
                 }

                 if (skb2) {

通过ip6_mr_input()方法(net/ipv6/ip6mr.c)继续 IPv6 组播路由代码:

                         ip6_mr_input(skb2);
                 }

            }
#endif
        if (likely(deliver))
                ip6_input(skb);
        else {
                /* discard */
                kfree_skb(skb);
        }

        return 0;
}

(net/ipv6/ip6_input.c)

当多播数据包不打算通过多播路由转发时(例如,当 CONFIG_IPV6_MROUTE 未设置时),我们将继续使用ip6_input()方法,正如您已经看到的,它实际上是ip6_input_finish()方法的包装器。在ip6_input_finish()方法中,我们再次调用ipv6_chk_mcast_addr()方法,但是这次第三个参数不为空,它是来自 IPv6 报头的源地址。这一次,我们在ipv6_chk_mcast_addr()方法中检查是否设置了源过滤,并相应地处理数据包。源筛选将在本章后面的“多播源筛选(MSF)”一节中讨论。接下来,我将描述多播监听器发现协议,它与 IPv4 IGMPv3 协议并行。

多播监听器发现(MLD)

MLD 协议用于在多播主机和路由器之间交换组信息。MLD 协议是非对称协议;它为多播路由器和多播侦听器指定了不同的行为。在 IPv4 中,组播组管理由互联网组管理协议(IGMP)处理,正如你在第六章中看到的。在 IPv6 中,多播组管理由 MLDv2 协议处理,该协议在 2004 年的 RFC 3810 中指定。MLDv2 协议源自 IPv4 使用的 IGMPv3 协议。然而,与 IGMPv3 协议相反,MLDv2 是 ICMPv6 协议的一部分,而 IGMPv3 是不使用任何 ICMPv4 服务的独立协议;这是 IPv6 中不使用 IGMPv3 协议的主要原因。请注意,您可能会遇到术语 GMP (群组管理协议),它用于指代 IGMP 和 MLD。

多播监听器发现协议的先前版本是 MLDv1,并且它在 RFC 2710 中被指定;它源自 IGMPv2。MLDv1 基于任意源多播(ASM)模型;这意味着您不需要指定从单个源地址或一组地址接收多播流量。MLDv2 通过添加对源特定多播(SSM)的支持来扩展 MLDv1 这意味着节点能够指定是否有兴趣监听来自特定单播源地址的数据包。这个特性被称为源过滤。在本节的后面,我将展示一个简短、详细的用户空间示例,说明如何使用源代码过滤。请参阅 RFC 4604“使用 Internet 组管理协议版本 3 (IGMPv3)和多播侦听程序发现协议版本 2 (MLDv2)进行特定于源的多播”

MLDv2 协议基于多播监听器报告和多播监听器查询。MLDv2 路由器(有时也称为“查询者”)周期性地发送多播监听器查询,以便了解节点的多播组的状态。如果在同一链路上有几个 MLDv2 路由器,则只选择其中一个作为查询者,所有其他路由器都设置为非查询者状态。如 RFC 3810 的 7.6.2 节所述,这是通过查询者选举机制来完成的。节点用多播监听器报告来响应这些查询,在报告中,它们提供关于它们所属的多播组的信息。当侦听器想要停止侦听某个多播组时,它会通知查询者,查询者必须查询该多播组地址的其他侦听器,然后才能将其从其多播地址侦听器状态中删除。MLDv2 路由器可以向多播路由协议提供关于监听器的状态信息。

现在您已经大致了解了什么是 MLD 协议,在下一节中,我将把您的注意力转向如何处理加入和离开多播组。

加入和离开多播组

在 IPv6 中有两种加入或离开多播组的方法。第一个是在内核中,通过调用ipv6_dev_mc_inc()方法,该方法获得一个网络设备对象和一个组播组地址作为参数。比如注册网络设备时,调用ipv6_add_dev()方法;每个设备应该加入接口本地所有节点多播组(ff01::1)和链路本地所有节点多播组(ff02::1)。

static struct inet6_dev *ipv6_add_dev(struct net_device *dev) {

. . .
         /* Join interface-local all-node multicast group */
         ipv6_dev_mc_inc(dev,&in6addr_interfacelocal_allnodes);

         /* Join all-node multicast group */
         ipv6_dev_mc_inc(dev,&in6addr_linklocal_allnodes);

. . .
}
(net/ipv6/addrconf.c)

路由器是设置了其procfs转发条目/proc/sys/net/ipv6/conf/all/forwarding的设备。除了前面提到的每台主机加入的两个多播组之外,路由器还加入了三个多播地址组。它们是链路本地所有路由器多播组(ff02::2)、接口本地所有路由器多播组(ff01::2)和站点本地所有路由器多播组(ff05::2)。

注意,设置 IPv6 procfs转发条目值是由addrconf_fixup_forwarding()方法处理的,该方法最终调用dev_forward_change()方法,该方法根据procfs条目的值(由idev->cnf.forwarding表示,如下面的代码片段所示)使指定的网络接口加入或离开这三个组播地址组:

static void dev_forward_change(struct inet6_dev *idev)
{
        struct net_device *dev;
        struct inet6_ifaddr *ifa;
     . . .
        dev = idev->dev;
     . . .
        if (dev->flags & IFF_MULTICAST) {
                if (idev->cnf.forwarding) {
                        ipv6_dev_mc_inc(dev, &in6addr_linklocal_allrouters);
                        ipv6_dev_mc_inc(dev, &in6addr_interfacelocal_allrouters);
                        ipv6_dev_mc_inc(dev, &in6addr_sitelocal_allrouters);
                } else {
                        ipv6_dev_mc_dec(dev, &in6addr_linklocal_allrouters);
                        ipv6_dev_mc_dec(dev, &in6addr_interfacelocal_allrouters);
                        ipv6_dev_mc_dec(dev, &in6addr_sitelocal_allrouters);
                }
        }
. . .
}
(net/ipv6/addrconf.c)

要从内核中离开一个多播组,应该调用ipv6_dev_mc_dec()方法。加入多播组的第二种方式是通过在用户空间中打开 IPv6 套接字,创建多播请求(ipv6_mreq对象)并将请求的ipv6mr_multiaddr设置为该主机想要加入的多播组地址,并将ipv6mr_interface设置为它想要设置的网络接口的ifindex。然后,它应该使用 IPV6_JOIN_GROUP 套接字选项:调用setsockopt()

int                sockd;
struct ipv6_mreq   mcgroup;
struct addrinfo    *results;
. . .

/* read an IPv6 multicast group address to which we want to join */
/* into the address info object (results) */
. . .

设置我们想要使用的网络接口(通过其ifindex值):

mcgroup.ipv6mr_interface=3;

在请求中为我们想要加入的组设置多播组地址(ipv6mr_multiaddr):

memcpy( &(mcgroup.ipv6mr_multiaddr),
     &(((struct sockaddr_in6 *) results->ai_addr)->sin6_addr),
     sizeof(struct in6_addr));

sockd  = socket(AF_INET6, SOCK_DGRAM,0);

用 IPV6_JOIN_GROUP 调用setsockopt()加入组播组;这个调用在内核中由ipv6_sock_mc_join()方法(net/ipv6/mcast.c)处理。

status = setsockopt(sockd, IPPROTO_IPV6, IPV6_JOIN_GROUP,
                    &mcgroup, sizeof(mcgroup));
. . .

可以使用 IPV6_ADD_MEMBERSHIP socket 选项来代替 IPV6_JOIN_GROUP。(它们是等价的。)注意,通过将网络接口的不同值设置为mcgroup.ipv6mr_interface,我们可以在多个网络设备上设置相同的多播组地址。mcgroup.ipv6mr_interface的值作为ifindex参数传递给ipv6_sock_mc_join()方法。在这种情况下,内核构建并发送一个 MLDv2 多播监听器报告包(ICMPV6_MLD2_REPORT),其中目的地址是ff02::16(所有支持 MLDv2 的路由器多播组地址)。根据 RFC 3810 中的第 5.2.14 节,所有支持 MLDv2 的多播路由器都应该侦听该多播地址。MLDv2 报头中的组播地址记录数(如图 8-3 中的所示)将为 1,因为只使用了一个组播地址记录,其中包含了我们想要加入的组播组的地址。主机想要加入的多播组地址是 ICMPv6 报头的一部分。带有路由器警报的逐跳选项报头在此数据包中设置。MLD 数据包包含一个逐跳选项报头,该报头又包含一个路由器警告选项报头;逐跳扩展报头的下一个报头是 IPPROTO_ICMPV6 (58),因为在逐跳报头之后是 ICMPV6 分组,它包含 MLDv2 消息。

9781430261964_Fig08-03.jpg

图 8-3 。MLDv2 多播侦听器报告

主机可以通过使用 IPV6_DROP_MEMBERSHIP 套接字选项调用setsockopt() 来离开多播组,这在内核中通过调用ipv6_sock_mc_drop()方法或关闭套接字来处理。请注意,IPV6_LEAVE_GROUP 等同于 IPV6_DROP_MEMBERSHIP。

在讨论了如何处理加入和离开多播组之后,是时候看看什么是 MLDv2 多播侦听器报告了。

MLDv2 多播侦听器报告

MLDv2 多播监听器报告在内核中由mld2_report结构表示:

struct mld2_report {
        struct icmp6hdr         mld2r_hdr;
        struct mld2_grec        mld2r_grec[0];
};
(include/net/mld.h)

mld2_report结构的第一个成员是mld2r_hdr,它是一个 ICMPv6 头;它的icmp6_type应该设置为 ICMPV6_MLD2_REPORT (143)。mld2_report结构的第二个成员是mld2r_grec[0],它是mld2_grec结构的一个实例,代表 MLDv2 组记录。(这是图 8-3 中的组播地址记录。)下面是mld2_grec结构的定义:

struct mld2_grec {
        __u8            grec_type;
        __u8            grec_auxwords;
        __be16          grec_nsrcs;
        struct in6_addr grec_mca;
        struct in6_addr grec_src[0];
};
(include/net/mld.h)

以下是对mld2_grec结构成员的描述:

  • grec_type:指定组播地址记录的类型。参见本章末尾“快速参考”部分的表 8-3 “组播地址记录(记录类型)”。
  • grec_auxwords:辅助数据的长度(图 8-3 中的辅助数据长度)。辅助数据字段(如果存在的话)包含与该多播地址记录相关的附加信息。通常是 0。另请参见 RFC 3810 中的第 5.2.10 节。
  • grec_nsrcs:源地址的数量。
  • grec_mca:该组播地址记录所属的组播地址。
  • grec_src[0]:单播源地址(或单播源地址的数组)。这些是我们想要过滤(阻止或允许)的地址。

在下一节中,我将讨论多播源过滤(MSF) 特性。您将在其中找到如何在源过滤中使用多播地址记录的详细示例。

组播源过滤(MSF)

通过多播源过滤,内核将丢弃来自非预期源的多播流量。该功能也称为特定源多播(SSM ),不是 MLDv1 的一部分。它是在 MLDv2 中引入的;参见 RFC 3810。它与任意源多播(ASM)相反,在 ASM 中,接收者对目的地多播地址表示兴趣。为了更好地理解多播源过滤是什么,我将在这里展示一个用户空间应用的例子,演示如何使用源过滤加入和离开多播组。

使用源过滤加入和离开多播组

主机可以通过在用户空间中打开 IPv6 套接字,创建多播组源请求(group_source_req对象),并在请求中设置三个参数: 来加入具有源过滤的多播组

  • gsr_group:该主机想要加入的组播组地址
  • gsr_source:希望允许的组播组源地址
  • ipv6mr_interface:要设置的网络接口的 ifindex

然后它应该使用 MCAST_JOIN_SOURCE_GROUP 套接字选项调用setsockopt()。下面是演示这一点的用户空间应用的代码片段(为了简洁起见,删除了检查系统调用是否成功):

int                       sockd;
struct group_source_req   mreq;
struct addrinfo           *results1;
struct addrinfo           *results2;

/* read an IPv6 multicast group address that we want to join into results1 */
/* read an IPv6 multicast group address which we want to allow into results2 */
memcpy(&(mreq.gsr_group),  results1->ai_addr,  sizeof(struct sockaddr_in6));
memcpy(&(mreq.gsr_source), results2->ai_addr,  sizeof(struct sockaddr_in6));

mreq.gsr_interface = 3;

sockd = socket(AF_INET6, SOCK_DGRAM, 0);
setsockopt(sockd, IPPROTO_IPV6, MCAST_JOIN_SOURCE_GROUP, &mreq, sizeof(mreq));

这个请求首先在内核中由ipv6_sock_mc_join()方法处理,然后由ip6_mc_source()方法处理。要离开群组,您应该使用 MCAST_LEAVE_SOURCE_GROUP 套接字选项调用setsockopt(),或者关闭您打开的套接字。

您可以设置您想要允许的另一个地址,并使用此套接字通过 MCAST_UNBLOCK_SOURCE 套接字选项再次调用setsockopt() 。这将向源过滤器列表添加额外的地址。对setsockopt()的每个这样的调用将触发发送具有一个多播地址记录的 MLDv2 多播监听器报告消息;记录类型将是 5(“允许新源”),源的数量将是 1(您想要解除阻止的单播地址)。我现在将展示一个使用 MCAST_MSFILTER 套接字选项进行源过滤的示例。

示例:使用 MCAST_MSFILTER 进行源过滤

您还可以使用 MCAST_MSFILTER 和一个group_filter对象在一个setsockopt()调用中阻止或允许来自多个多播地址的多播流量。首先,我们来看看用户空间中group_filter结构定义的定义,这个定义相当自明:

struct group_filter
  {
    /* Interface index.  */
    uint32_t gf_interface;

    /* Group address.  */
    struct sockaddr_storage gf_group;

    /* Filter mode.  */
    uint32_t gf_fmode;

    /* Number of source addresses.  */
    uint32_t gf_numsrc;
    /* Source addresses.  */
    struct sockaddr_storage gf_slist[1];
};
(include/netinet/in.h)

过滤模式(gf_fmode)可以是 MCAST_INCLUDE(当您希望允许来自某些单播地址的多播流量时)或 MCAST_EXCLUDE(当您希望禁止来自某些单播地址的多播流量时)。以下是这方面的两个例子:第一个将允许来自三个资源的多播流量,第二个将不允许来自两个资源的多播流量:

struct ipv6_mreq        mcgroup;
struct group_filter     filter;
struct sockaddr_in6     *psin6;

int                     sockd[2];

设置我们想要加入的组播组地址,ffff::9

inet_pton(AF_INET6,"ffff::9", &mcgroup.ipv6mr_multiaddr);

通过它的ifindex设置我们想要使用的网络接口(这里我们使用eth0,它的ifindex值为 2):

mcgroup.ipv6mr_interface=2;

设置过滤器参数:使用相同的ifindex (2),使用 MCAST_INCLUDE 来设置过滤器,以允许来自过滤器指定的源的流量,并将gf_numsrc设置为 3,因为我们想要准备 3 个单播地址的过滤器:

filter.gf_interface = 2;

我们要准备两个过滤器: 第一个过滤器允许来自一组三个多播地址的流量,第二个过滤器允许来自一组两个多播地址的流量。首先将过滤器模式设置为 MCAST_INCLUDE,这意味着允许来自此过滤器的流量:

filter.gf_fmode = MCAST_INCLUDE;

将过滤器的源地址数量(gf_numsrc)设置为 3:

filter.gf_numsrc = 3;

将过滤器(gf_group)的组地址设置为我们之前用于mcgrouopffff::9的地址:

psin6 = (struct sockaddr_in6 *)&filter.gf_group;
psin6->sin6_family = AF_INET6;
inet_pton(PF_INET6, "ffff::9", &psin6->sin6_addr);

我们想要允许的三个单播地址是2000::12000::22000::3

相应地设置filter.gf_slist[0]filter.gf_slist[1]filter.gf_slist[2]:

psin6 = (struct sockaddr_in6 *)&filter.gf_slist[0];
psin6->sin6_family = AF_INET6;
inet_pton(PF_INET6, "2000::1", &psin6->sin6_addr);

psin6 = (struct sockaddr_in6 *)&filter.gf_slist[1];
psin6->sin6_family = AF_INET6;
inet_pton(PF_INET6, "2000::2", &psin6->sin6_addr);

psin6 = (struct sockaddr_in6 *)&filter.gf_slist[2];
psin6->sin6_family = AF_INET6;
inet_pton(PF_INET6, "2000::3",&psin6->sin6_addr);

创建一个套接字,并加入一个多播组:

sockd[0] = socket(AF_INET6, SOCK_DGRAM,0);
status = setsockopt(sockd[0], IPPROTO_IPV6, IPV6_JOIN_GROUP,
        &mcgroup, sizeof(mcgroup));

激活我们创建的过滤器:

status=setsockopt(sockd[0], IPPROTO_IPV6, MCAST_MSFILTER, &filter,
   GROUP_FILTER_SIZE(filter.gf_numsrc));

这将触发向所有嵌入了多播地址记录对象(mld2_grec)的 MLDv2 路由器(ff02::16)发送 MLDv2 多播监听器报告(ICMPV6_MLD2_REPORT)。(参见前面对mld2_report结构和图 8-3 的描述。)字段mld2_grec的值如下:

  • grec_type将是 MLD2_CHANGE_TO_INCLUDE (3)。
  • grec_auxwords将为 0。(我们不使用辅助数据。)
  • grec_nsrcs是 3(因为我们想要使用具有 3 个源地址的过滤器,我们将gf_numsrc设置为 3)。
  • grec_mca将是ffff::9;这是多播地址记录所属的多播组地址。

以下三个单播源地址:

  • grec_src[0]2000::1
  • grec_src[1]2000::2
  • grec_src[2]2000::3

现在,我们想要创建一个过滤器,过滤掉我们想要排除的 2 个单播源地址。所以首先创建一个新的用户空间套接字:

sockd[1] = socket(AF_INET6, SOCK_DGRAM,0);

将过滤器模式设置为排除,并将过滤器的来源数量设置为 2:

filter.gf_fmode = MCAST_EXCLUDE;
filter.gf_numsrc = 2;

设置我们想要排除的两个地址,2001::1 和 2001::2:

psin6 = (struct sockaddr_in6 *)&filter.gf_slist[0];
psin6->sin6_family = AF_INET6;
inet_pton(PF_INET6, "2001::1", &psin6->sin6_addr);

psin6 = (struct sockaddr_in6 *)&filter.gf_slist[1];
psin6->sin6_family = AF_INET6;
inet_pton(PF_INET6, "2001::2", &psin6->sin6_addr);

创建一个套接字,并加入一个多播组:

status = setsockopt(sockd[1], IPPROTO_IPV6, IPV6_JOIN_GROUP,
        &mcgroup, sizeof(mcgroup));

激活过滤器:

status=setsockopt(sockd[1], IPPROTO_IPV6, MCAST_MSFILTER, &filter,
       GROUP_FILTER_SIZE(filter.gf_numsrc));

这将再次触发向所有 MLDv2 路由器发送 MLDv2 多播监听报告(icmp V6 _ ml D2 _ Report)(ff02::16)。这一次多播地址记录对象(mld2_grec)的内容将有所不同:

  • grec_type将 MLD2_CHANGE_TO_EXCLUDE (4)。

  • grec_auxwords将为 0。(我们不使用辅助数据。)

  • grec_nsrcs是 2(因为我们想要使用 2 个源地址,我们将gf_numsrc设置为 2)。

  • grec_mca会是ffff::9,和以前一样;这是多播地址记录所属的多播组地址。

  • 以下两个单播源地址:

  • grec_src[0]2001::1

  • grec_src[1]2002::2

image 注意我们可以显示我们通过cat/proc/net/mcfilter6创建的源过滤映射;这在内核中由igmp6_mcf_seq_show()方法处理。

例如,该映射中的前三个条目将显示对于ffff::9多播地址,我们允许(包括)来自2000::12000::22000::3的多播流量。请注意,对于前三个条目,INC (Include)列中的值是 1。对于第四和第五个条目,我们不允许来自2001::12001::2的流量。请注意,对于第四个和第五个条目,EX (Exclude)列中的值为 1。

cat  /proc/net/mcfilter6
Idx Device Multicast Address               Source Address                   INC    EXC
  2   eth0 ffff0000000000000000000000000009 20000000000000000000000000000001 1      0
  2   eth0 ffff0000000000000000000000000009 20000000000000000000000000000002 1      0
  2   eth0 ffff0000000000000000000000000009 20000000000000000000000000000003 1      0
  2   eth0 ffff0000000000000000000000000009 20010000000000000000000000000001 0      1
  2   eth0 ffff0000000000000000000000000009 20010000000000000000000000000002 0      1

image 注意通过用 MCAST_MSFILTER 调用setsockopt()方法创建过滤器是由ip6_mc_msfilter()方法在内核中处理的,在net/ipv6/mcast.c中。

MLD 路由器(有时也称为“查询者”)在启动时加入所有支持 MLDv2 的路由器多播组(ff02::16)。它周期性地发送多播监听器查询分组,以便知道哪些主机属于多播组,以及它们属于哪个多播组。这些是类型为 ICMPV6_MGM_QUERY 的 ICMPv6 数据包。这些查询包的目的地址是所有主机多播组(ff02::1)。当主机接收到 ICMPv6 多播监听器查询数据包时,ICMPv6 Rx 处理程序(icmpv6_rcv()方法)调用igmp6_event_query()方法来处理该查询。注意,igmp6_event_query()方法 处理 MLDv2 查询和 MLDv1 查询(因为两者都使用 ICMPV6_MGM_QUERY 作为 ICMPV6 类型)。igmp6_event_query()方法通过检查消息的长度来确定消息是 MLDv1 还是 MLDv2 在 MLDv1 中,长度为 24 字节,在 MLDv2 中,长度至少为 28 字节。处理 MLDv1 和 MLDv2 消息是不同的;对于 MLDv2,我们应该支持源滤波,如本节之前所述,而 MLDv1 不支持此功能。主机通过调用igmp6_send()方法发回一个多播监听器报告。多播侦听器报告数据包是一个 ICMPv6 数据包。

IPv6 MLD 路由器的一个例子是开源 XORP 项目的mld6igmp守护进程:http://www.xorp.org。MLD 路由器保存关于网络节点(MLD 监听器)的多播地址组的信息,并动态更新该信息。该信息可以提供给多播路由守护程序。深入研究 MLDv2 路由守护进程(如mld6igmp守护进程)的实现,或者其他多播路由守护进程的实现,超出了本书的范围,因为它是在用户空间中实现的。

根据 RFC 3810,MLDv2 应该与实现 MLDv1 的节点互操作;MLDv2 的实现必须支持以下两种 MLDv1 消息类型:

  • MLDv1 多播侦听器报告(ICMPV6_MGM_REPORT,十进制 131)
  • MLDv1 多播侦听器完成(ICMPV6_MGM_REDUCTION,十进制 132)

我们可以使用 MLDv1 协议代替 MLDv2 协议来处理多播监听消息;这可以通过使用以下方法来实现:

echo 1 > /proc/sys/net/ipv6/conf/all/force_mld_version

在这种情况下,当主机加入多播组时,将通过igmp6_send()方法发送多播监听器报告消息。此消息将使用 MLDv1 的 ICMPV6_MGM_REPORT (131)作为 ICMPV6 类型,而不是 MLDv2 中的 ICMPV6_MLD2_REPORT(143)。请注意,在这种情况下,您不能对此邮件使用源过滤请求,因为 MLDv1 不支持它。我们将通过调用igmp6_join_group()方法加入多播组。当您离开多播组时,将会发送一条多播监听器完成消息。在此消息中,ICMPv6 类型为 ICMPV6_MGM_REDUCTION (132)。

在下一节中,我将非常简要地谈谈 IPv6 Tx 路径,它与 IPv4 Tx 路径非常相似,在本章中我不会深入讨论。

发送 IPv6 数据包

IPv6 Tx 路径非常类似于 IPv4 Tx 路径;甚至方法的名字也非常相似。同样在 IPv6 中,从第 4 层(传输层)发送 IPv6 数据包有两种主要方法:第一种是由 TCP、流控制传输协议(SCTP)和数据报拥塞控制协议(DCCP)使用的ip6_xmit()方法和方法。第二种方法是ip6_append_data()方法,,它被 UDP 和原始套接字使用。在本地主机上创建的数据包通过ip6_local_out()方法发送出去。ip6_output()方法被设置为与协议无关的dst_entry的输出回调;它首先调用 NF_INET_POST_ROUTING 钩子的 NF_HOOK()宏,然后调用ip6_finish_output()方法。如果需要分片,ip6_finish_output()方法调用ip6_fragment()方法处理;否则,它调用ip6_finish_output2()方法,最终发送数据包。有关实现细节,请查看 IPv6 Tx 路径代码;多半是在net/ipv6/ip6_output.c里。

在下一节中,我将简单介绍一下 IPv6 路由,它与 IPv4 路由非常相似,我在本章中不会深入讨论。

IPv6 路由

IPv6 路由的实现与第五章中讨论的 IPv4 路由实现非常相似,第五章讨论了 IPv4 路由子系统。与 IPv4 路由子系统一样,IPv6 也支持策略路由(当设置了 CONFIG_IPV6_MULTIPLE_TABLES 时)。在 IPv6 中,路由条目由rt6_info结构(include/net/ip6_fib.h)表示。rt6_info对象平行于 IPv4 rtable结构,flowi6结构(include/net/flow.h)平行于 IPv4 flowi4结构。(事实上,它们都有相同的flowi_common对象作为第一个成员。)有关实现细节,请查看 IPv6 路由模块:net/ipv6/route.cnet/ipv6/ip6_fib.c和策略路由模块net/ipv6/fib6_rules.c

摘要

在本章中,我讨论了 IPv6 子系统及其实现。我讨论了各种 IPv6 主题,如 IPv6 地址(包括特殊地址和组播地址)、IPv6 报头是如何构建的、IPv6 扩展报头是什么、自动配置过程、IPv6 中的 Rx 路径以及 MLD 协议。在下一章,我们将继续我们的内核网络内部的旅程,并讨论 netfilter 子系统及其实现。在接下来的“快速参考”一节中,我们将按照上下文的顺序,介绍与我们在本章中讨论的主题相关的顶级方法。

快速参考

我用 IPv6 子系统的重要方法的简短列表来结束这一章。本章提到了其中一些。随后,有三个表格和两个小节介绍了 IPv6 特殊地址和 IPv6 中路由表的管理。

方法

先说方法。

bool IPv6 _ addr _ any(const struct in 6 _ addr * a);

如果指定的地址是全零地址(“未指定的地址”),该方法返回true

bool IPv6 _ addr _ equal(const struct in 6 _ addr * a1,const struct in 6 _ addr * a2);

如果两个指定的 IPv6 地址相等,此方法返回true

静态内联 void IPv6 _ addr _ set(struct in 6 _ addr * addr,__be32 w1,__be32 w2,_be32 w3, _ be32 w4);

此方法根据四个 32 位输入参数设置 IPv6 地址。

bool IPv6 _ addr _ is _ multicast(const struct in 6 _ addr * addr);

如果指定的地址是组播地址,这个方法返回true

bool IPv6 _ ext _ HDR(u8 nexthdr);

如果指定的nexthdr是一个众所周知的扩展头,这个方法返回true

ipv6hdr *ipv6_hdr 结构(const struct sk _ buf * skb);

该方法返回指定skb的 IPv6 头(ipv6hdr)。

struct inet 6 _ dev * in 6 _ dev _ get(const struct net _ device * dev);

该方法返回与指定设备相关联的inet6_dev对象。

bool IPv6 _ is _ mld(struct sk _ buff * skb,int nexthdr,int offset);

如果指定的nexthdr是 ICMPv6 (IPPROTO_ICMPV6 ),并且位于指定偏移量处的 ICMPv6 头的类型是 MLD 类型,则该方法返回true。它应该是下列之一:

  • ICMPV6_MGM_QUERY
  • ICMPV6_MGM_REPORT
  • ICMPV6_MGM_REDUCTION
  • ICMPV6_MLD2_REPORT

bool raw 6 _ local _ deliver(struct sk _ buff *,int);

该方法尝试将数据包传递给原始套接字。如果成功,它将返回true

int ipv6_rcv(struct sk_buff *skb,struct net_device *dev,struct packet_type *pt,struct net _ device * orig _ dev);

该方法是 IPv6 数据包的主要接收处理程序。

bool IPv6 _ accept _ ra(struct inet 6 _ dev * idev);

如果主机被配置为接受路由器广告,此方法返回true,在以下情况下:

  • 如果启用转发,则应设置特殊混合模式,这意味着/proc/sys/net/ipv6/conf/<deviceName>/accept_ra为 2。
  • 如果没有启用转发,/proc/sys/net/ipv6/conf/<deviceName>/accept_ra应该为 1。

void ip6 _ route _ input(struct sk _ buff * skb);

该方法是 Rx 路径中主要的 IPv6 路由子系统查找方法。它根据路由子系统中的查找结果设置指定的skbdst entry

int ip6 _ forward(struct sk _ buff * skb);

这种方式是主要的转发方式。

struct dst _ entry * ip6 _ route _ output(struct net * net,const struct sock *sk,struct flow i6 * fl6);

该方法是 Tx 路径中主要的 IPv6 路由子系统查找方法。返回值是目的缓存条目(dst)。

image 注意ip6_route_input()方法和ip6_route_output()方法最终都通过调用fib6_lookup()方法来执行查找。

void in 6 _ dev _ hold(struct inet 6 _ dev * idev);和 void _ _ in6 _ dev _ put(struct inet 6 _ dev * idev);

该方法分别递增和递减指定idev对象的引用计数器。

int ip6 _ MC _ ms filter(struct sock * sk,struct group _ filter * gsf);

这个方法用 MCAST_MSFILTER 处理一个setsockopt()调用。

int ip6 _ MC _ input(struct sk _ buff * skb);

这个方法是多播数据包的主要接收处理程序。

int ip6 _ Mr _ input(struct sk _ buff * skb);

此方法是要转发的多播数据包的主要 Rx 处理程序。

int IPv6 _ dev _ MC _ Inc(struct net _ device * dev,const struct in 6 _ addr * addr);

该方法将指定的设备添加到由addr指定的多播组,或者如果没有找到,则创建这样的组。

int _ _ IPv6 _ dev _ MC _ dec(struct inet 6 _ dev * idev,const struct in 6 _ addr * addr);

此方法从指定的地址组中删除指定的设备。

bool IPv6 _ chk _ mcast _ addr(struct net _ device * dev,const struct in6_addr *group,const struct in 6 _ addr * src _ addr);

此方法检查指定的网络设备是否属于指定的多播地址组。如果第三个参数不为空,它还将检查源过滤是否允许从指定地址(src_addr)接收去往指定多播地址组的多播流量。

inline void addr conf _ addr _ solict _ mult(const struct in 6 _ addr * addr,struct in6_addr *solicited)

此方法计算链路本地请求节点多播地址。

void addr conf _ join _ solict(struct net _ device * dev,const struct in 6 _ addr * addr);

这个方法加入一个请求的地址多播组。

int IPv6 _ sock _ MC _ join(struct sock * sk,int ifindex,const struct in 6 _ addr * addr);

此方法处理多播组上的套接字连接。

int IPv6 _ sock _ MC _ drop(struct sock * sk,int ifindex,const struct in 6 _ addr * addr);

此方法处理多播组上的套接字离开。

int inet 6 _ add _ protocol(const struct inet 6 _ protocol * prot,无符号 char 协议);

此方法注册 IPv6 协议处理程序。它与 L4 协议注册(UDPv6、TCPv6 等)一起使用,也与扩展头(如片段扩展头)一起使用。

int IPv6 _ parse _ hopopts(struct sk _ buff * skb);

此方法解析逐跳选项标头,该标头必须是紧跟在 IPv6 标头之后的第一个扩展标头。

int ip6 _ local _ out(struct sk _ buff * skb);

此方法发送本地主机上生成的数据包。

int ip6 _ fragment(struct sk _ buff * skb,int(* output)(struct sk _ buff *));

此方法处理 IPv6 碎片。它是从ip6_finish_output()方法调用的。

参见 icmpv 6 _ param _ prob(struct sk _ buf * skb,u8 代码,int pos);

此方法发送 ICMPv6 参数问题(ICMPV6_PARAMPROB)错误。当解析扩展头或碎片整理过程中出现问题时,会调用该函数。

int do _ IPv6 _ setsockopt(struct sock * sk,int level,int optname,char __user *optval,signed int opt len);static int do _ IPv6 _ getsockopt(struct sock * sk、int level、int optname、char __user *optval、int __user *optlen、unsigned int flags);

这些方法是通用的 IPv6 处理程序,分别用于在 IPv6 套接字上调用setsockopt()getsockopt()方法(net/ipv6/ipv6_sockglue.c)。

int igmp 6 _ event _ query(struct sk _ buff * skb);

此方法处理 MLDv2 和 MLDv1 查询。

void ip6 _ route _ input(struct sk _ buff * skb);

该方法通过基于指定的skb构建一个flow6对象并调用ip6_route_input_lookup()方法来执行路由查找。

宏指令

这是宏指令。

IPV6_ADDR_MC_SCOPE()

此宏返回指定 IPv6 多播地址的范围,该地址位于多播地址的第 11-14 位。

IPV6_ADDR_MC_FLAG_TRANSIENT()

如果设置了指定多播地址标志的 T 位,则该宏返回 1。

IPV6 _ ADDR _ MC _ FLAG _ 前缀( )

如果设置了指定多播地址标志的 P 位,此宏返回 1。

IPV6_ADDR_MC_FLAG_RENDEZVOUS()

如果设置了指定多播地址标志的 R 位,此宏返回 1。

桌子

这些是桌子。

表 8-2 显示了 IPv6 扩展报头的 Linux 符号、值和描述。您可以在本章的“扩展标题”一节中找到更多详细信息。

表 8-2 。IPv6 扩展标头

|

Linux 符号

|

价值

|

描述

|
| --- | --- | --- |
| nexthdr _ 跃点 | Zero | 逐跳选项头。 |
| NEXTHDR_TCP | six | TCP 段。 |
| NEXTHDR_UDP 文件 | Seventeen | UDP 消息。 |
| NEXTHDR_IPV6 | Forty-one | IPv6 中的 IPv6。 |
| NEXTHDR_ROUTING | Forty-three | 路由标题。 |
| NEXTHDR_FRAGMENT | forty-four | 分段/重组标题。 |
| NEXTHDR_GRE 版 | Forty-seven | GRE 标题。 |
| NEXTHDR_ESP | Fifty | 封装安全负载。 |
| NEXTHDR_AUTH 节 | Fifty-one | 认证头。 |
| NEXTHDR_ICMP | Fifty-eight | IPv6 的 ICMP。 |
| NEXTHDR_NONE(无) | Fifty-nine | 没有下一个标题。 |
| NEXTHDR_DEST | Sixty | 目标选项标题。 |
| NEXTHDR_MOBILITY | One hundred and thirty-five | 移动标题。 |

表 8-3 通过其 Linux 符号和值 ?? 显示了组播地址记录类型。有关更多详细信息,请参见本章中的“MLDv2 多播监听器报告”一节。

表 8-3 。多播地址记录(记录类型)

|

Linux 符号

|

价值

|
| --- | --- |
| MLD2 _ 模式 _ 包含 | one |
| MLD2_MODE_IS_EXCLUDE | Two |
| MLD2 _ 更改为 _ 包含 | three |
| MLD2 _ 更改为 _ 排除 | four |
| MLD2_ALLOW_NEW_SOURCES | five |
| MLD2_BLOCK_OLD_SOURCES | six |

(include/uapi/linux/icmpv6.h)

表 8-4 按 Linux 符号和值显示了 ICMPv6“参数问题”消息的代码。这些代码提供了有关所发生问题类型的更多信息。

表 8-4 。ICMPv6 参数问题代码

|

Linux 符号

|

价值

|
| --- | --- |
| icmp V6 _ HDR _ 字段 | 遇到 0 个错误的标头字段 |
| ICMPV6_UNK_NEXTHDR | 遇到 1 个未知的标题字段 |
| icmp V6 _ UNK _ 选项 | 2 遇到未知的 IPv6 选项 |

特殊地址

以下所有变量都是in6_addr结构的实例:

  • in6addr_any:表示未指定的全零器件(::)。
  • in6addr_loopback:表示环回设备(::1)。
  • in6addr_linklocal_allnodes:表示链路本地所有节点组播地址(ff02::1)。
  • in6addr_linklocal_allrouters:表示链路本地所有路由器组播地址(ff02::2)。
  • in6addr_interfacelocal_allnodes:表示接口本地所有节点(ff01::1)。
  • in6addr_interfacelocal_allrouters:表示接口本地所有路由器(ff01::2)。
  • in6addr_sitelocal_allrouters:表示本地所有路由器的地址(ff05::2)。
(include/linux/in6.h)

IPv6 中的路由表管理

像在 IPv4 中一样,我们可以使用iproute2ip route命令和net-toolsroute命令来管理添加和删除路由表:

  • 通过ip -6 route add添加路由是由inet6_rtm_newroute()方法通过调用ip6_route_add()方法来处理的。
  • 通过调用ip6_route_del()方法,由inet6_rtm_delroute()方法处理ip -6 route del删除路由。
  • 通过ip -6 route show显示路由表是由inet6_dump_fib()方法处理的。
  • 通过调用ip6_route_add()方法,发送由ipv6_route_ioctl()方法处理的 SIOCADDRT IOCTL 来实现由route -A inet6 add添加路由。
  • 通过发送 SIOCDELRT IOCTL 来实现由route -A inet6 del删除路由,这由ipv6_route_ioctl()方法通过调用ip6_route_del()方法来处理。

九、NetFilter

第八章讨论了 IPv6 子系统的实现。本章讨论 netfilter 子系统。netfilter 框架是由 Rusty Russell 于 1998 年创建的,他是最著名的 Linux 内核开发人员之一,是对旧版本的ipchains (Linux 2.2.x)和ipfwadm (Linux 2.0.x)的改进。netfilter 子系统提供了一个框架,允许在网络堆栈中数据包遍历的各个点(netfilter 挂钩)注册回调,并对数据包执行各种操作,如更改地址或端口、丢弃数据包、记录日志等。这些 netfilter 钩子 为注册回调的 netfilter 内核模块提供基础设施,以便执行 netfilter 子系统的各种任务。

Netfilter 框架

netfilter 子系统提供本章中讨论的以下功能:

  • 数据包选择(iptables)
  • 包过滤
  • 网络地址转换(NAT)
  • 数据包篡改(在路由之前或之后修改数据包报头的内容)
  • 连接跟踪
  • 收集网络统计数据

以下是一些基于 Linux 内核 netfilter 子系统的常见框架:

  • IPVS (IP 虚拟服务器): 一种传输层负载均衡解决方案(net/netfilter/ipvs)。很早的内核就支持 IPv4 IPVS,从内核 2.6.28 开始,支持 IPv6 中的 IPVS。对 IPVS 的 IPv6 内核支持是由谷歌的朱利叶斯·沃尔茨和文斯·布萨姆开发的。更多详情见 IPVS 官网,www.linuxvirtualserver.org
  • IP 集: 一个框架,由一个叫做ipset的用户空间工具和一个内核部分(net/netfilter/ipset)组成。IP 集基本上是一组 IP 地址。IP sets 框架是由 Jozsef Kadlecsik 开发的。更多细节请见http://ipset.netfilter.org
  • iptables: 大概是最流行的 Linux 防火墙, iptables 是 netfilter 的前端,它为 netfilter 提供了一个管理层:比如添加和删除 netfilter 规则,显示统计数据,添加一个表,将一个表的计数器清零等等。

根据协议,内核中有不同的 iptables 实现:

  • 【IPv4 的 iptables:(net/ipv4/netfilter/ip_tables.c
  • 【IPv6 的 IP 6 tables:(net/ipv6/netfilter/ip6_tables.c)
  • ARP 表用于 ARP: ( net/ipv4/netfilter/arp_tables.c)
  • 以太网的数据表😦net/bridge/netfilter/ebtables.c)

在用户空间中,有iptablesip6tables命令行工具,分别用于设置、维护和检查 IPv4 和 IPv6 表。参见man 8 iptablesman 8 ip6tablesiptablesip6tables都使用setsockopt() / getsockopt()系统调用从用户空间与内核通信。我应该在这里提到两个有趣的正在进行的 netfilter 项目。xtables2项目——主要由 Jan Engelhardt 开发,在撰写本文时仍在进行中——使用基于 netlink 的接口与内核 netfilter 子系统通信。详见项目网站http://xtables.de。第二个项目是nftables项目,它是一个新的包过滤引擎,可以替代iptablesnftables解决方案基于使用一个虚拟机和一个统一的实现,而不是前面提到的四个iptables对象(iptablesip6tablesarptablesebtables)。Patrick McHardy 在 2008 年的 netfilter 研讨会上首次提出了nftables项目。内核基础设施和用户空间实用程序是由 Patrick McHardy 和 Pablo Neira Ayuso 开发的。更多详细信息,请参见http://netfilter.org/projects/nftableshttp://lwn.net/Articles/324989/的“Nftables:一个新的包过滤引擎”。

有许多 netfilter 模块扩展了核心 netfilter 子系统的核心功能;除了一些例子,我不在这里深入描述这些模块。从管理的角度来看,网上和各种管理指南中有许多关于这些 netfilter 扩展的信息资源。另见 netfilter 项目官方网站:www.netfilter.org

Netfilter 挂钩

网络堆栈中有五个点有 netfilter 挂钩:在前面章节讨论 IPv4 和 IPv6 中的 Rx 和 Tx 路径时,您会遇到这些点。请注意,挂钩的名称对于 IPv4 和 IPv6 是通用的:

  • NF_INET_PRE_ROUTING: 这个钩子在 IPv4 的ip_rcv()方法中,在 IPv6 的ipv6_rcv()方法中。ip_rcv()方法是 IPv4 的协议处理器,ipv6_rcv()方法是 IPv6 的协议处理器。在路由子系统中执行查找之前,它是所有传入数据包到达的第一个挂钩点。
  • NF_INET_LOCAL_IN: 这个钩子在 IPv4 的ip_local_deliver()方法中,在 IPv6 的ip6_input()方法中。寻址到本地主机的所有传入分组在第一次通过 NF_INET_PRE_ROUTING 钩点之后以及在路由子系统中执行查找之后到达该钩点。
  • NF_INET_FORWARD: 这个钩子在 IPv4 的ip_forward()方法中,在 IPv6 的ip6_forward()方法中。所有转发的数据包在第一次通过 NF_INET_PRE_ROUTING 挂钩点并在路由子系统中执行查找后到达该挂钩点。
  • NF_INET_POST_ROUTING: 这个钩子在 IPv4 的ip_output()方法中,在 IPv6 的ip6_finish_output2()方法中。转发的数据包在通过 NF_INET_FORWARD 钩子点后到达这个钩子点。同样,在本地机器中创建并发送出去的数据包在通过 NF_INET_LOCAL_OUT 钩子点之后到达 NF_INET_POST_ROUTING。
  • NF_INET_LOCAL_OUT: 这个钩子在 IPv4 的__ip_local_out()方法中,在 IPv6 的__ip6_local_out()方法中。在本地主机上创建的所有传出数据包在到达 NF_INET_POST_ROUTING 挂钩点之前到达该点。
(include/uapi/linux/netfilter.h)

前几章提到的 NF_HOOK 宏在内核网络栈中数据包遍历的一些不同点被调用;它在include/linux/netfilter.h中定义:

static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb,
                  struct net_device *in, struct net_device *out,
                  int (*okfn)(struct sk_buff *))
{
    return NF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN);
}

NF_HOOK()的参数如下:

  • pf:协议族。IPV4 的 NFPROTO_IPV4 和 IPV6 的 NFPROTO_IPV6。
  • hook:前面提到的五个 netfilter 钩子之一(比如 NF_INET_PRE_ROUTING 或者 NF_INET_LOCAL_OUT)。
  • skb:SKB 对象代表正在处理的数据包。
  • in:输入网络设备(net_device对象)。
  • out:输出网络设备(net_device对象)。存在输出设备为空的情况,因为它还是未知的;比如在ip_rcv()方法中,net/ipv4/ip_input.c,在执行路由查找之前调用,你还不知道哪个是输出设备;NF_HOOK()宏在这个方法中用一个空输出设备调用。
  • 一个指向延续函数的指针,当钩子终止时,这个函数将被调用。它有一个参数,SKB。

netfilter 挂钩的返回值必须是下列值之一(也称为 netfilter 判断):

  • NF_DROP (0):无声地丢弃数据包。
  • NF_ACCEPT (1):包照常继续在内核网络堆栈中遍历。
  • NF _ stocked(2):不继续遍历。该数据包由钩子方法处理。
  • NF_QUEUE (3):将数据包排入用户空间的队列。
  • NF_REPEAT (4):应该再次调用钩子函数。
(include/uapi/linux/netfilter.h)

现在您已经了解了各种 netfilter 挂钩,下一节将介绍如何注册 netfilter 挂钩。

Netfilter 挂钩的注册

要在前面提到的五个挂钩点之一注册一个挂钩回调,首先要定义一个nf_hook_ops对象(或者一个nf_hook_ops对象的数组),然后注册它;nf_hook_ops结构在include/linux/netfilter.h中定义:

struct nf_hook_ops {
    struct list_head list;

    /* User fills in from here down. */
    nf_hookfn     *hook;
    struct module *owner;
    u_int8_t      pf;
    unsigned int  hooknum;
    /* Hooks are ordered in ascending priority. */
    int           priority;
};

下面介绍一些nf_hook_ops结构的重要成员:

  • hook:你要注册的钩子回调。它的原型是:

    unsigned int nf_hookfn(unsigned int hooknum,
                           struct sk_buff *skb,
                           const struct net_device *in,
                           const struct net_device *out,
                           int (*okfn)(struct sk_buff *));
    
    
  • pf:协议族(IPV4 为 NFPROTO_IPV4,IPV6 为 NFPROTO_IPV6)。

  • hooknum:前面提到的五个 netfilter 钩子之一。

  • priority:同一个挂钩上可以注册多个挂钩回叫。首先调用优先级较低的钩子回调。nf_ip_hook_priorities enum定义了 IPv4 挂钩优先级的可能值(include/uapi/linux/netfilter_ipv4.h)。参见本章末尾“快速参考”部分的表 9-4 。

注册 netfilter 挂钩有两种方法:

  • int nf_register_hook(struct nf_hook_ops *reg):注册单个nf_hook_ops对象。
  • int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n):注册一个由 n 个对象组成的数组;第二个参数是数组中元素的数量。

在接下来的两节中,您将看到两个注册一个nf_hook_ops对象数组的例子。下一节的图 9-1 说明了在同一个钩子点注册多个钩子回调时优先级的使用。

连接跟踪

在现代网络中,仅根据 L4 和 L3 报头过滤流量是不够的。您还应该考虑流量基于会话的情况,例如 FTP 会话或 SIP 会话。对于 FTP 会话,我指的是这一系列事件,例如:客户端首先在 TCP 端口 21 上创建一个 TCP 控制连接,这是默认的 FTP 端口。从 FTP 客户端发送到服务器的命令(例如列出目录的内容)在此控制端口上发送。FTP 服务器在端口 20 上打开一个数据套接字,其中客户端的目的端口是动态分配的。应该根据其他参数(如连接状态或超时)过滤流量。这是使用连接跟踪层的主要原因之一。

连接跟踪允许内核跟踪会话。连接跟踪层的主要目标是作为 NAT 的基础。如果未设置 CONFIG_NF_CONNTRACK_IPV4,则无法构建 IPv4 NAT 模块 ( net/ipv4/netfilter/iptable_nat.c)。同样,如果没有设置 CONFIG_NF_CONNTRACK_IPV6,则不能构建 IPv6 NAT 模块(net/ipv6/netfilter/ip6table_nat.c)。但是,连接跟踪不依赖于 NAT 您可以在不激活任何 NAT 规则的情况下运行连接跟踪模块。本章稍后将讨论 IPv4 和 IPv6 NAT 模块。

image 注意本章末尾的“快速参考”部分提到了一些用于连接跟踪管理的用户空间工具(conntrack-tools)。这些工具可以帮助您更好地理解连接跟踪层。

连接跟踪初始化

称为ipv4_conntrack_opsnf_hook_ops对象数组定义如下:

static struct nf_hook_ops ipv4_conntrack_ops[] __read_mostly = {
        {
                .hook           = ipv4_conntrack_in,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_PRE_ROUTING,
                .priority       = NF_IP_PRI_CONNTRACK,
        },
        {
                .hook           = ipv4_conntrack_local,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_OUT,
                .priority       = NF_IP_PRI_CONNTRACK,
        },
        {
                .hook           = ipv4_helper,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_POST_ROUTING,
                .priority       = NF_IP_PRI_CONNTRACK_HELPER,
        },
        {
                .hook           = ipv4_confirm,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_POST_ROUTING,
                .priority       = NF_IP_PRI_CONNTRACK_CONFIRM,
        },
        {
                .hook           = ipv4_helper,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_IN,
                .priority       = NF_IP_PRI_CONNTRACK_HELPER,
        },
        {
                .hook           = ipv4_confirm,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_IN,
                .priority       = NF_IP_PRI_CONNTRACK_CONFIRM,
        },
};
(net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c)

您注册的两个最重要的连接跟踪挂钩是由ipv4_conntrack_in()方法处理的 NF_INET_PRE_ROUTING 挂钩和由ipv4_conntrack_local()方法处理的 NF_INET_LOCAL_OUT 挂钩。这两个钩子的优先级是 NF_IP_PRI_CONNTRACK (-200)。ipv4_conntrack_ops数组中的其他钩子具有 NF_IP_PRI_CONNTRACK_HELPER (300)优先级和 nf _ IP _ pri _ conntrack _ confirm(int _ max,2³¹-1)优先级。在 netfilter 挂钩中,首先执行具有较低优先级值的回调。(include/uapi/linux/netfilter_ipv4.h中的enum nf_ip_hook_priorities代表 IPv4 挂钩可能的优先级值)。ipv4_conntrack_local()方法和ipv4_conntrack_in()方法都调用nf_conntrack_in()方法,将相应的hooknum作为参数传递。nf_conntrack_in() 方法属于与协议无关的 NAT 核心,既用于 IPv4 连接跟踪,也用于 IPv6 连接跟踪;它的第二个参数是协议族,指定它是 IPv4 (PF_INET)还是 IPv6 (PF_INET6)。我从nf_conntrack_in()回调开始讨论。其他钩子回调函数ipv4_confirm()ipv4_help()将在本节稍后讨论。

image 注意当内核构建了连接跟踪支持(CONFIG_NF_CONNTRACK 已设置)时,即使没有激活 iptables 规则,也会调用连接跟踪钩子回调。自然,这需要一些性能成本。如果性能非常重要,并且您事先知道设备不会使用 netfilter 子系统,那么可以考虑构建没有连接跟踪支持的内核,或者将连接跟踪构建为内核模块而不加载它。

IPv4 连接跟踪钩子的注册是通过调用nf_conntrack_l3proto_ipv4_init()方法(net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c)中的nf_register_hooks()方法来完成的:

in nf_conntrack_l3proto_ipv4_init(void) {
       . . .
       ret = nf_register_hooks(ipv4_conntrack_ops,
                                 ARRAY_SIZE(ipv4_conntrack_ops))
       . . .
}

在图 9-1 中,可以看到连接跟踪回调 ( ipv4_conntrack_in()ipv4_conntrack_local()ipv4_helper()ipv4_confirm()),根据它们注册的钩子点。

9781430261964_Fig09-01.jpg

图 9-1 。连接跟踪挂钩(IPv4)

image 为了简单起见,图 9-1 没有包括更复杂的场景,比如使用 IPsec 或者分片或者组播的时候。为了简单起见,它还省略了在本地主机上生成并发送出去的数据包所调用的函数(如ip_queue_xmit()方法或ip_build_and_send_pkt()方法)。

连接跟踪的基本元素是nf_conntrack_tuple结构:

struct nf_conntrack_tuple {
        struct nf_conntrack_man src;

        /* These are the parts of the tuple which are fixed. */
        struct {
                union nf_inet_addr u3;
                union {
                        /* Add other protocols here. */
                        __be16 all;

                        struct {
                                __be16 port;
                        } tcp;
                        struct {
                                __be16 port;
                        } udp;
                        struct {
                                u_int8_t type, code;
                        } icmp;
                        struct {
                                __be16 port;
                        } dccp;
                        struct {
                                __be16 port;
                        } sctp;
                        struct {
                                __be16 key;
                        } gre;
                } u;

                /* The protocol. */
                u_int8_t protonum;

                /* The direction (for tuplehash) */
                u_int8_t dir;
        } dst;
};
(include/net/netfilter/nf_conntrack_tuple.h)

nf_conntrack_tuple结构表示一个方向的流程。dst结构中的联合包括各种协议对象(如 TCP、UDP、ICMP 等等)。对于每个传输层(L4)协议,都有一个连接跟踪模块,它实现协议特定的部分。因此,举例来说,TCP 协议有net/netfilter/nf_conntrack_proto_tcp.c,UDP 协议有net/netfilter/nf_conntrack_proto_udp.c,FTP 协议有net/netfilter/nf_conntrack_ftp.c,等等;这些模块支持 IPv4 和 IPv6。在本节的后面,您将看到连接跟踪模块的特定于协议的实现如何不同的示例。

连接跟踪条目

nf_conn结构表示连接跟踪条目:

struct nf_conn {
        /* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
           plus 1 for any connection(s) we are `master' for */
        struct nf_conntrack ct_general;

        spinlock_t lock;

        /* XXX should I move this to the tail ? - Y.K */
        /* These are my tuples; original and reply */
        struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];

        /* Have we seen traffic both ways yet? (bitset) */
        unsigned long status;

        /* If we were expected by an expectation, this will be it */
        struct nf_conn *master;

        /* Timer function; drops refcnt when it goes off. */
        struct timer_list timeout;

      . . .

        /* Extensions */
        struct nf_ct_ext *ext;
#ifdef CONFIG_NET_NS
        struct net *ct_net;
#endif

        /* Storage reserved for other modules, must be the last member */
        union nf_conntrack_proto proto;
};
(include/net/netfilter/nf_conntrack.h)

以下是对nf_conn结构中一些重要成员的描述:

  • ct_general:参考计数。
  • tuplehash:有两个tuplehash对象:tuplehash[0]是原方向,tuplehash[1]是回复。它们通常分别被称为tuplehash[IP_CT_DIR_ORIGINAL]tuplehash[IP_CT_DIR_REPLY]
  • status:条目的状态。当你开始跟踪一个连接条目时,它是 IP _ CT _ NEW 稍后,当连接建立时,它变成 IP_CT_ESTABLISHED。参见include/uapi/linux/netfilter/nf_conntrack_common.h中的ip_conntrack_info enum
  • master:预期的连接。由init_conntrack()方法设置,当一个期望的包到达时(这意味着由init_conntrack()方法调用的nf_ct_find_expectation()方法找到一个期望)。另请参阅本章后面的“连接跟踪助手和期望”一节。
  • timeout:连接条目的定时器。当没有流量时,每个连接条目在一段时间间隔后到期。时间间隔根据协议确定。当用__nf_conntrack_alloc()方法分配一个nf_conn对象时,超时定时器被设置为death_by_timeout()方法。

现在您已经了解了nf_conn struct及其一些成员,让我们来看看nf_conntrack_in()方法:

unsigned int nf_conntrack_in(struct net *net, u_int8_t pf, unsigned int hooknum,
                          struct sk_buff *skb)
{
        struct nf_conn *ct, *tmpl = NULL;
        enum ip_conntrack_info ctinfo;
        struct nf_conntrack_l3proto *l3proto;
        struct nf_conntrack_l4proto *l4proto;
        unsigned int *timeouts;
        unsigned int dataoff;
        u_int8_t protonum;
        int set_reply = 0;
        int ret;

        if (skb->nfct) {
                /* Previously seen (loopback or untracked)?  Ignore. */
                tmpl = (struct nf_conn *)skb->nfct;
                if (!nf_ct_is_template(tmpl)) {
                        NF_CT_STAT_INC_ATOMIC(net, ignore);
                        return NF_ACCEPT;
                }
                skb->nfct = NULL;
        }

首先,您尝试确定网络层(L3)协议是否可以被跟踪:

        l3proto = __nf_ct_l3proto_find(pf);

现在,您尝试确定是否可以跟踪传输层(L4)协议。对于 IPv4,通过ipv4_get_l4proto()方法(net/ipv4/netfilter/nf_conntrack_l3proto_ipv4)完成:

        ret = l3proto->get_l4proto(skb, skb_network_offset(skb),
                           &dataoff, &protonum);
        if (ret <= 0) {
           . . .
                ret = -ret;
                goto out;
}
        l4proto = __nf_ct_l4proto_find(pf, protonum);
        /* It may be an special packet, error, unclean...
         * inverse of the return code tells to the netfilter
         * core what to do with the packet. */

现在您可以检查特定于协议的错误条件(例如,参见net/netfilter/nf_conntrack_proto_udp.c中的udp_error()方法,它检查格式错误的数据包、带有无效校验和的数据包等等,或者参见net/netfilter/nf_conntrack_proto_tcp.c中的tcp_error()方法):

        if (l4proto->error != NULL) {
                ret = l4proto->error(net, tmpl, skb, dataoff, &ctinfo,
                                     pf, hooknum);
                if (ret <= 0) {
                        NF_CT_STAT_INC_ATOMIC(net, error);
                        NF_CT_STAT_INC_ATOMIC(net, invalid);
                        ret = -ret;
                        goto out;
                }
                /* ICMP[v6] protocol trackers may assign one conntrack. */
                if (skb->nfct)
                        goto out;
        }

此后立即调用的resolve_normal_ct()方法执行以下操作:

  • 通过调用hash_conntrack_raw()方法计算元组的散列。

  • 通过调用__nf_conntrack_find_get()方法执行元组匹配的查找,将散列作为参数传递。

  • 如果没有找到匹配,它通过调用init_conntrack()方法创建一个新的nf_conntrack_tuple_hash对象。这个nf_conntrack_tuple_hash对象被添加到未确认的 tuplehash 对象列表中。该列表嵌入在网络名称空间对象中;net结构包含一个netns_ct对象,它由网络名称空间特定的连接跟踪信息组成。它的成员之一是unconfirmed,是未确认的tuplehash对象列表(见include/net/netns/conntrack.h)。稍后,在__nf_conntrack_confirm()方法中,它将从未确认列表中删除。我将在本节的后面讨论__nf_conntrack_confirm()方法。

  • 每个 SKB 都有一个名为nfctinfo的成员,它代表连接状态(例如,它是新连接的 IP_CT_NEW),还有一个名为nfct(nf_conntrack struct的一个实例)的成员,它实际上是一个引用计数器。resolve_normal_ct()方法初始化这两者。

    ct = resolve_normal_ct(net, tmpl, skb, dataoff, pf, protonum,
                           l3proto, l4proto, &set_reply, &ctinfo);
    if (!ct) {
            /* Not valid part of a connection */
            NF_CT_STAT_INC_ATOMIC(net, invalid);
            ret = NF_ACCEPT;
            goto out;
    }
    
        if (IS_ERR(ct)) {
                /* Too stressed to deal. */
                NF_CT_STAT_INC_ATOMIC(net, drop);
                ret = NF_DROP;
                goto out;
}
        NF_CT_ASSERT(skb->nfct);

您现在调用nf_ct_timeout_lookup()方法来决定您想要对这个流应用什么超时策略。例如,对于 UDP,单向连接的超时为 30 秒,双向连接的超时为 180 秒;参见net/netfilter/nf_conntrack_proto_udp.cudp_timeouts数组的定义。对于复杂得多的协议 TCP,在tcp_timeouts数组(net/netfilter/nf_conntrack_proto_tcp.c)中有 11 个条目:

        /* Decide what timeout policy we want to apply to this flow. */
        timeouts = nf_ct_timeout_lookup(net, ct, l4proto);

您现在调用特定于协议的packet()方法(例如,UDP 的udp_packet()或 TCP 的tcp_packet()方法)。udp_packet()方法通过调用nf_ct_refresh_acct()方法根据连接的状态延长超时时间。对于未应答的连接(未设置 IPS_SEEN_REPLY_BIT 标志),它将被设置为 30 秒,而对于应答的连接,它将被设置为 180。同样,在 TCP 的情况下,tcp_packet()方法要复杂得多,这是由于 TCP 高级状态机的缘故。此外,udp_packet()方法总是返回 NF_ACCEPT 的结论,而tcp_packet()方法有时可能会失败:

        ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum, timeouts);
        if (ret <= 0) {
                 /* Invalid: inverse of the return code tells
                  * the netfilter core what to do */
                 pr_debug("nf_conntrack_in: Can't track with proto module\n");
                 nf_conntrack_put(skb->nfct);
                 skb->nfct = NULL;
                 NF_CT_STAT_INC_ATOMIC(net, invalid);
                 if (ret == -NF_DROP)
                         NF_CT_STAT_INC_ATOMIC(net, drop);
                 ret = -ret;
                 goto out;
        }

        if (set_reply && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
                 nf_conntrack_event_cache(IPCT_REPLY, ct);
        out:
        if (tmpl) {
                 /* Special case: we have to repeat this hook, assign the
                  * template again to this packet. We assume that this packet
                  * has no conntrack assigned. This is used by nf_ct_tcp. */
                 if (ret == NF_REPEAT)
                         skb->nfct = (struct nf_conntrack *)tmpl;
                 else
                         nf_ct_put(tmpl);
        }

        return ret;
}

在 NF_INET_POST_ROUTING 钩子和 NF_INET_LOCAL_IN 钩子中调用的ipv4_confirm()方法通常会调用__nf_conntrack_confirm()方法,该方法会将元组从未确认列表中移除。

连接跟踪助手和期望

有些协议有不同的数据流和控制流,例如,FTP(文件传输协议)和 SIP(会话发起协议,即 VoIP 协议)。通常在这些协议中,控制信道与另一端协商一些配置设置,并就数据流使用哪些参数达成一致。netfilter 子系统更难处理这些协议,因为 netfilter 子系统需要知道流是相互关联的。为了支持这些类型的协议,netfilter 子系统提供了连接跟踪帮助程序,它扩展了连接跟踪的基本功能。这些模块创建期望(nf_conntrack_expect对象),这些期望告诉内核它应该在指定的连接上期待一些流量,并且两个连接是相关的。知道两个连接是相关的,就可以在主连接上定义也适用于相关连接的规则。您可以使用基于连接跟踪状态的简单 iptables 规则来接受其连接跟踪状态相关的数据包:

iptables -A INPUT -m conntrack --ctstate RELATED -j ACCEPT

image 注意联系不仅是期望的结果。例如,如果 netfilter 找到与嵌入 ICMP 的 L3/L4 报头中的元组相匹配的conntrack条目,则诸如“需要 ICMP 碎片”之类的 ICMPv4 错误包将是相关的。更多细节见icmp_error_message()方法,net/ipv4/netfilter/nf_conntrack_proto_icmp.c

连接跟踪助手由nf_conntrack_helper结构(include/net/netfilter/nf_conntrack_helper.h)表示。它们分别通过nf_conntrack_helper_register()方法和nf_conntrack_helper_unregister()方法进行注册和取消注册。因此,例如,nf_conntrack_ftp_init() ( net/netfilter/nf_conntrack_ftp.c)调用nf_conntrack_helper_register()方法来注册 FTP 连接跟踪助手。连接跟踪助手保存在哈希表中(nf_ct_helper_hash)。ipv4_helper()钩子回调在两个钩子点注册,NF_INET_POST_ROUTING 和 NF_INET_LOCAL_IN(参见前面“连接跟踪初始化”一节中ipv4_conntrack_ops数组的定义)。因此,当 FTP 数据包到达 NF_INET_POST_ROUTING 回调函数ip_output()或 NF_INET_LOCAL_IN 回调函数ip_local_deliver()时,调用ipv4_helper()方法,该方法最终调用注册的连接跟踪帮助器的回调函数。在 FTP 的情况下,注册的帮助器方法是help()方法,net/netfilter/nf_conntrack_ftp.c。这个方法寻找特定于 FTP 的模式,比如“PORT”FTP 命令;参见下面的代码片段(net/netfilter/nf_conntrack_ftp.c)中对help()方法中find_pattern()方法的调用。如果匹配,通过调用nf_ct_expect_init()方法创建一个nf_conntrack_expect对象:

static int help(struct sk_buff *skb,
         unsigned int protoff,
         struct nf_conn *ct,
         enum ip_conntrack_info ctinfo)
{
       struct nf_conntrack_expect *exp;
    . . .
       for (i = 0; i < ARRAY_SIZE(search[dir]); i++) {
              found = find_pattern(fb_ptr, datalen,
                            search[dir][i].pattern,
                            search[dir][i].plen,
                            search[dir][i].skip,
                            search[dir][i].term,
                            &matchoff, &matchlen,
                            &cmd,
                            search[dir][i].getnum);
              if (found) break;
}

if (found == -1) {
             /* We don't usually drop packets.  After all, this is
                connection tracking, not packet filtering.
                However, it is necessary for accurate tracking in
                this case. */
             nf_ct_helper_log(skb, ct, "partial matching of `%s'",
                             search[dir][i].pattern);

image 注意正常情况下,连接跟踪不会丢包。在某些情况下,由于某些错误或异常情况,数据包会被丢弃。下面是这种情况的一个例子:早先对find_pattern()的调用返回–1,这意味着只有部分匹配;并且由于没有找到完全的模式匹配,该分组被丢弃。

              ret = NF_DROP;
              goto out;
       } else if (found == 0) { /* No match */
              ret = NF_ACCEPT;
              goto out_update_nl;
       }

       pr_debug("conntrack_ftp: match `%.*s' (%u bytes at %u)\n",
               matchlen, fb_ptr + matchoff,
               matchlen, ntohl(th->seq) + matchoff);

       exp = nf_ct_expect_alloc(ct);
    . . .
       nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, cmd.l3num,
                         &ct->tuplehash[!dir].tuple.src.u3, daddr,
                         IPPROTO_TCP, NULL, &cmd.u.tcp.port);
    . . .
}
(net/netfilter/nf_conntrack_ftp.c)

稍后,当通过init_conntrack()方法创建一个新连接时,您检查它是否有期望,如果有,您设置 IPS_EXPECTED_BIT 标志并设置连接的主节点(ct->master)来引用创建期望的连接:

static struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net, struct nf_conn *tmpl,
               const struct nf_conntrack_tuple *tuple,
               struct nf_conntrack_l3proto *l3proto,
               struct nf_conntrack_l4proto *l4proto,
               struct sk_buff *skb,
               unsigned int dataoff, u32 hash)
{
        struct nf_conn *ct;
        struct nf_conn_help *help;
        struct nf_conntrack_tuple repl_tuple;
        struct nf_conntrack_ecache *ecache;
        struct nf_conntrack_expect *exp;
        u16 zone = tmpl ? nf_ct_zone(tmpl) : NF_CT_DEFAULT_ZONE;
        struct nf_conn_timeout *timeout_ext;
        unsigned int *timeouts;

        . . .
        ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC,
                                  hash);
    . . .

        exp = nf_ct_find_expectation(net, zone, tuple);
        if (exp) {
                pr_debug("conntrack: expectation arrives ct=%p exp=%p\n",
                         ct, exp);
                /* Welcome, Mr. Bond.  We've been expecting you... */
                __set_bit(IPS_EXPECTED_BIT, &ct->status);
                ct->master = exp->master;
                if (exp->helper) {
                        help = nf_ct_helper_ext_add(ct, exp->helper,
                                                    GFP_ATOMIC);
                        if (help)
                                rcu_assign_pointer(help->helper, exp->helper);
                }
        . . .

请注意,助手监听预定义的端口。例如,FTP 连接跟踪助手监听端口 21(参见include/linux/netfilter/nf_conntrack_ftp.h中的 FTP_PORT 定义)。您可以通过两种方式之一设置不同的端口:第一种方式是通过模块参数——您可以通过向modprobe命令提供单个端口或逗号分隔的端口列表来覆盖默认端口值:

modprobe nf_conntrack_ftp ports=2121
modprobe nf_conntrack_ftp ports=2022,2023,2024

第二种方法是使用 CT 目标:

iptables -A PREROUTING -t raw -p tcp --dport 8888 -j CT --helper ftp

请注意,CT 目标(net/netfilter/xt_CT.c)是在内核 2.6.34 中添加的。

image 注意 Xtables 目标扩展由xt_target结构表示,并通过xt_register_target()方法注册单个目标,或者通过xt_register_targets()方法注册一个目标数组。Xtables 匹配扩展由xt_match结构表示,并由xt_register_match()方法注册,或者由xt_register_matches()注册一个匹配数组。匹配扩展根据由匹配扩展模块定义的一些标准来检查分组;因此,例如,xt_length匹配模块(net/netfilter/xt_length.c)根据数据包的长度(IPv4 数据包情况下的 SKB 的tot_len)检查数据包,而xt_connlimit模块(net/netfilter/xt_connlimit.c)限制每个 IP 地址的并行 TCP 连接数。

本节详细介绍了连接跟踪初始化。下一节讨论 iptables,这可能是 netfilter 框架中最广为人知的部分。

防火墙

iptables 有两个部分。内核部分 IPv4 的内核在net/ipv4/netfilter/ip_tables.c中,IPv6 的内核在net/ipv6/netfilter/ip6_tables.c中。还有用户空间部分,它提供了访问内核 iptables 层的前端(例如,用iptables命令添加和删除规则)。每个表由xt_table结构表示(在include/linux/netfilter/x_tables.h中定义)。表的注册和注销分别由ipt_register_table()ipt_unregister_table()方法完成。这些方法在net/ipv4/netfilter/ip_tables.c中实现。在 IPv6 中,您也可以使用xt_table结构来创建表,但是表的注册和注销分别由ip6t_register_table()方法和ip6t_unregister_table()方法来完成。

网络名称空间对象包含特定于 IPv4 和 IPv6 的对象(分别为netns_ipv4netns_ipv6)。而netns_ipv4netns_ipv6对象又包含指向xt_table对象的指针。对于 IPv4,在struct netns_ipv4中你有例如iptable_filteriptable_manglenat_table等等(include/net/netns/ipv4.h)。在struct netns_ipv6中你有,比如说ip6table_filterip6table_mangleip6table_nat等等(include/net/netns/ipv6.h)。有关 IPv4 和 IPv6 网络名称空间 netfilter 表以及相应内核模块的完整列表,请参见本章末尾“快速参考”部分的表 9-2 和表 9-3 。

为了理解 iptables 是如何工作的,让我们看一个使用过滤器表的真实例子。为了简单起见,让我们假设过滤器表是唯一构建的,并且日志目标也是受支持的;我使用的唯一规则是用于日志记录,您很快就会看到。首先,我们来看看过滤表的定义:

#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
                            (1 << NF_INET_FORWARD) | \
                            (1 << NF_INET_LOCAL_OUT))

static const struct xt_table packet_filter = {
        .name           = "filter",
        .valid_hooks    = FILTER_VALID_HOOKS,
        .me             = THIS_MODULE,
        .af             = NFPROTO_IPV4,
        .priority       = NF_IP_PRI_FILTER,
};
(net/ipv4/netfilter/iptable_filter.c)

首先通过调用xt_hook_link()方法完成表的初始化,该方法将iptable_filter_hook()方法设置为packet_filter表的nf_hook_ops对象的钩子回调:

static struct nf_hook_ops *filter_ops __read_mostly;
static int __init iptable_filter_init(void)
{
     . . .
        filter_ops = xt_hook_link(&packet_filter, iptable_filter_hook);
     . . .
}

然后调用ipt_register_table()方法(注意,IPv4 netns对象net->ipv4,保存一个指向过滤表的指针iptable_filter):

static int __net_init iptable_filter_net_init(struct net *net)
{
    . . .
       net->ipv4.iptable_filter =
                ipt_register_table(net, &packet_filter, repl);
    . . .

       return PTR_RET(net->ipv4.iptable_filter);
}
(net/ipv4/netfilter/iptable_filter.c)

请注意,过滤器表中有三个挂钩:

  • 本地网络
  • 网络转发
  • 本地输出

对于本例,您使用iptable命令行设置以下规则:

iptables -A INPUT -p udp --dport=5001 -j LOG --log-level 1

此规则的含义是,您将把目标端口为 5001 的传入 UDP 数据包转储到 syslog 中。log-level修饰符是 0 到 7 范围内的标准系统日志级别;0 表示紧急,7 表示调试。注意,当运行一个iptables命令时,您应该用–t修饰符指定您想要使用的表;例如,iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE会向 NAT 表中添加一条规则。当没有用–t修饰符指定表名时,默认情况下使用过滤表。因此,通过运行iptables -A INPUT -p udp --dport=5001 -j LOG --log-level 1,您向过滤器表添加了一个规则。

image 注意你可以给 iptables 规则设置目标;通常这些可以是来自 Linux netfilter 子系统的目标(参见前面使用日志目标的例子)。您还可以编写自己的目标,并扩展 iptables 用户空间代码来支持它们。参见 Jan Engelhardt 和 Nicolas Bouliane 的“编写 Netfilter 模块”。

注意,为了在 iptables 规则中使用日志目标,必须设置 CONFIG_NETFILTER_XT_TARGET_LOG,如前面的示例所示。你可以参考net/netfilter/xt_LOG.c的代码作为iptables 目标模块的例子。

当一个目的端口为 5001 的 UDP 包到达网络驱动,上行到网络层(L3)时,遇到的第一个钩子是 NF_INET_PRE_ROUTING 钩子;过滤表回调没有在 NF_INET_PRE_ROUTING 中注册一个钩子。它只有三个钩子:NF_INET_LOCAL_IN、NF_INET_FORWARD 和 NF_INET_LOCAL_OUT,如前所述。所以您继续使用ip_rcv_finish()方法,在路由子系统中执行查找。现在有两种情况:数据包打算传送到本地主机或打算转发(让我们忽略数据包将被丢弃的情况)。在图 9-2 中,你可以看到两种情况下的数据包遍历。

9781430261964_Fig09-02.jpg

图 9-2 。我的流量和转发流量采用过滤表规则

交付给本地主机

首先你到达ip_local_deliver()方法;简单看一下这个方法:

int ip_local_deliver(struct sk_buff *skb)
{
    . . .
       return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
                       ip_local_deliver_finish);
}

可以看到,你在这个方法中有 NF_INET_LOCAL_IN 钩子,前面也提到过,NF_INET_LOCAL_IN 是过滤表钩子之一;所以 NF_HOOK()宏将调用iptable_filter_hook()方法。现在看一看iptable_filter_hook()方法:

static unsigned int iptable_filter_hook(unsigned int hook, struct sk_buff *skb,
                                    const struct net_device *in,
                        const struct net_device *out,
                                 int (*okfn)(struct sk_buff *))
{
        const struct net *net;
        . . .
        net = dev_net((in != NULL) ? in : out);
        . . .

        return ipt_do_table(skb, hook, in, out, net->ipv4.iptable_filter);
}
(net/ipv4/netfilter/iptable_filter.c)

实际上,ipt_do_table()方法调用了日志目标回调函数ipt_log_packet(),它将数据包报头写入 syslog。如果有更多的规则,他们会在这个时候被调用。因为没有更多的规则,您继续使用ip_local_deliver_finish()方法,数据包继续遍历到传输层(L4)由相应的套接字处理。

转发数据包

第二种情况是,在路由子系统中进行查找后,您发现该数据包将被转发,因此调用了ip_forward()方法:

int ip_forward(struct sk_buff *skb)
  {
  . . .
   return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
                        rt->dst.dev, ip_forward_finish);
   . . .

因为过滤表在 NF_INET_FORWARD 中有一个注册的钩子回调函数,正如前面提到的,您再次调用了iptable_filter_hook()方法。因此,和以前一样,您再次调用了ipt_do_table()方法,它将再次调用ipt_log_packet()方法。你会继续到ip_forward_finish()方法(注意ip_forward_finish是上面 NF_HOOK 宏的最后一个参数,代表 continuation 方法)。然后调用ip_output()方法,因为过滤表没有 NF_INET_POST_ROUTING 钩子,所以继续到ip_finish_output()方法。

image 注意你可以根据连接跟踪状态过滤数据包。下一个规则将转储到连接跟踪状态已建立的 syslog 数据包中:

iptables -A INPUT -p tcp -m conntrack --ctstate ESTABLISHED -j LOG --log-level 1

网络地址转换(NAT)

网络地址转换(NAT)模块主要处理 IP 地址转换,顾名思义,即端口操作。NAT 最常见的用途之一是使局域网中拥有私有 IP 地址的一组主机能够通过某个住宅网关访问互联网。例如,您可以通过设置 NAT 规则来做到这一点。安装在网关上的 NAT 可以使用这样的规则,并为主机提供访问 Web 的能力。netfilter 子系统具有针对 IPv4 和 IPv6 的 NAT 实现。IPv6 NAT 实现主要基于 IPv4 实现,并且从用户的角度来看,提供了类似于 IPv4 的接口。IPv6 NAT 支持在内核 3.7 中被合并。它提供了一些功能,如简单的负载平衡解决方案(通过在传入流量上设置 DNAT)等。IPv6 NAT 模块在net/ipv6/netfilter/ip6table_nat.c中。NAT 设置有很多种类型,网上也有很多关于 NAT 管理的文档。我讲两种常见的配置:SNAT 是源 NAT,源 IP 地址改变,DNAT 是目的 NAT,目的 IP 地址改变。您可以使用–j标志来选择 SNAT 或 DNAT。DNAT 和 SNAT 的实现都在net/netfilter/xt_nat.c。下一节讨论 NAT 初始化。

NAT 初始化

NAT 表和上一节中的过滤器表一样,也是一个xt_table对象。除了 NF_INET_FORWARD 钩子之外,它在所有钩子点上都被注册:

static const struct xt_table nf_nat_ipv4_table = {
        .name           = "nat",
        .valid_hooks    = (1 << NF_INET_PRE_ROUTING) |
                          (1 << NF_INET_POST_ROUTING) |
                          (1 << NF_INET_LOCAL_OUT) |
                          (1 << NF_INET_LOCAL_IN),
        .me             = THIS_MODULE,
        .af             = NFPROTO_IPV4,
};
(net/ipv4/netfilter/iptable_nat.c)

NAT 表的注册和注销分别通过调用ipt_register_table()ipt_unregister_table()(net/ipv4/netfilter/iptable_nat.c)来完成。网络名称空间(struct net)包括一个 IPv4 特定对象(netns_ipv4),该对象包括一个指向 IPv4 NAT 表(nat_table)的指针,如前面的“IP 表”部分所述。这个由ipt_register_table()方法创建的xt_table对象被分配给这个nat_table指针。您还定义了一个nf_hook_ops对象的数组并注册它:

 static struct nf_hook_ops nf_nat_ipv4_ops[] __read_mostly = {
        /* Before packet filtering, change destination */
        {
                .hook           = nf_nat_ipv4_in,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_PRE_ROUTING,
                .priority       = NF_IP_PRI_NAT_DST,
        },
        /* After packet filtering, change source */
        {
                .hook           = nf_nat_ipv4_out,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_POST_ROUTING,
                .priority       = NF_IP_PRI_NAT_SRC,
        },
        /* Before packet filtering, change destination */
        {
                .hook           = nf_nat_ipv4_local_fn,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_OUT,
                .priority       = NF_IP_PRI_NAT_DST,
        },
        /* After packet filtering, change source */
        {
                .hook           = nf_nat_ipv4_fn,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_IN,
                .priority       = NF_IP_PRI_NAT_SRC,
        },
};

nf_nat_ipv4_ops数组的注册在iptable_nat_init()方法中完成:

static int __init iptable_nat_init(void)
{
        int err;
        . . .
        err = nf_register_hooks(nf_nat_ipv4_ops, ARRAY_SIZE(nf_nat_ipv4_ops));
        if (err < 0)
                goto err2;
        return 0;
        . . .
}
(net/ipv4/netfilter/iptable_nat.c)

NAT 钩子回调和连接跟踪钩子回调

有一些钩子同时注册了 NAT 回调和连接跟踪回调。例如,在 NF_INET_PRE_ROUTING 钩子(传入数据包到达的第一个钩子)上,有两个注册的回调:连接跟踪回调ipv4_conntrack_in() 和 NAT 回调nf_nat_ipv4_in()。连接跟踪回叫的优先级ipv4_conntrack_in()是 NF_IP_PRI_CONNTRACK (-200),NAT 回叫的优先级nf_nat_ipv4_in()是 NF_IP_PRI_NAT_DST (-100)。因为优先级较低的同一个钩子的回调被首先调用,所以优先级为–200 的连接跟踪ipv4_conntrack_in()回调将在优先级为–100 的 NAT nf_nat_ipv4_in()回调之前被调用。ipv4_conntrack_in()方法位置见图 9-1 图nf_nat_ipv4_in()位置见图 9-4;两者都在同一个地方,在 NF_INET_PRE_ROUTING 点。这背后的原因是 NAT 在连接跟踪层执行查找,如果没有找到条目,NAT 不执行任何地址转换操作:

static unsigned int nf_nat_ipv4_fn(unsigned int hooknum,
                           struct sk_buff *skb,
                           const struct net_device *in,
                           const struct net_device *out,
                           int (*okfn)(struct sk_buff *))
{
        struct nf_conn *ct;
        . . .
        /* Don't try to NAT if this packet is not conntracked */
        if (nf_ct_is_untracked(ct))
                return NF_ACCEPT;
    . . .
}
(net/ipv4/netfilter/iptable_nat.c)

image 注意nf_nat_ipv4_fn ()方法是从 NAT PRE_ROUTING 回调中调用的,nf_nat_ipv4_in()

在 NF_INET_POST_ROUTING 钩子上,有两个注册的连接跟踪回调:回调ipv4_helper()(优先级为 NF_IP_PRI_CONNTRACK_HELPER,300)和回调ipv4_confirm()(优先级为 NF_IP_PRI_CONNTRACK_CONFIRM,INT_MAX,优先级最高的整数值)。你还有一个注册的 NAT 钩子回调,nf_nat_ipv4_out(),优先级为 NF_IP_PRI_NAT_SRC,为 100。这样一来,当到达 NF_INET_POST_ROUTING 钩子时,首先会调用 NAT 回调,nf_nat_ipv4_out(),然后会调用ipv4_helper()方法,ipv4_confirm()是最后被调用的。参见图 9-4 。

让我们来看看一个简单的 DNAT 规则,看看转发的包的遍历以及连接跟踪回调和 NAT 回调的调用顺序(为了简单起见,假设这个内核映像中没有构建过滤器表)。在图 9-3 所示的设置中,中间主机(AMD 服务器)运行 DNAT 规则:

iptables -t nat -A PREROUTING -j DNAT -p udp --dport 9999 --to-destination 192.168.1.8

9781430261964_Fig09-03.jpg

图 9-3 。一个简单的 DNAT 规则设置

此 DNAT 规则的含义是,在 UDP 目标端口 9999 上发送的传入 UDP 数据包会将其目标 IP 地址更改为 192.168.1.8。右侧机器(Linux 桌面)将 UDP 数据包发送到 192.168.1.9,UDP 目的端口为 9999。在 AMD 服务器中,目的 IPv4 地址被 DNAT 规则更改为 192.168.1.8,数据包被发送到左边的笔记本电脑。

在图 9-4 中,您可以看到第一个 UDP 包的遍历,它是根据前面提到的设置发送的。

9781430261964_Fig09-04.jpg

图 9-4 。NAT 和 netfilter 挂钩

通用的 NAT 模块是net/netfilter/nf_nat_core.c。NAT 实现的基本元素是nf_nat_l4proto结构(include/net/netfilter/nf_nat_l4proto.hnf_nat_l3proto结构。在 3.7 之前的内核中,您将会遇到nf_nat_protocol结构,而不是这两个结构,这两个结构作为添加 IPv6 NAT 支持的一部分取代了它们。这两种结构提供了独立于协议的 NAT 核心支持。

这两种结构都包含一个manip_pkt()函数指针,用于改变数据包报头。让我们看一个 TCP 协议的manip_pkt()实现的例子,在net/netfilter/nf_nat_proto_tcp.c中:

static bool tcp_manip_pkt(struct sk_buff *skb,
              const struct nf_nat_l3proto *l3proto,
              unsigned int iphdroff, unsigned int hdroff,
              const struct nf_conntrack_tuple *tuple,
              enum nf_nat_manip_type maniptype)
{
        struct tcphdr *hdr;
        __be16 *portptr, newport, oldport;
        int hdrsize = 8; /* TCP connection tracking guarantees this much */

        /* this could be an inner header returned in icmp packet; in such
           cases we cannot update the checksum field since it is outside of
           the 8 bytes of transport layer headers we are guaranteed */
        if (skb->len >= hdroff + sizeof(struct tcphdr))
                hdrsize = sizeof(struct tcphdr);

        if (!skb_make_writable(skb, hdroff + hdrsize))
                return false;

        hdr = (struct tcphdr *)(skb->data + hdroff);

根据maniptype设置newport:

  • 如果需要更改源端口,maniptype是 NF_NAT_MANIP_SRC。所以您从tuple->src中提取端口。
  • 如果需要更改目的端口,maniptype是 NF_NAT_MANIP_DST。所以您从tuple->dst中提取端口:
        if (maniptype == NF_NAT_MANIP_SRC) {
                /* Get rid of src port */
                newport = tuple->src.u.tcp.port;
                portptr = &hdr->source;
        } else {
                /* Get rid of dst port */
                newport = tuple->dst.u.tcp.port;
                portptr = &hdr->dest;
        }

你要改变 TCP 头的源端口(当maniptype为 NF_NAT_MANIP_SRC 时)或者目的端口(当maniptype为 NF_NAT_MANIP_DST 时),所以你需要重新计算校验和。您必须保留旧端口用于校验和重新计算,这将通过调用csum_update()方法和inet_proto_csum_replace2()方法立即完成:

        oldport = *portptr;
        *portptr = newport;

        if (hdrsize < sizeof(*hdr))
                return true;

重新计算校验和:

        l3proto->csum_update(skb, iphdroff, &hdr->check, tuple, maniptype);
        inet_proto_csum_replace2(&hdr->check, skb, oldport, newport, 0);
        return true;
}

NAT 钩子回调

特定于协议的 NAT 模块对于 IPv4 协议是net/ipv4/netfilter/iptable_nat.c,对于 IPv6 协议是net/ipv6/netfilter/ip6table_nat.c。这两个 NAT 模块各有四个钩子回调,如表 9-1 所示。

表 9-1 。IPv4 和 IPv6 NAT 回调

|

|

挂机回拨(IPv4)

|

挂钩回调(IPv6)

|
| --- | --- | --- |
| NF _ INET _ 预路由 | 网络地址转换 ipv4 输入 | 网络地址转换协议 ipv6 协议 |
| 物流配送 | nf_nat_ipv4_out | nf_nat_ipv6_out |
| 本地输出 | nf_nat_ipv4_local_fn | nf_nat_ipv6_local_fn |
| 本地网络 | nf_nat_ipv4_fn | nf_nat_ipv6_fn |

nf_nat_ipv4_fn()是这些方法中最重要的(对于 IPv4)。其他三个方法,nf_nat_ipv4_in()nf_nat_ipv4_out()nf_nat_ipv4_local_fn(),都调用了nf_nat_ipv4_fn()方法。让我们来看看nf_nat_ipv4_fn()方法:

static unsigned int nf_nat_ipv4_fn(unsigned int hooknum,
                              struct sk_buff *skb,
                              const struct net_device *in,
                              const struct net_device *out,
                              int (*okfn)(struct sk_buff *))
{
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
        struct nf_conn_nat *nat;
        /* maniptype == SRC for postrouting. */
        enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum);

        /* We never see fragments: conntrack defrags on pre-routing
         * and local-out, and nf_nat_out protects post-routing.
         */
        NF_CT_ASSERT(!ip_is_fragment(ip_hdr(skb)));

        ct = nf_ct_get(skb, &ctinfo);
        /* Can't track?  It's not due to stress, or conntrack would
         * have dropped it.  Hence it's the user's responsibilty to
         * packet filter it out, or implement conntrack/NAT for that
         * protocol. 8) --RR
         */
        if (!ct)
                return NF_ACCEPT;

        /* Don't try to NAT if this packet is not conntracked */
        if (nf_ct_is_untracked(ct))
                return NF_ACCEPT;

        nat = nfct_nat(ct);
        if (!nat) {
                /* NAT module was loaded late. */
                if (nf_ct_is_confirmed(ct))
                        return NF_ACCEPT;
                nat = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
                if (nat == NULL) {
                        pr_debug("failed to add NAT extension\n");
                        return NF_ACCEPT;
                }
        }

        switch (ctinfo) {
        case IP_CT_RELATED:
        case IP_CT_RELATED_REPLY:
                if (ip_hdr(skb)->protocol == IPPROTO_ICMP) {
                        if (!nf_nat_icmp_reply_translation(skb, ct, ctinfo,
                                                           hooknum))
                                return NF_DROP;
                        else
                                return NF_ACCEPT;
                }
                /* Fall thru... (Only ICMPs can be IP_CT_IS_REPLY) */
        case IP_CT_NEW:
                /* Seen it before?  This can happen for loopback, retrans,
                 * or local packets.
                 */
                if (!nf_nat_initialized(ct, maniptype)) {
                        unsigned int ret;

nf_nat_rule_find()方法调用ipt_do_table()方法,该方法遍历指定表中条目的所有匹配项,如果有匹配项,则调用目标回调:

                        ret = nf_nat_rule_find(skb, hooknum, in, out, ct);
                        if (ret != NF_ACCEPT)
                                return ret;
                } else {
                        pr_debug("Already setup manip %s for ct %p\n",
                                 maniptype == NF_NAT_MANIP_SRC ? "SRC" : "DST",
                                 ct);
                        if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
                                goto oif_changed;
                }
                break;

        default:
                /* ESTABLISHED */
                NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||
                             ctinfo == IP_CT_ESTABLISHED_REPLY);
                if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
                        goto oif_changed;
        }

        return nf_nat_packet(ct, ctinfo, hooknum, skb);

oif_changed:
        nf_ct_kill_acct(ct, ctinfo, skb);
        return NF_DROP;
}

连接跟踪扩展

内核 2.6.23 中添加了连接跟踪(CT)扩展。连接跟踪扩展的要点是只分配所需的内存,例如,如果没有加载 NAT 模块,则不会分配连接跟踪层中 NAT 所需的额外内存。有些扩展是由sysctls启用的,甚至取决于某些 iptables 规则(例如,-m connlabel)。每个连接跟踪扩展模块应该定义一个nf_ct_ext_type对象,并通过nf_ct_extend_register()方法执行注册(取消注册通过nf_ct_extend_unregister()方法完成)。每个扩展应该定义一个方法,将它的连接跟踪扩展附加到一个连接(nf_conn)对象,该对象应该从init_conntrack()方法中调用。因此,例如,您有用于时间戳 CT 扩展的nf_ct_tstamp_ext_add()方法和用于标签 CT 扩展的nf_ct_labels_ext_add()方法。连接跟踪扩展基础设施在net/netfilter/nf_conntrack_extend.c中实现。这些是撰写本文时的连接跟踪扩展模块(全部在net/netfilter下):

  • nf_conntrack_timestamp.c
  • nf_conntrack_timeout.c
  • nf_conntrack_acct.c
  • nf_conntrack_ecache.c
  • nf_conntrack_labels.c
  • nf_conntrack_helper.c

摘要

本章描述了 netfilter 子系统的实现。我介绍了 netfilter 挂钩以及它们是如何注册的。我还讨论了一些重要的主题,比如连接跟踪机制、iptables 和 NAT。第十章讲述了 IPsec 子系统及其实现。

快速参考

本节涵盖了与本章中讨论的主题相关的顶级方法,按其上下文排序,后面是三个表和一个关于工具和库的简短部分。

方法

下面是 netfilter 子系统的重要方法的简短列表。本章提到了其中一些。

struct XT _ table * ipt _ register _ table(struct net * net,const struct xt_table *table,const struct ipt _ replace * repl);

此方法在 netfilter 子系统中注册一个表。

void ipt _ unregister _ table(struct net * net,struct XT _ table * table);

此方法在 netfilter 子系统中注销一个表。

int nf _ register _ hook(struct nf _ hook _ ops * reg);

这个方法注册一个单独的nf_hook_ops对象。

int nf _ register _ hooks(struct nf _ hook _ ops * reg,unsigned int n);

该方法注册一个由 n 个 nf_hook_ops对象组成的数组;第二个参数是数组中元素的数量。

void nf _ unregister _ hook(struct nf _ hook _ ops * reg);

这个方法注销一个单独的nf_hook_ops对象。

void nf _ unregister _ hooks(struct nf _ hook _ ops * reg,unsigned int n);

此方法注销一个由 n nf_hook_ops个对象组成的数组;第二个参数是数组中元素的数量。

静态内联 void nf _ conntrack _ get(struct nf _ conntrack * nfct);

该方法增加相关nf_conntrack对象的引用计数。

静态内联 void nf _ conntrack _ put(struct nf _ conntrack * nfct);

该方法减少相关nf_conntrack对象的引用计数。如果达到 0,就调用nf_conntrack_destroy()方法。

int nf _ conn track _ helper _ register(struct nf _ conn track _ helper * me);

这个方法注册一个nf_conntrack_helper对象。

静态内联结构 nf _ conn * resolve _ normal _ CT(struct net * net,struct nf_conn *tmpl,struct sk_buff *skb,unsigned int dataoff,u_int16_t l3num,u_int8_t protonum,struct nf _ conn track _ L3 proto * L3 proto,struct nf _ conn track _ l4 proto * l4 proto,int *set_reply,enum IP _ conn track _ info * ctinfo);

这个方法试图通过调用__nf_conntrack_find_get()方法根据指定的 SKB 找到一个nf_conntrack_tuple_hash对象,如果没有找到这样的条目,就通过调用init_conntrack()方法创建一个。从nf_conntrack_in()方法(net/netfilter/nf_conntrack_core.c)调用resolve_normal_ct()方法。

struct nf _ conntrack _ 元组 _ hash * init _ conntrack(struct net * net,struct nf_conn tmpl,const struct nf _ conntrack _ 元组元组,struct nf _ conntrack _ L3 proto * L3 protocol,struct nf _ conntrack _ l4 proto * l4 proto,struct sk _ buff * skb,signed int dataoff,u32 hash);

这个方法分配一个连接跟踪nf_conntrack_tuple_hash对象。从resolve_normal_ct()方法中调用,它试图通过调用nf_ct_find_expectation()方法来寻找这个连接的期望。

static struct nf _ conn * _ _ _ nf _ conntrack _ alloc(struct net * net,u16 区域,const struct nf _ conntrack _ tuple * orig,const struct nf _ conntrack _ tuple * repl,gfp_t gfp,u32 hash);

这个方法分配一个nf_conn对象。将nf_conn对象的超时计时器设置为death_by_timeout()方法。

int XT _ register _ target(struct XT _ target * target);

这个方法注册一个 Xtable 目标扩展。

void XT _ unregister _ target(struct XT _ target * target);

此方法注销 Xtable 目标扩展。

int XT _ register _ targets(struct XT _ target * target,unsigned int n);

此方法注册 Xtable 目标扩展的数组; n 是目标数量。

void XT _ unregister _ targets(struct XT _ target * target,unsigned int n);

此方法注销 Xtable 目标扩展的数组; n 是目标数量。

int XT _ register _ match(struct XT _ match * target);

此方法注册一个 Xtable 匹配扩展。

void XT _ unregister _ match(struct XT _ match * target);

此方法注销 Xtable 匹配扩展。

int XT _ register _ matches(struct XT _ match * match,unsigned int n);

此方法注册 Xtable 匹配扩展的数组; n 是匹配的次数。

void XT _ unregister _ matches(struct XT _ match * match,unsigned int n);

此方法注销 Xtable 匹配扩展的数组; n 是匹配的次数。

int nf _ CT _ extend _ register(struct nf _ CT _ ext _ type * type);

此方法注册连接跟踪扩展对象。

void nf _ CT _ extend _ unregister(struct nf _ CT _ ext _ type * type);

此方法注销连接跟踪扩展对象。

int _ _ init iptable _ NAT _ init(void);

此方法初始化 IPv4 NAT 表。

int __init nf_conntrack_ftp_init(请参阅):

此方法初始化连接跟踪 FTP 帮助程序。调用nf_conntrack_helper_register()方法来注册 FTP 助手。

让我们看看本章中使用的宏。

NF_CT_DIRECTION(哈希)

这是一个宏,获取一个nf_conntrack_tuple_hash对象作为参数,返回关联元组(include/net/netfilter/nf_conntrack_tuple.h)的目的(dst对象)的方向(IP_CT_DIR_ORIGINAL,为 0,或者 IP_CT_DIR_REPLY,为 1)。

桌子

下面是表格,显示了 IPv4 网络名称空间和 IPv6 网络名称空间中的 netfilter 表以及 netfilter 挂钩优先级。

表 9-2 。IPv4 网络命名空间(netns_ipv4)表(xt_table 对象)

|

Linux 符号( netns_ipv4)

|

Linux 模块

|
| --- | --- |
| iptable_filter | net/IP v4/netfilter/iptable _ filter . c |
| iptable_mangle | net/IP v4/netfilter/iptable _ mangle . c |
| iptable_raw | net/IP v4/netfilter/iptable _ raw . c |
| ARP 表 _ 过滤器 | net/IP v4/netfilter/ARP _ tables . c |
| nat_table | net/IP v4/netfilter/iptable _ NAT . c |
| iptable_security | net/IP v4/netfilter/iptable _ SECURITY . c(注意:要设置 CONFIG_SECURITY)。 |

表 9-3 。IPv6 网络命名空间(netns_ipv6)表(xt_table 对象)

|

Linux 符号( netns_ipv6)

|

Linux 模块

|
| --- | --- |
| ip6 表格 _ 过滤器 | net/IPv6/netfilter/IP 6 table _ filter . c |
| ip6table_mangle | net/IPv6/netfilter/IP 6 table _ mangle . c |
| ip6table_raw | net/IPv6/netfilter/IP 6 table _ raw . c |
| ip6table_nat | net/IPv6/netfilter/IP 6 table _ NAT . c |
| ip6table_security | net/IPv6/netfilter/IP 6 table _ SECURITY . c(注意:要设置 CONFIG_SECURITY)。 |

表 9-4。 Netfilter 挂钩优先级

|

Linux 符号

|

|
| --- | --- |
| NF _ IP _ 优先级 _ 优先 | INT_MIN |
| nf _ ip _ pri _ conntrack _ 碎片整理 | -400 |
| NF_IP_PRI_RAW 格式 | -300 |
| NF _ IP _ 优先级 _ SELINUX _ 优先 | -225 |
| nf _ IP _ pri _ conntrack _ 连线轨迹 | -200 |
| S7-1200 可编程控制器 | -150 |
| NF_IP_PRI_NAT_DST 文件 | -100 |
| 网络过滤协议优先级过滤器 | Zero |
| NF_IP_PRI_SECURITY | Fifty |
| NF_IP_PRI_NAT_SRC | One hundred |
| 最后一个 | Two hundred and twenty-five |
| NF_IP_PRI_CONNTRACK_HELPER | Three hundred |
| NF _ IP _ PRI _ CONNTRACK _ 确认 | INT_MAX |
| NF _ IP _ 优先级 _ 最后 | INT_MAX |

参见include/uapi/linux/netfilter_ipv4.h中的nf_ip_hook_priorities enum定义。

工具和库

conntrack-tools由一个用户空间守护进程conntrackd和一个命令行工具conntrack组成。它提供了一个工具,系统管理员可以使用该工具与 netfilter 连接跟踪层进行交互。参见:http://conntrack-tools.netfilter.org/

有些库是由 netfilter 项目开发的,允许您执行各种用户空间任务;这些库以“libnetfilter”为前缀;比如libnetfilter_conntracklibnetfilter_loglibnetfilter_queue。更多详情见 netfilter 官网,www.netfilter.org

十、IPsec

第九章讲述了 netfilter 子系统及其内核实现。本章讨论互联网协议安全(IPsec)子系统。IPsec 是一组协议,通过对通信会话中的每个 IP 数据包进行身份验证和加密来保护 IP 流量。大多数安全服务是由两个主要的 IPsec 协议提供的:身份验证报头(AH)协议和封装安全负载(ESP)协议。此外,IPsec 还提供了防止尝试窃听和再次发送数据包(重放攻击)的保护。根据 IPv6 规范,IPsec 是强制性的,而在 IPv4 中是可选的。然而,包括 Linux 在内的大多数现代操作系统都支持 IPv4 和 IPv6 中的 IPsec。第一个 IPsec 协议定义于 1995 年(RFC 1825–1829)。1998 年,这些 RFC 被 RFC 2401–2412 否决。然后在 2005 年,这些 RFC 被 RFC 4301–4309 更新。

IPsec 子系统非常复杂,可能是 Linux 内核网络堆栈中最复杂的部分。考虑到组织和公民个人日益增长的安全需求,它的重要性是至关重要的。本章为你深入研究这个复杂的子系统提供了一个基础。

一般

IPsec 已经成为国际上大多数 IP 虚拟专用网技术 的标准。也就是说,也有基于不同技术的 VPN,如安全套接字层(SSL)和pptp(通过 GRE 协议隧道化 PPP 连接)。在 IPsec 的几种操作模式中,最重要的是传输模式和隧道模式。在传输模式下,只有 IP 数据包的有效载荷被加密,而在隧道模式下,整个 IP 数据包被加密并插入到具有新 IP 报头的新 IP 数据包中。当使用带有 IPsec 的 VPN 时,您通常在隧道模式下工作,尽管有时您在传输模式下工作(例如,L2TP/IPsec)。

我首先简单讨论一下 IPsec 中的互联网密钥交换(IKE) 用户空间守护进程和加密技术。这些主题大多不是内核网络堆栈的一部分,但是与 IPsec 操作相关,并且需要更好地理解内核 IPsec 子系统。接下来我将讨论 XFRM 框架,它是 IPsec 用户空间部分和 IPsec 内核组件之间的配置和监控接口,并解释 IPsec 数据包在 Tx 和 Rx 路径中的遍历。我用一小段关于 IPsec 中 NAT 穿越的内容来结束这一章,这是一个重要而有趣的特性,也是一个“快速参考”部分。下一节从 IKE 协议开始讨论。

互联网密钥交换

最流行的开源用户空间 Linux IPsec 解决方案是 Openswan(和从 Openswan 派生出来的libreswan)、strongSwan 和 racoon(属于ipsec-tools)。Racoon 是 Kame 项目的一部分,该项目旨在为 BSD 的变体提供免费的 IPv6 和 IPsec 协议栈实现。

要建立 IPsec 连接,您需要设置安全关联(SA)。 你可以在已经提到的用户空间项目的帮助下完成。SA 由两个参数定义:源地址和 32 位安全参数索引(SPI)。双方 ??(IPsec 术语中称为发起方响应方)要就一个密钥(或多个密钥)、认证、加密、数据完整性和密钥交换算法等参数,以及密钥生存期(仅 IKEv1)等其他参数达成一致。这可以通过两种不同的密钥分发方式来实现:手动密钥交换(由于安全性较低,很少使用)或 IKE 协议。Openswan 和 strongSwan 实现提供了一个 IKE 守护进程(Openswan 中的pluto和 strongSwan 中的charon),它使用 UDP 端口 500(源和目的地)来发送和接收 IKE 消息。两者都使用 XFRM Netlink 接口与 Linux 内核的本地 IPsec 栈通信。strongSwan 项目是 RFC 5996“互联网密钥交换协议第 2 版(IKEv2)”唯一完整的开源实现,而 Openswan 项目仅实现了一小部分强制子集。

可以在 Openswan 和 strongSwan 5.x 中使用 IKEv1 Aggressive 模式(对于 strongSwan,应该显式配置,在这种情况下charon守护进程的名称改为weakSwan);但是这种选择被认为是不安全的。由于内置了racoon的遗留客户端,IKEv1 仍被苹果操作系统(iOS 和 Mac OS X)使用。虽然许多实现使用 IKEv1,但是使用 IKEv2 有许多改进和优点。我将非常简要地提到其中的一些:在 IKEv1 中,建立一个 SA 比在 IKEv2 中需要更多的消息。IKEv1 非常复杂,而 IKEv2 要简单得多,也更健壮,主要是因为每个 IKEv2 请求消息都必须得到 IKEv2 响应消息的确认。在 IKEv1 中,没有确认,但有一个退避算法,在数据包丢失的情况下,它会一直尝试下去。然而,在 IKEv1 中,当双方执行重新传输时,可能会出现竞争,而在 IKEv2 中,这种情况不会发生,因为重新传输的责任只在发起方。IKEv2 的其他重要功能包括:IKEv2 集成了 NAT 穿越支持、流量选择器的自动缩小(left|rightsubnet两端不必完全匹配,但一个建议可以是另一个建议的子集)、允许分配虚拟 IPv4/IPv6 地址和内部 DNS 信息的 IKEv2 配置有效负载(替换 IKEv1 模式配置),以及 IKEv2 EAP 认证(替换危险的 IKEv1 扩展验证协议), 它通过在客户端使用潜在的弱 EAP 验证算法(例如 EAP-MSCHAPv2)之前首先请求 VPN 服务器证书和数字签名来解决潜在的弱 PSK 问题。

IKE 分为两个阶段:第一阶段称为主模式。 在这个阶段,每一方验证另一方的身份,使用 Diffie-Hellman 密钥交换算法建立一个共同的会话密钥。这种相互认证是基于 RSA 或 ECDSA 证书或预共享秘密(预共享密钥,PSKs),它们是基于密码的,被认为是较弱的。其他参数,如加密算法和要使用的身份验证方法也需要协商。如果这个阶段成功完成,这两个对等体就建立了 ISAKMP SA(互联网安全协会密钥管理协议安全协会)。第二阶段称为快速模式。在这个阶段, 双方就使用的密码算法达成一致。IKEv2 协议不区分阶段 1 和阶段 2,而是建立第一个 CHILD_SA 作为 IKE_AUTH 消息交换的一部分。CHILD_SA_CREATE 消息交换仅用于建立附加的 CHILD_SA,或者用于 IKE 和 IPsec SAs 的定期密钥更新。这就是为什么 IKEv1 需要九条消息来建立单个 IPsec SA,而 IKEv2 只需要四条消息就可以做到这一点。

下一节将简要讨论 IPsec 环境中的加密技术(对该主题更全面的讨论超出了本书的范围)。

IPsec 和密码学

Linux 有两个广泛使用的 IPsec 栈:在 2.6 内核中引入的本地 Netkey 栈(由 Alexey Kuznetsov 和 David S. Miller 开发),以及最初为 2.0 内核编写的 KLIPS 栈(它早于 netfilter!).Netkey 使用 Linux 内核加密 API,而 KLIPS 可能通过开放加密框架(OCF) 支持更多的加密硬件。OCF 的优势在于它支持使用异步调用来加密/解密数据。在 Linux 内核中,大多数加密 API 执行同步调用。我应该提一下acrypto内核代码,它是 Linux 内核的异步加密层。所有算法类型都有异步实现。许多硬件加密加速器使用异步加密接口来卸载加密请求。这仅仅是因为在加密工作完成之前,他们不能阻塞。他们必须使用异步 API。

异步 API 也可以使用软件实现的算法。例如,cryptd crypto 模板可以在异步模式下运行任意算法。并且在多核环境下工作时可以使用pcrypt crypto 模板。该模板通过向一组可配置的 CPU 发送传入的加密请求来并行化加密层。它还负责加密请求的顺序,因此在与 IPsec 一起使用时不会引入数据包重新排序。在某些情况下,pcrypt的使用可以大幅提高 IPsec 的速度。加密层有一个用户管理 API,被crconf ( http://sourceforge.net/projects/crconf/)工具用来配置加密层,因此异步加密算法可以在任何需要的时候配置。随着 2008 年发布的 Linux 2.6.25 内核,XFRM 框架开始为非常高效的 AEAD(关联数据认证加密)算法(例如 AES-GCM)提供支持,尤其是在英特尔 AES-NI 指令集可用且数据完整性几乎免费的情况下。深入研究 IPsec 中的加密技术超出了本书的范围。要了解更多信息,我建议阅读威廉·斯塔林斯(Prentice Hall,2013)的网络安全基础知识第五版中的相关章节。

下一节讨论 XFRM 框架,它是 IPsec 的基础设施。

XFRM 框架

IPsec 是由 XFRM(发音为“transform”)框架实现的,该框架起源于 USAGI 项目,旨在提供生产质量的 IPv6 和 IPsec 协议栈。术语转换指的是根据某种 IPsec 规则在内核堆栈中转换的传入数据包或传出数据包。XFRM 框架是在内核 2.5 中引入的。XFRM 基础设施是独立于协议族的,这意味着 IPv4 和 IPv6 有一个通用部分,位于net/xfrm下。IPv4 和 IPv6 都有自己的 ESP、AH 和 IPCOMP 实现。比如 IPv4 ESP 模块是net/ipv4/esp4.c,IPv6 ESP 模块是net/ipv6/esp6.c。除此之外,IPv4 和 IPv6 实现了一些特定于协议的模块来支持 XFRM 基础设施,比如net/ipv4/xfrm4_policy.cnet/ipv6/xfrm6_policy.c

XFRM 框架支持网络名称空间,这是一种轻量级进程虚拟化的形式,使单个进程或一组进程拥有自己的网络堆栈(我在第十四章中讨论了网络名称空间)。每个网络名称空间(struct net的实例)包括一个名为xfrm的成员,它是netns_xfrm结构的一个实例。这个对象包含了很多你在本章会遇到的数据结构和变量,比如 XFRM 策略的哈希表和 XFRM 状态的哈希表、sysctl参数、XFRM 状态垃圾收集器、计数器等等:

struct netns_xfrm {
        struct hlist_head       *state_bydst;
        struct hlist_head       *state_bysrc;
        struct hlist_head       *state_byspi;
        . . .
        unsigned int            state_num;
        . . .

        struct work_struct      state_gc_work;

     . . .

        u32                     sysctl_aevent_etime;
        u32                     sysctl_aevent_rseqth;
        int                     sysctl_larval_drop;
        u32                     sysctl_acq_expires;
};
(include/net/netns/xfrm.h)

XFRM 初始化

在 IPv4 中,XFRM 初始化是通过从net/ipv4/route.c中的ip_rt_init()方法调用xfrm_init()方法和xfrm4_init()方法来完成的。在 IPv6 中,从ip6_route_init()方法调用xfrm6_init()方法来执行 XFRM 初始化。用户空间和内核之间的通信是通过创建 NETLINK_XFRM netlink 套接字以及发送和接收 NETLINK 消息来完成的。netlink NETLINK_XFRM 内核套接字的创建方法如下:

static int __net_init xfrm_user_net_init(struct net *net)
{
        struct sock *nlsk;
        struct netlink_kernel_cfg cfg = {
                .groups = XFRMNLGRP_MAX,
                .input  = xfrm_netlink_rcv,
        };

        nlsk = netlink_kernel_create(net, NETLINK_XFRM, &cfg);
        . . .
        return 0;
}

从用户空间发送的消息(像 XFRM_MSG_NEWPOLICY 用于创建新的安全策略或 XFRM_MSG_NEWSA 用于创建新的安全关联)由xfrm_netlink_rcv()方法(net/xfrm/xfrm_user.c)处理,该方法又调用xfrm_user_rcv_msg()方法(我在第二章中讨论 netlink 套接字)。

XFRM 策略和 XFRM 状态是 XFRM 框架的基本数据结构。我首先描述什么是 XFRM 策略,然后描述什么是 XFRM 状态。

XFRM 策略

安全策略是告诉 IPsec 某个流是否应该被处理或者是否可以绕过 IPsec 处理的规则。xfrm_policy结构代表一个 IPsec 策略。一个策略包括一个选择器(一个xfrm_selector对象)。当策略的选择器与流匹配时,将应用策略。XFRM 选择器由源地址和目的地址、源端口和目的端口、协议等字段组成,这些字段可以标识流:

struct xfrm_selector {
        xfrm_address_t  daddr;
        xfrm_address_t  saddr;
        __be16  dport;
        __be16  dport_mask;
        __be16  sport;
        __be16  sport_mask;
        __u16   family;
        __u8    prefixlen_d;
        __u8    prefixlen_s;
        __u8    proto;
        int     ifindex;
        __kernel_uid32_t        user;
};
(include/uapi/linux/xfrm.h)

xfrm_selector_match()方法获取 XFRM 选择器、流和族(IPv4 的 AF_INET 或 IPv6 的 AF_INET6)作为参数,当指定的流与指定的 XFRM 选择器匹配时,返回true。请注意,xfrm_selector结构也用于 XFRM 状态,您将在本节的后面看到。安全策略由xfrm_policy结构表示:

struct xfrm_policy {
        . . .
        struct hlist_node             bydst;
        struct hlist_node             byidx;

        /* This lock only affects elements except for entry. */
        rwlock_t                      lock;
        atomic_t                      refcnt;
        struct timer_list             timer;

        struct flow_cache_object      flo;
        atomic_t                      genid;
        u32                           priority;
        u32                           index;
        struct xfrm_mark              mark;
        struct xfrm_selector          selector;
        struct xfrm_lifetime_cfg      lft;
        struct xfrm_lifetime_cur      curlft;
        struct xfrm_policy_walk_entry walk;
        struct xfrm_policy_queue      polq;
        u8                            type;
        u8                            action;
        u8                            flags;
        u8                            xfrm_nr;
        u16                           family;
        struct xfrm_sec_ctx           *security;
        struct xfrm_tmpl              xfrm_vec[XFRM_MAX_DEPTH];
};
(include/net/xfrm.h)

以下描述涵盖了xfrm_policy结构的重要成员:

  • refcnt:XFRM 策略引用计数器;在xfrm_policy_alloc( )方法中初始化为 1,由xfrm_pol_hold()方法递增,由xfrm_pol_put()方法递减。
  • timer:每策略定时器;在xfrm_policy_alloc()方法中,定时器回调被设置为xfrm_policy_timer()xfrm_policy_timer()方法处理策略过期:它负责通过调用xfrm_policy_delete()方法删除过期的策略,并通过调用km_policy_expired()方法向所有注册的密钥管理器发送事件(XFRM_MSG_POLEXPIRE)。
  • lft:XFRM 策略生存期(xfrm_lifetime_cfg对象)。每个 XFRM 策略都有一个生存期,它是一个时间间隔(用时间或字节数表示)。

您可以使用ip命令和limit参数设置 XFRM 策略生存期值,例如:

ip xfrm policy add src 172.16.2.0/24 dst 172.16.1.0/24 limit byte-soft 6000 ...
  • 将 XFRM 策略生存期(lft)的soft_byte_limit设置为 6000;参见man 8 ip xfrm

通过在运行ip -stat xfrm policy show时检查生命周期配置条目,可以显示 XFRM 策略的生命周期(lft)。

  • curlft:XFRM 策略当前生命周期,,反映了生命周期上下文中策略的当前状态。curlft是一个xfrm_lifetime_cur对象。它由四个成员组成(它们都是 64 位的字段,无符号):

  • bytes:由 IPsec 子系统处理的字节数,在 Tx 路径中通过xfrm_output_one()方法递增,在 Rx 路径中通过xfrm_input()方法递增。

  • packets:IPsec 子系统处理的数据包数量,在 Tx 路径中通过xfrm_output_one()方法递增,在 Rx 路径中通过xfrm_input()方法递增。

  • add_time:添加策略的时间戳,在添加策略时初始化,在xfrm_policy_insert()方法和xfrm_sk_policy_insert()方法中。

  • use_time: The timestamp of last access to the policy. The use_time timestamp is updated, for example, in the xfrm_lookup() method or in the __xfrm_policy_check() method. Initialized to 0 when adding the XFRM policy, in the xfrm_policy_insert() method and in the xfrm_sk_policy_insert() method.

    image 注意您可以在运行ip -stat xfrm policy show时通过检查 lifetime current 条目来显示 XFRM 策略的当前 lifetime ( curlft)对象。

  • polq:一个队列,用于保存在没有 XFRM 状态与策略相关联时发送的数据包。默认情况下,这样的包通过调用make_blackhole()方法被丢弃。当xfrm_larval_drop sysctl条目设置为 0 ( /proc/sys/net/core/xfrm_larval_drop时,这些数据包被保存在 SKBs 的一个队列(polq.hold_queue)中;这个队列中最多可以保存 100 个数据包(XFRM_MAX_QUEUE_LEN)。这是通过用xfrm_create_dummy_bundle()方法创建一个虚拟 XFRM 包来实现的(详见本章后面的“XFRM 查找”一节)。默认情况下,xfrm_larval_drop sysctl条目被设置为 1(参见net/xfrm/xfrm_sysctl.c中的__xfrm_sysctl_init()方法)。

  • type:通常类型为 XFRM_POLICY_TYPE_MAIN (0)。当内核支持子策略(CONFIG_XFRM_SUB_POLICY 已设置)时,两个策略可以应用于同一个数据包,您可以使用 XFRM_POLICY_TYPE_SUB (1)类型。在内核中存在时间较短的策略应该是子策略。通常只有开发人员/调试和移动 IPv6 才需要此功能,因为您可能对 IPsec 应用一个策略,对移动 IPv6 应用一个策略。IPsec 策略通常是主策略,其生命周期比移动 IPv6(子)策略长。

  • action : 可以有以下两个值之一:

  • XFRM_POLICY_ALLOW (0):允许流量。

  • XFRM_POLICY_BLOCK(1):禁止流量(例如,在/etc/ipsec.conf中使用type=rejecttype=drop时)。

  • xfrm_nr:与策略相关联的模板数量—最多可以有六个模板(XFRM_MAX_DEPTH)。xfrm_tmpl结构是 XFRM 状态和 XFRM 策略之间的中间结构。它在copy_templates()方法net/xfrm/xfrm_user.c中被初始化。

  • family : IPv4 或 IPv6。

  • security:安全上下文(xfrm_sec_ctx对象),允许 XFRM 子系统限制可以通过安全关联(XFRM 状态)发送或接收数据包的套接字。更多细节请见http://lwn.net/Articles/156604/

  • xfrm_vec:XFRM 模板的数组(xfrm_tmpl对象)。

内核将 IPsec 安全策略存储在安全策略数据库(SPD) 中。SPD 的管理是通过从用户空间套接字发送消息来完成的。比如:

  • 添加 XFRM 策略(XFRM_MSG_NEWPOLICY)由xfrm_add_policy()方法处理。
  • 删除 XFRM 策略(XFRM_MSG_DELPOLICY)由xfrm_get_policy()方法处理。
  • 显示 SPD (XFRM_MSG_GETPOLICY)由xfrm_dump_policy()方法处理。
  • 刷新 SPD (XFRM_MSG_FLUSHPOLICY)是由xfrm_flush_policy()方法处理的。

下一节描述什么是 XFRM 状态。

XFRM 状态(安全关联)

xfrm_state结构表示一个 IPsec 安全关联(SA) ( include/net/xfrm.h)。它代表单向流量,包括加密密钥、标志、请求 id、统计信息、重放参数等信息。通过从用户空间套接字发送请求(XFRM_MSG_NEWSA)来添加 XFRM 状态;它在内核中由xfrm_state_add()方法(net/xfrm/xfrm_user.c)处理。同样,通过发送 XFRM_MSG_DELSA 消息来删除状态,它在内核中由xfrm_del_sa()方法处理:

struct xfrm_state {
        . . .
        union {
                struct hlist_node       gclist;
                struct hlist_node       bydst;
        };
        struct hlist_node       bysrc;
        struct hlist_node       byspi;

        atomic_t                refcnt;
        spinlock_t              lock;

        struct xfrm_id          id;
        struct xfrm_selector    sel;
        struct xfrm_mark        mark;
        u32                     tfcpad;

        u32                     genid;

        /* Key manager bits */
        struct xfrm_state_walk  km;

        /* Parameters of this state. */
        struct {
                u32             reqid;
                u8              mode;
                u8              replay_window;
                u8              aalgo, ealgo, calgo;
                u8              flags;
                u16             family;
                xfrm_address_t  saddr;
                int             header_len;
                int             trailer_len;
        } props;

        struct xfrm_lifetime_cfg lft;

        /* Data for transformer */
        struct xfrm_algo_auth   *aalg;
        struct xfrm_algo        *ealg;
        struct xfrm_algo        *calg;
        struct xfrm_algo_aead   *aead;

        /* Data for encapsulator */
        struct xfrm_encap_tmpl  *encap;

        /* Data for care-of address */
        xfrm_address_t  *coaddr;

        /* IPComp needs an IPIP tunnel for handling uncompressed packets */
        struct xfrm_state       *tunnel;

        /* If a tunnel, number of users + 1 */
        atomic_t                tunnel_users;

        /* State for replay detection */
        struct xfrm_replay_state replay;
        struct xfrm_replay_state_esn *replay_esn;

        /* Replay detection state at the time we sent the last notification */
        struct xfrm_replay_state preplay;
        struct xfrm_replay_state_esn *preplay_esn;

        /* The functions for replay detection. */
        struct xfrm_replay      *reply;

        /* internal flag that only holds state for delayed aevent at the
         * moment
        */
        u32                     xflags;

        /* Replay detection notification settings */
        u32                     replay_maxage;
        u32                     replay_maxdiff;

        /* Replay detection notification timer */
        struct timer_list       rtimer;

        /* Statistics */
        struct xfrm_stats       stats;

        struct xfrm_lifetime_cur curlft;
        struct tasklet_hrtimer  mtimer;

        /* used to fix curlft->add_time when changing date */
        long            saved_tmo;

        /* Last used time */
        unsigned long           lastused;

        /* Reference to data common to all the instances of this
         * transformer. */
        const struct xfrm_type  *type;
        struct xfrm_mode        *inner_mode;
        struct xfrm_mode        *inner_mode_iaf;
        struct xfrm_mode        *outer_mode;

        /* Security context */
        struct xfrm_sec_ctx     *security;

        /* Private data of this transformer, format is opaque,
         * interpreted by xfrm_type methods. */
        void                    *data;
};
(include/net/xfrm.h)

以下描述详细介绍了xfrm_state结构的一些重要成员:

  • refcnt:参考计数器,由xfrm_state_hold()方法递增,由__xfrm_state_put()方法或xfrm_state_put()方法递减(当参考计数器达到 0 时,后者也通过调用__xfrm_state_destroy()方法释放 XFRM 状态)。

  • id:id(xfrm_id对象)由三个唯一定义它的字段组成:目的地址、spi 和安全协议(AH、ESP 或 IPCOMP)。

  • props:XFRM 状态的属性。例如:

  • mode:可以是五种模式之一(例如,传输模式为 XFRM_MODE_TRANSPORT,隧道模式为 XFRM _ MODE _ TUNNEL 参见include/uapi/linux/xfrm.h

  • flag:比如 XFRM_STATE_ICMP。这些标志在include/uapi/linux/xfrm.h中可用。这些标志可以从用户空间设置,例如,使用ip命令和flag选项:ip xfrm add state flag icmp...

  • family:IPv6 的 IPv4。

  • saddr:XFRM 状态的源地址。

  • lft:XFRM 状态生存期(xfrm_lifetime_cfg对象)。

  • stats:一个xfrm_stats对象,代表 XFRM 状态统计。您可以通过ip –stat xfrm show显示 XFRM 状态统计。

内核将 IPsec 安全关联存储在安全关联数据库(SAD)中。xfrm_state对象存储在netns_xfrm(前面讨论过的 XFRM 名称空间)的三个散列表中:state_bydststate_bysrcstate_byspi。这些表的关键字分别由xfrm_dst_hash()xfrm_src_hash()xfrm_spi_hash()方法计算。当添加一个xfrm_state对象时,它被插入到这三个散列表中。如果 spi 的值为 0(值 0 通常不用于 SPI——当它为 0 时,我将很快提到),则xfrm_state对象不会添加到state_byspi哈希表中(参见net/xfrm/xfrm_state.c中的__xfrm_state_insert()方法)。

image 注意值为 0 的 spi 仅用于采集状态。内核向密钥管理器发送获取消息,如果流量与策略匹配,则添加一个带有 spi 0 的临时获取状态,但该状态尚未解决。只要获取状态存在,内核就不会费心发送进一步的获取;可以在net->xfrm.sysctl_acq_expires处配置寿命。如果状态得到解决,这个获取状态将被实际状态替换。

可以通过以下方式在 SAD 中进行查找:

  • xfrm_state_lookup()方法:在state_byspi哈希表中。
  • xfrm_state_lookup_byaddr()方法:在state_bysrc哈希表中。
  • xfrm_state_find()方法:在state_bydst哈希表中。

ESP 协议是最常用的 IPsec 协议;它支持加密和认证。下一节讨论 IPv4 ESP 实现。

ESP 实施(IPv4)

RFC 4303 中规定了电潜泵协议;它支持加密和认证。虽然它也支持仅加密和仅身份验证模式,但它通常与加密和身份验证一起使用,因为这样更安全。这里我还应该提到 AES-GCM 等新的认证加密(AEAD)方法,它可以在一个通道中完成加密和数据完整性计算,并且可以在多个内核上高度并行化,因此借助英特尔 AES-NI 指令集,可以实现几个 Gbit/s 的 IPsec 吞吐量。ESP 协议支持隧道模式和传输模式;协议标识符是 50 (IPPROTO_ESP)。ESP 为每个数据包添加新的报头和报尾。根据图 10-1 所示的 ESP 格式,有以下字段:

  • SPI: 一个 32 位的安全参数索引。 和源地址一起标识一个 SA。
  • 序列号: 32 位,每发送一个包递增 1,以防止重放攻击。
  • 净荷数据: 一个可变大小的加密数据块。
  • 填充:为加密数据块填充 以满足对齐要求(0-255 字节)。
  • 填充长度:以字节为单位的填充大小(1 字节)。
  • 下一个报头:下一个报头的类型(1 字节)。
  • 认证数据: 【完整性校验值】(ICV)。

9781430261964_Fig10-01.jpg

图 10-1 。ESP 格式

下一节讨论 IPv4 ESP 初始化。

IPv4 ESP 初始化

我们首先定义一个esp_type ( xfrm_type对象)和esp4_protocol ( net_protocol对象),然后注册它们:

static const struct xfrm_type esp_type =
{
        .description    = "ESP4",
        .owner          = THIS_MODULE,
        .proto          = IPPROTO_ESP,
        .flags          = XFRM_TYPE_REPLAY_PROT,
        .init_state     = esp_init_state,
        .destructor     = esp_destroy,
        .get_mtu        = esp4_get_mtu,
        .input          = esp_input,
        .output         = esp_output
};

static const struct net_protocol esp4_protocol = {
        .handler        =       xfrm4_rcv,
        .err_handler    =       esp4_err,
        .no_policy      =       1,
        .netns_ok       =       1,
};

static int __init esp4_init(void)
{

每个协议族都有一个xfrm_state_afinfo对象的实例,它包括协议族特定的状态方法;因此,IPv4 有xfrm4_state_afinfo(net/ipv4/xfrm4_state.c),IPv6 有xfrm6_state_afinfo。这个对象包括一个名为type_mapxfrm_type对象数组。通过调用xfrm_register_type()方法注册 XFRM 类型会将指定的xfrm_type设置为该数组中的一个元素:

        if (xfrm_register_type(&esp_type, AF_INET) < 0) {
                pr_info("%s: can't add xfrm type\n", __func__);
                return -EAGAIN;
        }

注册 IPv4 ESP 协议就像注册任何其他 IPv4 协议一样,通过调用inet_add_protocol()方法来完成。注意,IPv4 ESP 使用的协议处理程序,即xfrm4_rcv()方法,也被 IPv4 AH 协议(net/ipv4/ah4.c)和 IPv4 IPCOMP (IP 有效载荷压缩协议)协议(net/ipv4/ipcomp.c)使用。

        if (inet_add_protocol(&esp4_protocol, IPPROTO_ESP) < 0) {
                pr_info("%s: can't add protocol\n", __func__);
                xfrm_unregister_type(&esp_type, AF_INET);
                return -EAGAIN;
        }
        return 0;
}
(net/ipv4/esp4.c)

接收 IPsec 数据包(传输模式)

假设您在 IPv4 的传输模式下工作,您收到了一个目的地为本地主机的 ESP 数据包。传输模式下的 ESP 不加密 IP 报头,只加密 IP 有效负载。图 10-2 显示了一个传入的 IPv4 ESP 数据包的遍历,本节描述了其各个阶段。我们将通过本地交付的所有通常阶段,从ip_rcv()方法开始,我们将到达ip_local_deliver_finish()方法。因为 IPv4 报头中的 protocol 字段的值是 ESP (50),所以我们调用它的处理程序,这就是xfrm4_rcv()方法,正如您前面看到的。xfrm4_rcv()方法进一步调用通用的xfrm_input()方法,后者通过调用xfrm_state_lookup()方法在 SAD 中执行查找。如果查找失败,数据包将被丢弃。在查找命中的情况下,调用相应 IPsec 协议的input回调方法:

int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
{
         struct xfrm_state *x;
         do {
                 . . .

9781430261964_Fig10-02.jpg

图 10-2 。正在接收 IPv4 ESP 数据包,本地传递,传输模式。注意:图中描述了一个 IPv4 ESP 数据包。对于 IPv4 AH 包,调用 ah_input()方法,而不是 esp_input()方法;同样,对于 IPv4 IPCOMP 数据包,将调用 ipcomp_input()方法,而不是 esp_input()方法

state_byspi散列表中执行查找:

x = xfrm_state_lookup(net, skb->mark, daddr, spi, nexthdr, family);

如果查找失败,则静默丢弃数据包:

if (x == NULL) {
                         XFRM_INC_STATS(net, LINUX_MIB_XFRMINNOSTATES);
                         xfrm_audit_state_notfound(skb, family, spi, seq);
                         goto drop;
}

在这种情况下,对于 IPv4 ESP 传入流量,与状态(x->type)相关联的 XFRM 类型是 ESP XFRM 类型(esp_type);它的input回调被设置为esp_input(),如前面的“IPv4 ESP 初始化”一节所述。

通过调用x->type->input(),在下面一行中esp_input()方法被调用;此方法返回原始数据包在被 ESP 加密之前的协议号:

nexthdr = x->type->input(x, skb);
. . .

使用 XFRM_MODE_SKB_CB 宏将原始协议号保存在 SKB 的控制缓冲区(cb)中;稍后将使用它来修改数据包的 IPv4 报头,您将会看到:

XFRM_MODE_SKB_CB(skb)->protocol = nexthdr;

esp_input()方法终止后,调用xfrm4_transport_finish()方法。该方法修改 IPv4 报头的各个字段。看一看xfrm4_transport_finish()的方法:

int xfrm4_transport_finish(struct sk_buff *skb, int async)
{
        struct iphdr *iph = ip_hdr(skb);

IPv4 头(iph->protocol)的协议此时为 50(ESP);您应该将它设置为原始数据包的协议号(在它被 ESP 加密之前),以便它将被 L4 套接字处理。原始数据包的协议号保存在XFRM_MODE_SKB_CB(skb)->protocol中,正如您在本节前面看到的:

iph->protocol = XFRM_MODE_SKB_CB(skb)->protocol;

. . .
__skb_push(skb, skb->data - skb_network_header(skb));
iph->tot_len = htons(skb->len);

自 IPv4 标头被修改后,重新计算校验和:

ip_send_check(iph);

调用任何 netfilter NF_INET_PRE_ROUTING 钩子回调,然后调用xfrm4_rcv_encap_finish()方法:

        NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,
                xfrm4_rcv_encap_finish);
        return 0;
}

xfrm4_rcv_encap_finish()方法调用ip_local_deliver()方法。现在,IPv4 报头中的protocol成员的值是原始的传输协议(UDPv4、TCPv4 等等),所以从现在开始,您继续进行通常的数据包遍历,数据包被传递到传输层(L4)。

发送 IPsec 数据包(传输模式)

图 10-3 显示了在传输模式下通过 IPv4 ESP 发送的输出数据包的 Tx 路径。在路由子系统中执行查找(通过调用ip_route_output_flow()方法)后的第一步是执行 XFRM 策略的查找,该策略可以应用于这个流。您可以通过调用xfrm_lookup()方法来实现这一点(我将在本节稍后讨论该方法的内部机制)。如果有查找命中,继续到ip_local_out()方法,然后,在调用如图图 10-3 所示的几个方法后,最终到达esp_output()方法,该方法加密数据包,然后通过调用ip_output()方法发送出去。

9781430261964_Fig10-03.jpg

图 10-3 。传输 IPv4 ESP 数据包,传输模式。为了简单起见,省略了创建虚拟束的情况(当没有 XFRM 状态时)和一些其他细节

下一节讨论如何在 XFRM 中执行查找。

XFRM 查找

系统发出的每个数据包都会调用xfrm_lookup()方法 。您希望这种查找尽可能高效。为了实现这个目标,使用了包。Bundles 允许您缓存重要的信息,比如路由、策略、策略数量等等;这些包是xfrm_dst结构的实例,通过使用流缓存来存储。当某个流的第一个包到达时,您在通用流缓存中创建一个条目,然后创建一个包(xfrm_dst对象)。在对该包的查找失败之后,完成包的创建,因为它是该流的第一个包。当此流的后续数据包到达时,您将会在执行流缓存查找时得到一个命中结果:

struct xfrm_dst {
        union {
                struct dst_entry        dst;
                struct rtable           rt;
                struct rt6_info         rt6;
        } u;
        struct dst_entry *route;
        struct flow_cache_object flo;
        struct xfrm_policy *pols[XFRM_POLICY_TYPE_MAX];
        int num_pols, num_xfrms;
#ifdef CONFIG_XFRM_SUB_POLICY
        struct flowi *origin;
        struct xfrm_selector *partner;
#endif
        u32 xfrm_genid;
        u32 policy_genid;
        u32 route_mtu_cached;
        u32 child_mtu_cached;
        u32 route_cookie;
        u32 path_cookie;
};
(include/net/xfrm.h)

xfrm_lookup()方法是一个非常复杂的方法。我讨论了它的重要部分,但我没有深入研究它的所有细微差别。图 10-4 显示了xfrm_lookup()方法的内部框图。

9781430261964_Fig10-04.jpg

图 10-4 。xfrm_lookup()内部

我们来看看xfrm_lookup()方法:

struct dst_entry *xfrm_lookup(struct net *net, struct dst_entry *dst_orig,
                              const struct flowi *fl, struct sock *sk, int flags)
{

xfrm_lookup()方法只处理 Tx 路径;因此,您通过以下方式将流向(dir)设置为 FLOW_DIR_OUT:

         u8 dir = policy_to_flow_dir(XFRM_POLICY_OUT);

如果一个策略与这个套接字相关联,您可以通过xfrm_sk_policy_lookup()方法执行查找,检查数据包流是否与策略选择器匹配。请注意,如果要转发数据包,则从__xfrm_route_forward()方法、 中调用了xfrm_lookup()方法,并且没有与数据包相关联的套接字,因为它不是在本地主机上生成的;在这种情况下,指定的sk参数为空:

        if (sk && sk->sk_policy[XFRM_POLICY_OUT]) {
                num_pols = 1;
                pols[0] = xfrm_sk_policy_lookup(sk, XFRM_POLICY_OUT, fl);

        . . .
}

如果没有与这个套接字相关联的策略,那么通过调用flow_cache_lookup()方法,将指向xfrm_bundle_lookup方法的函数指针作为参数传递(resolver回调),在通用流缓存中执行查找。查找的关键是流对象(指定的fl参数)。如果在流缓存中没有找到条目,请分配一个新的流缓存条目。如果找到一个具有相同genid的条目,通过调用flo->ops->get(flo)来调用xfrm_bundle_flo_get()方法。最后,您通过调用resolver回调来调用xfrm_bundle_lookup()方法,该回调获取流对象作为参数(oldflo)。参见net/core/flow.cflow_cache_lookup()方法 的实现:

flo = flow_cache_lookup(net, fl, family, dir, xfrm_bundle_lookup, dst_orig);

获取包含流缓存对象作为成员的包(xfrm_dst对象):

xdst = container_of(flo, struct xfrm_dst, flo);

获取缓存数据,如策略数量、模板数量、策略和路线:

       num_pols = xdst->num_pols;
       num_xfrms = xdst->num_xfrms;
       memcpy(pols, xdst->pols, sizeof(struct xfrm_policy*) * num_pols);
       route = xdst->route;
}

dst = &xdst->u.dst;

接下来是处理虚拟包。一个虚拟束是 一个其中路由成员为空的束。当没有找到 XFRM 状态时,通过调用xfrm_create_dummy_bundle()方法在 XFRM 包查找过程中创建它(通过xfrm_bundle_lookup()方法)。在这种情况下,根据sysctl_larval_drop ( /proc/sys/net/core/xfrm_larval_drop)的值,可以选择两个选项中的一个:

  • 如果sysctl_larval_drop被置位(这意味着它的值是 1——默认情况下是这样,如本章前面提到的),该数据包应该被丢弃。

  • 如果未设置sysctl_larval_drop(其值为 0),则数据包保存在每个策略的队列(polq.hold_queue)中,该队列最多可包含 100 个(XFRM _ MAX _ QUEUE _ LEN)skb;这是通过xdst_queue_output()方法实现的。这些数据包会一直保留,直到 XFRM 状态被解析或超时。一旦状态被解析,包就被送出队列。如果在一段时间间隔后 XFRM 状态没有得到解决(xfrm_policy_queue对象的超时),那么通过xfrm_queue_purge()方法:

    if (route == NULL && num_xfrms > 0) {
            /* The only case when xfrm_bundle_lookup() returns a
             * bundle with null route, is when the template could
             * not be resolved. It means policies are there, but
             * bundle could not be created, since we don't yet
             * have the xfrm_state's. We need to wait for KM to
             * negotiate new SA's or bail out with error.*/
             if (net->xfrm.sysctl_larval_drop) {
    

    刷新队列

对于 IPv4,make_blackhole()方法调用ipv4_blackhole_route()方法。对于 IPv6,它调用ip6_blackhole_route()方法:

        return make_blackhole(net, family, dst_orig);
}

下一节将介绍 IPsec 最重要的特性之一—NAT 穿越,并解释它是什么以及为什么需要它。

IPsec 中的 NAT 穿越

为什么 NAT 设备不允许 IPsec 流量通过?NAT 会改变 IP 地址,有时还会改变数据包的端口号。因此,它会重新计算 TCP 或 UDP 报头的校验和。传输层校验和计算考虑了 IP 地址的来源和目的地。因此,即使只更改了 IP 地址,也应该重新计算 TCP 或 UDP 校验和。但是,在传输模式下使用 ESP 加密,NAT 设备无法更新校验和,因为 TCP 或 UDP 报头是使用 ESP 加密的。有些协议的校验和不包含 IP 报头(如 SCTP),所以这个问题不会发生。为了解决这些问题,开发了 IPsec 的 NAT 穿越标准(或者,按照 RFC 3948 中的官方术语,“IPsec ESP 数据包的 UDP 封装”)。UDP 封装可以应用于 IPv4 数据包,也可以应用于 IPv6 数据包。NAT 穿越解决方案不限于 IPsec 流量;这些技术通常是客户端到客户端的网络应用所需要的,尤其是点对点和基于互联网协议的语音(VoIP)应用。

VoIP NAT 穿越有一些部分解决方案,比如 STUN,TURN,ICE 等等。我应该在这里提到,strongSwan 实现了 IKEv2 中介扩展服务(http://tools.ietf.org/html/draft-brunner-ikev2-mediation-00),它允许位于 NAT 路由器后面的两个 VPN 端点使用类似于 TURN 和 ice 的机制建立直接的对等 IPsec 隧道。例如,STUN 用于 VoIP 开源 Ekiga 客户端(以前称为 gnomemeeting)。这些解决方案的问题是它们不能处理 NAT 设备。称为 SBCs (会话边界控制器)的设备为 VoIP 中的 NAT 穿越提供了完整的解决方案。SBC 可以在硬件中实现(例如,Juniper Networks 提供了集成路由器的 SBC 解决方案),也可以在软件中实现。这些 SBC 解决方案对实时协议(RTP)发送的媒体流量执行 NAT 穿越,有时也对会话发起协议(SIP)发送的信令流量执行 NAT 穿越。NAT 穿越在 IKEv2 是可选的。Openswan、strongSwan 和 racoon 支持 NAT 穿越,但 Openswan 和 racoon 仅支持 IKEv1 的 NAT-T,而 strongSwan 支持 IKEv1 和 IKEv2 的 NAT 穿越。

NAT-T 操作模式

NAT 穿越是如何工作的?首先,请记住 NAT-T 仅适用于 ESP 流量,而不适用于 AH。另一个限制是 NAT-T 不能用于手动键控,只能用于 IKEv1 和 IKEv2。这是因为 NAT-T 与交换 IKEv1/IKEv2 消息联系在一起。首先,您必须告诉用户空间守护进程(pluto)您想要使用 NAT 遍历特性,因为它在默认情况下是不激活的。在 Openswan 中,您可以通过将nat_traversal=yes添加到/etc/ipsec.conf中的连接参数来实现这一点。不在 NAT 后面的客户端不受添加此项的影响。在 strongSwan 中,IKEv2 charon守护进程始终支持 NAT 穿越,并且该功能不能被停用。在 IKE 的第一阶段(主模式),你检查两个对等体是否都支持 NAT-T,在 IKEv1 中,当一个对等体支持 NAT-T 时,其中一个 ISAKAMP 头成员(厂商 ID)会告知它是否支持 NAT-T,在 IKEv2,NAT-T 是标准的一部分,不必公布。如果满足这一条件,您可以通过发送 NAT-D 有效载荷消息来检查两个 IPsec 对等方之间的路径中是否有一个或多个 NAT 设备。如果也满足这个条件,NAT-T 通过在 IP 报头和 ESP 报头之间插入 UDP 报头来保护原始 IPsec 编码的分组。UDP 报头中的源端口和目的端口都是 4500。此外,NAT-T 每 20 秒发送一次保活消息,以便 NAT 保留其映射。保持活动消息也在 UDP 端口 4500 上发送,并通过其内容和值(一个字节,0xFF)来识别。当此数据包到达 IPsec 对等方时,在通过 NAT 后,内核会剥离 UDP 报头并解密 ESP 有效负载。参见net/ipv4/xfrm4_input.c中的xfrm4_udp_encap_rcv()方法。

摘要

本章介绍了 IPsec 和 XFRM 框架(IPsec 的基础设施),以及 XFRM 策略和状态(XFRM 框架的基本数据结构)。我还讨论了 IKE、ESP4 实现、传输模式下 ESP4 的 Rx/Tx 路径以及 IPsec 中的 NAT 穿越。第十一章处理以下传输层(L4)协议:UDP、TCP、SCTP 和 DCCP 。接下来的“快速参考”部分涵盖了与本章讨论的主题相关的主要方法,按其上下文排序。

快速参考

我用 IPsec 的重要方法的简短列表来结束这一章。本章提到了其中一些。之后,我包含了一个 XFRM SNMP MIB 计数器表。

方法

先说方法。

bool xfrm _ selector _ match(const struct xfrm _ selector * sel,const struct flowi *fl,无符号短族);

当指定的流与指定的 XFRM 选择器匹配时,该方法返回true。调用 IPv4 的__xfrm4_selector_match()方法或 IPv6 的__xfrm6_selector_match()方法。

int xfrm _ policy _ match(const struct xfrm _ policy * pol,const struct flowi *fl,u8 type,u16 family,int dir);

如果指定的策略可以应用于指定的流,则该方法返回 0,否则返回–errno

struct xfrm _ policy * xfrm _ policy _ alloc(struct net * net,GFP _ t GFP);

这个方法分配并初始化一个 XFRM 策略。它将其引用计数器设置为 1,初始化读写锁,将策略名称空间(xp_net)指定为指定的网络名称空间,将其定时器回调设置为xfrm_policy_timer(),将其状态解析分组队列定时器(policy->polq.hold_timer)回调设置为xfrm_policy_queue_process()

void xfrm _ policy _ destroy(struct xfrm _ policy * policy);

此方法删除指定 XFRM 策略对象的计时器,并释放指定 XFRM 策略内存。

void xfrm _ pol _ hold(struct xfrm _ policy * policy);

此方法将指定 XFRM 策略的引用计数增加 1。

静态内联 void xfrm _ pol _ put(struct xfrm _ policy * policy);

此方法将指定 XFRM 策略的引用计数减 1。如果引用计数达到 0,调用xfrm_policy_destroy()方法。

struct xfrm _ state _ af info * xfrm _ state _ get _ af info(unsigned int family);

该方法返回与指定协议族相关的xfrm_state_afinfo对象。

struct dst _ entry * xfrm _ bundle _ create(struct xfrm _ policy * policy,struct xfrm_state **xfrm,int nx,const struct flowi *fl,struct dst _ entry * dst);

这个方法创建一个 XFRM 包。从xfrm_resolve_and_create_bundle()方法调用。

int policy _ to _ flow _ dir(int dir);

此方法根据指定的策略方向返回流向。例如,当指定方向为 XFRM_POLICY_IN 时,返回 FLOW_DIR_IN,依此类推。

静态 struct xfrm _ dst * xfrm _ create _ dummy _ bundle(struct net * net,struct dst_entry *dst,const struct flowi *fl,int num_xfrms,u16 系列);

这个方法创建了一个虚拟包。当找到策略但没有匹配的状态时,从xfrm_bundle_lookup()方法调用。

struct xfrm _ dst * xfrm _ alloc _ dst(struct net * net,int family);

这个方法分配 XFRM bundle 对象。从xfrm_bundle_create()方法和xfrm_create_dummy_bundle()方法调用。

int xfrm_policy_insert(int dir,struct xfrm_policy *policy,int excl);

该方法将 XFRM 策略添加到 SPD 中。从xfrm_add_policy()方法(net/xfrm/xfrm_user.c)或从pfkey_spdadd()方法(net/key/af_key.c)调用。

int xfrm _ policy _ delete(struct xfrm _ policy * pol,int dir);

此方法释放指定 XFRM 策略对象的资源。需要方向参数(dir)来将每个名称空间netns_xfrm对象中的policy_count中的相应 XFRM 策略计数器减 1。

int xfrm _ state _ add(struct xfrm _ state * x);

此方法将指定的 XFRM 状态添加到 SAD。

int xfrm _ state _ delete(struct xfrm _ state * x);

此方法从 SAD 中删除指定的 XFRM 状态。

void _ _ xfrm _ state _ destroy(struct xfrm _ state * x);

该方法通过将 XFRM 状态添加到 XFRM 状态垃圾列表并激活 XFRM 状态垃圾收集器来释放 XFRM 状态的资源。

int xfrm _ state _ walk(struct net * net,struct xfrm_state_walk walk,int (func)(struct xfrm_state ,int,void),void * data);

这个方法遍历所有 XFRM 状态(net->xfrm.state_all)并调用指定的func回调。

struct xfrm _ state * xfrm _ state _ alloc(struct net * net);

该方法分配并初始化 XFRM 状态。

void xfrm _ queue _ purge(struct sk _ buff _ head * list);

该方法刷新每个策略的状态解析队列(polq.hold_queue)。

int xfrm _ input(struct sk _ buff * skb,int nexthdr,__be32 spi,int encap _ type);

这个方法是主要的 Rx IPsec 处理程序。

静态 struct dst _ entry * make _ black hole(struct net * net,u16 家族,struct dst _ entry * dst _ orig);

当没有已解析的状态并且设置了sysctl_larval_drop时,从xfrm_lookup()方法调用该方法。对于 IPv4,make_blackhole()方法调用ipv4_blackhole_route()方法;对于 IPv6,它调用ip6_blackhole_route()方法。

int xdst _ queue _ output(struct sk _ buff * skb);

该方法处理将数据包添加到每策略状态解析数据包队列(pq->hold_queue)。这个队列最多可以包含 100 个(XFRM_MAX_QUEUE_LEN)数据包。

struct net * xs _ net(struct xfrm _ state * x);

该方法返回与指定的xfrm_state对象相关联的名称空间对象(xs_net)。

struct net * XP _ net(const struct xfrm _ policy * XP);

该方法返回与指定的xfrm_policy对象相关联的名称空间对象(xp_net)。

int xfrm _ policy _ id 2 dir(u32 index);

此方法根据指定的索引返回策略的方向。

int esp _ input(struct xfrm _ state * x,struct sk _ buff * skb);

此方法是主要的 IPv4 ESP 协议处理程序。

struct IP _ esp _ HDR * IP _ esp _ HDR(const struct sk _ buf * skb);

此方法返回与指定 SKB 关联的 ESP 头。

int verify _ new policy _ info(struct xfrm _ user policy _ info * p);

该方法验证指定的xfrm_userpolicy_info对象包含有效值。(xfrm_userpolicy_info是从用户空间传递过来的对象)。如果是有效对象,则返回 0,否则返回-EINVAL 或-EAFNOSUPPORT。

桌子

表 10-1 列出了 XFRM SNMP MIB 计数器。

表 10-1。 XFRM SNMP MIB 计数器

|

Linux 符号

|

SNMP (procfs)符号

|

计数器可能递增的方法

|
| --- | --- | --- |
| Linux _ MIB _ xfterminerror | XfrmInError | xfrm_input() |
| Linux _ MIB _ xfterminbuffer error | XfrmInBufferError | xfrm_input(),__xfrm_policy_check() |
| Linux _ MIB _ xfterminhderror | XfrmInHdrError | xfrm_input(),__xfrm_policy_check() |
| Linux _ MIB _ xfrmannotations | XfrmInNoStates | xfrm_input() |
| Linux _ MIB _ xfterminstate protoerror | XfrmInStateProtoError | xfrm_input() |
| Linux _ MIB _ xfterminstate mode error | XfrmInStateModeError | xfrm_input() |
| Linux _ MIB _ xfterminstate sequence error | XfrmInStateSeqError | xfrm_input() |
| Linux _ MIB _ xfterminstate expired | XfrmInStateExpired | xfrm_input() |
| LINUX_MIB_XFRMINSTATEMISMATCH | XfrmInStateMismatch | xfrm_input()__xfrm_policy_check() |
| Linux _ MIB _ xfterminstate invalid | XfrmInStateInvalid | xfrm_input() |
| linux _ mib _ xfrmintmplmismatch | XfrmInTmplMismatch | __xfrm_policy_check() |
| linux _ mib _ xfrminnopols | XfrmInNoPols | __xfrm_policy_check() |
| Linux _ MIB _ xfterminpolblock | XfrmInPolBlock | __xfrm_policy_check() |
| Linux _ MIB _ xfterminpolerror | XfrmInPolError | __xfrm_policy_check() |
| Linux _ MIB _ xfrmouteerror | XfrmOutError | xfrm_output_one(),xfrm_output() |
| linux _ mib _ xfrmoutbundlegenerror | XfrmOutBundleGenError | xfrm_resolve_and_create_bundle() |
| Linux _ MIB _ xfrmoutbundleeckerror | XfrmOutBundleCheckError | xfrm_resolve_and_create_bundle() |
| linux _ mib _ xfrmoutnostates | XfrmOutNoStates | xfrm_lookup() |
| linux _ mib _ xfrmoutstateprotoerror | XfrmOutStateProtoError | xfrm_output_one() |
| linux _ mib _ xfrmoutstatemodeerror | XfrmOutStateModeError | xfrm_output_one() |
| linux _ mib _ xfrmoutstateseqerror | XfrmOutStateSeqError | xfrm_output_one() |
| LINUX_MIB_XFRMOUTSTATEEXPIRED | XfrmOutStateExpired | xfrm_output_one() |
| LINUX_MIB_XFRMOUTPOLBLOCK | XfrmOutPolBlock | xfrm_lookup() |
| LINUX_MIB_XFRMOUTPOLDEAD | XfrmOutPolDead | n/a |
| LINUX_MIB_XFRMOUTPOLERROR | XfrmOutPolError | xfrm_bundle_lookup()xfrm_resolve_and_create_bundle() |
| Linux _ MIB _ xfrmfwdhdreerror | XfrmFwdHdrError | __xfrm_route_forward() |
| linux _ mib _ xfrmoutstateinvalid | XfrmOutStateInvalid | xfrm_output_one() |

image IPsec git 树:git://git . kernel . org/pub/SCM/Linux/kernel/git/klassert/IPsec . git

ipsec git 树用于修复 ipsec 网络子系统;这个树中的开发是针对 David Miller 的 net git 树完成的。

ipsec-next git 树:git://git . kernel . org/pub/SCM/Linux/kernel/git/klass et/IPSec-next . git

ipsec-next 树用于以 linux-next 为目标的 ipsec 更改;这个树的开发是针对 David Miller 的 net-next git 树进行的。

IPsec 子系统维护人员是 Steffen Klassert、Herbert Xu 和 David S. Miller。

十一、第四层协议

第十章讨论了 Linux IPsec 子系统及其实现。在本章中,我将讨论四种传输层(L4)协议。我将从两个最常用的传输层(L4)协议开始我们的讨论,这两个协议是用户数据报协议(UDP)和传输控制协议(TCP),它们已经使用了很多年。随后,我将讨论较新的流控制传输协议(SCTP)和数据报拥塞控制协议(DCCP),它们结合了 TCP 和 UDP 的特性。我将从描述套接字 API 开始这一章,它是传输层(L4)和用户空间之间的接口。我将讨论套接字如何在内核中实现,以及数据如何从用户空间流向传输层,以及如何从传输层流向用户空间。在使用这些协议时,我还将处理从网络层(L3)到传输层(L4)的数据包传递。我将在这里主要讨论这四个协议的 IPv4 实现,尽管有些代码是 IPv4 和 IPv6 共有的。

套接字

每个操作系统都必须为其网络子系统提供一个入口点和一个 API。Linux 内核网络子系统通过标准的 POSIX 套接字 API 提供到用户空间的接口,该 API 由 IEEE (IEEE Std 1003.1g-2000,描述网络 API,也称为 POSIX.1g)指定。这个 API 基于 Berkeley sockets API(也称为 BSD sockets),它起源于 4.2BSD Unix 操作系统,是几个操作系统中的行业标准。在 Linux 中,传输层以上的一切都属于用户空间。遵循 Unix 范式“一切都是文件”,套接字与文件相关联,这一点您将在本章后面看到。使用统一套接字 API 使得移植应用更加容易。以下是可用的套接字类型:

  • 套接字(SOCK_STREAM): 提供了一个可靠的、字节流的通信通道。TCP 套接字是流套接字的一个例子。
  • 数据报 套接字(SOCK_DGRAM): 提供消息的交换(称为数据报)。数据报套接字提供了一个不可靠的通信通道,因为数据包可能会被丢弃、无序到达或被复制。UDP 套接字是数据报套接字的一个例子。
  • Raw****sockets(SOCK _ Raw):使用对 IP 层的直接访问,并允许发送或接收流量,而无需任何协议特定的传输层格式化。
  • 可靠传递消息 (SOCK_RDM): 用于透明进程间通信(TIPC),最初由爱立信于 1996 年至 2005 年开发,用于集群应用。参见http://tipc.sourceforge.net
  • 有序的包流****(SOCK _ seq packet):这个套接字类型类似于 SOCK_STREAM 类型,也是面向连接的。这两种类型之间的唯一区别是使用 SOCK_SEQPACKET 类型维护记录边界。通过 MSG_EOR(记录结束)标志,接收器可以看到记录边界。本章不讨论有序数据包流类型。
  • DCCP 套接字 (SOCK_DCCP): 数据报拥塞控制协议是一种传输协议,提供不可靠数据报的拥塞控制流。它结合了 TCP 和 UDP 的功能。这将在本章的后一节讨论。
  • 数据链接套接字****(SOCK _ PACKET):SOCK _ PACKET 在 AF_INET 家族中被认为是过时的。参见net/socket.c中的__sock_create()方法。

下面是 sockets API 提供的一些方法的描述(下面列表中出现的所有内核方法都是在net/socket.c中实现的):

  • socket() :新建一个套接字;将在“创建套接字”小节中讨论
  • bind() :将套接字与本地端口和 IP 地址关联;通过sys_bind()方法在内核中实现。
  • send() :发送消息;通过sys_send()方法在内核中实现。
  • recv() :接收消息;通过sys_recv()方法在内核中实现。
  • listen() :允许一个套接字接收来自其他套接字的连接;通过sys_listen()方法在内核中实现。与数据报套接字无关。
  • accept() :接受套接字上的连接;通过sys_accept()方法在内核中实现。仅与基于连接的套接字类型相关(SOCK_STREAM、SOCK_SEQPACKET)。
  • connect() :建立到对等套接字的连接;通过sys_connect()方法在内核中实现。与基于连接的套接字类型(SOCK_STREAM 或 SOCK_SEQPACKET)以及无连接套接字类型(SOCK_DGRAM)相关。

本书重点介绍内核网络实现,所以我就不深究用户空间 socket API 的细节了。如果你想了解更多信息,我推荐以下书籍:

  • 由 W. Richard Stevens、Bill Fenner 和 Andrew m . Rudoff(Addison-Wesley Professional,2003 年)编写的《Unix 网络编程,第 1 卷:套接字网络 API(第 3 版)》。
  • 《Linux 编程接口》,作者 Michael Kerrisk(无淀粉出版社,2010 年)。

image 注意所有的 socket API 调用都由socketcall()方法处理,在net/socket.c中。

现在,您已经了解了一些套接字类型,您将了解创建套接字时内核中会发生什么。在下一节中,我将介绍实现套接字的两种结构:struct socketstruct sock。我还将描述它们之间的区别,我将描述msghdr struct及其成员。

创建套接字

内核中有两种结构代表一个套接字:第一种是struct socket ,它提供了一个到用户空间的接口,由sys_socket()方法创建。我将在本节稍后讨论sys_socket()方法。第二个是struct sock ,提供了到网络层(L3)的接口。因为sock结构驻留在网络层,所以它是一个协议不可知的结构。我将在本节稍后讨论sock结构。socket结构较短:

struct socket {
        socket_state            state;

        kmemcheck_bitfield_begin(type);
        short                   type;
        kmemcheck_bitfield_end(type);

        unsigned long           flags;

        . . .

        struct file             *file;
        struct sock             *sk;
        const struct proto_ops  *ops;
};

(我nclude/linux/net.h)

以下是对socket结构成员的描述:

  • state :套接字可以有几种状态,比如 SS_UNCONNECTED、SS_CONNECTED 等等。创建 INET 套接字时,其状态为 SS _ UNCONNECTED 见inet_create()法。流套接字成功连接到另一台主机后,其状态为 SS_CONNECTED。参见include/uapi/linux/net.h中的socket_state enum
  • type :套接字的类型,如 SOCK_STREAM 或 SOCK _ RAW 参见include/linux/net.h中的enum sock_type
  • flags :套接字标志;例如,SOCK_EXTERNALLY_ALLOCATED 标志是在分配套接字时在 TUN 设备中设置的,而不是由socket()系统调用设置的。参见drivers/net/tun.c中的tun_chr_open()方法。套接字标志在include/linux/net.h中定义。
  • file :与套接字关联的文件。
  • sk :与套接字关联的sock对象。sock对象代表网络层(L3)的接口。创建套接字时,会创建相关的sk对象。例如,在 IPv4 中,创建套接字时调用的inet_create()方法分配一个sock对象sk,并将其与指定的套接字对象相关联。
  • ops :这个对象(proto_ops对象的一个实例)主要由这个套接字的回调组成,比如connect()listen()sendmsg()recvmsg()等等。这些回调是用户空间的接口。sendmsg()回调实现了几个库级例程,比如write()send()sendto()sendmsg()。非常相似的是,recvmsg()回调实现了几个库级例程,比如read()recv()recvfrom()recvmsg()。每个协议根据协议要求定义一个自己的proto_ops对象。因此,对于 TCP,它的proto_ops对象包括一个listen回调、inet_listen()和一个accept回调、inet_accept()。另一方面,在客户机-服务器模型中不工作的 UDP 协议将listen()回调定义为sock_no_listen()方法,并将accept()回调定义为sock_no_accept()方法。这两种方法唯一做的事情是返回错误–EOPNOTSUPP。有关 TCP 和 UDP proto_ops对象的定义,请参见本章末尾“快速参考”部分的表 11-1 。proto_ops结构在include/linux/net.h中定义。

sock结构是套接字的网络层表示;它很长,以下是对我们的讨论很重要的一些字段:

struct sock {

        struct sk_buff_head     sk_receive_queue;
        int                     sk_rcvbuf;

        unsigned long           sk_flags;

        int                     sk_sndbuf;
        struct sk_buff_head     sk_write_queue;
        . . .
        unsigned int            sk_shutdown  : 2,
                                sk_no_check  : 2,
                                sk_protocol  : 8,
                                sk_type      : 16;
        . . .

        void                    (*sk_data_ready)(struct sock *sk, int bytes);
        void                    (*sk_write_space)(struct sock *sk);
};
(include/net/sock.h)

以下是对sock结构成员的描述:

  • sk_receive_queue:输入数据包的队列。
  • sk_rcvbuf:接收缓冲区的大小,以字节为单位。
  • sk_flags:各种旗帜,像 SOCK_DEAD 或者 SOCK _ DEAD 参见include/net/sock.h中的sock_flags enum定义。
  • sk_sndbuf:发送缓冲区的大小,以字节为单位。
  • sk_write_queue:输出数据包的队列。

image 注意稍后,在“TCP 套接字初始化”部分,您将看到sk_rcvbufsk_sndbuf是如何初始化的,以及如何通过写入procfs条目来改变。

  • sk_no_check:禁用校验和标志。可以用 SO_NO_CHECK 套接字选项设置。
  • sk_protocol:协议标识,根据socket()系统调用的第三个参数(protocol)设置。
  • sk_type:套接字的类型,如 SOCK_STREAM 或 SOCK _ RAW 参见include/linux/net.h中的enum sock_type
  • sk_data_ready:通知套接字新数据已经到达的回调。
  • sk_write_space:回调,表示有空闲内存可以进行数据传输。

创建套接字是通过从用户空间调用socket()系统调用来完成的:

sockfd = socket(int socket_family, int socket_type, int protocol);

下面是对socket()系统调用的参数描述:

  • socket_family:例如,可以是 IPv4 的 AF_INET、IPv6 的 AF_INET6 或 UNIX 域套接字的 AF_UNIX 等。(UNIX 域套接字是进程间通信(IPC)的一种形式,它允许在同一主机上运行的进程之间进行通信。)

  • socket_type:例如,可以是流套接字的 SOCK_STREAM、数据报套接字的 SOCK_DGRAM 或原始套接字的 SOCK_RAW 等等。

  • protocol:可以是以下任意一种:

  • 对于 TCP 套接字,为 0 或 IPPROTO_TCP。

  • 0 或 IPPROTO_UDP 用于 UDP 套接字。

  • 原始套接字的有效 IP 协议标识符(如 IPPROTO_TCP 或 IP proto _ ICMP);参见 RFC 1700,“分配的号码”

socket()系统调用(sockfd)的返回值是文件描述符,它应该作为参数传递给这个套接字的后续调用。socket()系统调用在内核中由sys_socket()方法处理。让我们来看看socket()系统调用的实现:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
        int retval;
        struct socket *sock;
        int flags;

        . . .
        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
                goto out;
        . . .
        retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
        if (retval < 0)
                goto out_release;
out:
        . . .
        return retval;

}
(net/socket.c)

sock_create()方法调用特定于地址族的套接字创建方法create();在 IPv4 的情况下,它是inet_create()方法。(参见net/ipv4/af_inet.c中的inet_family_ops定义。)方法inet_create()创建与套接字关联的sock对象(sk);sock对象代表网络层套接字接口。sock_map_fd()方法返回一个与套接字关联的fd(文件描述符);通常情况下,socket()系统调用会返回这个fd

从用户空间套接字发送数据,或者在用户空间套接字中从传输层接收数据,在内核中分别由sendmsg()recvmsg()方法处理,这两个方法获得一个msghdr对象作为参数。msghdr对象包括要发送或填充的数据块,以及一些其他参数。

struct msghdr {
        void             *msg_name;       /* Socket name                                         */
        int              msg_namelen;     /* Length of name                                      */
        struct iovec     *msg_iov;        /* Data blocks                                         */
        __kernel_size_t  msg_iovlen;      /* Number of blocks                                    */
        void             *msg_control;    /* Per protocol magic (eg BSD file descriptor passing) */
        __kernel_size_t  msg_controllen;  /* Length of cmsg list                                 */
        unsigned int     msg_flags;
};
(include/linux/socket.h)

以下是对msghdr结构 中一些重要成员的描述:

  • msg_name:目的套接字地址。为了得到目标套接字,通常将msg_name不透明指针转换为struct sockaddr_in指针。例如,参见udp_sendmsg()方法。
  • msg_namelen:地址的长度。
  • iovec:数据块的向量。
  • msg_iovlen:矢量iovec中的块数。
  • msg_control:控制信息(又称辅助数据)。
  • msg_controllen:控制信息的长度。
  • msg_flags:收到消息的标志,如 MSG_MORE。(例如,请参阅本章后面的“使用 UDP 发送数据包”一节。)

注意,内核可以处理的最大控制缓冲区长度受sysctl_optmem_max ( /proc/sys/net/core/optmem_max)中值的限制。

在本节中,我描述了在发送和接收数据包时使用的socketmsghdr struct的内核实现。在下一节中,我将从描述 UDP 协议开始讨论传输层协议(L4 ), UDP 协议是本章要讨论的协议中最简单的一种。

用户数据报协议

UDP 协议在 1980 年的 RFC 768 中被描述。UDP 协议是 IP 层周围的一个薄层,仅添加端口、长度和校验和信息。它可以追溯到 1980 年,提供不可靠的、面向消息的传输,没有拥塞控制。许多协议都使用 UDP。例如,我将提到 RTP 协议(实时传输协议,它用于通过 IP 网络传输音频和视频。这种类型的流量可以容忍一些数据包丢失。RTP 通常用于 VoIP 应用中,通常与基于 SIP(会话发起协议)的客户端结合使用。(这里需要提到的是,其实 RTP 协议也可以使用 TCP,RFC 4571 中有规定,但是这个用的不多。)这里我应该提一下 UDP-Lite,它是 UDP 协议的扩展,支持可变长度校验和(RFC 3828)。大多数 UDP-Lite 是在net/ipv4/udplite.c中实现的,但是您也会在主 UDP 模块net/ipv4/udp.c中遇到它。UDP 报头长度为 8 个字节:

struct udphdr {
        __be16  source;
        __be16  dest;
        __be16  len;
        __sum16 check;
};
(include/uapi/linux/udp.h)

以下是对 UDP 报头成员的描述:

  • source:源端口(16 位),范围 1-65535。
  • dest:目的端口(16 位),范围 1-65535。
  • len:字节长度(有效载荷长度和 UDP 头长度)。
  • checksum:数据包的校验和。

图 11-1 显示了一个 UDP 头。

9781430261964_Fig11-01.jpg

图 11-1 。UDP 报头(IPv4)

在本节中,您了解了 UDP 头及其成员。为了理解使用 sockets API 的用户空间应用如何与内核通信(发送和接收数据包),您应该知道 UDP 初始化是如何完成的,这将在下一节中描述。

UDP 初始化

我们定义了udp_protocol对象(net_protocol对象)并用inet_add_protocol()方法添加它。这将udp_protocol对象设置为全局协议数组(inet_protos)中的一个元素。

static const struct net_protocol udp_protocol = {
        .handler =      udp_rcv,
        .err_handler =  udp_err,
        .no_policy =    1,
        .netns_ok =     1,
};
(net/ipv4/af_inet.c)

static int __init inet_init(void)
{
        . . .
        if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
                pr_crit("%s: Cannot add UDP protocol\n", __func__);
        . . .
}
(net/ipv4/af_inet.c)

我们进一步定义了一个udp_prot对象,并通过调用proto_register()方法注册它。该对象主要包含回调;当在用户空间中打开 UDP 套接字并使用套接字 API 时,会调用这些回调。例如,在 UDP 套接字上调用setsockopt()系统调用将调用udp_setsockopt()回调。

struct proto udp_prot = {
         .name              = "UDP",
         .owner             = THIS_MODULE,
         .close             = udp_lib_close,
         .connect           = ip4_datagram_connect,
         .disconnect        = udp_disconnect,
         .ioctl             = udp_ioctl,
         . . .
         .setsockopt        = udp_setsockopt,
         .getsockopt        = udp_getsockopt,
         .sendmsg           = udp_sendmsg,
         .recvmsg           = udp_recvmsg,
         .sendpage          = udp_sendpage,
         . . .
};

(net/ipv4/udp.c)
int __init inet_init(void)
{
    int rc = -EINVAL;
    . . .
    rc = proto_register(&udp_prot, 1);
    . . .

}
(net/ipv4/af_inet.c)

image 注意UDP 协议和其他核心协议在启动时通过inet_init()方法初始化。

既然您已经了解了 UDP 初始化及其用于发送数据包的回调,也就是本节中显示的udp_prot对象的udp_sendmsg()回调,那么是时候了解 UDP 如何在 IPV4 中发送数据包了。

使用 UDP 发送数据包

从 UDP 用户空间套接字发送数据可以通过几个系统调用来完成:send()sendto()sendmsg()write();最终它们都由内核中的udp_sendmsg()方法处理。用户空间应用构建一个包含数据块的msghdr对象,并将这个msghdr对象传递给内核。让我们来看看这个方法:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                size_t len)
{

通常,UDP 数据包会立即发送。这种行为可以通过 UDP_CORK socket 选项(在内核 2.5.44 中引入)来改变,它会导致传递给udp_sendmsg()方法的数据包数据不断累积,直到通过取消设置该选项来释放最后一个数据包。通过设置 MSG_MORE 标志可以获得相同的结果:

        int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
        struct inet_sock *inet = inet_sk(sk);
              . . .

首先,我们做一些理智检查。例如,指定的len不能大于 65535(记住 UDP 头中的len字段是 16 位):

        if (len > 0xFFFF)
                 return -EMSGSIZE;

我们需要知道目的地地址和目的地端口,以便构建一个flowi4对象,这是用udp_send_skb()方法或ip_append_data()方法发送 SKB 所需要的。目标端口不应为 0。这里有两种情况:目的地在msghdrmsg_name中被指定,或者套接字被连接,其状态为 TCP_ESTABLISHED。请注意,UDP(与 TCP 相反)几乎是一种完全无状态的协议。UDP 中 TCP_ESTABLISHED 的概念主要意味着套接字已经通过了一些健全性检查。

        if (msg->msg_name) {
                struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
                if (msg->msg_namelen < sizeof(*usin))
                        return -EINVAL;
                if (usin->sin_family != AF_INET) {
                        if (usin->sin_family != AF_UNSPEC)
                                return -EAFNOSUPPORT;
                }

                daddr = usin->sin_addr.s_addr;
                dport = usin->sin_port;

Linux 代码承认 IANA 没有保留任何 UDP/TCP 端口。TCP 和 UDP 中端口 0 的保留可以追溯到 RFC 1010,“分配的号码”(1987),并且它仍然存在于 RFC 1700 中,该 RFC 1700 已被在线数据库淘汰(参见 RFC 3232),它们仍然存在于 RFC 1700 中。参见www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml

                if (dport == 0)
                        return -EINVAL;
        } else {
                if (sk->sk_state != TCP_ESTABLISHED)
                        return -EDESTADDRREQ;
                daddr = inet->inet_daddr;
                dport = inet->inet_dport;
                /* Open fast path for connected socket.
                   Route will not be used, if at least one option is set.
                 */
                connected = 1;
}

               . . .

用户空间应用可以通过在msghdr对象中设置msg_controlmsg_controllen来发送控制信息(也称为辅助数据)。辅助数据实际上是一系列带有附加数据的cmsghdr对象。(更多细节见man 3 cmsg。)您可以通过分别调用sendmsg()recvmsg()方法来发送和接收辅助数据。例如,您可以创建 IP_PKTINFO 辅助消息来设置到未连接的 UDP 套接字的源路由。(参见man 7 ip。)当msg_controllen不为 0 时,这是一个控制信息消息,由ip_cmsg_send()方法处理。ip_cmsg_send()方法通过解析指定的msghdr对象构建一个ipcm_cookie (IP 控制消息 Cookie)对象。ipcm_cookie结构包括在处理数据包时进一步使用的信息。例如,当使用 IP_PKTINFO 辅助消息时,您可以通过设置控制消息中的地址字段来设置源地址,这最终会设置ipcm_cookie对象中的addripcm_cookie是一个短结构:

struct ipcm_cookie {
        __be32                  addr;
        int                     oif;
        struct ip_options_rcu   *opt;
        __u8                    tx_flags;
};
(include/net/ip.h)

让我们继续讨论 udp_sendmsg()方法:

         if (msg->msg_controllen) {
                 err = ip_cmsg_send(sock_net(sk), msg, &ipc);
                 if (err)
                         return err;
                 if (ipc.opt)
                         free = 1;
                 connected = 0;
         }
         . . .
         if (connected)
                 rt = (struct rtable *)sk_dst_check(sk, 0);
         . . .

如果路由条目为空,则应执行路由查找:

        if (rt == NULL) {
                struct net *net = sock_net(sk);
                fl4 = &fl4_stack;
                flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos,
                                   RT_SCOPE_UNIVERSE, sk->sk_protocol,
                                   inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP,
                                   faddr, saddr, dport, inet->inet_sport);

                security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
                rt = ip_route_output_flow(net, fl4, sk);
                if (IS_ERR(rt)) {
                        err = PTR_ERR(rt);
                        rt = NULL;
                        if (err == -ENETUNREACH)
                                IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);
                        goto out;
                }

        . . .

在内核 2.6.39 中,增加了无锁传输快速路径。这意味着当 corking 特性未设置时,我们不持有套接字锁,而是调用udp_send_skb()方法,当 corking 特性设置时,我们通过调用lock_sock()方法持有套接字锁,然后发送数据包:

        /* Lockless fast path for the non-corking case. */
if (!corkreq) {
                skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
                                  sizeof(struct udphdr), &ipc, &rt,
                                  msg->msg_flags);
                err = PTR_ERR(skb);
                if (!IS_ERR_OR_NULL(skb))
                         err = udp_send_skb(skb, fl4);
                 goto out;
        }

现在我们处理设置了软木塞特征的情况:

       lock_sock(sk);
do_append_data:
        up->len += ulen;

ip_append_data()方法缓冲要传输的数据,但不传输它。随后调用udp_push_pending_frames()方法将实际执行传输。注意,udp_push_pending_frames()方法也通过指定的getfrag回调来处理碎片:

        err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,
                             sizeof(struct udphdr), &ipc, &rt,
                             corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

如果该方法失败,我们应该刷新所有挂起的 skb。这是通过调用udp_flush_pending_frames()方法实现的,该方法将通过ip_flush_pending_frames()方法释放套接字(sk_write_queue)的写队列中的所有 skb:

        if (err)
                udp_flush_pending_frames(sk);
        else if (!corkreq)
                err = udp_push_pending_frames(sk);
        else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
                up->pending = 0;
        release_sock(sk);

在本节中,您了解了如何使用 UDP 发送数据包。现在,为了完成我们对 IPv4 中 UDP 的讨论,我们应该了解一下 IPv4 中的 UDP 是如何接收来自网络层(L3)的数据包的。

使用 UDP 从网络层(L3)接收数据包

从网络层(L3)接收 UDP 包的主要处理程序是udp_rcv()方法。它只是调用了__udp4_lib_rcv()方法(net/ipv4/udp.c):

int udp_rcv(struct sk_buff *skb)
{
        return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

我们来看看__udp4_lib_rcv()方法:

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
                   int proto)
{
        struct sock *sk;
        struct udphdr *uh;
        unsigned short ulen;
        struct rtable *rt = skb_rtable(skb);
        __be32 saddr, daddr;
        struct net *net = dev_net(skb->dev);
        . . .

我们从 SKB 获取 UDP 报头、报头长度以及源地址和目的地址:

        uh   = udp_hdr(skb);
        ulen = ntohs(uh->len);
        saddr = ip_hdr(skb)->saddr;
        daddr = ip_hdr(skb)->daddr;

我们将跳过一些正在执行的健全性检查,比如确保 UDP 报头长度不大于数据包的长度,以及指定的proto是 UDP 协议标识符(IPPROTO_UDP)。如果数据包是广播或组播数据包,将通过__udp4_lib_mcast_deliver()方法进行处理:

        if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
             return __udp4_lib_mcast_deliver(net, skb, uh,
                                                saddr, daddr, udptable);

接下来,我们在 UDP 套接字哈希表中执行查找:

        sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
              if (sk != NULL) {

我们到达这里是因为我们执行的查找找到了一个匹配的套接字。因此,通过调用udp_queue_rcv_skb()方法进一步处理 SKB,该方法调用通用的sock_queue_rcv_skb()方法,该方法又将指定的 SKB 添加到sk->sk_receive_queue的尾部(通过调用__skb_queue_tail()方法):

        int ret = udp_queue_rcv_skb(sk, skb);
        sock_put(sk);

        /* a return value > 0 means to resubmit the input, but
        * it wants the return to be -protocol, or 0
        */
        if (ret > 0)
             return -ret;

一切都好;返回 0 表示成功:

            return 0;
        }
        . . .

我们来到这里是因为查找套接字失败了。这意味着我们不应该处理数据包。例如,当目的端口上没有监听 UDP 套接字时,就会发生这种情况。如果校验和不正确,我们应该悄悄地丢弃数据包。如果它是正确的,我们应该发送一个 ICMP 回复给发送者。这应该是一个 ICMP 消息“目的地不可达”,代码为“端口不可达”接下来,我们应该释放数据包并更新 SNMP MIB 计数器:

        /* No socket. Drop packet silently, if checksum is wrong */
        if (udp_lib_checksum_complete(skb))
            goto csum_error;

下一个命令递增 UDP_MIB_NOPORTS ( NoPorts ) MIB 计数器。请注意,您可以通过cat /proc/net/snmpnetstat –s查询各种 UDP MIB 计数器。

UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

        /*
        * Hmm.  We got an UDP packet to a port to which we
        * don't wanna listen.  Ignore it.
        */
        kfree_skb(skb);
        return 0;

图 11-2 展示了我们在本节中关于接收 UDP 数据包的讨论。

9781430261964_Fig11-02.jpg

图 11-2 。接收 UDP 数据包

我们关于 UDP 的讨论到此结束。下一节描述 TCP 协议,它是本章讨论的协议中最复杂的。

TCP (传输控制协议)

TCP 协议在 1981 年的 RFC 793 中被描述为 。从那以后的几年里,基本的 TCP 协议有了许多更新、变化和补充。一些增加是针对特定类型的网络(高速、卫星),而另一些是为了提高性能。

TCP 协议是当今互联网上最常用的传输协议。许多众所周知的协议都是基于 TCP 的。最广为人知的协议大概就是 HTTP 了,这里还要提一下其他一些广为人知的协议比如ftpsshtelnetsmtpssl。与 UDP 相反,TCP 协议提供可靠的面向连接的传输。通过使用序列号和确认,传输变得可靠。

TCP 是一个非常复杂的协议;我们不会在本章中讨论 TCP 实现的所有细节、优化和细微差别,因为这本身就需要一本单独的书。TCP 功能由两部分组成:连接管理和数据传输与接收。在这一节中,我们将重点介绍 TCP 初始化和 TCP 连接设置,这属于第一个要素,即连接管理,以及接收和发送数据包,这属于第二个要素。这些是进一步深入研究 TCP 协议实现的重要基础。我们应该注意到,TCP 协议通过拥塞控制来自我调节字节流。已经规定了许多不同的拥塞控制算法,Linux 提供了一个可插入和可配置的架构来支持各种各样的算法。深入研究单个拥塞控制算法的细节超出了本书的范围。

每个 TCP 数据包都以 TCP 报头开始。为了理解 TCP 的操作,您必须了解 TCP 报头。下一节描述 IPv4 TCP 报头。

TCP 报头

TCP 报头长度为 20 字节,但在使用 TCP 选项时可扩展至 60 字节:

struct tcphdr {
        __be16  source;
        __be16  dest;
        __be32  seq;
        __be32  ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u16   res1:4,
                doff:4,
                fin:1,
                syn:1,
                rst:1,
                psh:1,
                ack:1,
                urg:1,
                ece:1,
                cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u16   doff:4,
                res1:4,
                cwr:1,
                ece:1,
                urg:1,
                ack:1,
                psh:1,
                rst:1,
                syn:1,
                fin:1;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif
        __be16  window;
        __sum16 check;
        __be16  urg_ptr;
};
(include/uapi/linux/tcp.h)

以下是对tcphdr结构成员的描述:

  • source:源端口(16 位),范围 1-65535。
  • dest:目的端口(16 位),范围 1-65535。
  • seq:序列号(32 位)。
  • ack_seq:确认号(32 位)。如果 ACK 标志被置位,该域的值就是接收方所期望的下一个序列号。
  • res1:留作将来使用(4 位)。它应该总是设置为 0。
  • doff:数据偏移(4 位)。TCP 报头的大小乘以 4 个字节;最小值为 5 (20 字节),最大值为 15 (60 字节)。

以下是 TCP 标志;每个都是 1 位:

  • fin:没有来自发送方的更多数据(当一个端点想要关闭连接时)。
  • syn:SYN 标志最初是在两个端点之间建立 3 次握手时发送的。
  • rst:当不是用于当前连接的段到达时,使用复位标志。
  • psh:数据应该尽快传递到用户空间。
  • ack:表示 TCP 报头中的确认号(ack_seq)值是有意义的。
  • urg:表示紧急指针有意义。
  • ece : ECN - Echo 标志。 ECN 代表“显式拥塞通知”ECN 提供了一种机制,可以在不丢弃数据包的情况下发送有关网络拥塞的端到端通知。它是由 RFC 3168 于 2001 年提出的“对 IP 的显式拥塞通知(ECN)的添加”。
  • cwr:拥塞窗口减少标志。
  • window : TCP 接收窗口大小,以字节为单位(16 位)。
  • check:TCP 报头和 TCP 数据的校验和。
  • urg_ptr:仅当urg标志被置位时才有意义。它表示相对于序列号的偏移量,表示最后一个紧急数据字节(16 位)。

图 11-3 显示了一个 TCP 报头的示意图。

9781430261964_Fig11-03.jpg

图 11-3 。TCP 报头(IPv4)

在本节中,我描述了 IPv4 TCP 报头及其成员。您可以看到,与只有 4 个成员的 UDP 报头相比,TCP 报头有更多的成员,因为 TCP 是一种复杂得多的协议。在下一节中,我将描述 TCP 初始化是如何完成的,这样您将了解接收和发送 TCP 包的回调的初始化是如何发生的以及在哪里发生的。

TCP 初始化

我们定义了tcp_protocol对象(net_protocol对象)并用inet_add_protocol()方法添加它:

static const struct net_protocol tcp_protocol = {
        .early_demux    =       tcp_v4_early_demux,
        .handler        =       tcp_v4_rcv,
        .err_handler    =       tcp_v4_err,
        .no_policy      =       1,
        .netns_ok       =       1,
};
(net/ipv4/af_inet.c)

static int __init inet_init(void)
  {
        . . .
        if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
            pr_crit("%s: Cannot add TCP protocol\n", __func__);
        . . .
}
(net/ipv4/af_inet.c)

我们进一步定义了一个tcp_prot对象,并通过调用proto_register()方法来注册它,就像我们对 UDP 所做的那样:

struct proto tcp_prot = {
        .name                   = "TCP",
        .owner                  = THIS_MODULE,
        .close                  = tcp_close,
        .connect                = tcp_v4_connect,
        .disconnect             = tcp_disconnect,
        .accept                 = inet_csk_accept,
        .ioctl                  = tcp_ioctl,
        .init                   = tcp_v4_init_sock,
        . . .
};

(net/ipv4/tcp_ipv4.c)

static int __init inet_init(void)
{
        int rc;
        . . .
        rc = proto_register(&tcp_prot, 1);
        . . .
}
(net/ipv4/af_inet.c)

注意,在tcp_prot定义中,init函数指针被定义为tcp_v4_init_sock()回调,它执行各种初始化,比如通过调用tcp_init_xmit_timers()方法设置定时器、设置套接字状态等等。相反,在简单得多的协议 UDP 中,根本没有定义init函数指针,因为在 UDP 中没有要执行的特殊初始化。我们将在本节稍后讨论tcp_v4_init_sock()回调。

在下一节中,我将简要描述 TCP 协议使用的定时器。

TCP 定时器

TCP 定时器在net/ipv4/tcp_timer.c中处理。TCP 使用四种计时器:

  • 重发定时器 : 负责重新发送在指定时间间隔内未被确认的数据包。当数据包丢失或损坏时,可能会发生这种情况。该定时器在每个数据段发送后启动;如果 ACK 在定时器到期之前到达,则定时器被取消。
  • 延迟 ACK 定时器 : 延迟发送 ACK 包。当 TCP 接收到必须确认但不需要立即确认的数据时,该位置位。
  • 保活定时器 : 检查连接是否断开。有这样的情况,会话长时间闲置,一方宕机。保持活动计时器检测到这种情况,并调用tcp_send_active_reset()方法来重置连接。
  • 零窗口探测定时器 (也称为 持久定时器 ): 当接收缓冲区满时,接收方通告零窗口,发送方停止发送。现在,当接收方发送一个具有新窗口大小的数据段,而该数据段丢失时,发送方将永远等待下去。解决方案是这样的:当发送方得到一个零窗口时,它使用一个持久定时器来探测接收方的窗口大小;当获得非零窗口大小时,持久定时器停止。

TCP 套接字初始化

要使用 TCP 套接字,用户空间应用应该创建一个 SOCK_STREAM 套接字,并调用socket()系统调用。这是在内核中由tcp_v4_init_sock()回调处理的,回调调用tcp_init_sock()方法来完成真正的工作。注意,tcp_init_sock()方法执行独立于地址族的初始化,它也是从tcp_v6_init_sock()方法调用的。tcp_init_sock()方法的重要任务如下:

  • 将套接字的状态设置为 TCP_CLOSE。
  • 通过调用tcp_init_xmit_timers()方法初始化 TCP 定时器。
  • 初始化 socket 发送缓冲区(sk_sndbuf)和接收缓冲区(sk_rcvbuf);sk_sndbuf设置为sysctl_tcp_wmem[1],默认为 16384 字节,sk_rcvbuf设置为sysctl_tcp_rmem[1],默认为 87380 字节。这些默认值在tcp_init()方法中设置;通过分别写入/proc/sys/net/ipv4/tcp_wmem/proc/sys/net/ipv4/tcp_rmem,可以覆盖sysctl_tcp_wmemsysctl_tcp_rmem数组的默认值。参见Documentation/networking/ip-sysctl.txt中的“TCP 变量”部分。
  • 初始化无序队列和prequeue
  • 初始化各种参数。例如,根据 2013 年的 RFC 6928“增加 TCP 的初始窗口”,TCP 初始拥塞窗口被初始化为 10 个段(TCP_INIT_CWND)。

现在您已经了解了 TCP 套接字是如何初始化的,我将讨论如何建立 TCP 连接。

TCP 连接设置

TCP 连接建立和拆除以及 TCP 连接属性被描述为状态机中的转换。在每个给定的时刻,TCP 套接字可以处于一个指定的状态;例如,当调用listen()系统调用时,套接字进入 TCP_LISTEN 状态。对象的状态由它的成员sk_state来表示。有关所有可用状态的列表,请参考include/net/tcp_states.h

三次握手用于在 TCP 客户端和 TCP 服务器之间建立 TCP 连接:

  • 首先,客户端向服务器发送一个 SYN 请求。其状态更改为 TCP_SYN_SENT。
  • 正在监听的服务器套接字(其状态为 TCP_LISTEN)创建一个请求套接字来表示 TCP_SYN_RECV 状态下的新连接,并发回一个 SYN ACK。
  • 接收 SYN ACK 的客户端将其状态更改为 TCP_ESTABLISHED,并向服务器发送 ACK。
  • 服务器接收到 ACK 并将请求套接字更改为 TCP_ESTABLISHED 状态的子套接字,因为现在连接已经建立,可以发送数据了。

image 注意要进一步了解 TCP 状态机的细节,请参考tcp_rcv_state_process()方法(net/ipv4/tcp_input.c),它是状态机引擎,适用于 IPv4 和 IPv6。(它由tcp_v4_do_rcv()方法和tcp_v6_do_rcv()方法调用。)

下一节将描述如何使用 IPv4 中的 TCP 从网络层(L3)接收数据包。

使用 TCP 从网络层(L3)接收 数据包

从网络层(L3)接收 TCP 包的主要处理程序是tcp_v4_rcv()方法(net/ipv4/tcp_ipv4.c)。让我们来看看这个函数:

int tcp_v4_rcv(struct sk_buff *skb)
{
       struct sock *sk;
       . . .

首先,我们进行一些健全性检查(例如,检查数据包类型是否不是 PACKET_HOST,或者数据包大小是否比 TCP 报头短),如果有任何问题,就丢弃数据包;然后进行一些初始化,并通过调用__inet_lookup_skb()方法执行相应套接字的查找,首先通过调用__inet_lookup_established()方法在已建立的套接字散列表中执行查找。在查找失败的情况下,它通过调用__inet_lookup_listener()方法在监听套接字哈希表中执行查找。如果没有找到套接字,则在此阶段丢弃该数据包。

        sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
        . . .
        if (!sk)
                goto no_tcp_socket;

现在我们检查套接字是否属于某个应用。当当前有一个应用拥有该套接字时,sock_owned_by_user()宏返回 1,当没有应用拥有该套接字时,它返回值 0:

        if (!sock_owned_by_user(sk)) {
        . . .
                {

如果没有应用拥有套接字,我们就到达这里,因此它可以接受数据包。首先,我们试图通过调用tcp_prequeue()方法将数据包放入prequeue中,因为在prequeue中处理数据包会更有效。如果prequeue中的处理不可行(例如,当队列没有空间时),则tcp_prequeue()将返回false;在这种情况下,我们将调用tcp_v4_do_rcv()方法,稍后我们将对此进行讨论:

                if (!tcp_prequeue(sk, skb))
                        ret = tcp_v4_do_rcv(sk, skb);
        }

当应用拥有套接字时,这意味着它处于锁定状态,因此它不能接受数据包。在这种情况下,我们通过调用sk_add_backlog()方法将包添加到 backlog 中:

                } else if (unlikely(sk_add_backlog(sk, skb,
                                                   sk->sk_rcvbuf + sk->sk_sndbuf))) {
                        bh_unlock_sock(sk);
                        NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
                        goto discard_and_relse;
                }
        }

我们来看看tcp_v4_do_rcv()方法:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{

如果套接字处于 TCP_ESTABLISHED 状态,我们调用tcp_rcv_established()方法:

        if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
        . . .
                if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
                        rsk = sk;
                        goto reset;
                }
                return 0;

如果套接字处于 TCP_LISTEN 状态,我们调用tcp_v4_hnd_req()方法:

        if (sk->sk_state == TCP_LISTEN) {
                struct sock *nsk = tcp_v4_hnd_req(sk, skb);

        }

如果我们不在 TCP_LISTEN 状态,我们调用tcp_rcv_state_process()方法:

        if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
                rsk = sk;
                goto reset;
        }
        return 0;

reset:
        tcp_v4_send_reset(rsk, skb);

}

在本节中,您了解了 TCP 数据包的接收。在下一节中,我们将通过描述在 IPv4 中如何使用 TCP 发送数据包来结束本章的 TCP 部分。

使用 TCP 发送个数据包

与 UDP 一样,从用户空间中创建的 TCP 套接字发送数据包可以通过几个系统调用来完成:send()sendto()sendmsg()write()。最终它们都被tcp_sendmsg()方法(net/ipv4/tcp.c)处理。这个方法将有效负载从用户空间复制到内核,并作为 TCP 段发送。它比udp_sendmsg()方法复杂得多。

int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                size_t size)
{
        struct iovec *iov;
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *skb;
        int iovlen, flags, err, copied = 0;
        int mss_now = 0, size_goal, copied_syn = 0, offset = 0;
        bool sg;
        long timeo;
        . . .

我不会深入研究用这种方法将数据从用户空间复制到 SKB 的所有细节。一旦构建了 SKB,它就用调用tcp_write_xmit()方法的tcp_push_one()方法发送,而tcp_write_xmit()方法又调用tcp_transmit_skb()方法:

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
                            gfp_t gfp_mask)
{

icsk_af_ops对象(INET 连接套接字 ops)是一个特定于地址族的对象。在 IPv4 TCP 的情况下,它在tcp_v4_init_sock()方法中被设置为一个名为ipv4_specificinet_connection_sock_af_ops对象。queue_xmit()回调被设置为通用的ip_queue_xmit()方法。参见net/ipv4/tcp_ipv4.c

    . . .
    err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
    . . .
}
(net/ipv4/tcp_output.c)

现在,您已经了解了 TCP 和 UDP,可以开始下一节了,这一节将讨论 SCTP(流控制传输协议)协议。SCTP 协议结合了 UDP 和 TCP 的特性,比它们都新。

SCTP (流控制传输协议)

SCTP 协议是在 2007 年的 RFC 4960 中指定的。它在 2000 年首次被指定。它是为 IP 网络上的公共交换电话网(PSTN)信令而设计的,但也可用于其他应用。IETF SIGTRAN(信令传输)工作组最初开发了 SCTP 协议,后来将该协议移交给传输区域工作组(TSVWG ),以使 SCTP 继续发展为通用传输协议。LTE(长期演进)使用 SCTP;其中一个主要原因是 SCTP 协议能够检测到链路何时断开或数据包何时被快速丢弃,而 TCP 不具备这一功能。TCP 和 SCTP 中的 SCTP 流量控制和拥塞控制算法非常相似。SCTP 协议对通告的接收器窗口大小使用变量(a_rwnd);该变量表示接收器缓冲区中的当前可用空间。如果接收方指示a_rwnd为 0(没有可用的接收空间),则发送方无法发送任何新数据。SCTP 的重要特征如下:

  • SCTP 结合了 TCP 和 UDP 的功能。它是一种像 TCP 一样具有拥塞控制的可靠传输协议;它像 UDP 一样是面向消息的协议,而 TCP 是面向流的。
  • SCTP 协议通过其 4 次握手(与 TCP 3 次握手相比)提供了改进的安全性,以防止 SYN 泛洪攻击。我将在本章后面的“建立 SCTP 协会”一节中讨论 4 次握手。
  • SCTP 支持多宿主,即两个端点上都有多个 IP 地址。这提供了网络级的容错能力。我将在本节稍后讨论 SCTP 组块。
  • SCTP 支持多流,这意味着它可以并行发送数据块流。在某些环境中,这可以减少流式多媒体的延迟。我将在本节稍后讨论 SCTP 组块。
  • 在多宿主的情况下,SCTP 使用心跳机制来检测空闲/不可到达的对等体。我将在本章后面讨论 SCTP 心跳机制。

在简短描述了 SCTP 协议之后,我们现在将讨论 SCTP 初始化是如何完成的。sctp_init()方法为各种结构分配内存,初始化一些sysctl变量,在 IPv4 和 IPv6 中注册 SCTP 协议:

int sctp_init(void)
{
       int status = -EINVAL;
        . . .
        status = sctp_v4_add_protocol();

       if (status)
               goto err_add_protocol;

       /* Register SCTP with inet6 layer.  */
       status = sctp_v6_add_protocol();
       if (status)
               goto err_v6_add_protocol;
       . . .
}
(net/sctp/protocol.c)

SCTP 协议的注册是通过定义net_protocol的实例(IPv4 的实例名为sctp_protocol,IPv6 的实例名为sctpv6_protocol并调用inet_add_protocol()方法来完成的,这与您在 UDP 协议等其他传输协议中看到的非常相似。我们还调用register_inetaddr_notifier()来接收关于添加或删除网络地址的通知。这些事件将由sctp_inetaddr_event()方法处理,该方法将相应地更新 SCTP 全球地址列表(sctp_local_addr_list)。

static const struct net_protocol sctp_protocol = {
        .handler     = sctp_rcv,
        .err_handler = sctp_v4_err,
        .no_policy   = 1,
};
(net/sctp/protocol.c)

static int sctp_v4_add_protocol(void)
{
        /* Register notifier for inet address additions/deletions. */
        register_inetaddr_notifier(&sctp_inetaddr_notifier);

        /* Register SCTP with inet layer.  */
        if (inet_add_protocol(&sctp_protocol, IPPROTO_SCTP) < 0)
                return -EAGAIN;

        return 0;
}
(net/sctp/protocol.c)

image sctp_v6_add_protocol()的方法(net/sctp/ipv6.c)很相似,这里就不赘述了。

每个 SCTP 数据包都以 SCTP 报头开始。我现在将描述 SCTP 报头的结构。在下一节中,我将与 SCTP·肯斯展开讨论。

SCTP 包和块

每个 SCTP 数据包都有一个 SCTP 公共报头,其后是一个或多个数据块。每个块可以包含数据或 SCTP 控制信息。几个数据块可以打包成一个 SCTP 数据包(建立和终止连接时使用的三个数据块除外:INIT、INIT_ACK 和 SHUTDOWN_COMPLETE)。这些块使用类型-长度-值(TLV)格式,您在第二章中第一次遇到这种格式。

SCTP 普通表头

typedef struct sctphdr {
        __be16 source;
        __be16 dest;
        __be32 vtag;
        __le32 checksum;
} __attribute__((packed)) sctp_sctphdr_t;
(include/linux/sctp.h)

以下是对sctphdr结构成员的描述:

  • sourceSCTP 来源连接埠。
  • dest : SCTP destination port。
  • vtag:验证标签,32 位随机值。
  • checksum:SCTP 公共头和所有块的校验和。

SCTP 块头

SCTP 块头由struct sctp_chunkhdr表示:

typedef struct sctp_chunkhdr {
        __u8 type;
        __u8 flags;
        __be16 length;
} __packed sctp_chunkhdr_t;
(include/linux/sctp.h)

以下是对sctp_chunkhdr结构成员的描述:

  • type:SCTP 型。例如,数据块的类型是 SCTP_CID_DATA。参见本章末尾“快速参考”部分的表 11-2 ,块类型,也可参见include/linux/sctp.h中的块 ID enum定义(sctp_cid_t)。
  • flags:通常情况下,发送方应将其中的 8 位全部置 0,接收方忽略。存在使用不同值的情况。例如,在 ABORT chunk 中,我们这样使用 T 位(LSB ):如果发送者填写了验证标签,则它被设置为 0,如果验证标签被反映,则它被设置为 1。
  • SCTP 块的长度。

SCTPchunk

SCTP 区块由struct sctp_chunk表示。每个块对象包含这个块的源地址和目的地址,以及一个根据其类型的子头(subh union 的成员)。例如,对于数据包,我们有sctp_datahdr子报头,对于 INIT 类型,我们有sctp_inithdr子类型:

struct sctp_chunk {
        . . .
        atomic_t refcnt;

        union {
                __u8 *v;
                struct sctp_datahdr        *data_hdr;
                struct sctp_inithdr        *init_hdr;
                struct sctp_sackhdr        *sack_hdr;
                struct sctp_heartbeathdr   *hb_hdr;
                struct sctp_sender_hb_info *hbs_hdr;
                struct sctp_shutdownhdr    *shutdown_hdr;
                struct sctp_signed_cookie  *cookie_hdr;
                struct sctp_ecnehdr        *ecne_hdr;
                struct sctp_cwrhdr         *ecn_cwr_hdr;
                struct sctp_errhdr         *err_hdr;
                struct sctp_addiphdr       *addip_hdr;
                struct sctp_fwdtsn_hdr     *fwdtsn_hdr;
                struct sctp_authhdr        *auth_hdr;
        } subh;

        struct sctp_chunkhdr    *chunk_hdr;
        struct sctphdr          *sctp_hdr;

        struct sctp_association *asoc;

        /* What endpoint received this chunk? */
        struct sctp_ep_common   *rcvr;

        . . .

        /* What is the origin IP address for this chunk?  */
        union sctp_addr source;
        /* Destination address for this chunk. */
        union sctp_addr dest;

        . . .

        /* For an inbound chunk, this tells us where it came from.
         * For an outbound chunk, it tells us where we'd like it to
         * go.  It is NULL if we have no preference.
         */
        struct sctp_transport *transport;

};
(include/net/sctp/structs.h)

我们现在将描述一个 SCTP 关联(它是 TCP 连接的对等物)。

SCTP 关联

在 SCTP,我们使用术语关联而不是关联;连接指的是两个 IP 地址之间的通信,而关联指的是可能有多个 IP 地址的两个端点之间的通信。SCTP 协会由struct sctp_association代表:

struct sctp_association {
       ...

        sctp_assoc_t assoc_id;

        /* These are those association elements needed in the cookie.  */
        struct sctp_cookie c;

        /* This is all information about our peer.  */
        struct {
                struct list_head transport_addr_list;

                . . .
                __u16 transport_count;
                __u16 port;
                . . .

                struct sctp_transport *primary_path;
                struct sctp_transport *active_path;

        } peer;

        sctp_state_t state;
        . . .
        struct sctp_priv_assoc_stats stats;
};
(include/net/sctp/structs.h).

以下是对sctp_association结构中一些重要成员的描述:

  • assoc_id:关联唯一 id。它是通过sctp_assoc_set_id()方法设置的。

  • c:附加到关联的状态 cookie ( sctp_cookie对象)。

  • peer:表示关联的对等端点的内部结构。添加对等点是通过sctp_assoc_add_peer()方法完成的;删除一个对等点是通过sctp_assoc_rm_peer()方法完成的。下面是对一些peer结构重要成员的描述:

  • transport_addr_list:表示对等体的一个或多个地址。建立关联后,我们可以使用sctp_connectx()方法向列表中添加地址或从中删除地址。

  • transport_count:对等地址列表中对等地址的计数器(transport_addr_list)。

  • primary_path:表示初始连接的地址(INIT INIT_ACK 交换)。如果主路径处于活动状态,关联将尝试始终使用主路径。

  • active_path:发送数据时当前使用的对等体的地址。

  • state:关联所在的州,如 SCTP _ 州 _ 已关闭或 SCTP _ 州 _ 已建立。本节稍后将讨论各种 SCTP 状态。

将多个本地地址添加到 SCTP 关联或从一个关联中删除多个地址可以通过sctp_bindx()系统调用来完成,以支持前面提到的多宿主特性。每个 SCTP 关联包括一个对等对象,它代表远程端点;对等对象包括远程端点的一个或多个地址的列表(transport_addr_list)。在建立关联时,我们可以通过调用sctp_connectx()系统调用向列表中添加一个或多个地址。SCTP 关联由sctp_association_new()方法创建,并由sctp_association_init()方法初始化。在任何给定时刻,SCTP 关联可以处于 8 种状态之一;因此,例如,当它被创建时,它的状态是 STATE _ 状态 _ 关闭。稍后,这些状态会改变;例如,请参阅本章后面的“设置 SCTP 协会”一节。这些状态由sctp_state_t enum ( include/net/sctp/constants.h)表示。

要在两个端点之间发送数据,必须完成初始化过程。在该过程中,设置这两个端点之间的 SCTP 关联;cookie 机制用于防止同步攻击。这一过程将在下一节中讨论。

成立 SCTP 协会

初始化过程是一个 4 次握手,包括以下步骤:

  • 一个端点(“A”)向它想要与之通信的端点(“Z”)发送 INIT 块。该块将在 INIT 块的 Initiate 标记字段中包括本地生成的标记,并且它还将包括值为 0(零)的验证标记(SCTP 报头中的vtag)。
  • 发送初始化块后,关联进入 SCTP 状态 COOKIE 等待状态。
  • 另一个端点(“Z”)向“A”发送 INIT-ACK 块作为回复。该块将包括在 INIT-ACK 块的发起标签字段中本地生成的标签和作为验证标签的远程发起标签(SCTP 报头中的vtag)。“Z”还应该生成一个状态 cookie,并将其与 INIT-ACK 回复一起发送。
  • 当“A”接收到 INIT-ACK 块时,它离开 SCTP 状态 COOKIE 等待状态。从现在开始,“A”将在所有发送的数据包中使用远程启动标签作为验证标签(SCTP 报头中的vtag)。“A”将在 cookie 回送块中发送它接收到的状态 COOKIE。“A”将进入 SCTP 状态 COOKIE 回应状态。
  • 当“Z”接收到 COOKIE ECHO 块时,它将构建一个 TCB(传输控制块)。TCB 是一种数据结构,包含 SCTP 连接两端的连接信息。“Z”将进一步将其状态改变为 STATE _ 状态 _ 已建立,并以 COOKIE ACK 组块进行回复。这是在“Z”上最终建立关联的地方,此时,该关联将使用保存的标签。
  • 当“A”接收到 COOKIE ACK 时,它将从 SCTP 状态 COOKIE 回应状态转移到 SCTP 状态已建立状态。

image 注意当一些强制参数丢失时,或者当接收到无效的参数值时,端点可以用中止块来响应 INIT、INIT ACK 或 COOKIE ECHO 块。应该在回复中指定中止块的原因。

现在您已经了解了 SCTP 关联以及它们是如何创建的,您将看到 SCTP 如何接收 SCTP 数据包,以及 SCTP 数据包是如何发送的。

用 SCTP 接收数据包

接收 SCTP 包的主要处理程序是sctp_rcv()方法,它获取一个 SKB 作为单个参数(net/sctp/input.c)。首先进行一些健全性检查(大小、校验和等)。如果一切正常,我们继续检查这个数据包是否是一个“出乎意料的”(OOTB)数据包。如果数据包的格式正确(即没有校验和错误),则该数据包是 OOTB 数据包,但是接收方无法识别该数据包所属的 SCTP 协会。(参见 RFC 4960 中的第 8.4 节。)OOTB 包由sctp_rcv_ootb()方法处理,该方法遍历包的所有块,并根据 RFC 中指定的块类型采取行动。因此,例如,放弃中止的块。如果这个包不是 OOTB 包,它通过调用sctp_inq_push()方法被放入 SCTP inqueue,并通过sctp_assoc_bh_rcv()方法或sctp_endpoint_bh_rcv()方法继续它的旅程。

用 SCTP 发送数据包

对用户空间 SCTP 套接字的写入到达sctp_sendmsg()方法(net/sctp/socket.c)。通过调用sctp_primitive_SEND()方法将数据包传递到较低层,该方法又调用状态机回调sctp_do_sm() ( net/sctp/sm_sideeffect.c),带有 SCTP_ST_PRIMITIVE_SEND。下一个阶段是调用sctp_side_effects(),最终调用sctp_packet_transmit()方法。

SCTP 心跳

心跳机制通过交换心跳和心跳确认 SCTP 数据包来测试传输或路径的连通性。一旦达到未返回心跳确认的阈值,它就声明传输 IP 地址关闭。默认情况下,每 30 秒发送一次心跳块,以监控空闲目标传输地址的可达性。该时间间隔可通过设置/proc/sys/net/sctp/hb_interval进行配置。默认值为 30000 毫秒(30 秒)。发送心跳块是由sctp_sf_sendbeat_8_3()方法执行的。方法名中出现8_3的原因是它引用了 RFC 4960 中的 8.3 节(路径心跳)。当端点接收到心跳块时,如果它处于 SCTP 状态 COOKIE 回应状态或 SCTP 状态已建立状态,它将使用心跳回应块进行回复。

SCTP 多数据流

流是单个关联中的单向数据流。出站流的数量和入站流的数量是在关联设置期间声明的(由 INIT chunk 声明),并且这些流在整个关联生存期内都是有效的。用户空间应用可以通过创建一个sctp_initmsg对象并初始化它的sinit_num_ostreamssinit_max_instreams,然后用 SCTP_INITMSG 调用setsockopt()方法来设置流的数量。流数量的初始化也可以通过sendmsg()系统调用来完成。这反过来设置了sctp_sock对象的initmsg对象中的相应字段。添加流的最大原因之一是消除行首阻塞 (HoL 阻塞)情况。行首阻塞是一种性能限制现象,当一行数据包被第一个数据包阻塞时会发生这种现象,例如,在 HTTP 管道中的多个请求中。使用 SCTP 多数据流时,不存在这个问题,因为每个数据流都是单独排序的,并保证按顺序传送。因此,一旦其中一个流由于丢失/拥塞而被阻塞,其他流可能不会被阻塞,并且数据将继续被传送。这是因为一个流可以被阻塞,而其他流没有被阻塞,

image 注意关于为 SCTP 使用套接字,我要提一下lksctp-tools项目(http://lksctp.sourceforge.net/)。这个项目为 SCTP ( libsctp)提供了一个 Linux 用户空间库,包括 C 语言头文件(netinet/sctp.h),用于访问标准套接字没有提供的特定于 SCTP 的应用编程接口,以及 SCTP 周围的一些帮助实用程序。我还应该提到 RFC 6458,“流控制传输协议(SCTP)的套接字 API 扩展”,它描述了流控制传输协议(SCTP)到套接字 API 的映射。

SCTP 多宿主

SCTP 多宿主是指在两个端点上都有多个 IP 地址。SCTP 的一个非常好的特性是,如果本地 ip 地址被指定为通配符,端点默认是多宿主的。此外,关于多宿主特性也有很多困惑,因为人们期望简单地通过绑定到多个地址,关联将最终成为多宿主的。这是不正确的,因为我们只实现目的地多宿主。换句话说,两个连接的端点都必须是多宿主的,它才能具有真正的故障转移能力。如果本地关联只知道一个目的地址,则只有一条路径,因此没有多宿主。

随着本节对 SCTP 多宿主的描述,本章的 SCTP 部分也就结束了。在下一节,我将介绍 DCCP 协议,这是本章讨论的最后一个传输协议。

DCCP:数据报拥塞控制协议

DCCP 是一种不可靠的拥塞控制传输层协议,因此它借鉴了 UDP 和 TCP 的优点,同时增加了新功能。和 UDP 一样,它是面向消息的,不可靠。像 TCP 一样,它是面向连接的协议,也使用 3 次握手来建立连接。通过几个研究机构的参与,DCCP 的开发得到了学术界想法的帮助,但是到目前为止它还没有在更大规模的互联网系统中进行测试。例如,在需要较小延迟的应用中,以及在允许少量数据丢失的应用中,如电话和流媒体应用中,使用 DCCP 是有意义的。

DCCP 中的拥塞控制与 TCP 中的不同之处在于,拥塞控制算法(称为 CCID)可以在端点之间协商,并且拥塞控制可以应用于连接(在 DCCP 称为半连接)的正向和反向路径。到目前为止,已经规定了两类可插拔拥塞控制。第一种是基于速率的、平滑的“TCP 友好”算法(CCID-3,RFC 4342 和 5348),对于这种算法,有一个实验性的小数据包变体,称为 CCID-4 (RFC 5622,RFC 4828)。第二种类型的拥塞控制,“类 TCP”(RFC 4341)将带有选择性确认的基本 TCP 拥塞控制算法(SACK,RFC 2018)应用于 DCCP 流。端点至少需要实现一个 CCID 才能运行。第一个 DCCP Linux 实现在 Linux 内核 2.6.14 (2005)中发布。本章描述了 DCCPv4 (IPv4)的实现原理。深入研究单个 DCCP 拥塞控制算法的实现细节超出了本书的范围。

现在我已经大致介绍了 DCCP 协议,接下来我将描述 DCCP 报头。

DCCP 头球

每个 DCCP 数据包都以 DCCP 报头开始。DCCP 报头的最小长度是 12 个字节。DCCP 使用可变长度的报头,长度范围从 12 到 1020 个字节,具体取决于是否使用短序列号以及使用哪些 TLV 数据包选项。DCCP 序列号针对每个数据包递增(而不是像 TCP 中那样针对每个字节),并且可以从 6 个字节缩短到 3 个字节。

struct dccp_hdr {
        __be16  dccph_sport,
                dccph_dport;
        __u8    dccph_doff;
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    dccph_cscov:4,
                dccph_ccval:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u8    dccph_ccval:4,
                dccph_cscov:4;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif
        __sum16 dccph_checksum;
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    dccph_x:1,
                dccph_type:4,
                dccph_reserved:3;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u8    dccph_reserved:3,
                dccph_type:4,
                dccph_x:1;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif
        __u8    dccph_seq2;
        __be16  dccph_seq;
};
(include/uapi/linux/dccp.h)

以下是对dccp_hdr结构中重要成员的描述:

  • dccph_sport:源端口(16 位)。
  • dccph_dport:目的端口(16 位)。
  • dccph_doff:数据偏移(8 位)。DCCP 报头的大小是 4 字节的倍数。
  • dccph_cscov:确定校验和包含数据包的哪一部分。当部分校验和用于可以容忍较低百分比的损坏的应用时,它可能会提高性能。
  • dccph_ccval:从发送者到接收者的 CCID 特定信息(不总是使用)。
  • dccph_x:扩展序列号位(1 位)。当使用 48 位扩展序列号和确认号时,设置该标志。
  • dccph_type:DCCP 头类型(4 位)。例如,这可以是用于数据分组的 DCCP 数据包数据或用于确认的 DCCP 数据包确认。参见本章末尾“快速参考”部分的表 11-3 、【DCCP 数据包类型】。
  • dccph_reserved:保留以备将来使用(1 位)。
  • dccph_checksum:校验和(16 位)。DCCP 报头和数据的 Internet 校验和,计算方法类似于 UDP 和 TCP。如果使用部分校验和,则只对应用数据的dccph_cscov指定的长度进行校验和检查。
  • dccph_seq2:顺序号。这在处理扩展序列号(8 位)时使用。
  • dccph_seq:顺序号。对于每个数据包(16 位),该值递增 1。

image DCCP 序号取决于dccph_x。(详见dccp_hdr_seq()法,include/linux/dccp.h)。

图 11-4 显示了一个 DCCP 割台。设置了dccph_x标志,所以我们使用 48 位扩展序列号。

9781430261964_Fig11-04.jpg

图 11-4 。DCCP 报头(扩展序列号位被置位,dccph_x=1)

图 11-5 显示了一个 DCCP 割台。没有设置dccph_x标志,所以我们使用 24 位序列号。

9781430261964_Fig11-05.jpg

图 11-5 。DCCP 报头(扩展序列号位未置位,dccph_x=0)

DCCP 初始化

DCCP 初始化与 TCP 和 UDP 中的情况非常相似。考虑到 DCCPv4 的情况(net/dccp/ipv4.c),首先定义一个proto对象(dccp_v4_prot,并设置其 DCCP 特定的回调;我们还定义了一个net_protocol对象(dccp_v4_protocol)并初始化它:

static struct proto dccp_v4_prot = {
         .name                   = "DCCP",
         .owner                  = THIS_MODULE,
         .close                  = dccp_close,
         .connect                = dccp_v4_connect,
         .disconnect             = dccp_disconnect,
         .ioctl                  = dccp_ioctl,
         .init                   = dccp_v4_init_sock,
         . . .
         .sendmsg                = dccp_sendmsg,
         .recvmsg                = dccp_recvmsg,
         . . .

}

(net/dccp/ipv4.c)

static const struct net_protocol dccp_v4_protocol = {
        .handler        = dccp_v4_rcv,
        .err_handler    = dccp_v4_err,
        .no_policy      = 1,
        .netns_ok       = 1,
};

(net/dccp/ipv4.c)

我们在dccp_v4_init()方法中注册了dccp_v4_prot对象和dccp_v4_protocol对象:

static int __init dccp_v4_init(void)
{
         int err = proto_register(&dccp_v4_prot, 1);

         if (err != 0)
                 goto out;

         err = inet_add_protocol(&dccp_v4_protocol, IPPROTO_DCCP);
         if (err != 0)
                 goto out_proto_unregister;
(net/dccp/ipv4.c)

DCCP 套接字初始化

从用户空间在 DCCP 创建套接字使用了socket()系统调用,其中域参数(SOCK_DCCP)表示要创建一个 DCCP 套接字。在内核中,这导致 DCCP 套接字通过dccp_v4_init_sock()回调进行初始化,这依赖于dccp_init_sock()方法来执行实际的工作:

static int dccp_v4_init_sock(struct sock *sk)
{
        static __u8 dccp_v4_ctl_sock_initialized;
        int err = dccp_init_sock(sk, dccp_v4_ctl_sock_initialized);

        if (err == 0) {
                if (unlikely(!dccp_v4_ctl_sock_initialized))
                        dccp_v4_ctl_sock_initialized = 1;
                inet_csk(sk)->icsk_af_ops = &dccp_ipv4_af_ops;
        }

        return err;
}
(net/dccp/ipv4.c)

dccp_init_sock()方法最重要的任务如下:

  • 用相同的默认值初始化 DCCP 套接字字段(例如,套接字状态设置为 DCCP _ 关闭)
  • DCCP 定时器的初始化(通过dccp_init_xmit_timers()方法)
  • 通过调用dccp_feat_init()方法初始化特征协商部分。功能协商是 DCCP 的一个显著特征,通过它,端点可以就连接每一端的属性达成一致。它扩展了 TCP 功能协商,并在 RFC 4340,sec。6.

使用 DCCP 从网络层(L3)接收数据包

从网络层(L3)接收 DCCP 数据包的主要处理程序是dccp_v4_rcv ()方法:

static int dccp_v4_rcv(struct sk_buff *skb)
{
        const struct dccp_hdr *dh;
        const struct iphdr *iph;
        struct sock *sk;
        int min_cov;

首先,我们丢弃无效的数据包。例如,如果数据包不是针对该主机的(数据包类型不是 PACKET_HOST),或者数据包的大小比 DCCP 报头(12 字节)短:

        if (dccp_invalid_packet(skb))
                  goto discard_it;

然后,我们根据流程执行查找:

        sk = __inet_lookup_skb(&dccp_hashinfo, skb,
                               dh->dccph_sport, dh->dccph_dport);

如果找不到套接字,数据包将被丢弃:

        if (sk == NULL) {
               . . .
               goto no_dccp_socket;
        }

我们做了一些与最小校验和覆盖相关的检查,如果一切正常,我们继续使用通用的sk_receive_skb()方法将数据包传递到传输层(L4)。注意dccp_v4_rcv()方法在结构和功能上与tcp_v4_rcv()方法非常相似。这是因为 Linux 中的 DCCP 的原作者阿纳尔多·卡瓦略·德·梅洛已经非常努力地在代码中使 TCP 和 DCCP 之间的相似之处变得明显而清晰。

         . . .
         return sk_receive_skb(sk, skb, 1);
         }
(net/dccp/ipv4.c)

用 DCCP 发送数据包

从 DCCP 用户空间套接字发送数据最终由内核中的dccp_sendmsg()方法处理(net/dccp/proto.c)。这类似于 TCP 的情况,其中tcp_sendmsg()内核方法处理从 TCP 用户空间套接字发送的数据。我们来看看dccp_sendmsg()的方法:

int dccp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                  size_t len)
{
         const struct dccp_sock *dp = dccp_sk(sk);
         const int flags = msg->msg_flags;
        const int noblock = flags & MSG_DONTWAIT;
        struct sk_buff *skb;
        int rc, size;
        long timeo;

分配一个 SKB:

        skb = sock_alloc_send_skb(sk, size, noblock, &rc);
        lock_sock(sk);
        if (skb == NULL)
                goto out_release;

        skb_reserve(skb, sk->sk_prot->max_header);

将数据块从msghdr对象复制到 SKB:

        rc = memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len);
        if (rc != 0)
                goto out_discard;

        if (!timer_pending(&dp->dccps_xmit_timer))
                dccp_write_xmit(sk);

根据为连接选择的拥塞控制类型(基于窗口或基于速率),dccp_write_xmit()方法将使数据包稍后发送(通过dccps_xmit_timer()到期)或通过dccp_xmit_packet()方法立即发送。这又依赖于dccp_transmit_skb()方法来初始化传出的 DCCP 报头,并将其传递给 L3 特定的queue_xmit发送回调(对 IPv4 使用ip_queue_xmit()方法,对 IPv6 使用inet6_csk_xmit()方法)。我将用一小段关于 DCCP 和纳特的话来结束我们关于 DCCP 的讨论。

DCCP 和纳特

一些 NAT 设备不允许 DCCP 通过(通常是因为它们的固件通常很小,因此不支持“外来的”IP 协议,如 DCCP)。RFC 5597(2009 年 9 月)提出了 NAT 的行为要求,以支持 NAT-ed DCCP 通信。然而,目前还不清楚这些建议在多大程度上被应用到消费者设备中。DCCP-UDP 的动机之一是缺少让 DCCP 通过的 NAT 设备。1).在与 TCP 的比较中,有一个细节可能很有趣。默认情况下,后者支持同时打开(RFC 793,第 3.4 节),而 RFC 4340 第 4.6 节中 DCCP 的初始规范不允许使用同时打开。为了支持 NAPT 遍历,RFC 5596 在 2009 年 9 月更新了 RFC 4340,采用了“近乎同时打开”技术,在列表中添加了一种数据包类型(DCCP 监听,RFC 5596,第 2.2.1 节),并更改了状态机以支持另外两种状态(2.2.2 ),从而支持近乎同时打开。动机是 NAT“打孔”技术,然而,这需要存在 DCCP 的 NAT(问题同上)。由于这个先有鸡还是先有蛋的问题,DCCP 在互联网上没有看到太多的曝光。也许 UDP 封装会改变这一点。但这样一来,它就不再被视为真正的传输层协议。

摘要

本章讨论了四种传输协议:最常用的 UDP 和 TCP,以及较新的协议 SCTP 和 DCCP。您了解了这些协议之间的基本区别。您了解了 TCP 是一种比 UDP 复杂得多的协议,因为它使用一个状态机和几个定时器,并且需要确认。您了解了每种协议的报头,以及如何使用这些协议发送和接收数据包。我讨论了 SCTP 协议的一些独特功能,比如多宿主和多数据流。

下一章将讨论无线子系统及其在 Linux 中的实现。在接下来的“快速参考”部分,我将介绍与本章中讨论的主题相关的顶级方法,按照它们的上下文进行排序,并且我还将展示本章中提到的两个表。

快速参考

我将用本章中讨论的套接字和传输层协议的重要方法的简短列表来结束本章。本章提到了其中一些。之后,有一个宏和三个表。

方法

下面是方法。

int ip_cmsg_send(struct net *net,struct msghdr *msg,struct ipcm _ cookie * IPC);

该方法通过解析指定的msghdr对象构建一个ipcm_cookie对象。

void sock _ put(struct sock * sk);

该方法减少指定的sock对象的引用计数。

void sock _ hold(struct sock * sk);

该方法增加指定sock对象的引用计数。

int sock_create(int family,int type,int protocol,struct socket * * RES);

这个方法执行一些健全性检查,如果一切正常,它通过调用sock_alloc()方法分配一个套接字,然后调用net_families[family]->create。(在 IPv4 的情况下,它是inet_create()方法。)

int sock _ map _ FD(struct socket * sock,int flags);

这个方法分配一个文件描述符并填充文件条目。

bool sock _ flag(const struct sock * sk,enum sock_flags 标志);

如果在指定的sock对象中设置了指定的flag,则该方法返回true

内部 TCP _ v4 _ rcv(struct sk _ buf * skb):

此方法是处理来自网络层(L3)的传入 TCP 数据包的主要处理程序。

void TCP _ init _ sock(struct sock * sk);

此方法执行独立于地址族的套接字初始化。

struct tcphdr * TCP _ HDR(const struct sk _ buff * skb);

该方法返回与指定的skb相关联的 TCP 报头。

int TCP _ send msg(struct ki OCB * iocb,struct sock *sk,struct msghdr *msg,size _ t size);

这个方法处理从用户空间发送的 TCP 数据包。

struct TCP _ sock * TCP _ sk(const struct sock * sk);

该方法返回与指定 sock 对象(sk)关联的tcp_sock对象。

int UDP _ rcv(struct sk _ buf * skb);

此方法是处理来自网络层(L3)的 UDP 数据包的主要处理程序。

struct UDP HDR * UDP _ HDR(const struct sk _ buff * skb);

该方法返回与指定的skb相关联的 UDP 头。

int UDP _ sendmail(struct KIO CB * iocb、struct sock *sk、struct msghdr * msg、size _ t len);

这个方法处理从用户空间发送的 UDP 包。

sctphdr *sctp_hdr 结构(const struct sk _ buf * skb);

该方法返回与指定的skb相关联的 SCTP 头。

struct SCTP _ sock * SCTP _ sk(const struct sock * sk):

该方法返回与指定的sock对象相关联的 SCTP 套接字(sctp_sock对象)。

int SCTP _ sendmail(struct KIO CB * iocb、struct sock *sk、struct msghdr * msg、size _ t msg _ len);

该方法处理从用户空间发送的 SCTP 数据包。

struct SCTP _ association * SCTP _ association _ new(const struct SCTP _ endpoint * EP,const struct sock *sk,sctp_scope_t scope,GFP _ t GFP);

这个方法分配并初始化一个新的 SCTP 关联。

void SCTP _ association _ free(struct SCTP _ association * asoc);

这种方法释放了 SCTP 协会的资源。

void SCTP _ chunk _ hold(struct SCTP _ chunk * ch);

此方法递增指定 SCTP 块的引用计数。

void SCTP _ chunk _ put(struct SCTP _ chunk * ch);

此方法递减指定 SCTP 块的引用计数。如果引用计数达到 0,它通过调用sctp_chunk_destroy()方法来释放它。

int SCTP _ rcv(struct sk _ buf * skb):

这个方法是输入 SCTP 包的主要输入处理程序。

static int dccp _ v4 _ rcv(struct sk _ buff * skb);

此方法是处理来自网络层(L3)的 DCCP 数据包的主要 Rx 处理程序。

int dccp _ sendmail(struct KIO CB * iocb、struct sock *sk、struct msghdr * msg、size _ t len);

这个方法处理从用户空间发送的 DCCP 包。

宏指令

这是宏指令。

sctp_chunk_is_data()

如果指定的块是数据块,此宏返回 1;否则,它返回 0。

桌子

看看本章中使用的表格。

表 11-1。 TCP 和 UDP prot_ops 对象

|

prot_ops 回调

|

三氯苯酚

|

用户数据报协议(User Datagram Protocol)

|
| --- | --- | --- |
| release | inet_release | inet_release |
| bind | inet_bind | inet_bind |
| connect | inet_stream_connect | inet_dgram_connect |
| socketpair | sock_no_socketpair | sock_no_socketpair |
| accept | inet_accept | sock_no_accept |
| getname | inet_getname | inet_getname |
| poll | tcp_poll | udp_poll |
| ioctl | inet_ioctl | inet_ioctl |
| listen | inet_listen | sock_no_listen |
| shutdown | inet_shutdown | inet_shutdown |
| setsockopt | sock_common_setsockopt | sock_common_setsockopt |
| getsockopt | sock_common_getsockopt | sock_common_getsockopt |
| sendmsg | inet_sendmsg | inet_sendmsg |
| recvmsg | inet_recvmsg | inet_recvmsg |
| mmap | sock_no_mmap | sock_no_mmap |
| sendpage | inet_sendpage | inet_sendpage |
| splice_read | tcp_splice_read | - |
| compat_setsockopt | compat_sock_common_setsockopt | compat_sock_common_setsockopt |
| compat_getsockopt | compat_sock_common_getsockopt | compat_sock_common_getsockopt |
| compat_ioctl | inet_compat_ioctl | inet_compat_ioctl |

image 参见net/ipv4/af_inet.cinet_stream_opsinet_dgram_ops的定义。

表 11-2 。组块类型

|

组块类型

|

Linux 符号

|

价值

|
| --- | --- | --- |
| 有效载荷数据 | SCTP_CID_DATA | Zero |
| 开始 | SCTP_CID_INIT | one |
| 初始确认 | SCTP_CID_INIT_ACK | Two |
| 选择性确认 | SCTP_CID_SACK | three |
| 心跳请求 | SCTP 心跳 | four |
| 心跳确认 | SCTP 心跳确认 | five |
| 流产 | SCTP_CID_ABORT | six |
| 关机 | SCTP_CID_SHUTDOWN | seven |
| 关闭确认 | SCTP_CID_SHUTDOWN_ACK | eight |
| 操作误差 | SCTP_CID_ERROR | nine |
| 状态 Cookie | SCTP_CID_COOKIE_ECHO | Ten |
| Cookie 确认 | SCTP_CID_COOKIE_ACK | Eleven |
| 显式拥塞通知回应(ECNE) | SCTP _ cid _ ECN _ exception | Twelve |
| 拥塞窗口减少(CWR) | SCTP_CID_ECN_CWR | Thirteen |
| 关闭完成 | SCTP _ CID _ 关闭 _ 完成 | Fourteen |
| SCTP 身份验证块(RFC 4895) | SCTP_CID_AUTH | 0x0F |
| 传输序列号 | SCTP_CID_FWD_TSN | 0xC0 |
| 地址配置更改块 | sctp _ cid _ asconf | 0xC1 |
| 地址配置确认块 | sctp _ cid _ asconf _ ack | 0x80 |

表 11-3 。DCCP 数据包类型

|

Linux 符号

|

描述

|
| --- | --- |
| DCCP _ PKT _ 请求 | 由客户端发送以启动连接(三次启动握手的第一部分)。 |
| DCCP _ PKT _ 回应 | 由服务器发送以响应 DCCP 请求(三次启动握手的第二部分)。 |
| dccp _ pkt _ 数据 | 用于传输应用数据。 |
| DCCP_PKT_ACK | 用于传输纯确认。 |
| DCCP_PKT_DATAACK | 用于传输带有附带确认信息的应用数据。 |
| DCCP_PKT_CLOSEREQ | 由服务器发送,请求客户端关闭连接。 |
| DCCP_PKT_CLOSE | 由客户端或服务器用来关闭连接;引发一个 DCCP 重置包作为响应。 |
| DCCP_PKT_RESET | 用于正常或异常终止连接。 |
| DCCP_PKT_SYNC | 用于在数据包大量丢失后重新同步序列号。 |
| DCCP_PKT_SYNCACK | 确认 DCCP_PKT_SYNC。 |

十二、Linux 中的无线技术

第十一章处理第 4 层协议,它使我们能够与用户空间通信。本章讨论了 Linux 内核中的无线栈。我描述了 Linux 无线协议栈(mac80211 子系统)并讨论了其中一些重要机制的实现细节,如 IEEE 802.11n 中使用的包聚合和块确认,以及省电模式。为了理解无线子系统的实现,熟悉 802.11 MAC 报头是必不可少的。本章深入介绍了 802.11 MAC 报头、其成员及其用法。我还讨论了一些常见的无线拓扑,如基础设施基站、独立基站和网状网络。

Mac80211 子系统

在 20 世纪 90 年代末,在 IEEE 中有关于无线局域网(WLANS)协议的讨论。无线局域网 IEEE 802.11 规范的原始版本于 1997 年发布,并于 1999 年修订。在接下来的几年中,增加了一些扩展,正式称为 802.11 修正案。这些扩展可以分为 PHY(物理)层扩展、MAC(媒体访问控制)层扩展、管理扩展等等。例如,PHY 层扩展是 1999 年的 802.11b、802.11a(也是 1999 年的)和 2003 年的 802.11g。MAC 层扩展例如是用于 QoS 的 802.11e 和用于网状网络的 802.11s。本章的“网状网络”部分涉及 IEEE802.11s 修正案的 Linux 内核实现。IEEE802.11 规范经过修订,2007 年发布了 1232 页的第二版。2012 年,发布了一份 2793 页的规范,从http://standards.ieee.org/findstds/standard/802.11-2012.html开始提供。在本章中,我将该规范称为 IEEE 802.11-2012。以下是 802.11 重要修订的部分列表:

  • IEEE 802.11d: 国际(国与国)漫游扩展(2001)。
  • IEEE 802.11e: 增强:QoS,包括数据包突发(2005)。
  • IEEE 802.11h: 针对欧洲兼容性的频谱管理 802.11 a(2004)。
  • IEEE 802.11i: 增强的安全性(2004 年)。
  • IEEE 802.11j: 日本的扩展(2004)。
  • IEEE 802.11k: 无线电资源测量增强(2008 年)。
  • IEEE 802.11n: 使用 MIMO(多输入多输出天线)提高吞吐量(2009)。
  • IEEE 802.11p: WAVE:车载环境(如救护车、客车)的无线接入。它有一些特性,如不使用 BSS 概念和较窄的(5/10 MHz)信道。注意,在撰写本文时,Linux 还不支持 IEEE 802.11p。
  • IEEE 802.11v: 无线网络管理。
  • IEEE 802.11w: 受保护的管理帧。
  • 美国 IEEE 802.11y:3650–3700 MHz 操作(2008 年)
  • IEEE 802.11z: 直接链路建立的扩展(DLS)(2007 年 8 月至 2011 年 12 月)。

直到大约 2001 年,大约在 IEEE 802.11 第一个规范被批准四年后,笔记本电脑才变得非常流行;这些笔记本电脑中有许多是带无线网络接口出售的。如今,每台笔记本电脑都将 WiFi 作为标准设备。对于当时的 Linux 社区来说,为这些无线网络接口提供 Linux 驱动程序并提供 Linux 网络无线堆栈是非常重要的,这样才能保持与其他操作系统(如 Windows、Mac OS 等)的竞争力。在架构和设计方面做得较少。正如当时的 Linux 内核无线维护者 Jeff Garzik 所说,“他们只是希望他们的硬件能够工作”。当开发第一个 Linux 无线驱动程序时,还没有通用的无线 API。因此,当开发人员从零开始实现他们的驱动程序时,在驱动程序之间有许多代码重复的情况。一些驱动程序是基于 FullMAC 的,这意味着大多数管理层(MLME) 是在硬件中管理的。在此后的几年中,开发了一种新的 802.11 无线堆栈,称为 mac80211。它在 2007 年 7 月被集成到 Linux 内核中,用于 2.6.22 Linux 内核。mac80211 堆栈基于 d80211 堆栈,d 80211 堆栈是一家名为 Devicescape 的公司开发的开源、GPL 许可的堆栈。

我不能深入研究 PHY 层的细节,因为这个主题非常广泛,值得单独写一本书。但是,我必须指出,802.11 和 802.3 有线以太网有许多不同之处。这里有两个主要区别:

  • 以太网适用于 CSMA/CD,而 802.11 适用于 CSMA/CA。CSMA/CA 代表载波侦听多路访问/冲突避免,CSMA/CD 代表载波侦听多路访问/冲突检测。正如您可能猜到的那样,不同之处在于碰撞检测。使用以太网,当介质空闲时,站点开始传输;如果在传输过程中检测到冲突,传输将停止,并开始一个随机退避周期。无线站在传输时无法检测冲突,而有线站可以。使用 CSMA/CA 时,无线站会等待空闲介质,然后才传输帧。在发生冲突的情况下,工作站不会注意到它,但是因为不应该为该数据包发送确认帧,所以如果超时后没有收到确认,它将重新传输。
  • 无线流量对干扰很敏感。因此,802.11 规范要求除广播和组播以外的每个帧在收到时都必须得到确认。没有及时得到确认的数据包应该重新传输。请注意,自 IEEE 802.11e 以来,有一种模式不需要确认,即 QoSNoAck 模式,但在实践中很少使用。

802.11 MAC 报头

每个 MAC 帧由 MAC 报头、可变长度的帧体和 32 位 CRC 的 FCS(帧校验序列)组成。图 12-1 显示了 802.11 标题。

9781430261964_Fig12-01.jpg

图 12-1 。IEEE 802.11 报头。请注意,并不总是使用所有成员,这一节将很快解释

802.11 报头在 mac80211 中由ieee80211_hdr结构表示:

struct ieee80211_hdr {
        __le16 frame_control;
        __le16 duration_id;
        u8 addr1[6];
        u8 addr2[6];
        u8 addr3[6];
        __le16 seq_ctrl;
        u8 addr4[6];
} __packed;
(include/linux/ieee80211.h)

与仅包含三个字段(源 MAC 地址、目的 MAC 地址和以太网类型)的以太网报头(struct ethhdr)相比,802.11 报头包含多达六个地址和一些其他字段。但是,对于典型的数据帧,只使用三个地址(例如,接入点或 AP/客户端通信)。对于 ACK 帧,只使用接收方地址。注意图 12-1 只显示了四个地址,但是当使用网状网络时,使用了带有两个额外地址的网状扩展报头。

我现在转向 802.11 报头字段的描述,从 802.11 报头中的第一个字段开始,称为帧控制。这是一个重要的字段,在很多情况下,它的内容决定了 802.11 MAC 报头中其他字段的含义(尤其是地址)。

框架控件

帧控制长度为 16 位。图 12-2 显示了它的字段和每个字段的大小。

9781430261964_Fig12-02.jpg

图 12-2 。帧控制字段

以下是对框架控制成员的描述:

  • Protocol version:我们用的 MAC 802.11 的版本。目前只有一个 MAC 版本,因此该字段始终为 0。

  • 802.11 中有三种类型的数据包——管理、控制和数据:

  • 管理数据包 (IEEE80211_FTYPE_MGMT)用于管理操作,如关联、认证、扫描等。

  • 控制包 (IEEE80211_FTYPE_CTL)通常与数据包有一定的关联性;例如,PS-Poll 分组用于从 AP 缓冲器中检索分组。再比如:一个要传输的站先发送一个名为 RTS(请求发送)的控制包;如果介质空闲,目的站将发回一个名为 CTS(允许发送)的控制包。

  • 数据包 (IEEE80211_FTYPE_DATA)是原始数据包。空包是原始包的特例,不携带数据,主要用于电源管理控制目的。我将在本章后面的“节能模式”一节中讨论空数据包。

  • Subtype:对于所有上述三种类型的数据包(管理、控制和数据),都有一个子类型字段,用于标识所用数据包的特征。例如:

  • 管理帧中子类型字段的值 0100 表示该分组是探测请求(IEEE80211_STYPE_PROBE_REQ)管理分组,其在扫描操作中使用。

  • 控制分组中子类型字段的值 1011 表示这是请求发送(IEEE80211_STYPE_RTS)控制分组。数据包的子类型字段的值 0100 表示这是一个空数据(IEEE80211_STYPE_NULLFUNC)包,用于电源管理控制。

  • 数据分组的子类型的值 1000(IEEE 80211 _ STYPE _ QOS _ 数据)意味着这是 QOS 数据分组;这个子类型是由 IEEE802.11e 修正案添加的,它处理 QoS 增强。

  • ToDS:该位被置位时,表示该包是给分布式系统的。

  • FromDS:该位被置位时,表示数据包来自分发系统。

  • More Frag:使用分段时,该位设为 1。

  • Retry:当一个包被重发时,该位被设置为 1。重传的一个典型例子是发送的数据包没有及时收到确认。确认通常由无线驱动程序的固件发送。

  • Pwr Mgmt:当电源管理位被置位时,意味着站点将进入省电模式。我将在本章后面的“节能模式”一节中讨论节能模式。

  • More Data:当 AP 发送它为休眠站缓冲的数据包时,当缓冲区不为空时,它将More Data位设置为 1。因此,该站知道它应该检索更多的分组。当缓冲器清空后,该位被置 0。

  • Protected Frame:该位在帧体加密时置 1;只能加密数据帧和认证帧。

  • 对于被称为严格排序的 MAC 服务,帧的顺序很重要。当使用该服务时,顺序位被设置为 1。很少使用。

image 802.11h 修正案引入了行动框架(IEEE80211_STYPE_ACTION),处理频谱和发射功率管理。然而,由于缺乏用于管理分组子类型的空间,动作帧也被用于各种较新的标准修订中,例如,802.11n 中的 HT 动作帧

其他 802.11 MAC 报头成员

下面描述帧控制之后的 mac802.11 报头的其他成员:

  • Duration/ID : 持续时间以微秒为单位保存网络分配向量(NAV) 的值,它由Duration/ID字段的 15 位组成。第十六个字段是 0。当在省电模式下工作时,它是 PS-Poll 帧的站的 AID(关联 ID)(参见 IEEE 802.11-2012 中的 8.2.4.2(a))。网络分配矢量(NAV)是一种虚拟载波侦听机制。我不深入 NAV 内部,因为那超出了本章的范围。
  • Sequence Control : 这是一个 2 字节的字段,用于指定顺序控制。在 802.11 中,数据包可能会被多次接收,最常见的情况是由于某种原因没有收到确认。序列控制字段由一个片段号(4 位)和一个序列号(12 位)组成。序列号由发射站以ieee80211_tx_h_sequence()方式生成。在重传中的重复帧的情况下,它被丢弃,并且被丢弃的重复帧的计数器(dot11FrameDuplicateCount)增加 1;这在ieee80211_rx_h_check()方法中完成。控制数据包中不存在Sequence Control字段。
  • Address1 – Address4 : 有四个地址,但你并不总是用全。地址 1 是接收地址(RA),用于所有数据包。地址 2 是发送地址(TA),它存在于除 ACK 和 CTS 包以外的所有包中。地址 3 仅用于管理和数据包。当设置帧控制的 ToDS 和 FromDS 位时,使用地址 4;在无线分布系统中工作时会发生这种情况。
  • QoS Control:QoS 控制字段是由 802.11e 修正案添加的,并且只存在于 QoS 数据包中。因为它不是原始 802.11 规范的一部分,也不是原始 mac80211 实现的一部分,所以它不是 IEEE802.11 头(ieee80211_hdr struct)的成员。事实上,它被添加在 IEEE802.11 报头的末尾,可以通过ieee80211_get_qos_ctl()方法访问。QoS 控制字段包括tid(流量标识)、ACK 策略和称为 A-MSDU 存在的字段,该字段告知 A-MSDU 是否存在。我将在本章后面的“高吞吐量(ieee802.11n)”一节中讨论 A-MSDU。
  • HT 控制字段:HT (高吞吐量)控制字段由 802.11n 修订版添加(参见 802.11n-2009 规范的 7.1.3.5(a))。

本节介绍了 802.11 MAC 报头及其成员和用途。熟悉 802.11 MAC 报头对于理解 mac802.11 堆栈至关重要。

网络拓扑

802.11 无线网络中有两种流行的网络拓扑。我讨论的第一种拓扑是基础架构 BSS 模式,这是最流行的。您会在家庭无线网络和办公室中遇到基础设施 BSS 无线网络。稍后我将讨论 IBSS(特设)模式。注意,IBSS 是而不是基础设施 BSSIBSS 是独立的 BSS ,这是一个自组织网络,将在本节稍后讨论。

基础设施 BSS

在基础设施 BSS 模式下工作时,有一个中心设备,称为接入点(AP),还有一些客户端站。它们一起形成了一个 BSS(基本服务集)。这些客户站必须首先对 AP 执行关联和认证,以便能够通过 AP 发送分组。在许多情况下,客户站在认证和关联之前执行扫描,以便获得关于 AP 的细节。关联是排他的:在给定时刻,一个客户端只能与一个 AP 关联。当客户端与 AP 成功关联时,它会获得一个 AID(关联 ID),这是一个唯一的编号(对于此 BSS ),范围为 1–2007。AP 实际上是一种无线网络设备,带有一些附加硬件(如以太网端口、led、重置为制造商默认值的按钮等)。管理守护程序在 AP 设备上运行。这种软件的一个例子是hostapd守护进程。该软件处理 MLME 层的一些管理任务,例如认证和关联请求。它通过注册自己经由nl80211接收相关的管理帧来实现这一点。hostapd项目是一个开源项目,它使几个无线网络设备能够作为 AP 运行。

客户端可以通过向 AP 发送分组来与其他客户端(或者与桥接到 AP 的不同网络中的站)通信,这些分组由 AP 中继到它们的最终目的地。为了覆盖一个大的区域,你可以部署多个接入点,并通过有线连接它们。这种类型的部署称为扩展服务集(ESS) 。在 ESS 部署中,有两个或更多 BSS。在一个 BSS 中发送的可能到达附近 BSS 的多播和广播在附近的 BSS 站中被拒绝(802.11 报头中的bssid不匹配)。在这样的部署中,每个 AP 通常使用不同的信道来最小化干扰。

IBSS,或特设模式

IBSS 网络通常是在没有预先规划的情况下形成的,只在需要 WLAN 的时候形成。IBSS 网络也称为自组织网络。创建 IBSS 是一个简单的过程。您可以通过从命令行运行此iw命令来设置 IBSS(注意,2412 参数用于使用通道 1):

iw wlan0 ibss join AdHocNetworkName 2412

或者在使用iwconfig工具时,使用这两个命令:

iwconfig wlan0 mode ad-hoc
iwconfig wlan0 essid AdHocNetworkrName

这通过调用ieee80211_sta_create_ibss()方法(net/mac80211/ibss.c)来触发 IBSS 创建。然后ssid(在本例中为AdHocNetworkName)必须手动(或以其他方式)分发给每个想要连接到自组织网络的人。和 IBSS 一起工作时,你没有 AP。IBSS 的 bssid 是一个随机的 48 位地址(基于调用get_random_bytes()方法)。自组织模式中的电源管理比基础设施 BSS 中的电源管理稍微复杂一些;它使用公告交通指示地图(ATIM)消息。mac802.11 不支持 ATIM,本章不讨论它。

下一节描述省电模式,这是 mac80211 网络堆栈最重要的机制之一。

省电模式

除了转发数据包,AP 还有另一个重要的功能:为进入省电模式的客户站缓冲数据包。客户端通常是电池供电的设备。无线网络接口有时会进入省电模式。

进入省电模式

当客户站进入省电模式时,它通常通过发送空数据包来通知 AP。其实从技术上讲,不一定是空数据包;它是 PM=1 (PM 是帧控制中的电源管理标志)的分组就足够了。获得这种空分组的 AP 开始将去往该站的单播分组保存在称为ps_tx_buf的特殊缓冲器中;每个车站都有这样的缓冲区。该缓冲区实际上是一个数据包链表,每个站最多可容纳 128 个数据包(STA_MAX_TX_BUFFER)。如果缓冲区已满,它将开始丢弃最先收到的数据包(FIFO)。除此之外,还有一个称为bc_buf的缓冲区,用于多播和广播数据包(在 802.11 堆栈中,多播数据包应该由同一 BSS 中的所有站点接收和处理)。bc_buf缓冲区也可以容纳多达 128 个数据包(AP_MAX_BC_BUFFER)。当无线网络接口处于节能模式时,它无法接收或发送数据包。

退出省电模式

不时地,相关的站被它自己唤醒(通过一些定时器);然后它检查 AP 周期性发送的特殊管理包,称为信标 。通常,一个 AP 每秒发送 10 个信标;在大多数 AP 上,这是一个可配置的参数。这些信标包含信息元素中的数据,这些信息元素构成了管理包中的数据。被唤醒的站点通过调用ieee80211_check_tim()方法(include/linux/ieee80211.h)检查一个名为 TIM(交通指示图)的特定信息元素。TIM 是 2008 年条目的数组。因为 TIM 的大小是 251 字节(2008 位),所以允许您发送一个部分虚拟位图,它的大小要小一些。如果该站的 TIM 中的条目被设置,这意味着 AP 保存了该站的单播包,因此该站应该清空 AP 为其保存的包的缓冲区。该站开始发送空分组(或者,更罕见地,称为 PS-Poll 分组的特殊控制分组),以从 AP 检索这些缓冲的分组。通常在缓冲区清空后,站点进入睡眠状态(然而,根据规范,这不是强制性的)。

处理多播/广播缓冲区

每当至少一个站处于睡眠模式时,AP 缓冲多播和广播分组。多播/广播站的 AID 是 0;所以,在这种情况下,你设置 TIM[0]为真。传递组(DTIM)是一种特殊类型的 TIM,它不是在每个信标中发送,而是在预定数量的信标间隔(DTIM 周期)内发送一次。发送 DTIM 后,AP 发送其缓冲的广播和组播数据包。您通过调用ieee80211_get_buffered_bc()方法从多播/广播缓冲区(bc_buf)中检索包。在图 12-3 中,你可以看到一个 AP,它包含一个站(sta_info对象)的链表,每个站都有自己的单播缓存(ps_tx_buf)和一个单独的bc_buf缓存,用于存储组播和广播数据包。

9781430261964_Fig12-03.jpg

图 12-3 。在 AP 中缓冲数据包

AP 在 mac80211 中被实现为一个ieee80211_if_ap对象。每个这样的ieee80211_if_ap对象都有一个名为ps(ps_data的一个实例)的成员,节电数据存储在其中。ps_data结构的成员之一是广播/组播缓冲器bc_buf

在图 12-4 中,你可以看到一个客户端发送的 PS-Poll 数据包的流程,目的是从 AP 单播缓冲区ps_tx_buf中检索数据包。请注意,除了最后一个数据包,AP 发送所有带有 IEEE 80211 _ FCTL _ 更多数据标志的数据包。因此,客户端知道它应该继续发送 PS-Poll 分组,直到缓冲区被清空。为了简单起见,此图中不包括 ACK 流量,但这里应该提到的是,数据包应该得到确认。

9781430261964_Fig12-04.jpg

图 12-4 。从客户端发送 PSPOLL 数据包,以从 AP 内的 ps_tx_buf 缓冲区中检索数据包

image 注意 电源管理省电模式是两个不同的话题。电源管理处理执行挂起(无论是挂起到 RAM 还是挂起到磁盘,也称为休眠,或者在某些情况下,挂起到 RAM 和挂起到磁盘,也称为混合挂起)的机器,并在net/mac80211/pm.c中处理。在驱动程序中,电源管理由恢复/挂起方法处理。另一方面,省电模式处理进入睡眠模式和唤醒的处理站;它与挂起和休眠无关。

本节描述了省电模式和缓冲机制。下一节讨论管理层及其处理的不同任务。

管理层(MLME)

802.11 管理架构中有三个组件:

  • 物理层管理实体(PLME)。
  • 系统管理实体(SME)。
  • MAC 层管理实体(MLME)。

扫描

扫描有两种:被动扫描和主动扫描。被动扫描意味着被动地监听信标,而不发送任何用于扫描的数据包。当执行被动扫描(扫描通道的标志包含 IEEE80211_CHAN_PASSIVE_SCAN)时,站点从一个通道移动到另一个通道,尝试接收信标。在一些更高的 802.11a 频带中需要被动扫描,因为在听到 AP 信标之前,你根本不允许传输任何东西。对于主动扫描,每个站发送一个探测请求包;这是一个管理数据包,带有子类型探测请求(IEEE80211_STYPE_PROBE_REQ)。同样通过主动扫描,站点从一个通道移动到另一个通道,在每个通道上发送一个探测请求管理包(通过调用ieee80211_send_probe_req()方法)。这是通过调用ieee80211_request_scan()方法来完成的。通过调用ieee80211_hw_config()方法,将 IEEE 80211 _ CONF _ 改变 _ 频道作为参数传递,可以改变频道。注意,在站点操作的信道和它操作的频率之间存在一一对应关系;在给定频道的情况下,ieee80211_channel_to_frequency()方法(net/wireless/util.c)返回电台运行的频率。

证明

通过调用ieee80211_send_auth()方法(net/mac80211/util.c)来完成认证。它发送带有认证子类型(IEEE80211_STYPE_AUTH)的管理帧。有许多认证类型;最初的 IEEE802.11 规范只谈到了两种形式:开放系统认证和共享密钥认证。IEEE802.11 规范要求的唯一强制身份验证方法是开放系统身份验证(WLAN_AUTH_OPEN)。这是一个非常简单的认证算法—事实上,它是一个空认证算法。任何请求使用该算法进行身份验证的客户端都将通过身份验证。认证算法的另一个选项的例子是共享密钥认证(WLAN_AUTH_SHARED_KEY)。在共享密钥身份验证中,工作站应该使用有线等效保密(WEP)密钥进行身份验证。

联合

为了关联,站发送带有关联子类型的管理帧(IEEE80211_STYPE_ASSOC_REQ)。关联是通过调用ieee80211_send_assoc()方法(net/mac80211/mlme.c)完成的。

重新组合

当一个站点在 ESS 内的接入点之间移动时,它被称为漫游。漫游站通过发送具有重新关联子类型的管理帧(IEEE80211 _ STYPE _ REASSOC _ REQ)向新的 AP 发送重新关联请求。重新关联是通过调用ieee80211_send_assoc()方法完成的;关联和重新关联之间有许多相似之处,因此该方法同时处理两者。此外,通过重新关联,如果成功,AP 会向客户端返回一个 AID(关联 ID)。

本节讨论了管理层(MLME)及其支持的一些操作,如扫描、认证、关联等等。在下一节中,我将描述一些 mac80211 实现细节,这些细节对于理解无线协议栈非常重要。

Mac80211 实现

Mac80211 有一个 API,用于与底层设备驱动程序接口。mac80211 的实现很复杂,充满了许多小细节。我无法给出 mac80211 API 和实现的详尽描述;我确实讨论了一些要点,可以为那些想要深入研究代码的人提供一个良好的起点。mac80211 API 的一个基本结构是ieee80211_hw结构(include/net/mac80211.h);它代表硬件信息。ieee80211_hwpriv(指向私有区域的指针)指针属于不透明类型(void *)。大多数无线设备驱动都为这个私有区域定义了一个私有结构,比如lbtf_private (Marvell 无线驱动)或者iwl_priv(英特尔的iwlwifi)。ieee80211_hw struct的内存分配和初始化由ieee80211_alloc_hw()方法完成。下面是一些与ieee80211_hw结构相关的方法:

  • int ieee80211_register_hw(struct ieee80211_hw *hw):由无线驱动调用,用于注册指定的ieee80211_hw对象。
  • void ieee80211_unregister_hw(struct ieee80211_hw *hw):注销指定的 802.11 硬件设备。
  • struct ieee80211_hw *ieee80211_alloc_hw(size_t priv_data_len, const struct ieee80211_ops *ops):分配一个ieee80211_hw对象并初始化。
  • ieee80211_rx_irqsafe():此方法用于接收数据包。它在net/mac80211/rx.c中实现,并从底层无线驱动程序中调用。

如您之前所见,传递给ieee80211_alloc_hw()方法的ieee80211_ops对象由指向驱动程序回调的指针组成。并非所有这些回调都必须由驱动程序实现。以下是对这些方法的简短描述:

  • tx() : 发送处理器调用每个发送的数据包。它通常返回 NETDEV_TX_OK(除了在某些有限的条件下)。
  • start() : 激活硬件设备,在第一个硬件设备启用前被调用。它打开帧接收。
  • stop() : 关闭帧接收,通常关闭硬件。
  • add_interface() : 当连接到硬件的网络设备启用时调用。
  • remove_interface() : 通知驱动程序接口正在关闭。
  • config():处理配置请求,如硬件通道配置。
  • configure_filter() : 配置设备的 Rx 滤波器。

图 12-5 显示了 Linux 无线子系统架构的框图。你可以看到无线设备驱动层和 mac80211 层之间的接口是ieee80211_ops对象及其回调。

9781430261964_Fig12-05.jpg

图 12-5 。Linux 无线架构

另一个重要的结构是sta_info struct ( net/mac80211/sta_info.h),代表一个车站。这个结构的成员包括各种统计计数器、各种标志、debugfs条目、用于缓冲单播包的ps_tx_buf数组等等。电台被组织在散列表(sta_hash)和列表(sta_list)中。与sta_info相关的重要方法如下:

  • int sta_info_insert(struct sta_info *sta):增加一个电台。
  • int sta_info_destroy_addr(struct ieee80211_sub_if_data *sdata, const u8 *addr):删除一个电台(通过调用__sta_info_destroy()方法)。
  • struct sta_info *sta_info_get(struct ieee80211_sub_if_data *sdata, const u8 *addr):取站;车站的地址(它是bssid)作为参数传递。

Rx 路径

ieee80211_rx()函数 ( net/mac80211/rx.c)是主接收处理程序。接收到的数据包的状态(ieee80211_rx_status)由无线驱动程序传递给嵌入在 SKB 控制缓冲器(cb)中的 mac80211。IEEE80211_SKB_RXCB()宏用于获取该状态。例如,Rx 状态的flag字段指定数据包的 FCS 检查是否失败(RX_FLAG_FAILED_FCS_CRC)。本章“快速参考”部分的表 12-1 中给出了flag字段的各种可能值。在ieee80211_rx()方法中,调用ieee80211_rx_monitor()删除 FCS(校验和)并删除无线接口处于监控模式时可能添加的无线报头(struct ieee80211_radiotap_header)。(例如,在嗅探的情况下,您可以在监控模式下使用网络接口。并非所有无线网络接口都支持监控模式,请参阅本章后面的“无线模式”一节。)

如果您使用 HT (802.11n),如果需要,您可以通过调用ieee80211_rx_reorder_ampdu()方法来执行 AMPDU 重新排序。然后调用__ieee80211_rx_handle_packet()方法,最终调用ieee80211_invoke_rx_handlers()方法。然后一个接一个地调用不同的接收处理程序(使用一个名为 CALL_RXH 的宏)。调用这些处理程序的顺序很重要。每个处理器检查它是否应该处理分组。如果它决定不处理这个包,那么你返回 RX_CONTINUE 并继续下一个处理程序。如果它决定它应该处理这个包,那么你返回 RX_QUEUED。

在某些情况下,处理程序会决定丢弃数据包;在这些情况下,它返回 RX_DROP_MONITOR 或 RX_DROP_UNUSABLE。例如,如果您收到一个 PS-Poll 数据包,而接收方的类型显示它不是 AP,则返回 RX_DROP_UNUSABLE。另一个例子:对于一个管理帧,如果 SKB 的长度小于最小值(24),则丢弃该数据包并返回 RX_DROP_MONITOR。或者如果该分组不是管理分组,则该分组也被丢弃并且 RX_DROP_MONITOR 被返回。下面是实现这一点的ieee80211_rx_h_mgmt_check()方法的代码片段:

ieee80211_rx_h_mgmt_check(struct ieee80211_rx_data *rx)
{
        struct ieee80211_mgmt *mgmt = (struct ieee80211_mgmt *) rx->skb->data;
        struct ieee80211_rx_status *status = IEEE80211_SKB_RXCB(rx->skb);

        . . .
        if (rx->skb->len < 24)
                return RX_DROP_MONITOR;

        if (!ieee80211_is_mgmt(mgmt->frame_control))
                return RX_DROP_MONITOR;
               .  .  .
}
(net/mac80211/rx.c)

Tx 路径

ieee80211_tx()方法是传输(net/mac80211/tx.c)的主要处理程序。首先,它调用__ieee80211_tx_prepare()方法,该方法执行一些检查并设置某些标志。然后它调用invoke_tx_handlers()方法,该方法一个接一个地调用各种传输处理程序(使用一个名为 CALL_TXH 的宏)。如果一个发送处理程序发现它不应该对数据包做任何事情,它返回 TX_CONTINUE,你继续下一个处理程序。如果它决定应该处理某个数据包,它返回 TX_QUEUED,如果它决定应该丢弃该数据包,它返回 TX_DROP。invoke_tx_handlers()方法在成功时返回 0。让我们简短地看一下ieee80211_tx()方法的实现:

static bool ieee80211_tx(struct ieee80211_sub_if_data *sdata,
                         struct sk_buff *skb, bool txpending,
                         enum ieee80211_band band)
{
        struct ieee80211_local *local = sdata->local;
        struct ieee80211_tx_data tx;
        ieee80211_tx_result res_prepare;
        struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
        bool result = true;
        int led_len;

执行健全性检查,如果 SKB 长度小于 10:

if (unlikely(skb->len < 10)) {
        dev_kfree_skb(skb);
        return true;
}

/* initialises tx */
led_len = skb->len;

res_prepare = ieee80211_tx_prepare(sdata, &tx, skb);

if (unlikely(res_prepare == TX_DROP)) {
        ieee80211_free_txskb(&local->hw, skb);
        return true;
} else if (unlikely(res_prepare == TX_QUEUED)) {
        return true;
}

调用 Tx 处理程序;如果一切正常,继续调用__ieee80211_tx()方法:

        . . .
        if (!invoke_tx_handlers(&tx))
                result = __ieee80211_tx(local, &tx.skbs, led_len,
                                        tx.sta, txpending);

        return result;
}
(net/mac80211/tx.c)

分裂

802.11 中的分片只针对单播包。每个站被分配一个碎片阈值大小(以字节为单位)。大于此阈值的数据包应该被分段。您可以通过减小碎片阈值大小,使数据包更小来减少冲突的数量。您可以通过运行iwconfig或检查相应的debugfs条目来检查站点的碎片阈值(参见本章后面的“Mac80211 debugfs一节)。您可以使用iwconfig命令设置碎片阈值;因此,例如,您可以通过以下方式将碎片阈值设置为 512 字节:

iwconfig wlan0 frag 512

每个片段都被确认。如果存在更多片段,片段头中的更多片段字段被设置为 1。每个片段都有一个片段号(帧控制的序列控制字段中的一个子字段)。接收器上的片段重组是根据片段编号完成的。发射器端的分段通过ieee80211_tx_h_fragment()方法(net/mac80211/tx.c)完成。接收器端的重组通过ieee80211_rx_h_defragment()方法(net/mac80211/rx.c)完成。分段与聚合(用于更高的吞吐量)是不兼容的,并且考虑到高速率和短(时间)分组,现在很少使用它。

Mac80211 调试程序

debugfs 是一种能够将调试信息导出到用户空间的技术。它在sysfs文件系统下创建条目。debugfs是一个专门用于调试信息的虚拟文件系统。对于 mac80211,处理 mac80211 debugfs大多在net/mac80211/debugfs.c。安装debugfs后,可以查看各种 mac802.11 统计和信息条目。安装debugfs是这样进行的:

mount -t debugfs none_debugs /sys/kernel/debug

image 注意在构建内核时必须设置 CONFIG_DEBUG_FS,以便能够挂载和使用debugfs

比如说你的phyphy0;以下是对/sys/kernel/debug/ieee80211/phy0下部分词条的讨论:

  • total_ps_buffered:这是 AP 为电台缓冲的数据包总数(单播和多播/广播)。对于单播,total_ps_buffered计数器增加ieee80211_tx_h_unicast_ps_buf(),对于多播或广播,ieee80211_tx_h_multicast_ps_buf()计数器增加。

  • /sys/kernel/debug/ieee80211/phy0/statistics下,有各种统计信息,例如:

  • frame_duplicate_count表示重复帧的数量。这个debugfs条目表示重复帧计数器dot11FrameDuplicateCount,其由ieee80211_rx_h_check()方法递增。

  • transmitted_frame_count表示发送的数据包数量。这个debugfs条目代表dot11TransmittedFrameCount;它通过ieee80211_tx_status()方法递增。

  • retry_count表示重发次数。这个debugfs条目代表dot11RetryCount;它也通过ieee80211_tx_status()方法递增。

  • fragmentation_threshold:碎片阈值的大小,以字节为单位。参见前面的“碎片化”部分。

  • /sys/kernel/debug/ieee80211/phy0/netdev:wlan0下,你有一些给出接口信息的条目;例如,如果接口处于站模式,则aid表示站的关联 id,assoc_tries表示站尝试执行关联的次数,bssid表示站的 bssid,依此类推。

  • 每个站都使用速率控制算法。它的名字由下面的debugfs条目导出:/sys/kernel/debug/ieee80211/phy1/rc/name

无线模式

您可以将无线网络接口设置为在多种模式下运行,具体取决于其预期用途和部署它的网络拓扑。在某些情况下,你可以用iwconfig命令设置模式,而在某些情况下,你必须使用像hostapd这样的工具。请注意,并非所有设备都支持所有模式。参见www.linuxwireless.org/en/users/Drivers获得支持不同模式的 Linux 驱动列表。或者,您也可以检查驱动程序代码中的wiphy成员的interface_modes字段(在ieee80211_hw对象中)被初始化为哪些值。interface_modes被初始化为nl80211_iftype enum的一个或多个模式,如 NL80211_IFTYPE_STATION 或 NL80211_IFTYPE_ADHOC(参见:include/uapi/linux/nl80211.h)。以下是这些无线模式的详细描述:

  • AP 模式: 在此模式下,设备充当 AP (NL80211_IFTYPE_AP)。AP 维护和管理相关站的列表。网络(BSS)名称是 AP 的 MAC 地址(bssid)。BSS 还有一个人可读的名称,称为 SSID。
  • 站基础架构模式: 基础架构模式下的管理站(NL80211_IFTYPE_STATION)。
  • 监控模式: 在监控模式(NL80211_IFTYPE_MONITOR)下,所有传入的数据包都是未经过滤的。这对嗅探很有用。通常可以在监控模式下传输数据包。这被称为包注入;这些数据包标有特殊标志(IEEE80211_TX_CTL_INJECTED)。
  • Ad Hoc (IBSS)模式:Ad Hoc(IBSS)网络中的一个站点(NL80211_IFTYPE_ADHOC)。在 Ad Hoc 模式下,网络中没有 AP 设备。
  • 无线分布系统(WDS)模式:WDS 网络中的一个站(NL80211_IFTYPE_WDS)。
  • 网状模式: 网状网络(NL80211_IFTYPE_MESH_POINT)中的一个站点,将在本章后面的“网状网络(802.11s)”一节中讨论。

下一节将讨论提供更高性能的 ieee802.11n 技术,以及它如何在 Linux 无线协议栈中实现。您还将了解 802.11n 中的块确认和数据包聚合,以及如何使用这些技术来提高性能。

高吞吐量(ieee802.11n)

802.11g 被批准后不久,在 IEEE 中创建了一个新的任务组,称为高吞吐量任务组(TGn) 。IEEE 802.11n 在 2009 年底成为最终规范。IEEE 802.11n 协议允许与传统设备共存。有一些厂商在官方批准之前已经销售了基于 802.11n 草案的 802.11n 预标准设备。Broadcom 开创了基于草案发布无线接口的先例。2003 年,它发布了基于 802.11g 草案的无线设备芯片组。遵循这一先例,早在 2005 年,一些供应商就发布了基于 802.11n 草案的产品。例如,英特尔 Santa Rose 处理器具有英特尔下一代 Wireless-N(英特尔 WiFI Link 5000 系列),支持 802.11n。其他英特尔无线网络接口,如 4965AGN,也支持 802.11n。其他供应商,包括 Atheros 和 Ralink,也发布了基于 802.11n 草案的无线设备。WiFi 联盟于 2007 年 6 月开始认证 802.11n 草案设备。一长串供应商发布了符合 Wi-Fi 认证的 802.11n 草案 2.0 的产品。

802.11n 可以在 2.4 GHz 和/或 5 GHz 频带上工作,而 802.11g 和 802.11b 仅在 2.4 GHz 射频频带上工作,802.11a 仅在 5 GHz 射频频带上工作。802.11n MIMO(多输入多输出)技术增加了无线覆盖区域内流量的范围和可靠性。MIMO 技术在接入点和客户端使用多个发射机和接收机天线,以支持同步数据流。结果是增加了范围和吞吐量。使用 802.11n,您可以实现高达 600 Mbps 的理论 PHY 速率(由于介质访问规则等原因,实际吞吐量会低得多)。

802.11n 为 802.11 MAC 层增加了许多改进。最广为人知的是分组聚合,它将多个应用数据分组连接成单个传输帧。添加了块确认(BA)机制(将在下一节讨论)。BA 允许单个数据包确认多个数据包,而不是为每个收到的数据包发送 ACK。两个连续分组之间的等待时间被缩短。这使得能够以单个分组的固定开销成本发送多个数据分组。BA 协议是在 2005 年的 802.11e 修正案中引入的。

分组聚合

有两种类型的数据包聚合:

  • AMSDU: 聚合 Mac 业务数据单元
  • AMPDU: 聚合 Mac 协议数据单元

注意,AMSDU 仅在 Rx 上受支持,在 Tx 上不受支持,并且完全独立于本节描述的块 Ack 机制;因此,本节中的讨论仅适用于 AMPDU。

块确认会话有两方:发起方接收方。每个块会话都有一个不同的流量标识符(TID)。发起者通过调用ieee80211_start_tx_ba_session()方法启动块确认会话。这通常是通过驱动器中的速率控制算法来完成的。例如,对于 ath9k 无线驱动程序,速率控制回调函数ath_tx_status()调用ieee80211_start_tx_ba_session()方法。ieee80211_start_tx_ba_session()方法将状态设置为 HT_ADDBA_REQUESTED_MSK,并通过调用ieee80211_send_addba_request()方法发送 ADDBA 请求包。对ieee80211_send_addba_request()的调用传递会话的参数,比如想要的重排序缓冲区大小和会话的 TID。

重排序缓冲区大小限制在 64K(参见include/linux/ieee80211.hieee80211_max_ampdu_length_exp的定义)。这些参数是结构addba_req中功能成员capab的一部分。对 ADDBA 请求的响应应该在 1 Hz 内被接收,这在 x86_64 机器中是一秒(ADDBA _ RESP _ 间隔)。如果您没有及时得到响应,sta_addba_resp_timer_expired()方法将通过调用___ieee80211_stop_tx_ba_session()方法来停止 BA 会话。当另一端(接收方)收到 ADDBA 请求时,它首先发送一个 ACK(IEEE 802.11 中的每个数据包都应该得到确认,如前所述)。然后它通过调用ieee80211_process_addba_request()方法处理 ADDBA 请求;如果一切正常,它将这台机器的聚合状态设置为 OPERATIONAL(HT _ AGG _ STATE _ OPERATIONAL)并通过调用ieee80211_send_addba_resp()方法发送 ADDBA 响应。它还通过调用该定时器上的del_timer_sync() 来停止响应定时器(将sta_addba_resp_timer_expired()方法作为其回调的定时器)。会话开始后,发送包含多个 MPDU 数据包的数据块。因此,发起者通过调用ieee80211_send_bar()方法发送一个块确认请求(BAR)包。

阻塞确认请求(BAR)

BAR 是具有块确认请求子类型(IEEE80211_STYPE_BACK_REQ)的控制包。BAR 包包括 SSN(起始序列号),它是块中应该被确认的最早的 MSDU 的序列号。如果需要,接收方接收 BAR 并相应地重新排序ampdu缓冲区。图 12-6 显示了一个条形请求。

9781430261964_Fig12-06.jpg

图 12-6 。酒吧请求

发送 BAR 时,帧控制中的type子字段是 control (IEEE80211_FTYPE_CTL),而subtype子字段是 Block Ack 请求(IEEE80211_STYPE_BACK_REQ)。该栏由ieee80211_bar结构表示:

struct ieee80211_bar {
        __le16 frame_control;
        __le16 duration;
        __u8 ra[6];
        __u8 ta[6];
        __le16 control;
        __le16 start_seq_num;
} __packed;
(include/linux/ieee80211.h)

RA 是接收方地址,TA 是发送方(发起方)地址。BAR 请求的控制字段包括 TID。

块确认

有两种类型的块确认:立即块确认和延迟块确认。图 12-7 显示了立即块确认。

9781430261964_Fig12-07.jpg

图 12-7 。立即块确认

立即块确认和延迟块确认之间的区别在于,对于延迟块确认,BAR 请求本身首先用确认来应答,然后经过一段延迟后,用 BA(块确认)来应答。当使用延迟块确认时,有更多的时间来处理 BAR,当使用基于软件的处理时,这有时是需要的。使用立即块确认在性能方面更好。广管局本身也承认。当发起者没有更多的数据要发送时,它可以通过调用ieee80211_send_delba()方法来终止 Block Ack 会话;此函数向另一端发送 DELBA 请求包。DELBA 请求由ieee80211_process_delba()方法处理。导致块确认会话拆除的 DELBA 消息可以从块确认会话的发起者或接收者发送。AMPDU 的最大长度是 65535 个八位字节。请注意,数据包聚合仅适用于接入点和受管站点;规范不支持 IBSS 的数据包聚合。

网状网络(802.11s)

IEEE 802.11s 协议于 2003 年 9 月作为 IEEE 的一个研究组开始,并于 2004 年成为一个名为 TGs 的任务组。2006 年,15 个提案中的 2 个提案(“SEEMesh”和“Wi-Mesh”提案)合并为一个提案,形成了 D0.01. 802.11s 草案,于 2011 年 7 月获得批准,现已成为 IEEE 802.11-2012 的一部分。网状网络允许在完全和部分连接的网状拓扑上创建 802.11 基本服务集。这可以看作是对 802.11 特设网络的改进,后者需要全连接的网状拓扑。图 12-8 和 12-9 说明了这两种网状拓扑之间的区别。

9781430261964_Fig12-08.jpg

图 12-8 。全目

在部分连接的网格中,节点只连接到其他一些节点,而不是所有节点。这种拓扑在无线网状网络中更为常见。图 12-9 显示了一个局部网格的例子。

9781430261964_Fig12-09.jpg

图 12-9 。偏目

无线网状网络在多个无线跳上转发数据分组。每个网格节点充当其他网格节点的中继点/路由器。在内核 2.6.26 (2008)中,由于 open80211s 项目,对无线网状网络(802.11s)草案的支持被添加到网络无线堆栈中。open80211s 项目的目标是创建 802.11s 的第一个开放实现。该项目得到了 OLPC 项目和一些商业公司的赞助。Luis Carlos Cobo 和 Javier Cardona 以及来自 Cozybit 的其他开发人员开发了 Linux mac80211 Mesh 代码。

现在,您已经了解了一些关于网状网络和网状网络拓扑的知识,可以开始下一节了,这一节将介绍网状网络的 HWMP 路由协议。

HWMP 议定书

802.11s 协议定义了名为 HWMP(混合无线网状协议)的默认路由协议。HWMP 协议在第 2 层工作,处理 MAC 地址,而 IPV4 路由协议在第 3 层工作,处理 IP 地址。HWMP 路由基于两种类型的路由(因此被称为混合)。第一种是按需路由,第二种是主动路由。这两种机制的主要区别在于启动路径建立的时间(路径是用于第 2 层路由的名称)。在按需路由中,只有在协议栈接收到目的地的帧后,协议才会建立到目的地的路径。这最小化了维护网状网络所需的管理流量,代价是在数据流量中引入了额外的等待时间。如果已知网格节点是大量网格流量的接收者,则可以使用主动路由。在这种情况下,节点将周期性地在网状网络上宣布它自己,并触发从网络中的所有网状节点到它自己的路径建立。按需路由和主动路由都在 Linux 内核中实现。有四种类型的路由消息:

  • PREQ(路径请求):这种类型的消息是在您寻找某个您仍然没有路线到达的目的地时作为广播发送的。该 PREQ 消息在网状网络中传播,直到它到达其目的地。在每个站点上执行查找,直到到达最终目的地(通过调用mesh_path_lookup()方法)。如果查找失败,PREQ 将被转发(作为广播)到其它站。PREQ 消息在管理分组中发送;它的子类型是 action (IEEE80211_STYPE_ACTION)。它由hwmp_preq_frame_process()方法处理。
  • PREP(路径回复):这种类型是作为对 PREQ 消息的回复而发送的单播数据包。此数据包在反向路径上发送。准备消息也在管理包中发送,其subtype也是动作子类型(IEEE80211_STYPE_ACTION)。它由hwmp_prep_frame_process()方法处理。PREQ 和准备消息都是通过mesh_path_sel_frame_tx()方法发送的。
  • PERR(路径错误):如果途中出现故障,将发送 PERR。PERR 消息由mesh_path_error_tx()方法处理。
  • RANN(根通告) : 根网格点周期性地广播该帧。接收它的网状点经由它从其接收 RANN 的 MP 向根发送单播 RREQ。作为响应,根网状网将向每个 PREQ 发送 PREP 响应。

image 注意该路由考虑了无线电感知度量(广播时间度量)。广播时间度量通过airtime_link_metric_get()方法计算(基于速率和其他硬件参数)。网状点持续监控其链路,并与邻居更新度量值。

发送 PREQ 的站点可能会尝试将数据包发送到最终目的地,但仍然不知道到达该目的地的路由;这些数据包保存在 skb 的一个名为frame_queue的缓冲区中,它是mesh_path对象(net/mac80211/mesh.h)的一个成员。在这种情况下,当 PREP 最终到达时,该缓冲区的未决数据包被发送到最终目的地(通过调用mesh_path_tx_pending()方法)。对于未解析的目的地,每个目的地缓冲的最大帧数为 10 (MESH_FRAME_QUEUE_LEN)。网状组网的优势如下:

  • 快速部署
  • 最低配置,价格低廉
  • 易于在难以布线的环境中部署
  • 节点移动时的连接
  • 更高的可靠性:无单点故障和自我修复能力

缺点如下:

  • 许多广播会限制网络性能。
  • 目前并非所有的无线驱动程序都支持网格模式。

设置网状网络

在 Linux 中有两套用于管理无线设备和网络的用户空间工具:一套是较老的用于 Linux 的无线工具,这是一个基于 IOCTLs 的开源项目。无线工具的命令行实用程序的例子有iwconfigiwlistifrename等等。较新的工具是基于通用 netlink 套接字的iw(在第二章的中描述)。但是,有些任务只有更新的工具iw才能执行。您可以仅使用iw命令将无线设备设置为在网状模式下工作。

示例:设置无线网络接口(wlan0)在网状模式下工作的方法如下:

iw wlan0 set type mesh

image 注意设置无线网络接口(wlan0)在网状模式下工作也可以这样做:iw wlan0 set type mp

mp代表网格点。参见http://wireless.kernel.org/en/users/Documentation/iw中的“添加带 iw 的接口”

通过iw wlan0 mesh join "my-mesh-ID"连接网格

您可以通过以下方式显示电台的统计信息:

  • iw wlan0 station dump
  • iw wlan0 mpath dump

这里我还应该提到authsaewpa_supplicant工具,它们可以用来创建安全的网状网络,并且不依赖于iw

Linux 无线开发过程

大多数开发都是使用git分布式版本控制系统完成的,就像许多其他 Linux 子系统一样。git树主要有三种;最危险的是无线测试树。还有常规无线树和无线下一个树。以下是开发树的git库的链接:

  • 无线测试开发树:

    git://git.kernel.org/pub/scm/linux/kernel/git/linville/wireless-testing.git
    
  • 无线开发树:

    git://git.kernel.org/pub/scm/linux/kernel/git/linville/wireless-2.6.git
    
  • 无线-下一个发展树:

    git://git.kernel.org/pub/scm/linux/kernel/git/linville/wireless-next-2.6.git
    
    

补丁在无线邮件列表中发送和讨论:linux-wireless@vger.kernel.org。不时地,一个拉请求被发送到内核网络邮件列表netdev,在第一章中提到。

正如在涉及 Mac80211 子系统的“mac80211 子系统”一节中提到的,一些无线网络接口供应商在他们自己的站点上为他们的 Linux 驱动程序维护他们自己的开发树。在某些情况下,他们使用的代码不使用 mac80211 API 比如一些雷凌和 Realtek 的无线设备驱动。自 2006 年 1 月以来,Linux 无线子系统的维护者是 John W. Linville,他取代了 Jeff Garzik。mac80211 的维护者是 Johannes Berg,2007 年 10 月。有一些年度 Linux 无线峰会;第一次发生在 2006 年的比弗顿。这里有一个非常详细的 wiki 页面:http://wireless.kernel.org/。这个网站包括许多重要的文档。例如,一个表格指定了每个无线网络接口支持的模式。这个 wiki 页面中有许多关于许多无线设备驱动程序、硬件和各种工具的信息(例如 CRDA、中央管理域代理、hostapdiw等等)。

摘要

近年来,在 Linux 无线堆栈方面已经做了很多开发。最显著的变化是 mac80211 堆栈的集成和移植无线驱动程序以使用 mac80211 API,使代码更有组织性。情况比以前好多了;Linux 支持更多的无线设备。由于 open802.11s 项目,网状网络最近得到了推动。它被集成在 Linux 2.6.26 内核中。未来可能会看到更多支持新标准 IEEE802.11ac 的驱动程序,IEEE 802.11 AC 是一种只有 5 GHz 的技术,最大吞吐量可以达到每秒 1 千兆比特以上,以及更多支持 P2P 的驱动程序。

第十三章讨论了 Linux 内核中的 InfiniBand 和 RDMA。“快速参考”部分涵盖了与本章中讨论的主题相关的主要方法,按其上下文排序。

快速参考

我用一个 Linux 无线子系统的重要方法的简短列表来结束这一章,其中一些在这一章中被提到。表 12-1 显示了ieee80211_rx_status对象的flag成员的各种可能值。

方法

本节讨论这些方法。

见 ieee80211_send_bar(结构 ieee80211_vif *vif,u8 *ra,u16 tid,u16 SSN);

此方法发送块确认请求。

int IEEE 80211 _ start _ tx _ ba _ session(struct IEEE 80211 _ sta * pubsta,u16 tid,u16 time out);

该方法通过调用无线驱动程序ampdu_action()回调,传递 IEEE80211_AMPDU_TX_START 来启动块确认会话。因此,驱动程序稍后将调用ieee80211_start_tx_ba_cb()回调或ieee80211_start_tx_ba_cb_irqsafe()回调,这将启动聚合会话。

int IEEE 80211 _ stop _ tx _ ba _ session(struct IEEE 80211 _ sta * publica,u16 tid);

该方法通过调用无线驱动程序ampdu_action()函数,传递 IEEE80211_AMPDU_TX_STOP 来停止块确认会话。驱动程序稍后必须调用ieee80211_stop_tx_ba_cb()回调或ieee80211_stop_tx_ba_cb_irqsafe()回调。

静态 void IEEE 80211 _ send _ addba _ request(struct IEEE 80211 _ sub _ if _ data * sdata,const u8 *da,u16 tid,u8 dialog_token,u16 start_seq_num,u16 agg_size,u16 time out);

此方法发送 ADDBA 消息。ADDBA 消息是管理动作消息。

void IEEE 80211 _ process _ addba _ request(struct IEEE 80211 _ local * local,struct sta_info *sta,struct ieee80211_mgmt *mgmt,size _ t len);

该方法处理 ADDBA 消息。

静态 void IEEE 80211 _ send _ addba _ resp(struct IEEE 80211 _ sub _ if _ data * sdata,u8 *da,u16 tid,u8 dialog_token,u16 status,u16 policy,u16 buf_size,u16 time out);

此方法发送 ADDBA 响应。ADDBA 响应是一个管理包,带有动作的subtype(IEEE 80211 _ STYPE _ ACTION)。

静态 IEEE 80211 _ rx _ result debug _ no inline IEEE 80211 _ rx _ h _ amsdu(struct IEEE 80211 _ rx _ data * rx);

该方法处理 AMSDU 聚合(Rx 路径)。

void IEEE 80211 _ process _ delba(struct IEEE 80211 _ sub _ if _ data * sdata,struct sta_info *sta,struct ieee80211_mgmt *mgmt,size _ t len);

这个方法处理 DELBA 消息。

void IEEE 80211 _ send _ delba(struct IEEE 80211 _ sub _ if _ data * sdata,const u8 *da,u16 tid,u16 initiator,u16 reason _ code);

该方法发送 DELBA 消息。

void IEEE 80211 _ rx _ IRQ safe(struct IEEE 80211 _ HW * HW,struct sk _ buff * skb);

此方法接收数据包。可以在硬件中断上下文中调用ieee80211_rx_irqsafe()方法。

静态 void IEEE 80211 _ rx _ reorder _ ampdu(struct IEEE 80211 _ rx _ data * rx,struct sk _ buff _ head * frames);

这个方法处理 MPDU 重排序缓冲区。

静态 bool IEEE 80211 _ sta _ manage _ reorder _ buf(struct IEEE 80211 _ sub _ if _ data * sdata,struct tid_ampdu_rx *tid_agg_rx,struct sk _ buff _ head * frames);

这个方法处理 MPDU 重排序缓冲区。

静态 IEEE 80211 _ rx _ result debug _ no inline IEEE 80211 _ rx _ h _ check(struct IEEE 80211 _ rx _ data * rx);

该方法丢弃重传的重复帧,并增加dot11FrameDuplicateCount和站num_duplicates计数器。

void IEEE 80211 _ send _ null func(struct IEEE 80211 _ local * local,struct IEEE 80211 _ sub _ if _ data * sdata,int power save);

这个方法发送一个特殊的空数据帧。

void IEEE 80211 _ send _ pspoll(struct IEEE 80211 _ local * local,struct IEEE 80211 _ sub _ if _ data * sdata);

该方法向 AP 发送 PS-Poll 控制分组。

静态 void IEEE 80211 _ send _ assoc(struct IEEE 80211 _ sub _ if _ data * sdata);

此方法通过发送关联子类型分别为 IEEE80211_STYPE_ASSOC_REQ 或 IEEE80211 _ STYPE _ REASSOC _ REQ 的管理数据包来执行关联或重新关联。从ieee80211_do_assoc()方法调用ieee80211_send_assoc()方法。

void IEEE 80211 _ send _ auth(struct IEEE 80211 _ sub _ if _ data * sdata,u16 transaction,u16 auth_alg,u16 status,const u8 *extra,size_t extra_len,const u8 *bssid,const u8 *key,u8 key_len,u8 key_idx,u32 tx _ flags);

该方法通过发送具有认证子类型(IEEE80211_STYPE_AUTH)的管理包来执行认证。

静态内联 bool IEEE 80211 _ check _ Tim(const struct IEEE 80211 _ Tim _ ie * Tim,u8 tim_len,u16 aid);

该方法检查是否设置了tim[aid];aid 作为一个参数传递,它表示站点的关联 id。

int IEEE 80211 _ request _ scan(struct IEEE 80211 _ sub _ if _ data * sdata,struct CFG 80211 _ scan _ request * req);

此方法启动主动扫描。

void mesh _ path _ tx _ pending(struct mesh _ path * mpath);

这个方法从frame_queue发送数据包。

struct mesh _ path * mesh _ path _ lookup(struct IEEE 80211 _ sub _ if _ data * sdata,const u8 * dst);

该方法在网格点的网格路径表(路由表)中执行查找。mesh_path_lookup()方法的第二个参数是目标的硬件地址。如果表中没有条目,则返回 NULL,否则返回一个指向找到的网格路径结构的指针。

静态 void IEEE 80211 _ sta _ create _ ibss(struct IEEE 80211 _ sub _ if _ data * sdata);

此方法创建一个 IBSS。

int IEEE 80211 _ HW _ config(struct IEEE 80211 _ local * local,u32 已更改);

驱动程序为各种配置调用此方法;在大多数情况下,它将调用委托给driver config()方法,如果实现的话。第二个参数指定要采取的操作(例如,IEEE 80211 _ CONF _ 改变 _ 频道以改变频道,或 IEEE 80211 _ CONF _ 改变 _PS 以改变驱动程序的节能模式)。

struct IEEE 80211 _ HW * IEEE 80211 _ alloc _ HW(size _ t priv _ data _ len,const struct IEEE 80211 _ ops * ops);

此方法分配新的 802.11 硬件设备。

int ieee80211 _ 寄存器 _ 硬件(struct ieee80211 _ 硬件*硬件);

此方法注册 802.11 硬件设备。

void IEEE 80211 _ unregister _ HW(struct IEEE 80211 _ HW * HW);

此方法注销 802.11 硬件设备并释放其分配的资源。

int sta _ info _ insert(struct sta _ info * sta):

此方法将电台添加到电台哈希表和电台列表中。

int sta _ info _ destroy _ addr(struct IEEE 80211 _ sub _ if _ data * sdata,const u8 * addr);

此方法删除一个工作站并释放其资源。

struct sta _ info * sta _ info _ get(struct IEEE 80211 _ sub _ if _ data * sdata,const u8 * addr);

此方法通过在站点的哈希表中执行查找来返回指向站点的指针。

void IEEE 80211 _ send _ probe _ req(struct IEEE 80211 _ sub _ if _ data * sdata,u8 *dst,const u8 *ssid,size_t ssid_len,const u8 *ie,size_t ie_len,u32 ratemask,bool directed,u32 tx_flags,struct IEEE 80211 _ channel * channel,bool scan);

该方法发送探测请求管理包。

静态内联 void IEEE 80211 _ tx _ skb(struct IEEE 80211 _ sub _ if _ data * sdata,struct sk _ buff * skb);

此方法传输一个 SKB。

int IEEE 80211 _ channel _ to _ frequency(int chan,enum ieee80211_band 频带);

这个方法返回一个站点工作的频率,给定它的信道。信道和频率之间是一一对应的。

static int mesh _ path _ sel _ frame _ tx(enum mpath _ frame _ type action,u8 flags,const u8 *orig_addr,_le32 orig_sn,u8 *target,const u8 *da, _ le32 hop _ count,u8 ttl,__le32 lifetime,__le32 metric,__le32 preq_id,struct ieee80211 _ sub _ if _ data *

此方法发送 PREQ 或 PREP 管理数据包。

静态 void hwmp _ preq _ frame _ process(struct IEEE 80211 _ sub _ if _ data * sdata,struct ieee80211_mgmt *mgmt,const u8 *preq_elem,u32 metric);

此方法处理 PREQ 消息。

struct IEEE 80211 _ rx _ status * IEEE 80211 _ SKB _ RXCB(struct sk _ buff * skb);

该方法返回与控制缓冲区(cb)关联的ieee80211_rx_status对象,该控制缓冲区与指定的 SKB 关联。

静态 bool IEEE 80211 _ tx(struct IEEE 80211 _ sub _ if _ data * sdata,struct sk_buff *skb,bool txpending,enum IEEE 80211 _ band band);

这个方法是传输的主要处理程序。

桌子

表 12-1 显示了ieee80211_rx_status结构的标志成员(一个 32 位字段)的位和相应的 Linux 符号。

表 12-1。Rx Flags:IEEE 80211 _ Rx _ status 对象的标志字段的各种可能值

|

Linux 符号

|

少量

|

描述

|
| --- | --- | --- |
| 接收标志 MMIC 错误 | Zero | 在此帧中报告了 Michael MIC 错误。 |
| RX _ FLAG _ 解密 | one | 这个帧是用硬件解密的。 |
| RX _ FLAG _ MMIC _ 剥离 | three | 迈克尔麦克风从这个框架中剥离,硬件已经完成验证。 |
| RX_FLAG_IV_STRIPPED | four | IV/ICV 从该帧中被剥离。 |
| RX_FLAG_FAILED_FCS_CRC | five | 帧上的 FCS 检查失败。 |
| RX_FLAG_FAILED_PLCP_CRC | six | 对框架的 PCLP 检查失败。 |
| rx _ flag _MACTIME_START-rx _ 旗标 _ MAC time _ start | seven | 在 RX 状态中传递的时间戳是有效的,并且包含接收到 MPDU 的第一个符号的时间。 |
| RX_FLAG_SHORTPRE 函数 | eight | 该帧使用了短前导码。 |
| RX_FLAG_HT | nine | 使用 HT MCS,rate_idx是 MCS 索引 |
| RX_FLAG_40MHZ | Ten | 使用 HT40 (40 MHz)。 |
| RX_FLAG_SHORT_GI | Eleven | 使用了短保护间隔。 |
| rx _ flag _ no _ signal _ val-rx _ flag _ no _ signal _ val-rx _ flag _ no _ signal _ val-rx _ flag _ no _ signal _ val | Twelve | 信号强度值不存在。 |
| RX_FLAG_HT_GF | Thirteen | 该帧是在 HT-greenfield 传输中接收的 |
| rx _ flag _ ampdu _ 详细信息 | Fourteen | A-MPDU 的详细资料是已知的,特别是参考编号必须填写,并且是每个 A-MPDU 的唯一编号。 |
| RX_FLAG_AMPDU_REPORT_ZEROLEN | Fifteen | 驱动程序报告长度为 0 的子帧。 |
| RX_FLAG_AMPDU_IS_ZEROLEN | Sixteen | 这是一个零长度子帧,仅用于监控目的。 |
| RX_FLAG_AMPDU_LAST_KNOWN | Seventeen | 最后一个子帧是已知的,应该在单个 A-MPDU 的所有子帧上设置。 |
| RX_FLAG_AMPDU_IS_LAST | Eighteen | 这个子帧是 A-MPDU 的最后一个子帧。 |
| RX_FLAG_AMPDU_DELIM_CRC_ERROR | Nineteen | 在此子帧上检测到分隔符 CRC 错误。 |
| RX_FLAG_AMPDU_DELIM_CRC_KNOWN | Twenty | 定界符 CRC 字段是已知的(CRC 存储在ieee80211_rx_statusampdu_delimiter_crc字段中) |
| rx _ flag _MACTIME_END-rx _ 旗标 _ MAC time _ end | Twenty-one | 在接收状态中传递的时间戳是有效的,并且包含接收到 MPDU(包括 FCS)的最后一个符号的时间。 |
| S7-1200 可编程控制器 | Twenty-two | 使用 VHT MCS,rate_index是 MCS 指标 |
| RX_FLAG_80MHZ | Twenty-three | 使用了 80 MHz |
| RX_FLAG_80P80MHZ | Twenty-four | 使用了 80+80 MHz |
| RX_FLAG_160MHZ | Twenty-five | 使用了 160 MHz |

十三、InfiniBand

本章由 InfiniBand 专家 Dotan Barak 撰写。Dotan 是 Mellanox Technologies 的高级软件经理,在 RDMA 技术公司工作。Dotan 已经在 Mellanox 工作了 10 多年,担任过各种角色,包括开发人员和经理。此外,Dotan 还维护着一个关于 RDMA 技术的博客: http://www.rdmamojo.com

第十二章讲述了无线子系统及其在 Linux 中的实现。在这一章中,我将讨论 InfiniBand 子系统及其在 Linux 中的实现。尽管对于不熟悉 InfiniBand 技术的人来说,InfiniBand 技术可能被认为是一种非常复杂的技术,但是它背后的概念却非常简单,这一点您将在本章中看到。我将从远程直接内存访问(RDMA)开始我们的讨论,并讨论它的主要数据结构和它的 API。我将给出一些例子来说明如何使用 RDMA,并以一个关于从内核级和用户空间使用 RDMA API 的简短讨论来结束本章。

RDMA 和 InfiniBand—概述

远程直接内存访问(RDMA) 是一台机器访问——即读取或写入——远程机器上内存的能力。有几个主要的网络协议支持 RDMA: InfiniBand、融合以太网 RDMA(RoCE)和互联网广域 RDMA 协议(iWARP),它们都共享相同的 API。InfiniBand 是一种全新的网络协议,其规范可以在“InfiniBand 架构规范”文档中找到,该文档由 InfiniBand 贸易协会(IBTA)维护。 RoCE 允许您通过以太网实现 RDMA,其规范可以在 InfiniBand 规范的附件中找到。iWARP 是一种允许在 TCP/IP 上使用 RDMA 的协议,其规范可以在由 RDMA 联盟维护的文档“RDMA 协议规范”中找到。动词是从客户端代码使用 RDMA 的 API 的描述。在版本 2.6.11 中,RDMA API 实现被引入到 Linux 内核中。最初,它只支持 InfiniBand,在几个内核版本之后,它也加入了 iWARP 和 RoCE 支持。在描述 API 时,我只提到其中的一种,但下面的文字是指所有的。这个 API 的所有定义都可以在include/rdma/ib_verbs.h中找到。以下是关于 API 和 RDMA 堆栈实现的一些说明:

  • 有些函数是内联函数,有些不是。未来的实现可能会改变这种行为。

  • 大多数 API 都有前缀“IB”;但是,这个 API 支持 InfiniBand、iWARP 和 RoCE。

  • 标题ib_verbs.h包含以下人员使用的功能和结构:

  • RDMA 堆栈本身

  • RDMA 设备的低级驱动程序

  • 使用堆栈作为消费者的内核模块

我将集中讨论只与使用堆栈作为消费者的内核模块相关的函数和结构(第三种情况)。下一节讨论内核树中的 RDMA 堆栈组织。

RDMA 堆栈组织

几乎所有的内核 RDMA 堆栈代码都在内核树的drivers/infiniband下。以下是它的一些重要模块(这不是一个详尽的列表,因为我在本章中没有涵盖整个 RDMA 堆栈):

  • CM: 沟通经理(drivers/infiniband/core/cm.c)
  • IPoIB:IP over InfiniBand(drivers/infiniband/ulp/ipoib/)
  • 伊瑟: iSCSI 扩展为 RDMA ( drivers/infiniband/ulp/iser/)
  • RDS: 可靠数据报套接字(net/rds/)
  • SRP: SCSI RDMA 协议(drivers/infiniband/ulp/srp/)
  • 不同厂商的硬件底层驱动(drivers/infiniband/hw)
  • 动词:核心动词(drivers/infiniband/core/verbs.c)
  • uverbs: 用户动词(drivers/infiniband/core/uverbs_*.c)
  • MAD: 管理数据报(drivers/infiniband/core/mad.c)

图 13-1 显示了 Linux InfiniBand 栈架构。

9781430261964_Fig13-01.jpg

图 13-1 。Linux Infiniband 堆栈架构

在这一节中,我介绍了 RDMA 堆栈结构以及 Linux 内核中的内核模块。

RDMA 的技术优势

在这里,我将介绍 RDMA 技术的优势,并解释使其在许多市场广受欢迎的特性:

  • 零拷贝: 直接向远程存储器写入数据和从远程存储器读取数据的能力允许您直接访问远程缓冲区,而无需在不同的软件层之间进行拷贝。
  • 内核旁路: 从代码的同一个上下文(即用户空间或内核级)发送和接收数据节省了上下文切换时间。
  • CPU 卸载: 使用专用硬件发送或接收数据而无需任何 CPU 干预的能力允许减少远程端 CPU 的使用,因为它不执行任何活动操作。
  • 低延迟: RDMA 技术让你的短消息达到非常低的延迟。(在当前的硬件和服务器上,发送几十个字节的延迟可能会达到几百纳秒。)
  • 高带宽: 在一个以太网设备中,最大带宽受技术限制(即 10 或 40 Gbits/sec)。在 InfiniBand 中,相同的协议和设备可以在 2.5 千兆位/秒到 120 千兆位/秒的范围内使用。(在当前的硬件和服务器上,带宽最高可达 56 千兆位/秒。)

InfiniBand 硬件组件

与任何其他互连技术一样,在 InfiniBand 中,规范中描述了几个硬件组件,其中一些是数据包的端点(生成数据包和数据包的目标),一些在同一子网或不同子网之间转发数据包。这里我将介绍最常见的几种:

  • 主机通道适配器(HCA ): 可以放置在主机或任何其他系统(如存储设备)上的网络适配器。该组件发起数据包或者是数据包的目标。
  • 交换机 : 知道如何从一个端口接收数据包并将其发送到另一个端口的组件。如果需要,它可以复制多播消息。(InfiniBand 不支持广播。)与其他技术不同,每个交换机都是一个非常简单的设备,带有由子网管理器(SM)配置的转发表,SM 是一个配置和管理子网的实体(在本节的后面,我将更详细地讨论它的作用)。交换机不会自己学习任何东西,也不会解析和分析数据包;它只在同一子网内转发数据包。
  • 路由器 : 连接多个不同 InfiniBand 子网的组件。

子网是一组连接在一起的 HCA、交换机和路由器端口。在本节中,我描述了 InfiniBand 中的各种硬件组件,现在我将讨论 InfiniBand 中的设备、系统和端口的寻址。

在 InfiniBand 中寻址

以下是关于 InfiniBand 寻址的一些规则和一个示例:

  • 在 InfiniBand 中,组件的唯一标识符是全球唯一标识符(GUID),它是一个 64 位的值,在世界上是唯一的。
  • 子网中的每个节点都有一个节点 GUID。这是节点的标识符,也是节点的常量属性。
  • 子网中的每个端口,包括 HCA 和交换机中的端口,都有一个端口 GUID。这是端口的标识符,也是端口的常量属性。
  • 在由几个组件组成的系统中,可以有一个系统 GUID。该系统中的所有组件都具有相同的系统 GUID。

这里有一个例子演示了前面提到的所有 GUIDs:一个由几个交换芯片组合而成的大型交换系统。每个交换芯片都有一个唯一的节点 GUID。每个交换机中的每个端口都有一个唯一的端口 GUID。该系统中的所有芯片都具有相同的系统 GUID。

  • 全球标识符(GID) 用于标识结束端口或多播组。在索引 0 的 GID 表中,每个端口至少有一个有效的 GID。它基于端口 GUID 加上该端口所属的子网标识符。
  • 本地标识符(LID) 是由子网管理器分配给每个子网端口的 16 位值。交换机是一个例外,交换机管理端口具有 LID 分配,而不是其所有端口。每个端口只能分配一个 LID 或一系列连续的 LID,以便有多条路径到达该端口。在同一子网中的特定时间点,每个 LID 都是唯一的,交换机在转发数据包时使用 LID 来确定使用哪个出口端口。单播 LID 的范围是 0x001 至 0xbfff。多播 LIDs 范围是 0xc000 到 0xfffe。

InfiniBand 功能

这里我们将介绍 InfiniBand 协议的一些特性:

  • InfiniBand 允许您配置 HCA、交换机和路由器的端口分区,并允许您在同一物理子网内提供虚拟隔离。每个分区键(P_Key)都是一个 16 位的值,由以下各项组合而成:15 个 LSB 是键值,msb 是成员级别;0 是受限成员;1 是正式会员。每个端口都有一个由 SM 配置的 P_Key 表,每个队列对(QP,InfiniBand 中发送和接收数据的实际对象)都与该表中的一个 P_Key 索引相关联。一个 QP 只有在与其相关联的 P_Keys 中满足以下条件时,才能发送或接收来自远程 QP 的数据包:

  • 键值相等。

  • 其中至少有一个是正式会员。

  • 队列密钥(Q_Key): 一个不可靠的数据报(UD) QP 只有当报文的 Q_Key 等于这个 UD QP 的 Q_Key 值时,才会从一个远程 UD QP 得到单播或组播报文。

  • 虚拟通道(VL): 这是一种在单个物理链路上创建多个虚拟链路的机制。每个虚拟通道代表一组用于在每个端口发送和接收数据包的自主缓冲器。支持的 VLs 数量是端口的一个属性。

  • 服务级别(SL): InfiniBand 最多支持 16 个服务级别。该协议没有指定每个级别的策略。在 InfiniBand 中,使用 SL 到 VL 的映射和每个 VL 的资源来实现 QoS。

  • 故障转移: 连接的 QP 是只能向一个远程 QP 发送数据包或从其接收数据包的 qp。InfiniBand 允许为连接的 qp 定义主路径和备用路径。如果主路径出现问题,将自动使用备用路径,而不是报告错误。

在下一节中,我们将看看 InfiniBand 中的数据包是什么样子的。这在您调试 InfiniBand 中的问题时非常有用。

InfiniBand 数据包

InfiniBand 中的每个数据包都是几个报头的组合,在许多情况下,还有一个有效载荷,即客户端想要发送的消息数据。仅包含 ACK 或零字节消息的消息(例如,如果仅发送即时数据)将不包含有效载荷。这些报头描述了数据包发送的位置、数据包的目标、使用的操作、将数据包分成消息所需的信息以及检测数据包丢失错误所需的足够信息。

图 13-2 展示了 InfiniBand 数据包报头。

9781430261964_Fig13-02.jpg

图 13-2 。InfiniBand 数据包报头

以下是 InfiniBand 中的标头:

  • 本地路由头(LRH): 8 字节。永远存在。它标识数据包的本地源端口和目的端口。它还指定消息的请求 QoS 属性(SL 和 VL)。
  • 全局路由头(GRH): 40 字节。可选。存在于多播数据包或在多个子网中传输的数据包。它使用 GID 描述源端口和目的端口。其格式与 IPv6 报头相同。
  • 基础传输头(BTH): 12 字节。永远存在。它指定了源和目的 QPs、操作、包序列号和分区。
  • 扩展传输头(ETH): 从 4 到 28 个字节。可选。可能存在的额外标头系列,具体取决于服务的类别和所使用的操作。
  • 有效载荷: 可选。客户端想要发送的数据。
  • 即时数据: 4 字节。可选。可添加到发送和 RDMA 写操作的带外 32 位值。
  • 不变 CRC (ICRC): 4 字节。永远存在。它涵盖了数据包在子网中传输时不应更改的所有字段。
  • 变体 CRC (VCRC): 2 字节。永远存在。它覆盖了数据包的所有字段。

管理实体

SM 是子网中负责分析和配置子网的实体。以下是它的一些使命:

  • 发现子网的物理拓扑。
  • 为子网中的每个端口分配 lid 和其他属性,如活动 MTU、活动速度等。
  • 在子网交换机中配置转发表。
  • 检测拓扑中的任何变化(例如,是否在子网中添加或删除了新节点)。
  • 处理子网中的各种错误。

子网管理器通常是一个软件实体,可以运行在交换机(称为管理交换机)或子网中的任何节点上。

几个 SMs 可以在一个子网中运行,但其中只有一个是活动的,其余的将处于待机模式。有一个内部协议来执行主机选择并决定哪个 SM 将是活动的。如果活动 SM 关闭,备用 SM 之一将成为活动 SM。子网中的每个端口都有一个子网管理代理(SMA),,它是一个知道如何接收 SM 发送的管理消息、处理它们并返回响应的代理。子网管理员(SA)是 SM 的一部分。以下是它的一些使命:

  • 提供有关子网的信息,例如,有关如何从一个端口到达另一个端口的信息(即路径查询)。
  • 允许您注册以获得事件通知。
  • 提供子网管理服务,如加入或离开多播。这些服务可能导致 SM(重新)配置子网。

通信管理器(CM) 是一个能够在每个端口上运行的实体,如果该端口支持的话,以建立、维护和拆除 QP 连接。

RDMA 资源公司

在 RDMA API 中,在发送或接收任何数据之前,需要创建和处理大量资源。所有资源都在特定 RDMA 设备的范围内,这些资源不能在多个本地设备之间共享或使用,即使同一台机器上有多个设备也是如此。图 13-3 展示了 RDMA 资源创建层次结构。

9781430261964_Fig13-03.jpg

图 13-3 。RDMA 资源创造层级

RDMA 装置

客户端需要向 RDMA 堆栈注册,以便在系统中添加或删除任何 RDMA 设备时得到通知。初始注册后,所有现有的 RDMA 设备都会通知客户端。每个 RDMA 设备都将被调用一个回调,客户端可以通过以下方式开始使用这些设备:

  • 查询设备的各种属性
  • 修改设备属性
  • 创建、使用和销毁资源

ib_register_client()方法注册一个想要使用 RDMA 堆栈的内核客户端。将为当前存在于系统中的每个新的 InfiniBand 设备调用指定的回调,这些新的 InfiniBand 设备将被添加到系统中或从系统中移除(使用热插拔功能)。ib_unregister_client()方法注销想要停止使用 RDMA 堆栈的内核客户端。通常,在卸载驱动程序时调用它。下面是一个示例代码,展示了如何在内核客户端中注册 RDMA 堆栈:

static void my_add_one(struct ib_device *device)
{
...
}

static void my_remove_one(struct ib_device *device)
{
...
}

static struct ib_client my_client = {
    .name   = "my RDMA module",
    .add    = my_add_one,
    .remove = my_remove_one
};

static int __init my_init_module(void)
{
    int ret;

    ret = ib_register_client(&my_client);
    if (ret) {
        printk(KERN_ERR "Failed to register IB client\n");
        return ret;
    }

    return 0;
}

static void __exit my_cleanup_module(void)
{
    ib_unregister_client(&my_client);
}

module_init(my_init_module);
module_exit(my_cleanup_module);

以下是对处理 InfiniBand 设备的其他几种方法的描述。

  • ib_set_client_data()方法将客户端上下文设置为与 InfiniBand 设备相关联。
  • ib_get_client_data()方法使用ib_set_client_data()方法返回与 InfiniBand 设备相关联的客户端上下文。
  • ib_register_event_handler()方法为 InfiniBand 设备将要发生的每个异步事件注册一个要调用的回调。回调结构必须用 INIT_IB_EVENT_HANDLER 宏初始化。
  • ib_unregister_event_handler()方法注销事件处理程序。
  • ib_query_device()方法查询 InfiniBand 设备的属性。这些属性是不变的,不会在这个方法的后续调用中改变。
  • ib_query_port()方法查询 InfiniBand 设备端口的属性。其中一些属性是不变的,一些属性可能会在随后调用该方法时发生变化,例如,端口 LID、state 和其他一些属性。
  • rdma_port_get_link_layer()方法返回设备端口的链路层。
  • ib_query_gid()方法在特定索引中查询 InfiniBand 设备端口的 GID 表。ib_find_gid()方法返回端口的 GID 表中特定 GID 值的索引。
  • ib_query_pkey()方法在特定索引中查询 InfiniBand 设备端口的 P_Key 表。ib_find_pkey()方法返回端口的 P_Key 表中特定 P_Key 值的索引。

保护域(PD)

一个 PD 允许与其他几个 RDMA 资源相关联,例如 SRQ、QP、AH 或 MR,以便在它们之间提供一种保护手段。与 PDx 相关联的 RDMA 资源不能使用与 PDy 相关联的 RDMA 资源。试图混合这些资源将导致错误。通常,每个模块都有一个 PD。然而,如果一个特定的模块想要增加它的安全性,它将为它使用的每个远程 QP 或服务使用一个 PD。PD 的分配和取消分配是这样完成的:

  • 方法分配一个 PD。它将注册后调用驱动程序回调时返回的设备对象的指针作为参数。
  • ib_dealloc_pd()方法释放一个 PD。它通常在卸载驱动程序或销毁与 PD 相关的资源时调用。

地址句柄(AH)

在 UD QP 的发送请求中使用 AH 来描述消息从本地端口到远程端口的路径。如果所有 qp 使用相同的属性向相同的远程端口发送消息,则相同的 AH 可以用于多个 qp。以下是对与 AH 相关的四种方法的描述:

  • 方法创建了一个 AH。它将 PD 和 AH 的属性作为参数。AH 的 AH 属性可以直接填充,也可以通过调用ib_init_ah_from_wc()方法来填充,该方法将接收到的工作完成(ib_wc对象)作为一个参数,该参数包括成功完成的传入消息的属性,以及接收该消息的端口。我们可以调用ib_create_ah_from_wc()方法,而不是先调用ib_init_ah_from_wc()方法,然后再调用ib_create_ah()方法。
  • ib_modify_ah()方法修改现有 AH 的属性。
  • ib_query_ah()方法查询现有 AH 的属性。
  • 方法销毁一个 AH。当不需要向 AH 描述路径的节点发送任何进一步的消息时,就调用它。

存储区

RDMA 设备访问的每个内存缓冲区都需要注册。在注册过程中,在存储缓冲器上执行以下任务:

  • 将连续的内存缓冲区分隔成内存页面。
  • 将完成虚拟到物理转换的映射。
  • 检查存储器页面权限以确保它们支持 MR 的请求权限。
  • 内存页面被固定,以防止它们被换出。这保持了虚拟到物理的映射不变。

成功完成内存注册后,它有两个密钥:

  • 本地键(lkey): 本地工作请求访问该内存的键。
  • 远程键(rkey): 远程机器使用 RDMA 操作访问该存储器的键。

当引用这些内存缓冲区时,这些键将在工作请求中使用。即使使用不同的权限,相同的内存缓冲区也可以注册多次。以下是与 MR 相关的一些方法的描述:

  • ib_get_dma_mr()方法返回一个可用于 DMA 的系统内存区域。它将 PD 和 MR 请求的访问权限作为参数。
  • ib_dma_map_single()方法将由kmalloc()方法族分配的内核虚拟地址映射到 DMA 地址。这个 DMA 地址将用于访问本地和远程存储器。应该使用ib_dma_mapping_error()方法来检查映射是否成功。
  • ib_dma_unmap_single()方法取消了使用ib_dma_map_single()完成的 DMA 映射。当不再需要这个内存时,应该调用它。

image 注意有更多种类的ib_dma_map_single()允许页面映射,根据 DMA 属性映射,使用分散/聚集列表映射,或使用具有 DMA 属性的分散/聚集列表映射:ib_dma_map_page()ib_dma_map_single_attrs()ib_dma_map_sg()ib_dma_map_sg_attrs()。都有对应的 unmap 函数。

在访问 DMA 映射存储器之前,应调用以下方法:

  • ib_dma_sync_single_for_cpu()如果 DMA 区域将被 CPU 访问,或者ib_dma_sync_single_for_device()如果 DMA 区域将被 InfiniBand 设备访问。
  • ib_dma_alloc_coherent()方法分配一个可以被 CPU 访问的内存块,并将其映射到 DMA。
  • ib_dma_free_coherent()方法释放使用ib_dma_alloc_coherent()分配的内存块。
  • ib_reg_phys_mr()方法获取一组物理页面,注册它们,并准备一个可以被 RDMA 设备访问的虚拟地址。如果你想在创建后改变它,你应该调用ib_rereg_phys_mr()方法。
  • ib_query_mr()方法检索特定 MR 的属性,注意大多数低级驱动程序不实现这个方法。
  • ib_dereg_mr()方法注销 MR。

快速内存区域(FMR)池

内存区域的注册是一个“繁重的”过程,可能需要一些时间来完成,如果调用它时所需的资源不可用,执行它的上下文甚至可能会休眠。这种行为在某些情况下可能会有问题,例如在中断处理程序中。使用 FMR 池,您可以使用注册为“轻量级”的 fmr,并且可以在任何上下文中注册。FMR 泳池的 API 可以在include/rdma/ib_fmr_pool.h中找到。

存储窗口(MW)

可以通过两种方式实现对存储器的远程访问:

  • 注册启用了远程权限的内存缓冲区。
  • 注册一个内存区域,然后将一个内存窗口绑定到它。

这两种方式都将创建一个远程密钥(rkey),该密钥可用于以指定的权限访问该存储器。然而,如果您希望使rkey无效以防止对该内存的远程访问,执行内存区域注销可能是一个繁重的过程。在此内存区域上使用内存窗口,并在需要时绑定或解除绑定,可以为启用和禁用对内存的远程访问提供一个“轻量级”过程。以下是与医疗废物相关的三种方法的说明:

  • ib_alloc_mw()方法分配一个内存窗口。它接受 PD 和 MW 类型作为参数。
  • ib_bind_mw()方法通过向 QP 发送特殊的工作请求,将内存窗口绑定到具有特定地址、大小和远程权限的指定内存区域。当您希望允许临时远程访问它的内存时,就会调用它。将在 QP 的发送队列中生成一个工作完成来描述该操作的状态。如果ib_bind_mw()被一个已经绑定的内存窗口调用到同一个内存区域或者不同的区域,那么之前的绑定将会失效。
  • 方法释放指定的 MW 对象。

完成队列(CQ)

发送或接收队列的每个已发布的工作请求都被视为未完成,直到它或在它之后发布的任何工作请求都有相应的工作完成。当工作请求未完成时,它所指向的内存缓冲区的内容是不确定的:

  • 如果 RDMA 设备读取这个内存并通过网络发送它的内容,客户端就不知道这个缓冲区是否可以被重用或释放。如果这是一个可靠的 QP,成功的工作完成意味着消息被远程端接收到。如果这是一个不可靠的 QP,一个成功的工作完成意味着消息被发送。
  • 如果 RDMA 设备将消息写入该内存,客户端无法知道该缓冲区是否包含传入的消息。

工作完成指定相应的工作请求已经完成,并提供一些相关信息:状态、使用的操作码、大小等等。CQ 是包含工作完成的对象。客户端需要轮询 CQ,以读取它所拥有的工作完成。CQ 基于先进先出(FIFO) 原则工作:客户端将从其中出队的工作完成顺序将根据 RDMA 设备将它们排入 CQ 的顺序。客户端可以在轮询模式下读取工作完成,或者请求在新的工作完成被添加到 CQ 时获得通知。CQ 不能容纳比其大小更多的工作完成。如果添加的工作完成多于其容量,将添加一个出错的工作完成,将生成一个 CQ 错误异步事件,并且所有与之关联的工作队列都将出错。以下是一些与 CQ 相关的方法:

  • ib_create_cq()方法 创造了一个 CQ。它将以下内容作为其参数:注册后调用驱动程序回调时返回的设备对象的指针,以及 CQ 的属性,包括其大小和当此 CQ 上有异步事件或向其添加工作完成时将调用的回调。
  • ib_resize_cq() 方法改变 CQ 的大小。新的条目数不能少于当前填写 CQ 的工作完成数。
  • ib_modify_cq()方法改变 CQ 的调节参数。如果至少有特定数量的工作完成进入 CQ,或者超时将过期,将生成完成事件。使用它可能有助于减少 RDMA 设备中发生的中断数量。
  • ib_peek_cq()方法返回 CQ 中可用工作完成的数量。
  • ib_req_notify_cq()方法 请求在下一个工作完成或包括请求事件指示的工作完成被添加到 CQ 时生成完成事件通知。如果在调用了ib_req_notify_cq()方法之后,没有工作完成被添加到 CQ,则不会发生完成事件通知。
  • ib_req_ncomp_notif() 方法要求当 CQ 中存在特定数量的工作完成时,创建完成事件通知。与ib_req_notify_cq()方法不同,当调用ib_req_ncomp_notif()方法时,即使 CQ 当前持有这个数量的工作完成,也会生成一个完成事件通知。
  • ib_poll_cq()方法从 CQ 轮询工作完成。它按照工作完成被添加到 CQ 的顺序从其中读取工作完成,并从其中删除它们。

下面是一个清空 CQ 的代码示例,即从 CQ 中读取所有工作完成,并检查它们的状态:

struct ib_wc wc;
int num_comp = 0;

while (ib_poll_cq(cq, 1, &wc) > 0) {
    if (wc.status != IB_WC_SUCCESS) {
        printk(KERN_ERR "The Work Completion[%d] has a bad status %d\n",
                         num_comp, wc.status);
        return -EINVAL;
    }
    num_comp ++;
}

扩展可靠连接(XRC)域

XRC 域是用于限制传入消息可以作为目标的 XRC srq 的对象。该 XRC 域可以与和 XRC 一起工作的其他几个 RDMA 资源相关联,例如 SRQ 和 QP 。

共享接收队列(SRQ)

SRQ 是 RDMA 架构在接收端更具可伸缩性的一种方式。不是每个队列对都有一个单独的接收队列,而是有一个所有 qp 都连接到的共享接收队列。当他们需要使用接收请求时,他们从 SRQ 获取请求。图 13-4 展示了与 SRQ 相关的 qp。

9781430261964_Fig13-04.jpg

图 13-4 。与 SRQ 相关联的 qp

如果你有 N 个 qp,每个 qp 都可能在随机时间收到一串 M 的消息,你可以这么做:

  • 如果不使用 SRQ,您会发送 N*M 个接收请求。
  • 使用 SRQs,您发布 K*M(其中 K << N)个接收请求。

与 QP 不同,它没有任何机制来确定其中未完成的工作请求的数量,而使用 SRQ,您可以设置一个水印限制。当接收请求的数量低于此限制时,将为此 SRQ 创建一个 SRQ 限制异步事件。使用 SRQ 的缺点是,您无法预测哪个 QP 将使用来自 SRQ 的每个已发布的接收请求,因此每个已发布的接收请求能够容纳的消息大小必须是任何 qp 可能获得的最大传入消息大小。这个限制可以通过创建几个 srq 来处理,每个 srq 对应一个不同的最大消息大小,并根据它们的预期消息大小将它们与相关 qp 相关联。

以下是与 SRQ 相关的一些方法的描述和一个示例:

  • ib_create_srq()方法创建一个 SRQ。SRQ 需要一个 PD 和属性。
  • ib_modify_srq()方法修改 SRQ 的属性。它用于为 SRQ 极限事件设置新的水印值,或者为支持它的设备调整 SRQ 的大小。

下面是一个设置水印值的示例,当 SRQ 中的 RRs 数量降至 5 以下时,该值将获得一个异步事件:

struct ib_srq_attr srq_attr;
int ret;

memset(&srq_attr, 0, sizeof(srq_attr));
srq_attr.srq_limit = 5;

ret = ib_modify_srq(srq, &srq_attr, IB_SRQ_LIMIT);
if (ret) {
    printk(KERN_ERR "Failed to set the SRQ's limit value\n");
    return ret;
}

以下是对处理 SRQ 的几种其他方法的描述。

  • ib_query_srq()方法查询当前的 SRQ 属性。这种方法通常用于检查 SRQ 的内容极限值。在ib_srq_attr对象的srq_limit成员中的值 0 意味着没有任何 SRQ 限制水印集。
  • 方法销毁一个 SRQ。
  • ib_post_srq_recv()方法将接收请求的链表作为参数,并将它们添加到指定的共享接收队列中,以供将来处理。

下面是一个向 SRQ 提交单个接收请求的示例。它使用其在单个集合条目中注册的 DMA 地址,将传入消息保存在内存缓冲区中:

struct ib_recv_wr wr, *bad_wr;
struct ib_sge sg;
int ret;

memset(&sg, 0, sizeof(sg));
sg.addr   = dma_addr;
sg.length = len;
sg.lkey   = mr->lkey;

memset(&wr, 0, sizeof(wr));
wr.next     = NULL;
wr.wr_id    = (uintptr_t)dma_addr;
wr.sg_list  = &sg;
wr.num_sge  = 1;

ret = ib_post_srq_recv(srq, &wr, &bad_wr);
if (ret) {
    printk(KERN_ERR "Failed to post Receive Request to an SRQ\n");
    return ret;
}

队列对(QP)

队列对是用于在 InfiniBand 中发送和接收数据的实际对象。它有两个独立的工作队列:发送和接收队列。每个工作队列都有一个特定数量的工作请求(WR ),每个 WR 都支持多个分散/聚集元素,以及一个 CQ,其处理已经结束的工作请求将向其添加工作完成。这些工作队列可以使用相似或不同的属性来创建,例如,可以发送到每个工作队列的 wr 的数量。每个工作队列中的顺序是有保证的,也就是说,发送队列中工作请求的处理将根据发送请求提交的顺序开始。同样的行为也适用于接收队列。但是,它们之间没有任何关系—也就是说,未完成的发送请求可以被处理,即使它是在向接收队列提交接收请求之后提交的。图 13-5 显示了一个 QP。

9781430261964_Fig13-05.jpg

图 13-5 。QP(队列对)

创建后,每个 QP 在特定时间点在 RDMA 设备上都有一个唯一的编号。

QP 运输类型

InfiniBand 支持多种 QP 传输类型:

  • 可靠连接(RC): 一个 RC QP 连接到单个远程 RC QP,并且可靠性是有保证的——也就是说,具有与发送它们相同的内容的所有分组根据它们的顺序到达是有保证的。在发送方,每个消息都被分割成大小为路径 MTU 的数据包,并在接收方进行碎片整理。这个 QP 支持发送、RDMA 写、RDMA 读和原子操作。
  • 不可靠连接(UC): 一个 UC QP 连接到一个远程 UC QP,可靠性没有保证。此外,如果消息中的一个数据包丢失,则整个消息都会丢失。在发送方,每个消息都被分割成大小为路径 MTU 的数据包,并在接收方进行碎片整理。该 QP 支持发送和 RDMA 写操作。
  • 不可靠数据报(UD): 一个 UD QP 可以向子网中的任何 UD QP 发送单播消息。支持多播消息。可靠性不能保证。每个消息限于一个分组消息,其大小限于路径 MTU 大小。此 QP 仅支持发送操作。
  • 扩展可靠连接(XRC): 来自同一个节点的几个 qp 可以向特定节点中的远程 SRQ 发送消息。这有助于将两个节点之间的 QP 数量从 CPU 内核数量的数量级(即每个内核一个进程的 QP)减少到一个 QP。该 QP 支持 QP 钢筋混凝土公司支持的所有操作。这种类型只与用户空间应用相关。
  • 原始数据包:允许客户端构建一个完整的数据包,包括 L2 报头,并按原样发送。在接收端,RDMA 设备不会剥离任何报头。
  • 原始 IPv6/原始以太网类型:允许发送未经 IB 设备解释的原始数据包的 qp。目前,任何 RDMA 设备都不支持这两种类型。

有一些特殊的 QP 传输类型用于子网管理和特殊服务:

  • SMI/QP0: 用于子网管理数据包的 QP。
  • GSI/QP1: QP 用于一般服务数据包。

ib_create_qp()方法创建一个 QP。它采用一个 PD 和请求的属性作为参数来创建这个 QP。下面是一个使用已创建的 PD 创建 RC QP 的示例,它有两个不同的 CQ:一个用于发送队列,一个用于接收队列。

struct ib_qp_init_attr init_attr;
struct ib_qp *qp;

memset(&init_attr, 0, sizeof(init_attr));
init_attr.event_handler       = my_qp_event;
init_attr.cap.max_send_wr     = 2;
init_attr.cap.max_recv_wr     = 2;
init_attr.cap.max_recv_sge    = 1;
init_attr.cap.max_send_sge    = 1;
init_attr.sq_sig_type         = IB_SIGNAL_ALL_WR;
init_attr.qp_type             = IB_QPT_RC;
init_attr.send_cq             = send_cq;
init_attr.recv_cq             = recv_cq;

    qp = ib_create_qp(pd, &init_attr);
    if (IS_ERR(qp)) {
        printk(KERN_ERR "Failed to create a QP\n");
        return PTR_ERR(qp);
    }

QP 国家机器

QP 有一个状态机,它定义了 QP 在每个状态下能够做什么:

  • 复位状态: 每个 QP 都是在这个状态下产生的。在这种状态下,不能向它发送任何发送请求或接收请求。所有传入的消息都会被无声地丢弃。

  • 初始化状态: 在此状态下,不能向其发送任何请求。然而,接收请求可以被发布,但不会被处理。所有传入的消息都会被无声地丢弃。在将接收请求转移到 RTR(准备接收)之前,在这种状态下向 QP 发布接收请求是一个很好的做法。这样做可以防止远程 QP 发送需要使用接收请求的消息,但是这些消息还没有发布。

  • 准备接收(RTR)状态: 在此状态下,不能向其发送任何发送请求,但可以发送和处理接收请求。所有传入的消息都将被处理。在这种状态下收到的第一个传入消息将生成通信建立异步事件。只接收消息的 QP 可以保持这种状态。

  • 准备发送(RTS)状态 : 在这种状态下,发送请求和接收请求都可以被发送和处理。所有传入的消息都将被处理。这是 QPs 的常见状态。

  • 发送队列排空(SQD)状态: 在这种状态下,QP 完成其处理已经开始的所有发送请求的处理。只有当没有任何消息可以发送时,您才可以更改一些 QP 属性。这种状态分为两种内部状态:

  • 排出:消息仍在发送中。

  • 耗尽:消息发送完毕。

  • 发送队列错误(SQE)状态: 当不可靠传输类型的发送队列中出现错误时,RDMA 设备会自动将 QP 移至此状态。导致错误的发送请求将会以错误原因完成,并且所有连续的发送请求都将被刷新。接收队列仍将工作,也就是说,可以发送接收请求,并处理传入的消息。客户端可以从此状态中恢复,并将 QP 状态修改回 RTS。

  • 错误状态: 在此状态下,所有未完成的工作请求将被刷新。如果这是一种可靠的传输类型,并且发送请求有错误,或者无论使用哪种传输类型,接收队列中都有错误,则 RDMA 设备可以将 QP 移到此状态。所有传入的消息都会被无声地丢弃。

QP 可以通过ib_modify_qp()从任何状态转换到复位状态和错误状态。将 QP 移至错误状态将刷新所有未完成的工作请求。将 QP 移至重置状态将清除所有以前配置的属性,并删除 QP 正在处理的完成队列中在此 QP 上结束的所有未完成的工作请求和工作完成。图 13-6 展示了一个 QP 状态机图。

9781430261964_Fig13-06.jpg

图 13-6 。QP 国家机器

ib_modify_qp()方法修改 QP 的属性。它将需要修改的 QP 和将要修改的 QP 的属性作为参数。QP 的状态机可以根据图 13-6 所示的示意图进行改变。每种 QP 传输类型都需要在每个 QP 状态转换中设置不同的属性。

下面是一个将新创建的 RC QP 修改为 RTS 状态的示例,在该状态下,它可以发送和接收数据包。本地属性是发送队列的输出端口、使用的 SL 和起始数据包序列号。所需的远程属性是接收 PSN、QP 号码和它使用的端口的 LID。

    struct ib_qp_attr attr = {
        .qp_state        = IB_QPS_INIT,
        .pkey_index      = 0,
        .port_num        = port,
        .qp_access_flags = 0
    };

    ret = ib_modify_qp(qp, &attr,
              IB_QP_STATE         |
              IB_QP_PKEY_INDEX    |
              IB_QP_PORT          |
              IB_QP_ACCESS_FLAGS);
if (ret) {
          printk(KERN_ERR "Failed to modify QP to INIT state\n");
          return ret;
}

attr.qp_state              = IB_QPS_RTR;
attr.path_mtu              = mtu;
attr.dest_qp_num           = remote->qpn;
attr.rq_psn                = remote->psn;
attr.max_dest_rd_atomic    = 1;
attr.min_rnr_timer         = 12;
attr.ah_attr.is_global     = 0;
attr.ah_attr.dlid          = remote->lid;
attr.ah_attr.sl            = sl;
attr.ah_attr.src_path_bits = 0,
attr.ah_attr.port_num      = port

ret = ib_modify_qp(ctx->qp, &attr,
          IB_QP_STATE                 |
          IB_QP_AV                    |
          IB_QP_PATH_MTU              |
          IB_QP_DEST_QPN              |
          IB_QP_RQ_PSN                |
          IB_QP_MAX_DEST_RD_ATOMIC    |
          IB_QP_MIN_RNR_TIMER);
if (ret) {
  printk(KERN_ERR "Failed to modify QP to RTR state\n");
  return ret;
}

attr.qp_state       = IB_QPS_RTS;
attr.timeout        = 14;
attr.retry_cnt      = 7;
attr.rnr_retry      = 6;
attr.sq_psn         = my_psn;
attr.max_rd_atomic  = 1;
ret = ib_modify_qp(ctx->qp, &attr,
          IB_QP_STATE             |
          IB_QP_TIMEOUT           |
          IB_QP_RETRY_CNT         |
          IB_QP_RNR_RETRY         |
          IB_QP_SQ_PSN            |
          IB_QP_MAX_QP_RD_ATOMIC);
if (ret) {
  printk(KERN_ERR "Failed to modify QP to RTS state\n");
  return ret;
}

以下是对处理 QP 的几种其他方法的描述:

  • ib_query_qp()方法查询当前的 QP 属性。有些属性是不变的(客户端指定的值),有些属性是可以改变的(例如,状态)。
  • 方法销毁了一个 QP。当不再需要 QP 时,就叫它。

工作请求处理

每个发送到发送或接收队列的工作请求都被认为是未完成的,直到有一个工作完成,该工作完成是从与该工作请求的该工作队列或在该工作请求之后发送的同一工作队列中的工作请求相关联的 CQ 轮询的。接收队列中每个未完成的工作请求都会以工作完成结束。工作队列中的工作请求处理流程如图图 13-7 所示。

9781430261964_Fig13-07.jpg

图 13-7 。工作请求处理流程

在发送队列中,您可以选择(在创建 QP 时)是希望每个发送请求都以工作完成结束,还是希望选择以工作完成结束的发送请求—即选择性信号。对于无信号发送请求,您可能会遇到错误;但是,将为其生成状态为“不良”的工作完成。

当工作请求未完成时,用户不能(重新)使用或释放在发布此工作请求时在其中指定的资源。例如:

  • 当发布 UD QP 的发送请求时,AH 不能被释放。
  • 当提交接收请求时,无法读取分散/收集(s/g)列表中引用的内存缓冲区,因为不知道 RDMA 设备是否已经在其中写入了数据。

“防护”是指在之前的 RDMA 读取和原子操作处理结束之前,阻止处理特定发送请求的能力。例如,当使用从远程地址读取的 RDMA 并在同一个发送队列中发送数据或数据的一部分时,将栅栏指示添加到发送请求可能是有用的。如果没有防护,发送操作可能会在数据被检索并在本地内存中可用之前开始。当向 UC 或 RC QP 发送发送请求时,到目标的路径是已知的,因为它是在将 QP 转移到 RTR 状态时提供的。但是,当向 UD·QP 提交发送请求时,您需要添加一个 AH 来描述该消息的目标路径。如果存在与发送队列相关的错误,并且这是一种不可靠的传输类型,则发送队列将进入错误状态(即 SQE 状态),但接收队列仍将完全正常工作。客户端可以从此状态中恢复,并将 QP 状态改回 RTS。如果存在与接收队列相关的错误,QP 将被移至错误状态,因为这是一个不可恢复的错误。当工作队列被移至错误状态时,导致错误的工作请求以指示错误性质的状态结束,并且该队列中的其余工作请求因错误而被刷新。

RDMA 架构中支持的操作

InfiniBand 支持多种操作类型:

  • 发送:通过网络发送信息。远程端需要有一个可用的接收请求,消息将被写入其缓冲区。
  • 立即发送:使用额外的 32 位带外数据通过网络发送消息。远程端需要有一个可用的接收请求,消息将被写入其缓冲区。该即时数据将在接收器的工作完成时可用。
  • RDMA 写道:通过电线向远程地址发送信息。
  • RDMA 立即写信:通过电线发送消息,并将其写到远程地址。远程端需要有一个可用的接收请求。该即时数据将在接收器的工作完成时可用。这个操作可以看作是带有零字节消息的 RDMA 写+立即发送。
  • RDMA 读取:读取一个远程地址,并用其内容填充本地缓冲区。
  • 比较和交换:将一个远程地址的内容与 valueX 进行比较;如果它们相等,用 valueY 替换它的内容。所有这些都是以原子的方式执行的。原始远程存储器内容被发送并保存在本地。
  • 取加:以原子的方式给远程地址的内容加一个值。原始远程存储器内容被发送并保存在本地。
  • 屏蔽比较和交换:使用远程地址的 maskX 与 valueX 比较内容部分;如果相等,用 valueY 替换 maskY 中的部分内容。所有这些都是以原子的方式执行的。原始远程存储器内容被发送并保存在本地。
  • 屏蔽取加:以原子的方式给远程地址的内容加一个值,只改变屏蔽中指定的位。原始远程存储器内容被发送并保存在本地。
  • 绑定内存窗口:将一个内存窗口绑定到一个特定的内存区域。
  • 快速注册:使用工作请求注册快速存储区。
  • 局部无效:使用工作请求使快速内存区域无效。如果有人用它的旧lkey / rkey,会被认为是错误。它可以与发送/RDMA 读取相结合;在这种情况下,首先将执行发送/读取,然后该快速存储区域将被无效。

接收请求指定为使用接收请求的操作保存传入消息的位置。分散列表中指定的内存缓冲区的总大小必须等于或大于传入消息的大小。

对于 UD QP,由于事先不知道消息的来源(同一个子网或另一个子网,单播或组播消息),因此必须在接收请求缓冲区中添加额外的 40 字节,这是 GRH 报头的大小。前 40 个字节将用消息的 GRH 填充(如果有的话)。该 GRH 信息描述了如何将消息发送回发送者。消息本身将从分散列表中描述的内存缓冲区中的偏移量 40 开始。

ib_post_recv()方法获取接收请求的链表,并将它们添加到特定 QP 的接收队列中,以供将来处理。下面是一个为 QP 提交单个接收请求的示例。它使用其在单个收集条目中注册的 DMA 地址将传入消息保存在内存缓冲区中。qp是使用ib_create_qp()创建的 QP 的指针。内存缓冲区是一个使用kmalloc()分配并使用ib_dma_map_single()映射到 DMA 的块。使用的lkey来自使用ib_get_dma_mr()注册的 MR。

struct ib_recv_wr wr, *bad_wr;
struct ib_sge sg;
int ret;

memset(&sg, 0, sizeof(sg));
sg.addr   = dma_addr;
sg.length = len;
sg.lkey   = mr->lkey;

memset(&wr, 0, sizeof(wr));
wr.next     = NULL;
wr.wr_id    = (uintptr_t)dma_addr;
wr.sg_list  = &sg;
wr.num_sge  = 1;

ret = ib_post_recv(qp, &wr, &bad_wr);

if (ret) {
    printk(KERN_ERR "Failed to post Receive Request to a QP\n");
    return ret;
}

ib_post_send()方法将发送请求的链表作为参数,并将它们添加到特定 QP 的发送队列中,以供将来处理。下面是一个提交 QP 发送操作的单个发送请求的示例。它在单个集合条目中使用其注册的 DMA 地址发送内存缓冲区的内容。

struct ib_sge sg;
struct ib_send_wr wr, *bad_wr;
int ret;

memset(&sg, 0, sizeof(sg));
sg.addr   = dma_addr;
sg.length = len;
sg.lkey   = mr->lkey;

memset(&wr, 0, sizeof(wr));
wr.next       = NULL;
wr.wr_id      = (uintptr_t)dma_addr;
wr.sg_list    = &sg;
wr.num_sge    = 1;
wr.opcode     = IB_WR_SEND;
wr.send_flags = IB_SEND_SIGNALED;

ret = ib_post_send(qp, &wr, &bad_wr);

if (ret) {
    printk(KERN_ERR "Failed to post Send Request to a QP\n");
    return ret;
}

工作完成状态

每个工作完成可以成功结束,也可以出错结束。如果成功结束,则操作完成,并且根据传输类型可靠性级别发送数据。如果这个工作完成包含一个错误,内存缓冲区的内容是未知的。工作请求状态指示存在错误的原因有很多:违反保护、地址错误等等。违规错误不会执行任何重新传输。但是,有两个特殊的重试流程值得一提。这两种情况都是由 RDMA 设备自动完成的,它会重新传输数据包,直到问题得到解决或超过重新传输的次数。如果问题解决了,除了暂时的性能问题之外,客户端代码甚至不会意识到发生了这种情况。这仅与可靠的传输类型相关。

重试流程

如果接收方在预期的超时时间内没有向发送方返回任何 ACK 或 NACK,发送方可能会根据 QP 属性中配置的超时和重试计数属性再次发送消息。出现这样的问题可能有几种原因:

  • 远程 QP 的属性或路径不正确。
  • 远程 QP 状态(至少)没有到达 RTR 状态。
  • 远程 QP 状态移至错误状态。
  • 消息本身在从发送方到接收方的途中被丢弃(例如,CRC 错误)。
  • 消息的 ACK 或 NACK 在从接收方到发送方的途中被丢弃(例如,CRC 错误)。

图 13-8 显示了由于数据包丢失克服了数据包丢失的重试流程。

9781430261964_Fig13-08.jpg

图 13-8 。重试流(在可靠传输类型上)

如果发送方 QP 最终成功接收到 ACK/NACK,它将继续发送其余的消息。如果将来有任何邮件也有这个问题,也会对该邮件再次执行重试流程,而不会记录以前执行的任何操作。如果在重试几次之后,接收方仍然没有响应,那么在发送方将会有一个带有重试错误的工作完成。

接收器未就绪(RNR)流程

如果接收方从接收方队列中得到一个需要使用接收请求的消息,但是没有任何未完成的接收请求,接收方将向发送方发回一个 RNR NACK。过一会儿,根据 RNR NACK 中指定的时间,发送者将再次尝试发送消息。

如果最终接收方及时发送了一个接收方请求,并且传入的消息使用了它,那么将向发送方发送一个 ACK 来表明消息被成功保存。如果将来的任何邮件也有此问题,RNR 重试流程也会针对此邮件再次执行,而不会记录以前执行此操作的历史。如果即使在重试几次之后,接收方仍然没有发布接收方请求,并且为每个发送的消息向发送方发送了 RNR NACK,则在发送方将生成带有 RNR 重试错误的工作完成。图 13-9 显示了 RNR 重试流程,该流程在接收器端克服了一个丢失的接收请求。

9781430261964_Fig13-09.jpg

图 13-9 。RNR 重试流(在可靠传输类型上)

在这一节中,我介绍了工作请求状态和一些可能发生在消息上的错误流程。在下一节中,我将讨论多播组。

多播组

多播组是一种从一个 UD QP 向许多 UD qp 发送消息的方式。想要得到这个消息的每个 UD QP 需要被附加到多播组。当一个设备得到一个多播数据包时,它会将它复制到附属于该组的所有 qp。以下是与多播组相关的两种方法的描述:

  • ib_attach_mcast()方法将 UD QP 连接到 InfiniBand 设备内的多播组。它接受要附加的 QP 和多播组属性。
  • ib_detach_mcast()方法将 UD QP 从多播组中分离。

用户空间和内核级 RDMA API 的区别

RDMA 堆栈 API 的用户空间和内核级非常相似,因为它们覆盖相同的技术,并且需要能够提供相同的功能。当用户空间从 RDMA API 调用控制路径的方法时,它执行到内核级的上下文切换,以保护特权资源并同步需要同步的对象(例如,同一个 QP 号码不能同时分配给多个 QP)。

然而,用户空间和内核级 RDMA API 和功能之间存在一些差异:

  • 内核级中所有 API 的前缀都是“ib_”,而在用户空间中前缀是“ibv_”。
  • 有一些枚举和宏只存在于内核级的 RDMA API 中。
  • 有些 QP 类型只在内核中可用(例如,SMI 和 GSI qp)。
  • 有些特权操作只能在内核级执行,例如,注册物理内存、使用 WR 注册 MR 和 FMRs。
  • 有些功能在用户空间的 RDMA API 中是不可用的——例如,N 通知请求。
  • 内核 API 是异步的。存在异步事件或完成事件时调用的回调。在用户空间中,一切都是同步的,用户需要明确检查其运行上下文(即线程)中是否有异步事件或完成事件。
  • XRC 与内核级客户端无关。
  • 内核级引入了一些新特性,但是它们在用户空间还不可用。

用户空间 API 由用户空间库“libibverbs”提供尽管用户级的一些 RDMA 功能比内核级的少,但它足以享受 InfiniBand 技术的好处。

摘要

在本章中,您已经了解了 InfiniBand 技术的优势。我回顾了 RDMA 堆栈组织。我讨论了资源创建层次结构和所有重要的对象及其 API,这是编写使用 InfiniBand 的客户端代码所需要的。您还看到了一些使用这个 API 的例子。下一章将讨论像网络名称空间和蓝牙子系统这样的高级主题。

快速参考

我将用 RDMA API 的重要方法的简短列表来结束这一章。本章提到了其中一些。

方法

下面是方法。

int IB _ register _ client(struct IB _ client * client);

注册一个想要使用 RDMA 堆栈的内核客户端。

void IB _ unregister _ client(struct IB _ client * client);

注销想要停止使用 RDMA 堆栈的内核客户端。

void IB _ set _ client _ data(struct IB _ device * device,struct ib_client *client,void * data);

将客户端上下文设置为与 InfiniBand 设备相关联。

void *ib_get_client_data(结构 ib_device *device,结构 IB _ client * client);

读取与 InfiniBand 设备关联的客户端上下文。

int ib_register_event_handler(结构 ib _ event _ handler *事件处理程序);

为 InfiniBand 设备发生的每个异步事件注册一个要调用的回调。

int ib_unregister_event_handler(结构 ib _ event _ handler *事件处理程序);

取消注册 InfiniBand 设备发生的每个异步事件要调用的回调。

int ib_query_device(结构 ib _ device *设备,结构 ib _ device _ attr *设备属性);

查询 InfiniBand 设备的属性。

int ib_query_port(结构 ib_device *device,u8 port_num,结构 IB _ port _ attr * port _ attr);

查询 InfiniBand 设备端口的属性。

枚举 rdma _ link _ layer rdma _ port _ get _ link _ layer(struct IB _ device * device,u8 port _ num);

查询 InfiniBand 设备端口的链路层。

int IB _ query _ GID(struct IB _ device * device,u8 port_num,int index,union IB _ GID * GID);

在 InfiniBand 设备的端口 GID 表的特定索引中查询 GID。

int IB _ query _ pkey(struct IB _ device * device,u8 port_num,u16 index,u16 * pkey);

在 InfiniBand 设备的端口 P_Key 表中查询特定于 P_Key 的索引。

int IB _ find _ GID(struct IB _ device * device,union ib_gid *gid,u8 *port_num,u16 * index);

在 InfiniBand 设备的端口 GID 表中找到特定 GID 值的索引。

int ib_find_pkey(结构 IB _ 设备*设备,u8 端口号,u16 pkey,u16 *索引);

在 InfiniBand 设备的端口 P_Key 表中查找特定 P_Key 值的索引。

结构 ib_pd *ib_alloc_pd(结构 IB _ device * device);

分配一个 PD 供以后创建其他 InfiniBand 资源时使用。

int IB _ deal loc _ PD(struct IB _ PD * PD);

取消分配 PD。

struct IB _ ah * IB _ create _ ah(struct IB _ PD * PD,struct IB _ ah _ attr * ah _ attr);

创建将在 UD QP 中发布发送请求时使用的 AH。

int ib_init_ah_from_wc(结构 ib_device *device,u8 port_num,结构 ib_wc *wc,结构 ib_grh *grh,结构 IB _ ah _ attr * ah _ attr);

从接收消息的工作完成和 GRH 缓冲区初始化 AH 属性。那些 AH 属性可以在调用ib_create_ah()方法时使用。

struct IB _ ah * IB _ create _ ah _ from _ WC(struct IB _ PD * PD,struct ib_wc *wc,struct ib_grh *grh,u8 port _ num);

从接收消息的工作完成和 GRH 缓冲区创建 AH。

int ib_modify_ah(struct ib_ah *ah,struct IB _ ah _ attr * ah _ attr);

修改现有 AH 的属性。

int ib_query_ah(struct ib_ah *ah,struct IB _ ah _ attr * ah _ attr);

查询现有 AH 的属性。

int IB _ destroy _ ah(struct IB _ ah * ah);

消灭一个啊。

struct IB _ Mr * IB _ get _ DMA _ Mr(struct IB _ PD * PD,int Mr _ access _ flags);

返回可用于 DMA 的 MR 系统内存。

静态内联 int IB _ DMA _ mapping _ error(struct IB _ device * dev,u64 DMA _ addr);

检查 DMA 内存是否指向无效地址,即检查 DMA 映射操作是否失败。

静态内联 u64 IB _ DMA _ map _ single(struct IB _ device * dev,void *cpu_addr,size_t size,enum DMA _ data _ direction direction);

将内核虚拟地址映射到 DMA 地址。

静态内联 void IB _ DMA _ unmap _ single(struct IB _ device * dev,u64 addr,size_t size,enum DMA _ data _ direction direction);

取消虚拟地址的 DMA 映射。

静态内联 u64 IB _ DMA _ map _ single _ attrs(struct IB _ device * dev,void *cpu_addr,size_t size,enum DMA _ data _ direction direction,struct dma_attrs *attrs)

根据 DMA 属性将内核虚拟内存映射到 DMA 地址。

静态内联 void IB _ DMA _ unmap _ single _ attrs(struct IB _ device * dev,u64 addr,size_t size,enum DMA _ data _ direction direction,struct DMA _ attrs * attrs);

取消根据 DMA 属性映射的虚拟地址的 DMA 映射。

静态内联 u64 IB _ DMA _ map _ page(struct IB _ device * dev,struct page *page,无符号长偏移量,size_t size,enum dma_data_direction 方向);

将物理页面映射到 DMA 地址。

静态内联 void IB _ DMA _ unmap _ page(struct IB _ device * dev,u64 addr,size_t size,enum DMA _ data _ direction direction);

取消物理页面的 DMA 映射。

static inline int IB _ DMA _ map _ SG(struct IB _ device * dev,struct scatterlist *sg,int nents,enum DMA _ data _ direction direction);

将分散/收集列表映射到 DMA 地址。

静态内联 void IB _ DMA _ unmap _ SG(struct IB _ device * dev,struct scatterlist *sg,int nents,enum DMA _ data _ direction direction);

取消分散/收集列表的 DMA 映射。

静态内联 int IB _ DMA _ map _ SG _ attrs(struct IB _ device * dev,struct scatterlist *sg,int nents,enum DMA _ data _ direction direction,struct DMA _ attrs * attrs);

根据 DMA 属性将分散/收集列表映射到 DMA 地址。

静态内联 void IB _ DMA _ unmap _ SG _ attrs(struct IB _ device * dev,struct scatterlist *sg,int nents,enum DMA _ data _ direction direction,struct DMA _ attrs * attrs);

根据 DMA 属性取消分散/收集列表的 DMA 映射。

静态内联 u64 IB _ SG _ DMA _ address(struct IB _ device * dev,struct scatter list * SG);

返回分散/聚集条目的地址属性。

静态内联无符号 int IB _ SG _ DMA _ len(struct IB _ device * dev,struct scatter list * SG);

返回分散/聚集条目的长度属性。

静态内联 void IB _ DMA _ sync _ single _ for _ CPU(struct IB _ device * dev,u64 addr,size_t size,enum DMA _ data _ direction dir);

将 DMA 区域所有权转移给 CPU。它应该在 CPU 访问 DMA 映射区之前调用,该映射区的所有权先前已转移给设备。

静态内联 void IB _ DMA _ sync _ single _ for _ device(struct IB _ device * dev,u64 addr,size_t size,enum DMA _ data _ direction dir);

将 DMA 区域所有权转移给设备。应该在设备访问 DMA 映射区域之前调用该函数,该区域的所有权之前已经转移给了 CPU。

静态内联 void * IB _ DMA _ alloc _ coherent(struct IB _ device * dev,size_t size,u64 *dma_handle,GFP _ t flag);

分配一个 CPU 可以访问的内存块,映射到 DMA。

静态内联 void IB _ DMA _ free _ coherent(struct IB _ device * dev,size_t size,void *cpu_addr,u64 DMA _ handle);

释放使用ib_dma_alloc_coherent()分配的内存块。

struct IB _ Mr * IB _ reg _ phys _ Mr(struct IB _ PD * PD,struct IB _ phys _ buf * phys _ buf _ array,int num_phys_buf,int mr_access_flags,u64 * iova _ start);

获取一个物理页列表,并准备好供 InfiniBand 设备访问。

int IB _ rereg _ phys _ Mr(struct IB _ Mr * Mr,int mr_rereg_mask,struct ib_pd *pd,struct IB _ phys _ buf * phys _ buf _ array,int num_phys_buf,int mr_access_flags,u64 * iova _ start);

更改 MR 的属性。

int ib_query_mr(struct ib_mr *mr,struct IB _ Mr _ attr * Mr _ attr);

查询 MR 的属性。

int IB _ dereg _ Mr(struct IB _ Mr * Mr);

取消 MR 的注册

struct IB _ MW * IB _ alloc _ MW(struct IB _ PD * PD,enum ib_mw_type 类型);

分配一个 MW。此 MW 将用于允许远程访问 MR。

静态内联 int ib_bind_mw(struct ib_qp *qp,struct ib_mw *mw,struct IB _ MW _ bind * MW _ bind);

将一个 MW 绑定到一个 MR,以允许使用特定权限远程访问本地内存。

int IB _ dealloc _ MW(struct IB _ MW * MW);

解除分配一个 MW。

struct IB _ CQ * IB _ create _ CQ(struct IB _ device * device,ib_comp_handler comp_handler,void(* event _ handler)(struct IB _ event *,void *),void *cq_context,int cqe,int comp _ vector);

创建 CQ。此 CQ 将用于指示发送或接收队列的已结束工作请求的状态。

int ib_resize_cq(struct ib_cq *cq,int cqe);

更改 CQ 中的条目数。

int ib_modify_cq(structib_cq *cq,u16 cq_count,u16 CQ _ period);

修改 CQ 的审核属性。这种方法用于减少 InfiniBand 设备的中断次数。

int ib_peek_cq(structib_cq *cq,intwc _ CNT);

返回 CQ 中可用的工作完成数。

静态内联 int IB _ req _ notify _ CQ(struct IB _ CQ * CQ,enum ib_cq_notify_flags 标志);

请求在将下一个工作完成添加至 CQ 时生成完成通知事件。

静态内嵌 int IB _ req _ ncmp _ notf(struct IB _ CQ * CQ,int WC _ CNT);

当 CQ 中有特定数量的工作完成时,请求生成完成通知事件。

静态内联 int ib_poll_cq(struct ib_cq *cq,int num_entries,struct IB _ WC * WC);

从 CQ 中读取并删除一个或多个工作完成。它们是按照加入 CQ 的顺序来读的。

struct IB _ srq * IB _ create _ srq(struct IB _ PD * PD,struct IB _ srq _ init _ attr * srq _ init _ attr);

创建一个 SRQ,用作几个 qp 的共享接收队列。

int IB _ modify _ srq(struct IB _ srq * srq,struct ib_srq_attr *srq_attr,enum IB _ srq _ attr _ mask srq _ attr _ mask);

修改 SRQ 的属性。

int IB _ query _ srq(struct IB _ srq * srq,struct IB _ srq _ attr * srq _ attr);

查询 SRQ 的属性。在随后对此方法的调用中,SRQ 限制值可能会更改。

int ib_destroy_srq(结构 IB _ srq * srq);

摧毁一个 SRQ。

struct IB _ qp * IB _ create _ qp(struct IB _ PD * PD,struct IB _ qp _ init _ attr * qp _ init _ attr);

创建 QP。每个新的 QP 都分配有一个 QP 号码,其他 qp 不会同时使用这个号码。

int ib_modify_qp(struct ib_qp *qp,struct ib_qp_attr *qp_attr,int qp _ attr _ mask);

修改 QP 的属性,包括发送和接收队列属性以及 QP 状态。

int ib_query_qp(struct ib_qp *qp,struct ib_qp_attr *qp_attr,int qp_attr_mask,struct IB _ qp _ init _ attr * qp _ init _ attr);

查询 QP 的属性。在后续调用此方法时,可能会更改某些属性。

int IB _ destroy _ qp(struct IB _ qp * qp);

摧毁一个 QP。

静态内联 ib_post_srq_recv(结构 ib_srq *srq,结构 ib_recv_wr *recv_wr,结构 IB _ recv _ wr * * bad _ recv _ wr);

向 SRQ 添加接收请求的链接列表。

静态内联 ib_post_recv(结构 ib_qp *qp,结构 ib_recv_wr *recv_wr,结构 IB _ recv _ wr * * bad _ recv _ wr);

向 QP 的接收队列添加接收请求的链接列表。

静态内联 int ib_post_send(struct ib_qp *qp,struct ib_send_wr *send_wr,struct IB _ send _ wr * * bad _ send _ wr);

向 QP 的发送队列添加发送请求的链接列表。

int IB _ attach _ mcast(struct IB _ qp * qp,union ib_gid *gid,u16 lid);

将 UD QP 附加到多播组。

int IB _ detach _ mcast(struct IB _ qp * qp,union ib_gid *gid,u16 lid);

从多播组中分离一个 UD QP。

十四、高级主题

第十三章讲述了 InfiniBand 子系统及其在 Linux 中的实现。本章讨论了几个高级主题和一些逻辑上不适合其他章节的主题。本章首先讨论网络名称空间,这是一种轻量级的进程虚拟化机制,近年来被添加到 Linux 中。我将讨论一般的名称空间实现,特别是网络名称空间。您将了解到,为了实现名称空间,只需要两个新的系统调用。您还将看到几个例子,说明使用iproute2ip命令创建和管理网络名称空间是多么简单,以及将一个网络设备从一个网络名称空间移动到另一个网络名称空间以及将指定的进程附加到指定的网络名称空间是多么简单。cgroups 子系统还提供资源管理解决方案,这与名称空间不同。我将描述 cgroups 子系统及其两个网络模块net_priocls_cgroup,并给出两个使用这些 cgroup 网络模块的例子。

在本章的后面,您将了解繁忙的轮询套接字以及如何调优它们。繁忙轮询套接字特性为需要低延迟并愿意为更高的 CPU 利用率付出代价的套接字提供了一种有趣的性能优化技术。内核 3.11 提供了繁忙轮询套接字特性。我还将介绍蓝牙子系统、IEEE 802.15.4 子系统和近场通信(NFC)子系统;这三个子系统通常在短程网络中工作,并且针对这些子系统的新特征的开发正在快速进行。我还将讨论通知链,这是您在开发或调试内核网络代码和 PCI 子系统时可能会遇到的一种重要机制,因为许多网络设备都是 PCI 设备。我不会深入研究 PCI 子系统的细节,因为这本书不是关于设备驱动程序的。我将用三个简短的部分来结束这一章,一个是关于组队网络驱动程序(这是新的内核链路聚合解决方案),一个是关于以太网点对点(PPPoE)协议,最后一个是关于 Android。

网络名称空间

本节介绍 Linux 名称空间、它们的用途以及它们是如何实现的。它包括对网络名称空间的深入讨论,并给出一些示例来演示它们的用法。Linux 名称空间本质上是一个虚拟化解决方案。在 Xen 或 KVM 等解决方案进入市场之前,操作系统虚拟化已经在大型机中实现了很多年。对于 Linux 名称空间,这是一种进程虚拟化的形式,这个想法一点也不新鲜。在 Plan 9 操作系统中尝试过(参见 1992 年的这篇文章:《Plan 9 中名称空间的使用》,www.cs.bell-labs.com/sys/doc/names.html)。

名称空间是轻量级进程虚拟化的一种形式,它提供了资源隔离。与 KVM 或 Xen 等虚拟化解决方案不同,使用名称空间,您不需要在同一台主机上创建额外的操作系统实例,而是只使用一个操作系统实例。在这种情况下,我应该提到 Solaris 操作系统有一个名为 Solaris Zones 的虚拟化解决方案,它也使用单个操作系统实例,但是资源分区的方案与 Linux 名称空间的方案有些不同(例如,在 Solaris Zones 中有一个全局区域,它是主区域,具有更多功能)。在 FreeBSD 操作系统中,有一种称为jails,的机制,它也提供资源分区,而无需运行多于一个内核实例。

Linux 名称空间的主要思想是在进程组之间划分资源,以使一个进程(或几个进程)拥有与其他进程组中的进程不同的系统视图。例如,这个特性用于在 Linux 容器项目(http://lxc.sourceforge.net/)中提供资源隔离。Linux 容器项目还使用了 cgroups 子系统提供的另一种资源管理机制,这将在本章后面介绍。有了容器,您可以使用操作系统的一个实例在同一台主机上运行不同的 Linux 发行版。高性能计算(HPC)中使用的检查点/恢复功能也需要名称空间。比如用在 CRIU ( http://criu.org/Main_Page )),OpenVZ ( http://openvz.org/Main_Page)的一个软件工具,主要在用户空间为 Linux 进程实现检查点/恢复功能,虽然 CRIU 内核补丁合并的地方很少。我应该提到有一些项目在内核中实现检查点/恢复,但是这些项目在主线中没有被接受,因为它们太复杂了。以 CKPT 项目为例:https://ckpt.wiki.kernel.org/index.php/Main_Page。检查点/恢复功能(有时也称为检查点/重启)支持在文件系统上停止和保存多个进程,并在以后从文件系统中恢复这些进程(可能在不同的主机上),并从停止的地方继续执行。如果没有名称空间,检查点/恢复的使用案例非常有限,特别是只有使用它们才能进行实时迁移。网络名称空间的另一个用例是当您需要建立一个环境,该环境需要模拟不同的网络堆栈来进行测试、调试等。对于想了解更多关于检查点/重启的读者,我建议阅读 Sukadev Bhattiprolu、Eric W. Biederman、Serge Hallyn 和 Daniel Lezcano 撰写的文章“主流 Linux 中的虚拟服务器和检查点/重启”。

对于内核 2.4.19,挂载名称空间是 2002 年合并的第一种 Linux 名称空间。在内核 3.8 中,对于几乎所有的文件系统类型,用户名称空间是最后实现的。正如本节后面所讨论的,可能会开发额外的名称空间。要创建一个名称空间,除了用户名称空间之外,您应该对所有名称空间都具有 CAP_SYS_ADMIN 功能。尝试为除用户名称空间之外的所有名称空间创建没有 CAP_SYS_ADMIN 功能的名称空间,将导致–EPRM 错误(“不允许操作”)。许多开发人员参与了名称空间的开发,其中包括 Eric W. Biederman、Pavel Emelyanov、Al Viro、Cyrill Gorcunov、Andrew Vagin 等等。

在了解了关于进程虚拟化和 Linux 名称空间的一些背景知识,以及它们是如何使用的之后,现在就可以开始深入研究血淋淋的实现细节了。

名称空间实现

在撰写本文时,Linux 内核中已经实现了六个名称空间。下面是为了在 Linux 内核中实现名称空间并支持用户空间包中的名称空间而需要的主要添加和更改的描述:

  • 添加了一个名为nsproxy(名称空间代理)的结构。该结构包含指向实现的六个名称空间中的五个名称空间的指针。在nsproxy结构中没有指向用户命名空间的指针;然而,所有其他五个名称空间对象都包含一个指向拥有它们的用户名称空间对象的指针,并且在这五个名称空间的每一个中,用户名称空间指针都被称为user_ns。用户名称空间是一个特例;它是凭证结构(cred)的成员,称为user_nscred结构表示进程的安全上下文。每个流程描述符(task_struct)包含两个cred对象,用于有效和客观的流程描述符凭证。我不会深入研究用户名称空间实现的所有细节和细微差别,因为这不在本书的范围之内。一个nsproxy对象由create_nsproxy()方法创建,并由free_nsproxy()方法释放。一个指向nsproxy对象的指针,也称为nsproxy,被添加到流程描述符中(流程描述符由task_struct结构、include/linux/sched.h表示)。)让我们来看看nsproxy结构,因为它很短,应该是不言自明的:

    struct nsproxy {
          atomic_t count;
           struct uts_namespace *uts_ns;
           struct ipc_namespace *ipc_ns;
           struct mnt_namespace *mnt_ns;
           struct pid_namespace *pid_ns;
           struct net           *net_ns;
    };
    (include/linux/nsproxy.h)
    
  • 你可以在nsproxy结构中看到五个名称空间指针(没有用户名称空间指针)。在流程描述符(task_struct对象)中使用nsproxy对象代替五个名称空间对象是一种优化。当执行fork()时,一个新的子元素很可能和它的父元素存在于同一个名称空间集合中。因此,不是五次引用计数器递增(每个名称空间一次),而是一次引用计数器递增(对于nsproxy对象)。nsproxy count成员是一个引用计数器,当nsproxy对象由create_nsproxy()方法创建时,它被初始化为 1,由put_nsproxy()方法递减,由get_nsproxy()方法递增。请注意,nsproxy对象的pid_ns成员在内核 3.11 中被重命名为pid_ns_for_children

  • 添加了一个新的系统调用unshare()。该系统调用获得一个参数,该参数是 CLONE标志的位掩码。当 flags 参数由一个或多个名称空间 CLONE_NEW标志组成时,unshare()系统调用执行以下步骤:

  • 首先,它根据指定的标志创建一个新的名称空间(或几个名称空间)。这是通过调用unshare_nsproxy_namespaces()方法完成的,该方法又通过调用create_new_namespaces()方法创建了一个新的nsproxy对象和一个或多个名称空间。根据指定的 CLONE_NEW*标志确定新名称空间的类型。create_new_namespaces()方法返回一个新的nsproxy对象,它包含新创建的名称空间。

  • 然后,它通过调用switch_task_namespaces()方法将调用流程附加到新创建的nsproxy对象。

当 CLONE_NEWPID 是系统调用unshare()的标志时,的工作方式与其他标志不同;这是对fork()的隐含论证;只有子任务会发生在新的 PID 名称空间中,而不是调用unshare()系统调用的那个。其他 CLONE_NEW*标志会立即将调用进程放入一个新的名称空间。

为支持名称空间的创建而添加的六个 CLONE_NEW*标志将在本节稍后描述。unshare()系统调用的实现在kernel/fork.c中。

  • 添加了一个新的系统调用setns()。它将调用线程附加到现有的命名空间。它的原型是int setns(int fd, int nstype);这些参数是:

  • fd:表示名称空间的文件描述符。这些是通过打开/proc/<pid>/ns/目录的链接获得的。

  • nstype:可选参数。当它是新的 CLONE_NEW名称空间标志之一时,指定的文件描述符必须引用与指定的 CLONE_NEW标志的类型相匹配的名称空间。当没有设置nstype(其值为 0)时,fd参数可以引用任何类型的名称空间。如果nstype不对应于与指定的fd相关联的名称空间类型,则返回值–EINVAL。

你可以在kernel/nsproxy.c中找到setns()系统调用的实现。

  • 为了支持命名空间,添加了以下六个新的克隆标志:

  • CLONE_NEWNS(用于挂载名称空间)

  • CLONE_NEWUTS(用于 UTS 名称空间)

  • CLONE_NEWIPC(用于 IPC 名称空间)

  • CLONE_NEWPID(用于 PID 名称空间)

  • CLONE_NEWNET(用于网络名称空间)

  • CLONE_NEWUSER(用于用户名称空间)

传统上使用系统调用来创建一个新的进程。对它进行了调整,以支持这些新标志,这样它将创建一个附加到新名称空间(或多个名称空间)的新进程。请注意,在本章后面的一些示例中,您将会遇到使用 CLONE_NEWNET 标志来创建新的网络名称空间。

  • 有名称空间支持的六个子系统中的每个子系统都实现了自己独特的名称空间。例如,mount 名称空间由一个名为mnt_namespace的结构表示,network 名称空间由一个名为net的结构表示,这将在本节的后面讨论。我将在本章后面提到其他名称空间。

  • 对于名称空间的创建,添加了一个名为create_new_namespaces()的方法(kernel/nsproxy.c)。此方法获取一个 CLONE_NEW标志或 CLONE_NEW标志的位图作为第一个参数。它首先通过调用create_nsproxy()方法创建一个nsproxy对象,然后根据指定的标志关联一个名称空间;由于标志可以是标志的位掩码,create_new_namespaces()方法可以关联多个名称空间。我们来看看create_new_namespaces()的方法:

    static struct nsproxy *create_new_namespaces(unsigned long flags,
            struct task_struct *tsk, struct user_namespace *user_ns,
            struct fs_struct *new_fs)
    {
            struct nsproxy *new_nsp;
            int err;
    

分配一个nsproxy对象,并将其引用计数器初始化为 1:

        new_nsp = create_nsproxy();
        if (!new_nsp)
                return ERR_PTR(-ENOMEM);
        . . .

在成功创建了一个nsproxy对象之后,我们应该根据指定的标志创建名称空间,或者将一个现有的名称空间关联到我们创建的新的nsproxy对象。我们首先为挂载名称空间调用copy_mnt_ns(),然后为 UTS 名称空间调用copy_utsname()、。我将在这里简单描述一下copy_utsname()方法,因为 UTS 名称空间将在本章后面的“UTS 名称空间实现”一节中讨论。如果在copy_utsname()方法的指定标志中没有设置 CLONE_NEWUTS,则copy_utsname()方法不会创建新的 UTS 名称空间;它返回由tsk->nsproxy->uts_ns作为最后一个参数传递给copy_utsname()方法的 UTS 名称空间。如果设置了 CLONE_NEWUTS,copy_utsname()方法通过调用clone_uts_ns()方法克隆指定的 UTS 名称空间。clone_uts_ns()方法依次分配一个新的 UTS 命名空间对象,将指定的 UTS 命名空间(tsk->nsproxy->uts_ns)new_utsname对象复制到新创建的 UTS 命名空间对象的new_utsname对象中,并返回新创建的 UTS 命名空间。在本章后面的“UTS 名称空间实现”一节中,您将了解到更多关于new_utsname结构的内容:

         new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
         if (IS_ERR(new_nsp->uts_ns)) {
                 err = PTR_ERR(new_nsp->uts_ns);
                 goto out_uts;
         }
        . . .

在处理了 UTS 名称空间之后,我们继续调用copy_ipcs()方法来处理 IPC 名称空间,copy_pid_ns()来处理 PID 名称空间, copy_net_ns()来处理网络名称空间。注意,没有调用copy_user_ns()方法,因为nsproxy不包含指向用户名称空间的指针,如前所述。我将在这里简单描述一下copy_net_ns()方法。如果 CLONE_NEWNET 没有在create_new_namespaces()方法的指定标志中设置,那么copy_net_ns()方法将返回作为第三个参数传递给copy_net_ns()方法tsk->nsproxy->net_ns的网络名称空间,就像copy_utsname()所做的那样,正如您在本节前面所看到的。如果设置了 CLONE_NEWNET,copy_net_ns()方法通过调用net_alloc()方法分配一个新的网络名称空间,i通过调用setup_net()方法将其初始化,并将其添加到所有网络名称空间的全局列表中,net_namespace_list:

        new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
        if (IS_ERR(new_nsp->net_ns)) {
                err = PTR_ERR(new_nsp->net_ns);
                goto out_net;
        }
        return new_nsp;
}

注意,setns()系统调用,不创建新的名称空间,只是将调用线程附加到指定的名称空间,也调用create_new_namespaces(),但它将 0 作为第一个参数传递;这意味着通过调用create_nsproxy()方法只创建了一个nsproxy,但没有创建新的名称空间,而是调用线程与一个现有的网络名称空间相关联,该名称空间由setns()系统调用的指定fd参数标识。稍后在setns()系统调用实现中,switch_task_namespaces()方法被调用,它将刚刚创建的新nsproxy分配给调用线程(参见kernel/nsproxy.c)。

  • kernel/nsproxy.c中增加了一个名为exit_task_namespaces() 的方法。当进程终止时,通过do_exit()方法(kernel/exit.c)调用它。exit_task_namespaces()方法获取流程描述符(task_struct对象)作为单个参数。事实上,它唯一做的事情就是调用switch_task_namespaces()方法,传递指定的流程描述符和一个空的nsproxy对象作为参数。switch_task_namespaces()方法反过来使正在被终止的进程的进程描述符的nsproxy对象无效。如果没有其他进程使用这个nsproxy,它就会被释放。

  • 添加了一个名为get_net_ns_by_fd()的方法。这个方法获取一个文件描述符作为它的单个参数,并返回与指定文件描述符对应的 inode 相关联的网络名称空间。对于不熟悉文件系统和 inode 语义的读者来说,我建议阅读由 Daniel P. Bovet 和 Marco Cesati (O'Reilly,2005)在理解 Linux 内核中的第十二章“虚拟文件系统”的“Inode 对象”一节。

  • 增加了一个名为get_net_ns_by_pid() 的方法。该方法获取一个 PID 号作为单个参数,并返回该进程附加到的网络命名空间对象。

  • /proc/<pid>/ns下添加了六个条目,每个名称空间一个。这些文件在打开时应该被输入到setns()系统调用中。您可以使用ls –alreadlink来显示与名称空间相关联的惟一 proc inode 号。这个惟一的 proc inode 在创建名称空间时由proc_alloc_inum()方法创建,在释放名称空间时由proc_free_inum()方法释放。例如,参见kernel/pid_namespace.c中的create_pid_namespace()方法。在下面的例子中,右边方括号中的数字是每个名称空间的惟一 proc inode 号:

    ls -al /proc/1/ns/
    total 0
    dr-x--x--x 2 root root 0 Nov  3 13:32 .
    dr-xr-xr-x 8 root root 0 Nov  3 12:17 ..
    lrwxrwxrwx 1 root root 0 Nov  3 13:32 ipc -> ipc:[4026531839]
    lrwxrwxrwx 1 root root 0 Nov  3 13:32 mnt -> mnt:[4026531840]
    lrwxrwxrwx 1 root root 0 Nov  3 13:32 net -> net:[4026531956]
    lrwxrwxrwx 1 root root 0 Nov  3 13:32 pid -> pid:[4026531836]
    lrwxrwxrwx 1 root root 0 Nov  3 13:32 user -> user:[4026531837]
    lrwxrwxrwx 1 root root 0 Nov  3 13:32 uts -> uts:[4026531838]
    
    
  • 如果满足以下任一条件,命名空间可以保持活动状态:

  • /proc/<pid>/ns/描述符下的命名空间文件被保存。

  • 将命名空间 proc 文件绑定挂载到其他地方,例如,对于 PID 命名空间,通过:mount --bind /proc/self/ns/pid /some/filesystem/path

  • 对于六个名称空间中的每一个,定义了一个 proc 名称空间操作对象(proc_ns_operations结构的一个实例)。这个对象由回调组成,例如inum,以返回与名称空间或install相关联的唯一 proc inode 号,用于名称空间安装(在install回调中,执行名称空间特定的动作,例如将特定名称空间对象附加到nsproxy对象,等等;install回调由setns系统调用调用)。proc_ns_operations结构在include/linux/proc_fs.h中定义。以下是六个proc_ns_operations对象的列表:

  • utsns_operations为 UTS 命名空间(kernel/utsname.c)

  • ipcns_operations对于 IPC 名称空间(ipc/namespace.c)

  • mntns_operations对于挂载命名空间(fs/namespace.c)

  • pidns_operations对于 PID 名称空间(kernel/pid_namespace.c)

  • userns_operations用于用户命名空间(kernel/user_namespace.c)

  • netns_operations用于网络命名空间(net/core/net_namespace.c)

  • 对于每个名称空间,除了挂载名称空间之外,都有一个初始名称空间:

  • init_uts_ns:用于 UTS 命名空间(init/version.c)。

  • init_ipc_ns:对于 IPC 命名空间(ipc/msgutil.c)。

  • init_pid_ns:用于 PID 命名空间(kernel/pid.c)。

  • init_net:用于网络命名空间(net/core/net_namespace.c)。

  • init_user_ns:用于用户命名空间(kernel/user.c)。

  • 定义了一个初始的、默认的nsproxy对象:它被称为init_nsproxy,包含指向五个初始名称空间的指针;除了 mount 命名空间被初始化为空之外,都被初始化为对应的特定初始命名空间:

    struct nsproxy init_nsproxy = {
            .count  = ATOMIC_INIT(1),
            .uts_ns = &init_uts_ns,
    #if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
            .ipc_ns = &init_ipc_ns,
    #endif
            .mnt_ns = NULL,
            .pid_ns = &init_pid_ns,
    #ifdef CONFIG_NET
            .net_ns = &init_net,
    #endif
    };
    (kernel/nsproxy.c)
    
    
  • 添加了一个名为task_nsproxy()的方法;它以单个参数的形式获取一个流程描述符(task_struct对象),并返回与指定的task_struct对象相关联的nsproxy。参见include/linux/nsproxy.h

在撰写本文时,Linux 内核中有六个可用的名称空间:

  • 挂载名称空间:挂载名称空间允许进程查看自己的文件系统视图及其挂载点。在一个挂载命名空间中挂载文件系统不会传播到其他挂载命名空间。挂载名称空间是通过在调用clone()unshare()系统调用时设置 CLONE_NEWNS 标志来创建的。为了实现挂载名称空间,添加了一个名为mnt_namespace的结构(fs/mount.h),nsproxy持有一个指向名为mnt_nsmnt_namespace对象的指针。内核 2.4.19 提供了挂载名称空间。挂载名称空间主要在fs/namespace.c中实现。创建新的装载命名空间时,以下规则适用:

  • 所有以前的装载将在新的装载命名空间中可见。

  • 新挂载名称空间中的挂载/卸载对于系统的其余部分是不可见的。

  • 全局装载命名空间中的装载/卸载在新的装载命名空间中可见。

挂载名称空间使用一种 VFS 增强,称为共享子树,它是在 Linux 2.6.15 内核中引入的;特性引入了新的标志:MS_PRIVATE、MS_SHARED、MS_SLAVE 和 MS_UNBINDABLE。(参见http://lwn.net/Articles/159077/Documentation/filesystems/sharedsubtree.txt。)我不会讨论挂载名称空间实现的内部。对于想了解更多关于挂载名称空间用法的读者,我建议阅读以下文章:“应用挂载名称空间”,作者 Serge E. Hallyn 和 Ram Pai ( http://www.ibm.com/developerworks/linux/library/l-mount-namespaces/index.html )。

* **PID 名称空间:**PID 名称空间为不同 PID 名称空间中的不同进程提供了拥有相同 PID 的能力。这个特性是 Linux 容器的构建块。这对于进程的检查点/恢复非常重要,因为在一台主机上设置了检查点的进程可以在另一台主机上恢复,即使该主机上存在具有相同 PID 的进程。当在新的 PID 名称空间中创建第一个进程时,它的 PID 是 1。这个流程的行为有点像init流程的行为。这意味着当一个进程死亡时,它的所有孤立子进程现在将拥有 PID 1 作为其父进程的进程(child reaping)。向 PID 为 1 的进程发送 SIGKILL 信号不会终止该进程,无论 SIGKILL 信号是在哪个命名空间发送的,是在初始 PID 命名空间还是在任何其他 PID 命名空间。但是从另一个 PID 名称空间(父名称空间)中删除一个 PID 名称空间的init将会起作用。在这种情况下,所有存在于以前命名空间中的任务都将被终止,PID 命名空间将被停止。PID 名称空间是通过在调用clone()unshare()系统调用时设置 CLONE_NEWPID 标志来创建的。为了实现 PID 名称空间,添加了一个名为pid_namespace的结构(include/linux/pid_namespace.h),nsproxy保存了一个指向名为pid_nspid_namespace对象的指针。为了支持 PID 名称空间,应该设置 CONFIG_PID_NS。PID 名称空间在内核 2.6.24 中可用。PID 名称空间主要在kernel/pid_namespace.c`中实现。

  • 网络名称空间:网络名称空间允许创建看似内核网络堆栈的多个实例。当调用clone()unshare()系统调用时,通过设置 CLONE_NEWNET 标志来创建网络名称空间。为了实现网络名称空间,添加了一个名为net的结构(include/net/net_namespace.h),nsproxy持有一个指向名为net_nsnet对象的指针。为了支持网络命名空间,应该设置 CONFIG_NET_NS。我将在本节的后面讨论网络名称空间。内核 2.6.29 提供了网络名称空间。网络名称空间主要在net/core/net_namespace.c中实现。
  • IPC 名称空间:IPC 名称空间允许进程拥有自己的 System V IPC 资源和 POSIX 消息队列资源。IPC 名称空间是通过在调用clone()unshare()系统调用时设置 CLONE_NEWIPC 标志来创建的。为了实现 IPC 名称空间,添加了一个名为ipc_namespace的结构(include/linux/ipc_namespace.h),nsproxy持有一个指向名为ipc_nsipc_namespace对象的指针。为了支持 IPC 名称空间,应该设置 CONFIG_IPC_NS。内核 2.6.19 中的 IPC 名称空间提供了对 System V IPC 资源的支持。对 IPC 名称空间中 POSIX 消息队列资源的支持是后来在内核 2.6.30 中添加的。IPC 名称空间主要在ipc/namespace.c中实现。
  • UTS 名称空间:UTS 名称空间为不同的 UTS 名称空间提供了拥有不同的主机名或域名(或者由uname()系统调用返回的其他信息)的能力。UTS 名称空间是通过在调用clone()unshare()系统调用时设置 CLONE_NEWUTS 标志来创建的。在实现的六个名称空间中,UTS 名称空间实现是最简单的。为了实现 UTS 名称空间,添加了一个名为uts_namespace的结构(include/linux/utsname.h),nsproxy保存了一个指向名为uts_nsuts_namespace对象的指针。为了支持 UTS 命名空间,应该设置 CONFIG_UTS_NS。内核 2.6.19 提供了 UTS 名称空间。UTS 名称空间主要在kernel/utsname.c中实现。
  • 用户名称空间:用户名称空间允许映射用户和组 id。这种映射是通过写入两个为支持用户名称空间而添加的procfs条目来完成的:/proc/sys/kernel/overflowuid/proc/sys/kernel/overflowgid。附加到用户名称空间的进程可以具有与宿主不同的一组功能。当调用clone()unshare()系统调用时,通过设置 CLONE_NEWUSER 标志来创建用户名称空间。为了实现用户名称空间,添加了一个名为user_namespace的结构(include/linux/user_namespace.h)。user_namespace对象包含一个指向创建它的用户名称空间对象的指针(parent)。与其他五个名称空间不同,nsproxy不持有指向user_namespace对象的指针。我不会深入研究用户名称空间的更多实现细节,因为它可能是最复杂的名称空间,并且超出了本书的范围。为了支持用户名称空间,应该设置 CONFIG_USER_NS。从内核 3.8 开始,几乎所有文件系统类型都可以使用用户名称空间。用户名称空间主要在kernel/user_namespace.c中实现。

在四个用户空间包中增加了对名称空间的支持:

  • util-linux中:

  • 从版本 2.17 开始,unshare实用程序可以创建六个名称空间中的任何一个。

  • 从版本 2.23 开始提供的nsenter实用程序(实际上是围绕setns系统调用的一个轻量级包装器)。

  • iproute2中,网络名称空间的管理是通过ip netns命令完成的,在本章的后面你会看到几个这样的例子。此外,您可以使用ip link命令将网络接口移动到不同的网络名称空间,您将在本章后面的“将网络接口移动到不同的网络名称空间”一节中看到。

  • ethtool中,增加了对 enable 的支持,可以发现是否为指定的网络接口设置了 NETIF_F_NETNS_LOCAL 特性。如果设置了 NETIF_F_NETNS_LOCAL 功能,这表示网络接口位于该网络命名空间的本地,您不能将其移动到不同的网络命名空间。NETIF_F_NETNS_LOCAL 特性将在本节稍后讨论。

  • 在 wireless iw包中,添加了一个选项,允许将无线接口移动到不同的名称空间。

image 在 2006 年渥太华 Linux 研讨会(OLS)的一次演讲《全球 Linux 名称空间的多个实例》中,Eric w . Biederman(Linux 名称空间的主要开发者之一)提到了十个名称空间;他在本演示中提到的其他四个尚未实现的名称空间是:设备名称空间、安全名称空间、安全密钥名称空间和时间名称空间。(见https://www.kernel.org/doc/ols/2006/ols2006v1-pages-101-112.pdf .) For more information about namespaces, I suggest reading a series of six articles about it by Michael Kerrisk (https://lwn.net/Articles/531114/ ))。移动操作系统虚拟化项目引发了支持设备名称空间的开发工作;关于设备名称空间的更多信息,它还不是内核的一部分,参见 Jake Edge 的“设备名称空间”(http://lwn.net/Articles/564854/ ) and also (http://lwn.net/Articles/564977/ ). There was also some work for implementing a new syslog namespace (see the article “Stepping Closer to Practical Containers: “syslog” namespaces”, http://lwn.net/Articles/527342/ )。```sh`


*   `clone():`创建一个附加到新名称空间的新进程。名称空间的类型由作为参数传递的 CLONE_NEW*标志指定。请注意,您也可以使用这些 CLONE_NEW*标志的位掩码。`clone()`系统调用的实现在`kernel/fork.c`中。
*   `unshare()`:本节前面讨论过。
*   `setns()`:本节前面讨论过。

![image](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/linux-kern-net/img/sq.jpg) **注意**名称空间在内核中没有用户空间进程可以用来与之对话的名称。如果名称空间有名字,这就需要在另一个特殊的名称空间中全局保存它们。这将使实施变得复杂,并可能在检查点/恢复等方面引发问题。相反,用户空间进程应该打开`/proc/<pid>/ns/`下的名称空间文件,它们的文件描述符可以用来与特定的名称空间对话,以保持该名称空间的活力。名称空间由创建时生成的唯一 proc inode 号标识,释放时释放。六个名称空间结构中的每一个都包含一个名为`proc_inum`的整数成员,它是名称空间惟一的 proc inode 号,通过调用`proc_alloc_inum()`方法来分配。六个名称空间中的每一个都有一个`proc_ns_operations`对象,它包括特定于名称空间的回调;其中一个回调函数叫做`inum`,返回相关名称空间的`proc_inum`(关于`proc_ns_operations`结构的定义,请参考`include/linux/proc_fs.h`)。

在讨论网络名称空间之前,让我们描述一下最简单的名称空间,即 UTS 名称空间是如何实现的。这是理解其他更复杂的名称空间的良好起点。

UTS 命名空间实现

为了实现 UTS 命名空间,添加了一个名为`uts_namespace`的结构:

```sh
struct uts_namespace {
        struct kref kref;
        struct new_utsname name;
        struct user_namespace *user_ns;
        unsigned int proc_inum;
};
(include/linux/utsname.h)

下面是对uts_namespace结构成员的简短描述:

  • kref:参考计数器。它是一个通用的内核引用计数器,由kref_get()方法递增,由kref_put()方法递减。除了 UTS 命名空间,PID 命名空间也有一个kref对象作为引用计数器;所有其他四个名称空间都使用原子计数器进行引用计数。关于kref API 的更多信息请看Documentation/kref.txt
  • name:一个new_utsname对象,包含类似domainnamenodename的字段(稍后将讨论)。
  • user_ns:与 UTS 命名空间相关联的用户命名空间。
  • proc_inum:UTS 名称空间的唯一进程索引节点号。

nsproxy结构包含一个指向uts_namespace的指针:

struct nsproxy {
        . . .
        struct uts_namespace *uts_ns;
        . . .
};
(include/linux/nsproxy.h)

正如您之前看到的,uts_namespace对象包含了一个new_utsname结构的实例。让我们看一下new_utsname结构,这是 UTS 名称空间的本质:

struct new_utsname {
        char sysname[__NEW_UTS_LEN + 1];
        char nodename[__NEW_UTS_LEN + 1];
        char release[__NEW_UTS_LEN + 1];
        char version[__NEW_UTS_LEN + 1];
        char machine[__NEW_UTS_LEN + 1];
        char domainname[__NEW_UTS_LEN + 1];
};
(include/uapi/linux/utsname.h)

new_utsnamenodename成员是主机名,domainname是域名。添加了一个名为utsname()的方法;这个方法只是返回与当前运行的进程相关联的new_utsname对象(current):

static inline struct new_utsname *utsname(void)
{
         return &current->nsproxy->uts_ns->name;
}
(include/linux/utsname.h)

现在,新的gethostname()系统调用实现如下:

SYSCALL_DEFINE2(gethostname, char __user *, name, int, len)
{
        int i, errno;
        struct new_utsname *u;

        if (len < 0)
                return -EINVAL;
        down_read(&uts_sem);

调用utsname()方法,该方法访问与当前进程关联的 UTS 命名空间的new_utsname对象:

        u = utsname();
        i = 1 + strlen(u->nodename);
        if (i > len)
                i = len;
        errno = 0;

utsname()方法返回的new_utsname对象的nodename复制到用户空间:

        if (copy_to_user(name, u->nodename, i))
                errno = -EFAULT;
        up_read(&uts_sem);
        return errno;
}
(kernel/sys.c)

你可以在uname()系统调用的sethostbyname()和中找到类似的方法,它们也在kernel/sys.c中定义。我应该注意,UTS 名称空间实现也处理 UTS procfs条目。只有两个 UTS procfs条目,/proc/sys/kernel/domainname/proc/sys/kernel/hostname,它们是可写的(这意味着您可以从用户空间更改它们)。还有其他不可写的 UTS procfs条目,如/proc/sys/kernel/ostype/proc/sys/kernel/osrelease。如果您查看 UTS procfs条目uts_kern_table (kernel/utsname_sysctl.c)的表格,您会看到一些条目,如ostypeosrelease,具有“0444”模式,这意味着它们不可写,只有其中的两个条目hostnamedomainname具有“0644”模式,这意味着它们是可写的。UTS procfs条目的读写由proc_do_uts_string()方法处理。想要了解更多关于如何处理 UTS procfs条目的读者应该查看proc_do_uts_string()方法和get_uts()方法;两人都在kernel/utsname_sysctl.c

既然您已经了解了最简单的名称空间——UTS 名称空间是如何实现的,那么是时候了解网络名称空间及其实现了。

网络名称空间实现

网络名称空间在逻辑上是网络堆栈的另一个副本,具有自己的网络设备、路由表、邻居表、网络过滤表、网络套接字、网络procfs条目、网络sysfs条目和其他网络资源。网络名称空间的一个实用特性是,在给定名称空间(比如说ns1)中运行的网络应用将首先在/etc/netns/ns1下寻找配置文件,然后才在/etc下寻找。因此,举例来说,如果您创建了一个名为ns1的名称空间,并且您已经创建了/etc/netns/ns1/hosts,那么每个试图访问hosts文件的用户空间应用将首先访问/etc/netns/ns1/hosts,并且只有到那时(如果所寻找的条目不存在)它才会读取/etc/hosts。这个特性是使用绑定挂载实现的,并且只适用于使用ip netns add命令创建的网络名称空间。

网络命名空间对象(struct net)

现在让我们来看一下net结构的定义,它是代表网络名称空间的基本数据结构:

struct net {
        . . .
        struct user_namespace   *user_ns;       /* Owning user namespace */
        unsigned int            proc_inum;
        struct proc_dir_entry   *proc_net;
        struct proc_dir_entry   *proc_net_stat;
        . . .
        struct list_head        dev_base_head;
        struct hlist_head       *dev_name_head;
        struct hlist_head       *dev_index_head;
        . . .
        int                     ifindex;
        . . .
        struct net_device       *loopback_dev;  /* The loopback */
        . . .
        atomic_t                count;          /* To decided when the network
                                                *  namespace should be shut down.
                                                */

        struct netns_ipv4       ipv4;
#if IS_ENABLED(CONFIG_IPV6)
        struct netns_ipv6       ipv6;
#endif
#if defined(CONFIG_IP_SCTP) || defined(CONFIG_IP_SCTP_MODULE)
        struct netns_sctp       sctp;
#endif
       . . .

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
        struct netns_ct         ct;
#endif
#if IS_ENABLED(CONFIG_NF_DEFRAG_IPV6)
        struct netns_nf_frag    nf_frag;
#endif
        . . .
        struct net_generic __rcu  *gen;
#ifdef CONFIG_XFRM
        struct netns_xfrm       xfrm;
#endif
        . . .
};
(include/net/net_namespace.h)

下面是对net结构中几个成员的简短描述:

  • user_ns表示创建网络命名空间的用户命名空间;它拥有网络名称空间及其所有资源。它在setup_net()方法中被赋值。对于初始网络名称空间对象(init_net),创建它的用户名称空间是初始用户名称空间,init_user_ns
  • proc_inum是与网络名称空间相关联的唯一 proc inode 号。这个惟一的进程索引节点是由proc_alloc_inum()方法创建的,该方法还将proc_inum指定为进程索引节点号。proc_alloc_inum()方法由网络名称空间初始化方法net_ns_net_init()调用,通过调用网络名称空间清理方法net_ns_net_exit()中的proc_free_inum()方法释放。
  • proc_net代表网络名称空间procfs条目(/proc/net),因为每个网络名称空间维护其自己的procfs条目。
  • proc_net_stat代表网络名称空间procfs统计条目(/proc/net/stat),因为每个网络名称空间维护其自己的procfs统计条目。
  • dev_base_head指向所有网络设备的链表。
  • dev_name_head指向一个网络设备的哈希表,其中的键是网络设备名。
  • dev_index_head指向一个网络设备的哈希表,其中的键是网络设备索引。
  • ifindex是网络名称空间内分配的最后一个设备索引。索引在网络命名空间中被虚拟化;这意味着回送设备在所有网络名称空间中的索引总是为 1,而其他网络设备在不同的网络名称空间中可能具有相同的索引。
  • loopback_dev是环回设备。每个新的网络命名空间都是用一个网络设备创建的,即环回设备。在loopback_net_init()方法drivers/net/loopback.c中分配网络名称空间的loopback_dev对象。您不能将环回设备从一个网络名称空间移动到另一个网络名称空间。
  • count是网络名称空间引用计数器。当通过setup_net()方法创建网络名称空间时,它被初始化为 1。它通过get_net()方法递增,通过put_net()方法递减。如果在put_net()方法中count参考计数器达到 0,则调用__put_net()方法。然后,__put_net()方法将网络名称空间添加到要删除的网络名称空间的全局列表中,cleanup_list,然后删除它。
  • IPv4 子系统的ipv4(netns_ipv4结构的一个实例)。netns_ipv4结构包含 IPv4 特定字段,这些字段对于不同的名称空间是不同的。例如,在第六章的中,您看到了名为net的指定网络名称空间的组播路由表存储在net->ipv4.mrt中。我将在本节稍后讨论netns_ipv4
  • IPv6 子系统的ipv6(netns_ipv6结构的一个实例)。
  • 用于 SCTP 套接字的sctp(netns_sctp结构的一个实例)。
  • ct(netns_ct结构的一个实例,在第九章 )中讨论了 netfilter 连接跟踪子系统。
  • gen(net_generic结构的一个实例,在include/net/netns/generic.h中定义)是描述可选子系统的网络名称空间上下文的结构上的一组通用指针。例如,sit模块(简单互联网过渡,IPv6 隧道,在net/ipv6/sit.c实现)使用这个引擎将其私有数据放在struct net上。引入这一点是为了不使struct net淹没每个网络子系统的指针,每个网络子系统都愿意拥有每个网络名称空间上下文。
  • xfrm(netns_xfrm结构的一个实例,在 IPsec 子系统的第十章 )中多次提到。

让我们来看看 IPv4 特定的名称空间,netns_ipv4结构:

struct netns_ipv4 {
    . . .
#ifdef CONFIG_IP_MULTIPLE_TABLES
        struct fib_rules_ops    *rules_ops;
        bool                    fib_has_custom_rules;
        struct fib_table        *fib_local;
        struct fib_table        *fib_main;
        struct fib_table        *fib_default;
#endif
   . . .
        struct hlist_head       *fib_table_hash;
        struct sock             *fibnl;

        struct sock             **icmp_sk;
   . . .
#ifdef CONFIG_NETFILTER
        struct xt_table         *iptable_filter;
        struct xt_table         *iptable_mangle;
        struct xt_table         *iptable_raw;
        struct xt_table         *arptable_filter;
#ifdef CONFIG_SECURITY
        struct xt_table         *iptable_security;
#endif
        struct xt_table         *nat_table;
#endif

        int sysctl_icmp_echo_ignore_all;
        int sysctl_icmp_echo_ignore_broadcasts;
        int sysctl_icmp_ignore_bogus_error_responses;
        int sysctl_icmp_ratelimit;
        int sysctl_icmp_ratemask;
        int sysctl_icmp_errors_use_inbound_ifaddr;

        int sysctl_tcp_ecn;

        kgid_t sysctl_ping_group_range[2];
        long sysctl_tcp_mem[3];

        atomic_t dev_addr_genid;

#ifdef CONFIG_IP_MROUTE
#ifndef CONFIG_IP_MROUTE_MULTIPLE_TABLES
        struct mr_table         *mrt;
#else
        struct list_head        mr_tables;
        struct fib_rules_ops    *mr_rules_ops;
#endif
#endif
};
(net/netns/ipv4.h)

您可以在netns_ipv4结构中看到许多特定于 IPv4 的表和变量,比如路由表、netfilter 表、多播路由表等等。

网络名称空间实现:其他数据结构

为了支持网络名称空间,在网络设备对象(struct net_device)中添加了一个名为nd_net的成员,它是一个指向网络名称空间的指针。通过调用dev_net_set()方法来设置网络设备的网络名称空间,通过调用dev_net()方法来获取与网络设备相关联的网络名称空间。请注意,在给定时刻,一个网络设备只能属于一个网络命名空间。nd_net通常在网络设备注册或网络设备移动到不同的网络名称空间时设置。例如,在注册 VLAN 设备时,会使用上述两种方法:

static int register_vlan_device(struct net_device *real_dev, u16 vlan_id)
{
    struct net_device *new_dev;

要分配给新 VLAN 设备的网络名称空间是与真实设备相关联的网络名称空间,它作为参数传递给register_vlan_device()方法;我们通过调用dev_net(real_dev)获得这个名称空间:

    struct net *net = dev_net(real_dev);
    . . .
    new_dev = alloc_netdev(sizeof(struct vlan_dev_priv), name, vlan_setup);

    if (new_dev == NULL)
        return -ENOBUFS;

通过调用dev_net_set()方法切换网络名称空间:

    dev_net_set(new_dev, net);

    . . .
}

一个名为sk_net的成员,一个指向网络名称空间的指针,被添加到代表套接字的struct sock中。为一个sock对象设置网络名称空间是通过调用sock_net_set()方法来完成的,获取与一个sock对象相关联的网络名称空间是通过调用sock_net()方法来完成的。像在nd_net对象的情况下一样,sock对象在给定时刻也只能属于一个网络名称空间。

当系统引导时,会创建一个默认的网络名称空间init_net,。引导后,所有物理网络设备和所有套接字都属于该初始命名空间,网络环回设备也是如此。

一些网络设备和一些网络子系统应该具有网络命名空间特定的数据。为了实现这一点,添加了一个名为pernet_operations的结构;这个结构包括一个initexit回调:

struct pernet_operations {
        . . .
        int (*init)(struct net *net);
        void (*exit)(struct net *net);
        . . .
        int *id;
        size_t size;
};
(include/net/net_namespace.h)

需要网络名称空间特定数据的网络设备应该定义一个pernet_operations对象,并分别为设备特定的初始化和清理定义其init()exit()回调,并在模块初始化时调用register_pernet_device()方法,在模块被移除时调用unregister_pernet_device()方法,在这两种情况下将pernet_operations对象作为单个参数传递。例如,PPPoE 模块通过procfs条目/proc/net/pppoe导出关于 PPPoE 会话的信息。此procfs条目导出的信息取决于此 PPPoE 设备所属的网络名称空间(因为不同的 PPPoE 设备可能属于不同的网络名称空间)。所以 PPPoE 模块定义了一个名为pppoe_net_opspernet_operations对象:

static struct pernet_operations pppoe_net_ops = {
        .init = pppoe_init_net,
        .exit = pppoe_exit_net,
        .id   = &pppoe_net_id,
        .size = sizeof(struct pppoe_net),
}
(net/ppp/pppoe.c)

init回调pppoe_init_net()中,它只通过调用proc_create()方法创建 PPPoE procfs条目/proc/net/pppoe:

static __net_init int pppoe_init_net(struct net *net)
{
        struct pppoe_net *pn = pppoe_pernet(net);
        struct proc_dir_entry *pde;

        rwlock_init(&pn->hash_lock);

        pde = proc_create("pppoe", S_IRUGO, net->proc_net, &pppoe_seq_fops);
#ifdef CONFIG_PROC_FS
        if (!pde)
                return -ENOMEM;
#endif

        return 0;
}
(net/ppp/pppoe.c)

exit回调pppoe_exit_net()中,它只通过调用remove_proc_entry()方法移除 PPPoE procfs条目/proc/net/pppoe:

static __net_exit void pppoe_exit_net(struct net *net)
{
        remove_proc_entry("pppoe", net->proc_net);
}
(net/ppp/pppoe.c)

需要网络命名空间特定数据的网络子系统应该在初始化子系统时调用register_pernet_subsys(),在移除子系统时调用unregister_pernet_subsys()。你可以在net/ipv4/route.c里找例子,还有很多其他复习这些方法的例子。网络名称空间模块本身也定义了一个net_ns_ops对象,并在引导阶段注册它:

static struct pernet_operations __net_initdata net_ns_ops = {
        .init = net_ns_net_init,
        .exit = net_ns_net_exit,
};

static int __init net_ns_init(void)
{
    . . .
    register_pernet_subsys(&net_ns_ops);
    . . .
}
(net/core/net_namespace.c)

每次创建新的网络名称空间时,调用init回调(net_ns_net_init),每次删除网络名称空间时,调用exit回调(net_ns_net_exit)。net_ns_net_init()唯一做的事情是通过调用proc_alloc_inum()方法为新创建的名称空间分配一个惟一的 proc inode 新创建的唯一 proc inode 编号被分配给net->proc_inum:

static __net_init int net_ns_net_init(struct net *net)
{
        return proc_alloc_inum(&net->proc_inum);
}

net_ns_net_exit()方法做的唯一一件事就是通过调用proc_free_inum()方法删除这个惟一的 proc inode:

static __net_exit void net_ns_net_exit(struct net *net)
{
        proc_free_inum(net->proc_inum);
}

当您创建新的网络命名空间时,它只有网络环回设备。创建网络命名空间最常见的方法是:

  • 由一个用户空间应用创建一个网络名称空间,该应用使用clone()系统调用或unshare()系统调用,在两种情况下都设置 CLONE_NEWNET 标志。
  • 使用iproute2ip netns命令(您将很快看到一个例子)。
  • 使用util-linuxunshare实用程序,带有--net标志。

网络名称空间管理

接下来,您将看到一些使用iproute2包的ip netns命令来执行诸如创建网络名称空间、删除网络名称空间、显示所有网络名称空间等操作的例子。

  • Creating a network namespace named ns1 is done by:

    ip netns add ns1

    运行这个命令首先触发一个名为/var/run/netns/ns1的文件的创建,然后通过系统调用unshare()创建网络名称空间,并向其传递一个 CLONE_NEWNET 标志。然后通过一个bind挂载将/var/run/netns/ns1附加到网络名称空间(/proc/self/ns/net)(用 MS_BIND 调用mount()系统调用)。请注意,网络名称空间可以嵌套,这意味着从ns1中您还可以创建一个新的网络名称空间,等等。

  • Deleting a network namespace named ns1  is done by:

    ip netns del ns1

    请注意,如果有一个或多个进程附加到网络命名空间,这将不会删除该命名空间。如果没有这样的过程,则删除/var/run/netns/ns1文件。还要注意,当删除一个命名空间时,它的所有网络设备都被移动到初始的默认网络命名空间init_net,但网络命名空间本地设备除外,它们是设置了 NETIF_F_NETNS_LOCAL 特性的网络设备。此类网络设备将被删除。详见本章后面的“将网络接口移动到网络名称空间”部分和附录 A 。

  • Showing all the network namespaces in the system that were added by ip netns add is done by:

    ip netns list

    实际上,运行ip netns list只是显示/var/run/netns下的文件名。注意,ip netns add没有添加的网络名称空间ip netns list不会显示,因为创建这样的网络名称空间不会触发/var/run/netns下任何文件的创建。所以,比如运行ip netns list时,由unshare --net bash创建的网络名称空间不会出现。

  • Monitoring creation and removal of a network namespace is done by:

    ip netns monitor

    运行ip netns monitor后,当您通过ip netns add ns2添加新的命名空间时,您将在屏幕上看到以下消息:“添加 ns2”,通过ip netns delete ns2删除该命名空间后,您将在屏幕上看到以下消息:“删除 ns2”。注意,添加和删除网络名称空间不是通过分别运行ip netns addip netns delete``, does not trigger displaying any messages on screen by ip netns monitor。通过在/var/run/netns上设置一个inotify手表来执行ip netns monitor命令。请注意,如果您在使用ip netns add添加至少一个网络名称空间之前运行ip netns monitor,您将得到以下错误:inotify_add_watch failed: No such file or directory。原因是试图在/var/run/netns上设置一个尚不存在的手表失败。参见man inotify_init() and man inotify_add_watch()

* Start a shell in a specified namespace (ns1` in this example) is done by:

`ip netns exec ns1 bash`

注意,使用`ip netns exec`可以在指定的网络名称空间中运行**任何**命令。例如,以下命令将显示名为`ns1`的网络名称空间中的所有网络接口:

`ip netns exec ns1 ifconfig -a`` 

在最近版本的iproute2`(从版本 3.8 开始)中,您有了这两个额外的有用命令:

  • Show the network namespace associated with the specified pid:

    ip netns identify #pid

    这是通过读取/proc/<pid>/ns/net并迭代/var/run/netns下的文件来找到匹配(使用stat()系统调用)来实现的。

  • Show the PID of a process (or list of processes) attached to a network namespace called ns1 by:

    ip netns pids ns1

    这是通过读取/var/run/netns/ns1,然后迭代/proc/<pid>条目来找到匹配的/proc/pid/ns/net条目(使用stat()系统调用)来实现的。

image 关于各种 ip netns 命令选项的更多信息,参见man ip netns

将网络接口移动到不同的网络名称空间

使用ip命令可以将网络接口移动到名为ns1的网络名称空间。例如,出自:ip link set eth0 netns ns1。作为实现网络名称空间的一部分,一个名为 NETIF_F_NETNS_LOCAL 的新特性被添加到了net_device对象的特性中(net_device结构代表一个网络接口)。有关net_device结构及其特性的更多信息,参见附录 A 。您可以通过查看ethtool -k eth0输出或ethtool --show-features eth0输出中的netns-local标志来确定是否为指定的网络设备设置了 NETIF_F_NETNS_LOCAL 功能(这两个命令是等效的。)注意不能用ethtool设置 NETIF_F_NETNS_LOCAL 特性。该特征在被设置时表示网络设备是网络命名空间本地设备。例如,回环、网桥、VXLAN 和 PPP 设备都是网络命名空间本地设备。尝试移动 NETIF_F_NETNS_LOCAL 功能设置为不同名称空间的网络设备将会失败,并出现–EINVAL 错误,您将很快在下面的代码片段中看到这一点。当试图将网络接口移动到不同的网络名称空间时,调用dev_change_net_namespace()方法,例如通过:ip link set eth0 netns ns1。让我们来看看dev_change_net_namespace()的方法:

int dev_change_net_namespace(struct net_device *dev, struct net *net, const char *pat)
{
        int err;

        ASSERT_RTNL();

        /* Don't allow namespace local devices to be moved. */
        err = -EINVAL;

如果设备是本地设备,则返回–EINVAL(设置了net_device对象特性中的 NETIF_F_NETNS_LOCAL 标志)

        if (dev->features & NETIF_F_NETNS_LOCAL)
                goto out;
        . . .

通过将net_device对象的nd_net设置为新的指定名称空间来实际切换网络名称空间:

        dev_net_set(dev, net)
        . . .

out:
        return err;
}
(net/core/dev.c)

image 注意您可以将网络接口移动到名为ns1的网络名称空间,方法是指定附加到该名称空间的进程的 PID,而无需显式指定名称空间名称。例如,如果你知道一个 PID 为的进程附加到了 ns1,运行ip link set eth1 netns <pidNumber>会将eth1移动到ns1名称空间。实现细节:在指定其附属进程的 PID 之一时获取网络名称空间对象由get_net_ns_by_pid()方法实现,而在指定网络名称空间名称时获取网络名称空间对象由get_net_ns_by_fd()方法实现;两种方法都在net/core/net_namespace.c。为了将无线网络接口移动到不同的网络名称空间,您应该使用iw命令。例如,如果您想要将wlan0移动到一个网络名称空间,并且您知道一个 PID 为的进程被附加到那个名称空间,那么您可以运行 iw phy phy0 set netns <pidNumber>来将其移动到那个网络名称空间。实现细节参见net/wireless/nl80211.c中的nl80211_wiphy_netns()方法。

两个网络名称空间之间的通信

我将用一个两个网络名称空间如何相互通信的简短例子来结束网络名称空间一节。可以通过使用 Unix 套接字或者使用虚拟以太网(VETH)网络驱动程序创建一对虚拟网络设备并将其中一个移动到另一个网络命名空间来实现。例如,下面是前两个名称空间,ns1ns2:

ip netns add ns1
ip netns add ns2

ns1中启动 Shell:

ip netns exec ns1 bash

创建一个虚拟以太网设备(类型为veth):

ip link add name if_one type veth peer name if_one_peer

if_one_peer移动到ns2:

ip link set dev if_one_peer netns ns2

现在,您可以像往常一样使用ifconfig命令或ip命令在if_oneif_one_peer上设置地址,并从一个网络名称空间向另一个发送数据包。

image 注意网络名称空间对于内核映像不是强制性的。默认情况下,在大多数发行版中,网络名称空间是启用的(CONFIG_NET_NS 已设置)。但是,您可以在禁用网络名称空间的情况下构建和引导内核。

我在本节中讨论了什么是名称空间,特别是什么是网络名称空间。我提到了实现名称空间所需的一些主要变化,比如添加了 6 个新的 CLONE_NEW*标志,添加了两个新的系统调用,向流程描述符添加了一个nsproxy对象,等等。我还描述了所有名称空间中最简单的 UTS 名称空间的实现,以及网络名称空间的实现。给出了几个例子,展示了用iproute2包的ip netns命令操纵网络名称空间是多么简单。接下来我将描述 cgroups 子系统,它提供了另一种资源管理解决方案,以及属于它的两个网络模块。

群组

cgroups 子系统是由 Paul Menage、Rohit Seth 和其他 Google 开发人员在 2006 年启动的项目。它最初被称为“过程容器”,但后来被重命名为“控制组”它为进程组提供资源管理和资源核算。从内核 2.6.24 开始,它就已经是主线内核的一部分,并在几个项目中使用:例如由systemd(一个取代 SysV init 脚本的服务管理器;例如,Fedora 和 openSUSE)、本章前面提到的 Linux Containers 项目、Google containers ( https://github.com/google/lmctfy/)、libvirt ( http://libvirt.org/cgroups.html)等等。就性能而言,Cgroups 内核实现大多是非关键路径。cgroups 子系统实现了一个名为“cgroups”的新的虚拟文件系统(VFS)类型。所有的 cgroups 动作都是由文件系统动作完成的,比如在 cgroup 文件系统中创建 cgroups 目录,在这些目录中写入或读取条目,挂载 cgroup 文件系统等等。有一个名为libcgroup(又名libcg))的库,它提供了一组用于 cgroups 管理的用户空间实用程序:例如,cgcreate用于创建一个新的 cgroup,cgdelete用于删除一个 cgroup,, cgexec用于在指定的控制组中运行一个任务,等等。事实上,这是通过从libcg库中调用 cgroup 文件系统操作来完成的。libcg库在未来很可能会减少使用,因为它不在试图使用 cgroup 控制器的多方之间提供任何协调。将来可能所有的 cgroup 文件操作都将由一个库或一个守护进程来执行,而不是直接执行。目前实现的 cgroups 子系统需要某种形式的协调,因为每种资源类型只有一个控制器。当多个参与者修改它时,这必然会导致冲突。cgroups 控制器可以被许多项目同时使用,如libvirtsystemdlxc等等。当只通过 cgroups 文件系统操作工作时,当所有的项目都试图在太低的级别上通过 cgroups 强加它们自己的策略时,在彼此不了解的情况下,它们可能会意外地相互忽略。例如,当每一个都将与一个守护进程对话时,这样的冲突将被避免。有关libcg的更多信息,请参见http://libcg.sourceforge.net/

与名称空间相反,没有添加新的系统调用来实现 cgroup 子系统。与名称空间一样,可以嵌套几个 cgroups。在引导阶段添加了代码,主要是为了初始化 cgroups 子系统,以及各种子系统,比如内存子系统或安全子系统。以下是您可以使用 cgroups 执行的部分简短任务列表:

  • 用 cpusets cgroup 控制器将一组 CPU 分配给一组进程。您还可以使用 cpusets cgroup 控制器来控制 NUMA 节点内存的分配。
  • 操作内存不足(oom)杀手操作,或使用内存组控制器(memcg)创建内存量有限的进程。在本章的后面你会看到一个例子。
  • 使用设备组将权限分配给/dev下的设备。稍后,您将在“设备组-简单示例”一节中看到使用设备组的示例。
  • 为流量分配优先级(请参阅本章后面的“net_prio 模块”一节)。
  • 用冷冻器组冷冻过程。
  • 使用 cpuacct cgroup 报告 cgroup 任务的 CPU 资源使用情况。请注意,还有 cpu 控制器,它可以按优先级或绝对带宽提供 CPU 周期,并提供相同的统计信息或统计信息的超集。
  • 用类别标识符(classid)标记网络流量;请参阅本章后面的“cls_cgroup 分类器”一节。

接下来,我将非常简要地描述为支持 cgroups 所做的一些更改。

Cgroups 实现

cgroup 子系统非常复杂。这里有几个关于 cgroup 子系统的实现细节,应该可以为您提供一个深入研究其内部的良好起点:

  • 增加了一个名为cgroup_subsys的新结构 ( include/linux/cgroup.h)。它代表一个 cgroup 子系统(也称为 cgroup 控制器)。实现了以下 cgroup 子系统:

  • mem_cgroup_subsys : mm/memcontrol.c

  • blkio_subsys : block/blk-cgroup.c

  • cpuset_subsys : kernel/cpuset.c

  • devices_subsys : security/device_cgroup.c

  • freezer_subsys : kernel/cgroup_freezer.c

  • net_cls_subsys : net/sched/cls_cgroup.c

  • net_prio_subsys : net/core/netprio_cgroup.c

  • perf_subsys : kernel/events/core.c

  • cpu_cgroup_subsys : kernel/sched/core.c

  • cpuacct_subsys : kernel/sched/core.c

  • hugetlb_subsys : mm/hugetlb_cgroup.c

  • 添加了一个名为cgroup的新结构;它代表一个对照组(linux/cgroup.h)

  • A new virtual file system was added; this was done by defining the cgroup_fs_type object and a cgroup_ops object (instance of super_operations):

    static struct file_system_type cgroup_fs_type = {
            .name = "cgroup",
            .mount = cgroup_mount,
            .kill_sb = cgroup_kill_sb,
    };
    static const struct super_operations cgroup_ops = {
            .statfs = simple_statfs,
            .drop_inode = generic_delete_inode,
            .show_options = cgroup_show_options,
            .remount_fs = cgroup_remount,
    };
    (kernel/cgroup.c)
    

    像其他文件系统一样,用cgroup_init()方法中的register_filesystem()方法注册它;参见kernel/cgroup.c

  • 当初始化 cgroup 子系统时,默认创建下面的sysfs条目/sys/fs/cgroup;这是通过在cgroup_init()方法中调用kobject_create_and_add("cgroup", fs_kobj)来完成的。请注意,cgroup 控制器也可以安装在其他目录中。

  • 有一个名为subsyscgroup_subsys对象的全局数组,在kernel/cgroup.c中定义(注意从内核 3.11 开始,数组名从subsys改为cgroup_subsys)。此数组中有 CGROUP_SUBSYS_COUNT 个元素。名为/proc/cgroupsprocfs条目由 cgroup 子系统导出。可以用两种方式显示全局subsys数组的元素:

  • 通过运行cat /proc/cgroups

  • 通过libcgroup-toolslssubsys效用。

  • 创建新的 cgroup 需要始终在该 cgroup VFS 下生成以下四个控制文件:

  • 它的初始值是从它的父代继承的。它代表一个布尔变量,它的用法与release_agent 相关,只在最顶层的控制文件,稍后解释。

  • cgroup.event_control:这个文件允许使用eventfd()系统调用从 cgroup 获取通知。参见man 2 eventfd,和fs/eventfd.c

  • tasks:附属于该组的 PID 列表。将一个进程附加到一个 cgroup 是通过将其 PID 的值写入到tasks控制文件中来完成的,并且由cgroup_attach_task()方法kernel/cgroup.c来处理。显示一个进程附加到的 cgroups 是由cat /proc/<processPid>/cgroup完成的。这在内核中由kernel/cgroup.c中的proc_cgroup_show()方法处理。

  • cgroup.procs:附属于该 cgroup 的线程组 id 的列表。tasks条目允许将同一个进程的线程连接到不同的 cgroup 控制器,而cgroup.procs有一个进程级的粒度(单个进程的所有线程被一起移动并属于同一个 cgroup)。

  • 除了这四个控制文件之外,还为最顶层的 cgroup 根对象创建了一个名为release_agent的控制文件。这个文件的值是一个可执行文件的路径,当一个 cgroup 的最后一个进程终止时,这个可执行文件将被执行;应设置前面提到的notify_on_release,以便启用release_agent功能。release_agent可以被指定为一个 cgroup 挂载选项;例如,《??》中的 Fedora 就是这种情况。release_agent机制基于用户模式助手:每次激活release_agent时,都会调用call_usermodehelper()方法并创建一个新的用户空间进程,这在性能方面代价很高。参见:《对照组的过去、现在、未来》,lwn.net/Articles/574317/。关于release_agent的实现细节,请参见kernel/cgroup.c中的cgroup_release_agent()方法。

  • 除了这四个默认控制文件和release_agent最顶层的控制文件,每个子系统都可以创建自己的特定控制文件。这是通过定义一个cftype(控制文件类型)对象的数组并将该数组分配给cgroup_subsys对象的base_cftypes成员来实现的。例如,对于内存组控制器,我们对usage_in_bytes控制文件有这样的定义:

    static struct cftype mem_cgroup_files[] = {
            {
                    .name = "usage_in_bytes",
                    .private = MEMFILE_PRIVATE(_MEM, RES_USAGE),
                    .read = mem_cgroup_read,
                    .register_event = mem_cgroup_usage_register_event,
                    .unregister_event = mem_cgroup_usage_unregister_event,
            },
            . . .
    
    struct cgroup_subsys mem_cgroup_subsys = {
            .name = "memory",
            . . .
            .base_cftypes = mem_cgroup_files,
    };
    (mm/memcontrol.c)
    
    
  • 一个名为cgroups的成员被添加到流程描述符task_struct中,它是一个指向css_set对象的指针。css_set对象包含一个指向cgroup_subsys_state对象的指针数组(每个 cgroup 子系统一个这样的指针)。流程描述符本身(task_struct)不包含指向与其相关联的 cgroup 子系统的直接指针,但是这可以从这个cgroup_subsys_state指针.的数组中确定

添加了两个 cgroups 网络模块。我们将在本节稍后讨论它们:

  • net_prio ( net/core/netprio_cgroup.c)。
  • cls_cgroup ( net/sched/cls_cgroup.c)。

image 注意cgroup 子系统仍处于早期阶段,其功能和界面可能会有相当大的发展。

接下来,您将看到一个简短的示例,说明如何使用设备组控制器来更改设备文件的写权限。

群组设备控制器:一个简单的例子

让我们看一个使用设备组的简单示例。运行以下命令将创建一个设备组:

mkdir   /sys/fs/cgroup/devices/0

将在/sys/fs/cgroup/devices/0下创建三个控制文件:

  • devices.deny:访问被拒绝的设备。
  • devices.allow:允许访问的设备。
  • devices.list:可用设备。

每个这样的控制文件包含四个字段:

  • type:可能的值为:“a”表示全部,“c”表示字符设备,“b”表示块设备。
  • 设备主号码。
  • 设备次要编号。
  • 访问权限:' r '是读取权限,' w '是写入权限,' m '是执行mknod的权限。

默认情况下,创建新的设备组时,它拥有所有权限:

cat /sys/fs/cgroup/devices/0/devices.list
a *:* rwm

以下命令将当前 shell 添加到您之前创建的设备组中:

echo $$ > /sys/fs/cgroup/devices/0/tasks

以下命令将拒绝所有设备的访问:

echo a > /sys/fs/cgroup/devices/0/devices.deny
echo "test" > /dev/null
-bash: /dev/null: Operation not permitted

以下命令将返回所有设备的访问权限:

echo a >  /sys/fs/cgroup/devices/0/devices.allow

运行之前失败的以下命令现在将成功:

echo "test" > /dev/null

群组内存控制器:一个简单的例子

例如,您可以禁用内存不足(OOM)杀手:

mkdir /sys/fs/cgroup/memory/0
echo $$ > /sys/fs/cgroup/memory/0/tasks
echo 1 > /sys/fs/cgroup/memory/0/memory.oom_control

现在,如果你运行一些占用内存的用户空间程序,OOM 杀手将不会被调用。可以通过以下方式启用 OOM 杀手:

echo 0 > /sys/fs/cgroup/memory/0/memory.oom_control

您可以使用eventfd()系统调用,在用户空间应用中获取关于 cgroup 状态变化的通知。参见man 2 eventfd

image 注意您可以限制一个 cgroup 中的一个进程最多可拥有 20M 的内存,例如:

echo 20M >/sys/fs/cgroup/memory/0/memory . limit _ in _ bytes

net_prio 模块

网络优先级控制组(net_prio)提供了一个接口,用于设置各种用户空间应用生成的网络流量的优先级。通常这可以通过设置 SO_PRIORITY 套接字选项来完成,该选项设置 SKB 的优先级,但是并不总是希望使用该套接字选项。为了支持net_prio模块,一个名为priomap的对象,一个netprio_map结构的实例,被添加到了net_device对象中。让我们来看看netprio_map的结构:

struct netprio_map {
        struct rcu_head rcu;
        u32 priomap_len;
        u32 priomap[];
};
(include/net/netprio_cgroup.h)

priomap数组正在使用net_prio sysfs条目,您很快就会看到这一点。net_prio模块向 cgroup sysfs导出两个条目:net_prio.ifpriomapnet_prio.prioidxnet_prio.ifpriomap用于设置指定网络设备的priomap对象,您将在接下来的例子中看到。在 Tx 路径中,dev_queue_xmit()方法调用skb_update_prio()方法,根据priomap设置skb->priority,该priomap与出局网络设备(skb->dev)相关联。net_prio.prioidx是一个只读条目,显示 cgroup 的 id。net_prio模块是一个很好的例子,说明用不到 400 行代码开发一个 cgroup 内核模块是多么简单。net_prio模块由 Neil Horman 开发,可从内核 3.3 获得。更多信息见Documentation/cgroups/net_prio.txt。以下是如何使用网络优先级 cgroup 模块的示例(注意,如果 CONFIG_NETPRIO_CGROUP 设置为模块而非内置模块,则必须加载netprio_cgroup.ko内核模块):

mkdir /sys/fs/cgroup/net_prio
mount -t cgroup -onet_prio none /sys/fs/cgroup/net_prio
mkdir /sys/fs/cgroup/net_prio/0
echo "eth1 4" > /sys/fs/cgroup/net_prio/0/net_prio.ifpriomap

这个命令序列会将源自属于 net prio“0”组的进程并在接口eth1上传出的任何流量设置为优先级 4。最后一个命令触发向名为priomapnet_device对象中的字段写入一个条目。

image 注意为了使用 net_prio,需要设置 CONFIG_NETPRIO_CGROUP。

cls_cgroup 分类器

cls_cgroup分类器提供了用类标识符(classid)标记网络数据包的接口。您可以将它与tc工具结合使用,为来自不同 cgroups 的数据包分配不同的优先级,您很快就会看到这个例子。cls_cgroup模块将一个条目导出到 cgroup sysfsnet_cls.classid。对照组分类器(cls_cgroup)被合并到 kernel 2.6.29 中,由 Thomas Graf 开发。与上一节讨论的net_prio模块一样,这个 cgroup 内核模块也不到 400 行代码,这再次证明了通过内核模块添加 cgroup 控制器并不是一项繁重的任务。下面是一个使用控制组分类器的例子(注意,如果 CONFIG_NETPRIO_CGROUP 被设置为模块而不是内置的,您必须加载cls_cgroup.ko内核模块):

mkdir /sys/fs/cgroup/net_cls
mount -t cgroup -onet_cls none /sys/fs/cgroup/net_cls
mkdir /sys/fs/cgroup/net_cls/0
echo 0x100001 > /sys/fs/cgroup/net_cls/0/net_cls.classid

最后一个命令将 classid 10:1 分配给组 0。iproute2包包含一个名为tc的实用程序,用于管理流量控制设置。你可以使用这个类 id 的tc工具,例如:

tc qdisc add dev eth0 root handle 10: htb
tc class add dev eth0 parent 10: classid 10:1 htb rate 40mbit
tc filter add dev eth0 parent 10: protocol ip prio 10 handle 1: cgroup

更多信息见Documentation/cgroups/net_cls.txt(仅来自内核 3.10。)

image 为了配合cls_cgroup工作,需要设置 CONFIG_NET_CLS_CGROUP。

我将用一小段关于挂载 cgroup 的内容来结束关于 cgroup 子系统的讨论。

安装 c 组子系统

除了默认创建的/sys/fs/cgroup之外,还可以在其他挂载点挂载 cgroup 子系统。例如,您可以按以下顺序将内存控制器安装在/mycgroup/mymemtest上:

mkdir –p /mycgroup/mymemtest
mount -t cgroup -o memory mymemtest /mycgroup/mymemtest

以下是装载 cgroup 子系统时的一些装载选项:

  • all:挂载所有 cgroup 控制器。

  • none:不要安装任何控制器。

  • release_agent:一个可执行文件的路径,当一个 cgroup 的最后一个进程终止时,该可执行文件将被执行。Systemd使用release_agent组装载选项。

  • noprefix:避免在控制文件中使用前缀。每个 cgroup 控制器都有自己的控制文件前缀;例如,cpuset 控制器条目mem_exclusive显示为cpuset.mem_exclusive。noprefix 挂载选项避免了添加控制器前缀。例如,

    mkdir /cgroup
    mount -t tmpfs xxx /cgroup/
    mount -t cgroup -o noprefix,cpuset xxx /cgroup/
    ls /cgroup/
    cgroup.clone_children  mem_hardwall             mems
    cgroup.event_control   memory_migrate           notify_on_release
    cgroup.procs           memory_pressure          release_agent
    cpu_exclusive          memory_pressure_enabled  sched_load_balance
    cpus                   memory_spread_page       sched_relax_domain_level
    mem_exclusive          memory_spread_slab       tasks
    

image 注意想要深入研究如何解析 cgroups 挂载选项的读者应该研究一下parse_cgroupfs_options()方法,kernel/cgroup.c.

有关 cgroups 的更多信息,请参见以下资源:

  • Documentation/cgroups
  • cgroups 邮件列表:cgroups@vger.kernel.org
  • cgroups 邮件列表存档:http://news.gmane.org/gmane.linux.kernel.cgroups
  • git仓库:git://git.kernel.org/pub/scm/linux/kernel/git/tj/cgroup.git

image 注意 Linux 名称空间和 cgroups 是正交的,技术上不相关。您可以构建支持名称空间和不支持 cgroups 的内核,反之亦然。过去有一个名为“ns”的 cgroups 命名空间子系统的实验,但是代码最终被删除了。

您已经看到了什么是 cgroups,并且了解了它的两个网络模块,net_priocls_cgroup。您还看到了演示如何使用设备、内存和网络组控制器的简短示例。内核 3.11 及更高版本中添加的繁忙轮询套接字特性为套接字提供了更低的延迟。让我们看看它是如何实现的,以及它是如何配置和使用的。

忙轮询套接字

当套接字队列耗尽时,网络堆栈的传统操作方式是休眠,等待驱动程序将更多数据放入套接字队列,或者如果是非阻塞操作,则返回。由于中断和上下文切换,这会导致额外的延迟。对于需要尽可能低的延迟并愿意为更高的 CPU 利用率付出代价的套接字应用,Linux 增加了对内核 3.11 及更高版本的套接字进行忙轮询的功能(最初,这种技术被称为低延迟套接字轮询,但根据 Linus 的建议,它被更改为忙轮询套接字)。繁忙轮询采用更积极的方法将数据转移到应用。当应用请求更多数据而套接字队列中没有数据时,网络堆栈会主动调用设备驱动程序。驱动程序检查新到达的数据,并通过网络层(L3)将其推送到套接字。驱动程序可能会找到其他套接字的数据,并且也会推送这些数据。当轮询调用返回到网络堆栈时,套接字代码检查套接字接收队列上是否有新数据挂起。

为了让网络驱动程序支持忙轮询,它应该提供自己的忙轮询方法,并将其添加为net_device_ops对象的ndo_busy_poll回调。这个驱动程序ndo_busy_poll回调应该将数据包移动到网络堆栈中;例如,参见ixgbe_low_latency_recv()方法、drivers/net/ethernet/intel/ixgbe/ixgbe_main.c。这个ndo_busy_poll回调应该返回移动到堆栈中的包的数量,如果没有这样的包,则返回 0,如果有问题,则返回 LL_FLUSH_FAILED 或 LL_FLUSH_BUSY。未填充 ndo_busy_poll回调的未修改驱动程序将继续照常工作,并且不会忙于轮询。

提供低延迟的一个重要因素是忙轮询。有时,当驱动程序轮询例程返回时没有数据,更多的数据正在到达,只是错过了返回到网络堆栈。这就是繁忙轮询发挥作用的地方。网络堆栈在一段可配置的时间内轮询驱动程序,以便新数据包一到达就可以被拾取。

设备驱动程序的主动和繁忙轮询可以提供与硬件非常接近的减少的延迟。繁忙轮询可以同时用于大量套接字,但不会产生最佳结果,因为当使用相同的 CPU 内核时,一些套接字上的繁忙轮询会降低其他套接字的速度。图 14-1 对比了传统的接收流程和已启用忙轮询的套接字流程。

9781430261964_Fig14-01.jpg

图 14-1 。传统接收流与繁忙轮询套接字接收流

1\. Application checks for receive.              1\. Application checks for receive
2\. No immediate receive – thus block.           2\. Check device driver for pending packet (poll starts).
3\. Packet Received.                             3\. Meanwhile, packet received to NIC.
4\. Driver passes packet to the protocol layer.  4\. Driver processes pending packet
5\. Protocol/socket wakes application.           5\. Driver passes to the protocol layer
   - Bypass context switch and interrupt.
6\. Application receives data through sockets.   6\. Application receives data through sockets.
   Repeat.                                      Repeat.

全球启用

可以通过procfs参数为所有套接字全局打开套接字上的忙轮询,也可以通过设置 SO_BUSY_POLL 套接字选项为单个套接字打开忙轮询。全局使能有两个参数:net.core.busy_pollnet.core.busy_read,分别由/proc/sys/net/core/busy_poll/proc/sys/net/core/busy_read输出到procfs。默认情况下,这两个值都为零,这意味着繁忙轮询处于关闭状态。设置这些值将启用全局忙轮询。值为 50 通常会产生良好的结果,但是一些实验可能有助于为某些应用找到更好的值。

  • busy_read控制忙轮询时阻塞读操作的时间限制。对于非阻塞读取,如果套接字启用了繁忙轮询,则堆栈代码在将控制权返回给用户之前只轮询一次。
  • busy_poll控制 select 和 poll 将在多长时间内忙轮询,等待任何已启用忙轮询的套接字上的新事件。只有启用了忙读套接字操作的套接字才会被忙轮询。

更多信息,请参见:Documentation/sysctl/net.txt

每套接字启用

启用忙轮询的一个更好的方法是修改应用以使用 SO_BUSY_POLL 套接字选项,该选项设置套接字对象的sk_ll_usec(sock结构的一个实例)。通过使用这个套接字选项,应用可以指定哪些套接字在忙着轮询,以便只增加这些套接字的 CPU 利用率。来自其他应用和服务的套接字将继续使用传统的接收路径。SO_BUSY_POLL 的建议起始值是 50。sysctl.net.busy_read值必须设置为 0,并且sysctl.net.busy_poll值应按照Documentation/sysctl/net.txt中所述进行设置。

调谐和配置

这里有几种方法可以调整和配置忙轮询套接字:

  • 网络设备上rx-usecs的中断合并(ethtool -C设置应该在 100 左右,以降低中断率。这限制了由中断引起的上下文切换的数量。
  • 通过在网络设备上使用ethtool -K禁用 GRO 和 LRO 可以避免接收队列上的无序数据包。只有当混合的批量和低延迟流量到达同一队列时,这才应该是一个问题。一般来说,启用 GRO 和 LRO 通常能获得最佳效果。
  • 应用线程和网络设备 IRQ 应该绑定到不同的 CPU 内核。两组内核应该与网络设备位于同一个 CPU NUMA 节点上。当应用和 IRQ 在同一个内核上运行时,会有一点点损失。如果中断合并设置为较低的值,这种损失可能会非常大。
  • 为了获得最低延迟,关闭 I/O 内存管理单元(IOMMU)支持可能会有所帮助。在某些系统上,这可能已经被默认禁用。

性能

许多使用繁忙轮询套接字的应用应该显示出减少的延迟和抖动以及改进的每秒事务数。然而,随着 CPU 争用的增加,用太多忙于轮询的套接字使系统过载会损害性能。参数net.core.busy_pollnet.core.busy_read和 SO_BUSY_POLL 套接字选项都是可调的。试验这些值可能会为各种应用提供更好的结果。

我现在将开始讨论三个无线子系统,它们通常服务于短距离和低功耗设备:蓝牙子系统、IEEE 802.15.4 和 NFC。随着新的激动人心的功能稳步增加,人们对这三个子系统的兴趣越来越大。我将从蓝牙子系统开始讨论。

Linux 蓝牙子系统

蓝牙协议是主要用于小型和嵌入式设备的主要传输协议之一。如今,几乎每一台新的笔记本电脑或平板电脑、每一部手机以及许多电子产品中都包含了蓝牙网络接口。蓝牙协议是由移动供应商爱立信在 1994 年创建的。起初,它是用来替代点对点连接的电缆。后来,它发展到支持无线个人区域网络(pan)。蓝牙工作在 2.4 GHz 工业、科学和医疗(ISM)无线电频段,低功率传输无需许可证。蓝牙规范由成立于 1998 年的蓝牙特别兴趣小组(SIG)正式制定;参见https://www.bluetooth.org。SIG 负责蓝牙规范的开发和认证过程,这有助于确保不同厂商的蓝牙设备之间的互操作性。蓝牙核心规范是免费的。多年来,蓝牙有多种规范,我将提到最近的一种:

  • 蓝牙 2.0 +增强数据速率(EDR) 从 2004 年开始。
  • 蓝牙 v 2.1+EDR 2007;包括通过安全简单配对(SSP)来改进配对过程。
  • 2009 年起的蓝牙 v3.0 + HS(高速);主要的新功能是 AMP(备用 MAC/PHY),增加了 802.11 作为高速传输。
  • 蓝牙 4.0 + BLE(蓝牙低能耗,以前称为 WiBree)从 2010 年开始。

蓝牙协议有多种用途,如文件传输、音频流、医疗保健设备、网络等等。蓝牙是为短距离数据交换而设计的,通常在 10 米的范围内。蓝牙设备分为三类,范围如下:

  • 1 级–大约 100 米
  • 2 级–大约 10 米
  • 3 级–大约 1 米

Linux 蓝牙协议栈被称为 BlueZ。最初它是由高通发起的一个项目。它被正式集成到内核 2.4.6 (2001)中。图 14-2 显示了蓝牙堆栈。

9781430261964_Fig14-02.jpg

图 14-2 。蓝牙栈。注意:在 L2CAP 以上的层中,可能有本章未讨论的其他蓝牙协议,如 AVDTP(音频/视频分发传输协议)、HFP(免提模式)、音频/视频控制传输协议(AVCTP)等等

  • 较低的三层(无线电层、链路控制器和链路管理协议)在硬件或固件中实现。

  • 主机控制器接口(HCI) 指定主机如何与本地蓝牙设备(控制器)交互和通信。我将在本章后面的“HCI 层”部分讨论它。

  • L2CAP(逻辑链路控制和适配协议)提供从其他蓝牙设备发送和接收数据包的能力。应用可以使用 L2CAP 协议作为基于消息的、不可靠的数据传输协议,类似于 UDP 协议。从用户空间访问 L2CAP 协议是通过 BSD sockets API 完成的,这在第十一章中讨论过。请注意,在 L2CAP 中,数据包总是按照发送的顺序传送,这与 UDP 相反。在图 14-2 中,我展示了位于 L2CAP 之上的三个协议(如前所述,在 L2CAP 之上还有其他协议没有在本章中讨论)。

  • BNEP:蓝牙网络封装协议。在本章的后面,我将给出一个使用 BNEP 协议的例子。

  • RFCOMM:射频通信(RFCOMM)协议是一个可靠的基于流的协议。RFCOMM 只允许在 30 个端口上运行。RFCOMM 用于模拟串行端口上的通信和发送无帧数据。

  • SDP:服务发现协议。支持应用在其运行的 SDP 服务器中注册描述和端口号。客户端可以在提供描述的 SDP 服务器中执行查找。

  • SCO(面向同步连接)层:用于发送音频;我在这一章不深入研究它的细节,因为它超出了本书的范围。

  • 蓝牙规范定义了可能的应用,并规定了支持蓝牙的设备用来与其他蓝牙设备通信的一般行为。蓝牙 profiles 有很多,我就提几个最常用的:

  • 文件传输配置文件(FTP):操作和传输另一个系统的对象存储(文件系统)中的对象(文件和文件夹)。

  • 医疗设备配置文件(HDP):处理医疗数据。

  • 人机界面设备配置文件(HID):USB HID(人机界面设备)的包装,为鼠标和键盘等设备提供支持。

  • 对象推送配置文件(OPP)-推送对象配置文件。

  • 个人区域网络配置文件(PAN):通过蓝牙链接提供网络;在本章后面的 BNEP 部分你会看到一个例子。

  • 耳机模式(HSP):支持与手机配合使用的蓝牙耳机。

此图中的七层大致与操作系统模型的七层平行。无线电(RF)层平行于物理层,链路控制器平行于数据链路层,链路管理协议平行于网络协议,等等。Linux 蓝牙子系统由几个部分组成:

  • 蓝牙核心

  • HCI 设备和连接管理器、调度器;文件:net/bluetooth/hci*.cnet/bluetooth/mgmt.c

  • 蓝牙地址家族套接字;文件:net/bluetooth/af_bluetooth.c

  • SCO 音频链接;文件:net/bluetooth/sco.c

  • L2CAP(逻辑链路控制和适配协议);文件:net/bluetooth/l2cap*.c

  • LE(低能量)链路上的 SMP(安全管理协议);文件:net/bluetooth/smp.c

  • AMP 经理-替代 MAC/PHY 管理;文件:net/bluetooth/a2mp.c

  • HCI 设备驱动程序(硬件接口);文件:drivers/bluetooth/*。包括供应商特定的驱动程序和通用驱动程序,如蓝牙 USB 通用驱动程序btusb

  • RFCOMM 模块(RFCOMM 协议);文件:net/bluetooth/rfcomm/*

  • BNEP 模块(蓝牙网络封装协议);文件:net/bluetooth/bnep/*

  • ISDN 协议使用的 CMTP 模块(CAPI 报文传输协议)。CMTP 实际上已经过时了;文件:net/bluetooth/cmtp/*

  • HIDP 模块(人机接口设备协议);文件:net/bluetooth/hidp/*

我简要地讨论了蓝牙协议、蓝牙协议栈的架构、Linux 蓝牙子系统树以及蓝牙规范。在下一节中,我将描述 HCI 层,它是 LMP 上面的第一层(见本节前面的图 14-2 )。

HCI 层

我将从描述 HCI 设备开始 HCI 层的讨论,它代表一个蓝牙控制器。在这一部分的后面,我将描述 HCI 层和它下面的层,链路控制器层之间的接口,以及 HCI 和它上面的层,L2CAP 和 SCO 之间的接口。

人机界面设备

蓝牙设备由struct hci_dev表示。这个结构相当大(超过 100 个成员),这里将部分显示:

struct hci_dev {
         char            name[8];
         unsigned long   flags;
         __u8            bus;
         bdaddr_t        bdaddr;
         __u8            dev_type;
         . . .
         struct work_struct      rx_work;
         struct work_struct      cmd_work;
         . . .
         struct sk_buff_head     rx_q;
         struct sk_buff_head     raw_q;
         struct sk_buff_head     cmd_q;
         . . .
         int (*open)(struct hci_dev *hdev);
         int (*close)(struct hci_dev *hdev);
         int (*flush)(struct hci_dev *hdev);
         int (*send)(struct sk_buff *skb);
         void (*notify)(struct hci_dev *hdev, unsigned int evt);
         int (*ioctl)(struct hci_dev *hdev, unsigned int cmd, unsigned long arg);
}
(include/net/bluetooth/hci_core.h)

以下是对hci_dev结构中一些重要成员的描述:

  • flags:表示设备的状态,如 HCI_UP 或 HCI_INIT。

  • bus:与设备相关的总线,如 USB (HCI_USB)、UART (HCI_UART)、PCI (HCI_PCI)等。(参见include/net/bluetooth/hci.h)。

  • 每个 HCI 设备都有一个唯一的 48 位地址。由/sys/class/bluetooth/<hciDeviceName>/address出口到sysfs

  • dev_type:蓝牙设备有两种类型:

  • 基本速率器件(HCI_BREDR)。

  • 备用 MAC 和 PHY 设备(HCI_AMP)。

  • rx_work:通过hci_rx_work()回调处理接收保存在 HCI 设备的rx_q队列中的数据包。

  • cmd_work:通过hci_cmd_work()回调处理发送保存在 HCI 设备的cmd_q队列中的命令包。

  • rx_q:skb 的接收队列。在hci_recv_frame()方法中,当接收到 SKB 时,通过调用skb_queue_tail()方法将 skb 添加到rx_q中。

  • raw_q:通过调用hci_sock_sendmsg()方法中的skb_queue_tail()方法将 skb 添加到raw_q中。

  • cmd_q:命令队列。通过调用hci_sock_sendmsg()方法中的skb_queue_tail()方法,skb 被添加到cmd_q中。

hci_dev回调(如open()close()send()等)通常在蓝牙设备驱动程序的probe()方法中分配(例如参考通用 USB 蓝牙驱动程序drivers/bluetooth/btusb.c)。

HCI 层导出注册/注销 HCI 设备的方法(分别通过hci_register_dev()hci_unregister_dev()方法)。这两种方法都将一个hci_dev对象作为单个参数。如果没有定义指定的hci_dev对象的open()close()回调,注册将会失败。

有五种类型的 HCI 数据包:

  • HCI_COMMAND_PKT:从主机发送到蓝牙设备的命令。
  • HCI_ACLDATA_PKT:从蓝牙设备发送或接收的异步数据。ACL 代表异步面向连接的链路(ACL)协议。
  • HCI_SCODATA_PKT:从蓝牙设备发送或接收的同步数据(通常是音频)。SCO 代表面向同步连接(SCO)。
  • HCI_EVENT_PKT:事件(如连接建立)发生时发送。
  • HCI_VENDOR_PKT:用于某些蓝牙设备驱动程序中,以满足供应商的特定需求。

HCI 及其下层(链路控制器)

HCI 通过以下方式与其下一层(链路控制器)通信:

  • 通过调用hci_send_frame()方法发送数据包(HCI_ACLDATA_PKT 或 HCI_SCODATA_PKT ),该方法将调用委托给hci_dev对象的send()回调。hci_send_frame()方法获取一个 SKB 作为单个参数。
  • 通过调用hci_send_cmd()方法发送命令包(HCI_COMMAND_PKT)。例如,发送扫描命令。
  • 通过调用hci_acldata_packet()方法或调用hci_scodata_packet()方法接收数据包。
  • 通过调用hci_event_packet()方法接收事件包。处理 HCI 命令是异步的;因此,在发送命令包(HCI_COMMAND_PKT)一段时间后,HCIrx_work work_queue(hci_rx_work()方法)会收到一个或几个事件作为响应。有超过 45 个不同的事件(参见include/net/bluetooth/hci.h中的 HCI_EV_*)。例如,当使用命令行hcitool扫描附近的蓝牙设备时,到hcitool scan,会发送一个命令包(HCI_OP_INQUIRY)。因此,异步返回三个事件包,由hci_event_packet()方法处理:HCI_EV_CMD_STATUS、HCI_EV_EXTENDED_INQUIRY_RESULT 和 HCI_EV_INQUIRY_COMPLETE。

HCI 及其上面的层(L2CAP/SCO)

让我们来看看 HCI 层与其上层(L2CAP 层和 SCO 层)通信的方法:

  • HCI 在接收数据包时通过调用hci_acldata_packet()方法与其上面的 L2CAP 层通信,该方法调用 L2CAP 协议的l2cap_recv_acldata()方法。
  • HCI 通过调用 SCO 协议的sco_recv_scodata()方法调用hci_scodata_packet()方法,在接收 SCO 包时与其上面的 SCO 层进行通信。

人机界面连接

HCI 连接由hci_conn结构表示:

struct hci_conn {
        struct list_head list;
        atomic_t         refcnt;
        bdaddr_t         dst;
        . . .
        __u8              type;

}
(include/net/bluetooth/hci_core.h)

下面是对hci_conn结构的一些成员的描述:

  • refcnt:参考计数器。

  • dst:蓝牙目的地址。

  • type:表示连接的类型:

  • SCO_LINK 用于 SCO 连接。

  • ACL 连接的 ACL_LINK。

  • 用于扩展同步连接的 ESCO_LINK。

  • LE _ LINK–代表 LE(低能量)连接;是在内核 v2.6.39 中添加的,支持蓝牙 V4.0,增加了 LE 特性。

  • AMP _ LINK–在 3.6 版中添加,支持蓝牙放大器控制器。

HCI 连接是通过调用hci_connect()方法创建的。有三种类型的连接:SCO、ACL 和 LE 连接。

L2CAP

为了提供几个数据流,L2CAP 使用通道,这些通道由l2cap_chan结构(include/net/bluetooth/l2cap.h)表示。有一个全球频道链表,名为chan_list。对这个列表的访问由一个全局读写锁chan_list_lock序列化。

我在本章前面的“HCI 和它上面的层(L2CAP/SCO)”一节中描述的l2cap_recv_acldata()方法在 HCI 将数据包传递到 L2CAP 层时被调用。l2cap_recv_acldata()方法首先执行一些完整性检查,如果有问题就丢弃数据包,然后在收到完整数据包的情况下调用l2cap_recv_frame()方法。每个收到的数据包都以 L2CAP 报头开始:

struct l2cap_hdr {
        __le16     len;
        __le16     cid;
} __attribute__ ((packed));
(include/net/bluetooth/l2cap.h)

l2cap_recv_frame()方法通过检查l2cap_hdr对象的cid来检查接收包的通道 id。如果是 L2CAP 命令(?? 为 0x0001),则调用l2cap_sig_channel()方法来处理它。例如,当另一个蓝牙设备想要连接到我们的设备时,在 L2CAP 信号通道上接收到一个 L2CAP _ 连接 _ 请求,这个请求将由l2cap_connect_req()方法net/bluetooth/l2cap_core.c处理。在l2cap_connect_req()方法中,通过pchan->ops->new_connection()调用l2cap_chan_create()方法来创建 L2CAP 通道。L2CAP 通道状态设置为 BT_OPEN,配置状态设置为 CONF _ 非 _ 完整。这意味着应该配置通道以便使用它。

BNP

BNEP 协议支持蓝牙上的 IP,这实际上意味着在 L2CAP 蓝牙信道上运行 TCP/IP 应用。您也可以通过蓝牙 RFCOMM 上的 PPP 运行 TCP/IP 应用,但是通过串行 PPP 链接联网效率较低。BNEP 协议使用 PAN 协议。我将展示一个使用 BNEP 协议建立基于 IP 的蓝牙的简短例子,随后我将描述实现这种通信的内核方法。探究 BNEP 的细节超出了本书的范围。如果你想了解更多,请参阅 BNEP 规范,它可以在:http://grouper.ieee.org/groups/802/15/Bluetooth/BNEP.pdf中找到。创建 PAN 的一个非常简单的方法是运行:

  • 在服务器端:

  • pand --listen --role=NAP

  • 注:NAP 代表:网络接入点(NAP)

  • 在客户端

  • pand --connect btAddressOfTheServer

在两个端点上,创建一个虚拟接口(bnep0)。之后,您可以使用ifconfig命令(或使用ip命令)在bnep0上为两个端点分配 IP 地址,就像以太网设备一样,您将在这些端点之间通过蓝牙建立网络连接。详见http://bluez.sourceforge.net/contrib/HOWTO-PAN

pand --listen命令创建一个 L2CAP 服务器套接字,并调用accept()系统调用,而pand --connect btAddressOfTheServer创建一个 L2CAP 客户端套接字并调用connect()系统调用。当服务器端收到连接请求时,它发送一个 BNEPCONNADD 的 IOCTL,这个 IOCTL 在内核中由bnep_add_connection()方法(net/bluetooth/bnep/core.c)处理,它执行以下任务:

  • 创建一个 BNEP 会话(bnep_session对象)。
  • 通过调用__bnep_link_session()方法将 BNEP 会话对象添加到 BNEP 会话列表(bnep_session_list)。
  • 创建一个名为bnepX的网络设备(对于第一个 BNEP 设备,X 为 0,对于第二个设备,X 为 1,依此类推)。
  • 通过调用register_netdev()方法注册网络设备。
  • 创建一个名为“kbnepd btDeviceName”的内核线程。这个内核线程运行包含无限循环的bnep_session()方法来接收或发送数据包。只有当用户空间应用发送一个 BNEPCONNDEL 的 IOCTL,调用方法bnep_del_connection()来设置 BNEP 会话的终止标志时,或者当套接字的状态改变并且不再连接时,这个无限循环才会终止。
  • bnep_session()方法调用bnep_rx_frame()方法接收传入的数据包并将其传递给网络堆栈,它调用bnep_tx_frame()方法发送传出的数据包。

接收蓝牙数据包:示意图

图 14-3 显示了接收到的蓝牙 ACL 数据包的路径(与 SCO 相反,SCO 用于处理音频,处理方式不同)。通过hci_acldata_packet()方法,处理数据包的第一层是 HCI 层。然后它通过调用l2cap_recv_acldata()方法前进到更高的 L2CAP 层。

9781430261964_Fig14-03.jpg

图 14-3 。接收 ACL 数据包

l2cap_recv_acldata()方法调用l2cap_recv_frame()方法,后者从 SKB 获取 L2CAP 头(l2cap_hdr对象在前面已经描述过了)。

根据 L2CAP 报头的信道 ID 采取行动。

L2CAP 扩展功能

内核 2.6.36 中增加了对 L2CAP 扩展特性(也称为 eL2CAP)的支持。这些扩展功能包括:

  • 增强型重传模式(ERTM),一种具有错误和流量控制的可靠协议。
  • 流模式(SM),一种不可靠的流协议。
  • 帧校验序列(FCS),即每个接收到的数据包的校验和。
  • L2CAP 数据包的分段和重组(SAR)使重新传输更加容易。

其中一些扩展是新配置文件所必需的,如蓝牙健康设备配置文件(HDP)。请注意,这些功能以前也是可用的,但它们被认为是实验性的,默认情况下是禁用的,您应该设置 CONFIG_BT_L2CAP_EXT_FEATURES 来启用它们。

蓝牙工具

从用户空间访问内核是用套接字完成的,只做了很小的改动:我们使用 AF_BLUTOOTH 套接字,而不是 AF_INET 套接字。以下是一些重要且有用的蓝牙工具的简短描述:

  • hciconfig:配置蓝牙设备的工具。显示诸如接口类型(BR/EDR 或 AMP)、蓝牙地址、标志等信息。hciconfig工具的工作原理是打开一个原始 HCI 套接字(BTPROTO_HCI)并发送 IOCTLs 例如,为了启动或关闭 HCI 设备,分别发送 HCIDEVUP 或 HCIDEVDOWN。这些 IOCTLs 在内核中由hci_sock_ioctl()方法net/bluetooth/hci_sock.c处理。
  • 用于配置蓝牙连接和向蓝牙设备发送一些特殊命令的工具。例如hcitool scan会扫描附近的蓝牙设备。
  • hcidump:转储来自和去往蓝牙设备的原始 HCI 数据。
  • 发送一个 L2CAP 回应请求并接收回答。
  • 更友好的版本。
  • 更友好的版本。

您可以在以下网址找到有关 Linux 蓝牙子系统的更多信息:

  • Linux BlueZ,官方 Linux 蓝牙网站: http://www.bluez.org

  • Linux 蓝牙邮件列表:linux-bluetooth@vger.kernel.org

  • Linux 蓝牙邮件列表档案:http://www.spinics.net/lists/linux-bluetooth/

  • 注意,这个邮件列表是针对蓝牙内核补丁和蓝牙用户空间补丁的。

  • freenode.net上的 IRC 频道:

  • bluez(发展相关话题)

  • bluez-users(与发展无关的话题)

在这一节中,我描述了 Linux 蓝牙子系统,重点是这个子系统的网络方面。您了解了蓝牙协议栈的各个层,以及它们是如何在 Linux 内核中实现的。您还了解了重要的蓝牙内核结构,如 HCI 设备和 HCI 连接。接下来,我将描述第二个无线子系统,IEEE 802 . 15 . 4 子系统及其实现。

IEEE 802.15.4 和 6LoWPAN

IEEE 802.15.4 标准(IEEE Std 802.15.4-2011)为低速率无线个人区域网(LR-WPAN)指定了媒体接入控制(MAC) 层和物理层(PHY)。适用于短程网络中的低成本和低功耗设备。支持多种频段,其中最常见的是 2.4 GHz ISM 频段、915 MHz 和 868 MHz。IEEE 802.15.4 设备可用于无线传感器网络(WSNs) 、安全系统、工业自动化系统等。它被设计用来组织传感器、开关、自动化设备等的网络。最大允许比特率为 250 kb/s。该标准还支持 2.4 GHz 频段的 1000 kb/s 比特率,但不太常见。典型的个人操作空间约为 10 米。IEEE 802.15.4 标准由 IEEE 802.15 工作组(http://www.ieee802.org/15/)维护。IEEE 802.15.4 之上有几个协议;最著名的是 ZigBee 和 6LoWPAN。

ZigBee 联盟(ZA)已经发布了 IEEE802.15.4 的非 GPL 规范,以及 ZigBee IP (Z-IP)开放标准(http://www.zigbee.org/Specifications/ZigBeeIP/Overview.aspx)。它基于 IPv6、TCP、UDP、6LoWPAN 等互联网协议。对 IEEE 802.15.4 使用 IPv6 协议是一个很好的选择,因为 IPv6 地址有巨大的地址空间,这使得为每个 IPv6 节点分配唯一的可路由地址成为可能。IPv6 标头比 IPv4 标头简单,处理其扩展标头比处理 IPv4 标头选项简单。使用 IPv6 和 LR-WPAN 被称为低功率无线个人区域网(6LoWPAN)上的 IPv6。IPv6 不适合在 LR-WPAN 上使用,因此需要适配层,这将在本节稍后解释。有五个 RFC 与 6LoWPAN 相关:

  • RFC 4944:“通过 IEEE 802.15.4 网络传输 IPv6 数据包。”
  • RFC 4919:“IPv6 在低功耗无线个人区域网(6LoWPANs)上的应用:概述、假设、问题陈述和目标。”
  • RFC 6282:“基于 IEEE 802.15.4 的网络上 IPv6 数据报的压缩格式。”该 RFC 引入了一种新的编码格式,即 LOWPAN_IPHC 编码格式,而不是 LOWPAN_HC1 和 LOWPAN_HC2。
  • RFC 6775:“低功耗无线个人区域网(6LoWPANs)上 IPv6 的邻居发现优化。”
  • RFC 6550:“RPL:用于低功耗和有损耗网络的 IPv6 路由协议。”

实施 6LoWPAN 的主要挑战是:

  • 不同的数据包大小:IPv6 的 MTU 为 1280,而 IEEE802.15.4 的 MTU 为 127 (IEEE802154_MTU)。为了支持大于 127 字节的分组,应该定义 IPv6 和 IEEE 802.15.4 之间的适配层。该适配层负责 IPv6 分组的透明分段/碎片整理。
  • 不同地址:IPv6 地址是 128 位的,而 IEEE802.15.4 是 IEEE 64 位扩展的(IEEE 802154 _ ADDR _ 长),或者在关联之后和分配 PAN id 之后,是在 PAN 中唯一的 16 位短地址(IEEE 802154 _ ADDR _ 短)。主要的挑战是,我们需要压缩机制来减小 6LoWPAN 数据包的大小,该数据包主要由 IPv6 地址组成。6 例如,LoWPAN 可以利用 IEEE802.15.4 支持 16 位短地址的事实来避免对 64 位 IID 的需求。
  • IEEE 802.15.4 本身不支持多播,而 IPv6 对 ICMPv6 和依赖 ICMPv6 的协议(如邻居发现协议)使用多播。

IEEE 802.15.4 定义了四种类型的帧:

  • 信标帧(IEEE802154_FC_TYPE_BEACON)
  • MAC 命令帧(IEEE802154_FC_TYPE_MAC_CMD)
  • 确认帧(IEEE802154_FC_TYPE_ACK)
  • 数据帧(IEEE802154_FC_TYPE_DATA)

IPv6 数据包必须在第四种类型的数据帧上传输。虽然建议对数据包进行确认,但这不是强制性的。与 802.11 一样,有些设备驱动程序自己实现协议的大部分(HardMAC 设备驱动程序),有些设备驱动程序在软件中处理大部分协议(SoftMAC 设备驱动程序)。6LoWPAN 中有三种类型的节点:

  • 6LoWPAN 节点(6LN):主机或路由器。
  • 6LoWPAN 路由器(6LR):可以发送和接收路由器广告(RA)和路由器请求(RS)消息,以及转发和路由 IPv6 数据包。这些节点比简单的 6LoWPAN 节点更复杂,可能需要更多的内存和处理能力。
  • 6LoWPAN 边界路由器(6LBR):位于独立的 6LoWPAN 网络连接处或 6LoWPAN 网络和另一个 IP 网络之间的边界路由器。6LBR 负责 IP 网络和 6LoWPAN 网络之间的转发,以及 6LoWPAN 节点的 IPv6 配置。6LBR 比 6LN 需要更多的内存和处理能力。它们共享 LoWPAN 中节点的上下文,使用 6LoWPAN-ND 和 RPL 跟踪注册的节点。一般来说,6LBR 总是开着的,而 6LN 大部分时间都在睡觉。图 14-4 显示了一个使用 6LBR 的简单设置,它连接 IP 网络和基于 6LoWPAN 的无线传感器网络。

9781430261964_Fig14-04.jpg

图 14-4 。6LBR 将 IP 网络连接到 WSN,该网络在 6LoWPAN 上运行

邻居发现优化

我们应该对 IPv6 邻居协议进行优化和扩展有两个原因:

  • IEEE 802.15.4 链路层不支持多播,尽管它支持广播(它使用 0xFFFF 短地址进行消息广播)。
  • 邻居发现协议是为供电充足的设备设计的,IEEE 802.15.4 设备可以休眠以保存能量;此外,正如 RFC 所说,它们在有损耗的网络环境中运行。

处理邻居发现优化的 RFC 6775 增加了新的优化,例如:

  • 主机发起的路由器通告信息刷新。在 IPv6 中,路由器通常会定期发送路由器广告。此功能消除了从路由器向主机发送定期或主动路由器广告的需要。

  • 基于 EUI-64 的 IPv6 地址被认为是全球唯一的。当使用这样的地址时,不需要 DAD(重复地址检测)。

  • 增加了三个选项:

  • 地址注册选项(ARO):ARO 选项(33)可以是单播 NS 消息的一部分,主机将该消息作为 NUD(邻居不可到达性检测)的一部分发送,以确定它仍然可以到达默认路由器。当主机拥有非本地链路地址时,它会定期向默认路由器发送带有 ARO 选项的 NS 消息,以注册其地址。注销是通过发送一个包含生存期为 0 的 ARO 的 NS 来完成的。

  • 6 低 PAN 上下文选项(6CO):6CO 选项(34)携带用于低 PAN 报头压缩的前缀信息,并且类似于 RFC 4861 中规定的前缀信息选项(PIO)。

  • 权威边界路由器选项(ABRO):ABRO 选项(35)允许在路由拓扑上传播前缀和上下文信息。

  • 新增两条爸爸消息:

  • 重复地址请求(DAR)。157 的新型 ICMPv6。

  • 重复地址确认(DAC)。158 的新型 ICMPv6。

Linux 内核 6LoWPAN

6LoWPAN 基本实现被集成到 3.2 版 Linux 中。它是由西门子公司技术部门的嵌入式系统开放平台小组提供的。它有三层:

  • 网络层- net/ieee802154(包括 6lowpan 模块、原始 IEEE 802.15.4 套接字、netlink 接口等)。

  • MAC 层- net/mac802154。为 SoftMAC 设备驱动程序实现部分 MAC 层。

  • PHY 层-drivers/net/ieee802154–IEEE 802154 设备驱动程序。

  • 目前支持两种 802.15.4 设备:

  • AT86RF230/231 收发器驱动器

  • 微芯片 MRF24J40

  • 有 Fakelb 驱动(IEEE 802.15.4 环回接口)。

  • 这两款器件以及许多其它 802.15.4 收发器通过 SPI 连接。还有一个串行驱动程序,虽然它没有包含在主线内核中,仍然是实验性的。还有像atusb这样的设备,它们基于 AT86RF231 BN,但在撰写本文时还不在主流中。

6LoWPAN 初始化

lowpan_init_module()方法中,通过调用lowpan_netlink_init()方法完成 6LoWPAN netlink sockets 的初始化,通过调用dev_add_pack()方法为 6LoWPAN 数据包注册一个协议处理程序:

. . .
static struct packet_type lowpan_packet_type = {
        .type = __constant_htons(ETH_P_IEEE802154),
        .func = lowpan_rcv,
};
. . .
static int __init lowpan_init_module(void)
{
        . . .
        dev_add_pack(&lowpan_packet_type);
        . . .
}
(net/ieee802154/6lowpan.c)

lowpan_rcv()方法是 6LoWPAN 数据包的主要接收处理程序,其以太类型为 0x00F6 (ETH_P_IEEE802154)。它处理两种情况:

  • 接收未压缩的数据包(调度类型为 IPv6。)
  • 接收压缩包。

您使用虚拟链路来确保 6LoWPAN 和 IPv6 数据包之间的转换。此虚拟链路的一个端点使用 IPv6,MTU 为 1280,这是 6LoWPAN 接口。另一个说 6LoWPAN,MTU 为 127,这是 WPAN 接口。压缩的 6LoWPAN 数据包由lowpan_process_data()方法处理,该方法调用lowpan_uncompress_addr()解压缩地址,并调用lowpan_uncompress_udp_header()根据 IPHC 报头解压缩 UDP 报头。然后未压缩的 IPv6 数据包通过lowpan_skb_deliver()方式(net/ieee802154/6lowpan.c)传递到 6LoWPAN 接口。

图 14-5 显示了 6LoWPAN 适配层。

9781430261964_Fig14-05.jpg

图 14-5 。6LoWPAN 适配层

图 14-6 显示了一个数据包从 PHY 层 (驱动程序)经过 MAC 层到 6LoWPAN 适配层的路径。

9781430261964_Fig14-06.jpg

图 14-6 。接收数据包

我将不深究设备驱动程序实现的细节,因为这超出了我们的范围。我将提到每个设备驱动程序应该通过调用ieee802154_alloc_device()方法创建一个ieee802154_dev对象,作为参数传递一个ieee802154_ops对象。每个驱动都应该定义一些ieee802154_ops对象回调,像xmitstartstop等等。这仅适用于 SoftMAC 驱动程序。

我将在这里提到一个互联网草案,该草案旨在将 6LoWPAN 技术应用于蓝牙低能耗设备(这些设备是蓝牙 4.0 规范的一部分,如前一章所述)。参见“通过蓝牙低能耗传输 IPv6 数据包”。

image Contiki是实现物联网(IoT)概念的开源操作系统;Linux IEEE802.15.4 6LoWPAN 的一些补丁就是从它派生出来的,比如 UDP 头压缩和解压缩。它实现了 6LoWPAN 和 RPL。它是由亚当·邓克尔斯开发的。参见http://www.contiki-os.org/

有关 6LoWPAN 和 802.15.4 的其他资源:

  • 书籍:

  • “6LoWPAN:无线嵌入式互联网”,作者 Zach Shelby 和 Carsten Bormann,Wiley,2009 年。

  • 让-菲利普·瓦瑟尔和亚当·邓克尔斯(Contiki 开发者),摩根·考夫曼于 2010 年出版的《智能物体与 IP 互联:下一个互联网》。

  • 一篇关于 IPv6 邻居发现优化的文章:http://www.internetsociety.org/articles/ipv6-neighbor-discovery-optimization

lowpan-tools是一组管理 Linux LoWPAN 堆栈的实用程序。参见:http://sourceforge.net/projects/linux-zigbee/files/linux-zigbee-sources/0.3/

image 注意IEEE 802 . 15 . 4 并没有维护自己的git库(尽管过去有一个)。补丁被发送到netdev邮件列表;一些开发者首先将补丁发送到 linux zigbee 开发者邮件列表,以获得一些反馈:https://lists.sourceforge.net/lists/listinfo/linux-zigbee-devel

在这一节中,我描述了 IEEE 802.15.4 和 6LoWPAN 协议,以及它给 Linux 内核集成带来的挑战,比如添加邻居发现消息。在下一节中,我将介绍第三个无线子系统,这是本章介绍的三个无线子系统中距离最短的一个:近场通信(NFC)子系统。

近场通信

近场通信是一种非常短距离的无线技术(小于两英寸),旨在通过非常低的延迟链路以高达 424 kb/s 的速度传输少量数据。NFC 有效载荷的范围从非常简单的 URL 或原始文本到更复杂的带外数据,以触发连接切换。通过其非常短的范围和延迟,NFC 通过将接近度与 NFC 数据有效载荷触发的即时动作联系起来,实现了“点击和共享”的概念。用支持 NFC 的手机触摸 NFC 标签,例如,这将立即启动网络浏览器。

NFC 工作在 13.65MHz 频段,基于射频识别(RFID) ISO14443 和 FeliCa 标准。NFC 论坛(http://www.nfc-forum.org/)是一个负责通过一系列规范实现技术标准化的联盟,范围从 NFC 数字层到高级服务定义,如 NFC 连接切换或个人健康设备通信(PHDC)定义。所有采用的 NFC 论坛规范都是免费提供的。参见http://www.nfc-forum.org/specs/

NFC 论坛规范的核心是 NFC 数据交换格式(NDEF)定义。它定义了用于从 NFC 标签或在 NFC 对等体之间交换 NFC 有效载荷的 NFC 数据结构。所有 NDEF 都包含一个或多个嵌入实际有效载荷的 NDEF 记录。NDEF 记录头包含元数据,允许应用在 NFC 有效载荷和阅读器端触发的动作之间建立语义链接。

NFC 标签

NFC 标签很便宜,大多是静态和无电池的数据容器。它们通常由连接到非常少量闪存的感应天线组成,以许多不同的形式封装(标签、钥匙圈、贴纸等)。).根据 NFC 论坛的定义,NFC 标签是无源设备,即它们不能产生任何无线电场。相反,它们由 NFC 有源设备启动的射频场供电。NFC 论坛定义了四种不同的标签类型,每一种都带有强大的 RFID 和智能卡传统:

  • 类型 1 规格源自 Innovision/Broadcom Topaz 和 Jewel card 规格。它们可以以 106 kb/s 的速度暴露 96kb 到 2kb 的数据。
  • 类型 2 标签基于恩智浦 Mifare 超轻规格。它们非常类似于类型 1 标签。
  • 类型 3 标签建立在 Sony FeliCa 标签的非安全部分之上。它们比类型 1 和类型 2 标签更贵,但可以以 212 或 424 kb/s 的速度携带高达 1 兆字节的数据。
  • Type 4 规格基于恩智浦 DESFire 卡,支持高达 424 kb 和三种传输速度:106、212 或 424 kb/s。

NFC 设备

与 NFC 标签相反,NFC 设备可以产生自己的磁场来启动 NFC 通信。支持 NFC 的手机和 NFC 阅读器是最常见的 NFC 设备。它们支持比 NFC 标签更大的功能集。它们可以读取或写入 NFC 标签,但它们也可以伪装成一张卡,并被任何读取器视为简单的 NFC 标签。但是,NFC 技术相对于 RFID 的一个关键优势是,两个 NFC 设备可以以特定于 NFC 的对等模式相互通信。只要两个 NFC 设备在磁性范围内,这两个设备之间的链路就保持活动。在实践中,这意味着两个 NFC 设备可以在彼此物理接触的同时保持对等链路。这引入了一系列全新的移动使用案例,用户可以通过触摸他人的 NFC 设备来交换数据、上下文或凭证。

通信和操作模式

NFC 论坛定义了两种通信和三种操作模式。当两个 NFC 设备可以通过交替产生磁场来相互通话时,就建立了主动 NFC 通信。这意味着两个设备都有自己的电源,因为它们不依赖于任何感应产生的电力。主动通信只能在 NFC 点对点模式下建立。另一方面,在被动 NFC 通信中,只有一个 NFC 设备生成无线电场,另一个设备使用该场进行回复。

有三种 NFC 操作模式:

  • 读取器/写入器:NFC 设备(例如,支持 NFC 的移动电话)读取或写入 NFC 标签。
  • 对等:两个 NFC 设备建立逻辑链路控制协议(LLCP ),在该协议上可以复用若干 NFC 服务:用于交换 NDEF 格式数据的简单 NDEF 交换协议(SNEP ),用于发起运营商(蓝牙或 WiFi)切换的连接切换,或任何专有协议。
  • 卡仿真:NFC 设备通过伪装成 NFC 标签来回复阅读器轮询。支付和交易发行商依靠这种模式在 NFC 基础上实现非接触式支付。在卡仿真模式下,在可信执行环境(也称为“安全元件”)上运行的支付小程序控制 NFC 无线电,并将自己暴露为可从支持 NFC 的销售点终端读取的传统支付卡。

主机控制器接口

硬件控制器和主机堆栈之间的通信必须遵循精确定义的接口:主机控制器接口(HCI)。在这方面,NFC 硬件生态系统相当分散,因为大多数初始 NFC 控制器都实现了 ETSI 指定的 HCI,该 HCI 最初是为 SIM 卡和非接触式前端之间的通信而设计的。(参见http://www.etsi.org/deliver/etsi_ts/102600_102699/102622/07.00.00_60/ts_102622v070000p.pdf)。这种 HCI 不是为 NFC 特定用例定制的,因此每个制造商都定义了大量专有扩展来支持他们的功能。NFC 论坛试图通过定义自己的接口来解决这种情况,该接口更加面向 NFC,即 NFC 控制器接口(NCI)。行业趋势清楚地表明,制造商放弃 ETSI HCI,转而支持 NCI,建立一个更加标准化的硬件生态系统。

Linux NFC 支持

与 Android 操作系统 NFC 堆栈不同,标准的 Linux 操作系统 NFC 堆栈部分是由内核本身实现的。自 3.1 Linux 内核发布以来,基于 Linux 的应用将找到一个 NFC 专用套接字域,以及一个通用的 netlink 系列。(参见http://git.kernel.org/?p=linux/kernel/git/sameo/nfc-next.git;a=shortlog;h=refs/heads/master。)NFC 通用 netlink 系列旨在成为用于控制和监控 NFC 适配器的 NFC 带外通道。NFC 套接字域支持两个系列:

  • 用于发送 NFC 帧的原始套接字,这些帧未经修改就到达驱动程序
  • 用于实现 NFC 点对点服务的 LLCP 套接字

硬件抽象在 NFC 内核驱动程序中实现,这些驱动程序针对堆栈的各个部分进行注册,主要取决于它们支持的控制器所使用的主机-控制器接口。因此,Linux 应用可以在与硬件无关且完全兼容 POSIX 的 NFC 内核 API 上工作。Linux NFC 堆栈被分为内核和用户空间。内核 NFC 套接字允许用户空间应用通过原始协议发送特定于标签类型的命令来实现 NFC 标签支持。NFC 点对点协议(SNEP、连接切换、PHDC 等。)也可以通过 NFC 套接字传输它们的特定有效载荷来实现。最后,卡仿真模式建立在内核 NFC netlink API 的安全元素部分之上。Linux NFC 守护进程neard,位于内核之上,实现所有三种 NFC 模式,而不管 NFC 控制器物理连接到主机平台。(参见https://01.org/linux-nfc/。)

图 14-7 显示了 NFC 系统的概况。

9781430261964_Fig14-07.jpg

图 14-7 。NFC 概述

NFC 套接字

NFC 套接字有两种:原始和 LLCP。原始 NFC 套接字在设计时考虑了阅读器模式支持,因为它们提供了一种传输标签特定命令和接收标签回复的方式。在读取器和写入器模式下,neard守护进程使用 NFC 原始套接字来实现所有四种标签类型支持。LLCP 套接字实现 NFC 对等逻辑链路控制协议,在该协议之上neard实现所有 NFC 论坛指定的对等服务(SNEP、连接切换和 PHDC)。

根据所选的协议,NFC 套接字语义会有所不同。

原始套接字

  • connect:选择并启用检测到的 NFC 标签
  • bind:不支持
  • send/recv:发送和接收原始 NFC 有效载荷。NFC 核心实现不会修改这些有效载荷。

LLCP 套接字

  • connect:连接到检测到的对等设备上的特定 LLCP 服务,如 SNEP 或连接切换服务。
  • bind:将设备链接到特定的 LLCP 服务。该服务将通过 LLCP 服务名称查找(SNL)协议导出,以便任何 NFC 对等设备尝试与之连接。
  • send/recv:将 LLCP 服务有效载荷传输到 NFC 对等设备或从 NFC 对等设备传输。内核将处理 LLCP 特定的链路层封装和分段。
  • LLCP 传输可以是连接的,也可以是无连接的,这是通过 UNIX 标准 SOCK_STREAM 和 SOCK_DGRAM 套接字类型来处理的。NFC LLCP 套接字也支持 SOCK_RAW 类型,用于监视和嗅探目的。

NFC net link API

NFC 通用 netlink API 旨在实现带外 NFC 特定操作。它还处理来自 NFC 控制器的任何可发现的安全元件。通过 NFC netlink 命令,您可以:

  • 列出所有可用的 NFC 控制器。
  • 开启和关闭 NFC 控制器。
  • 启动(和停止)NFC 轮询以发现 NFC 标签和设备。
  • 在本地控制器和远程 NFC 对等设备之间启用 NFC 点对点(也称为 LLCP)链接。
  • 发送 LLCP 服务名称查找请求,以便发现远程对等端上可用的 LLCP 服务。
  • 启用和禁用 NFC 可发现安全元件(通常是基于 SIM 卡或嵌入式安全元件)。
  • 向启用的安全元素发送 ISO7816 帧。
  • 触发 NFC 控制器固件下载。

netlink API 不仅可以从 NFC 应用发送同步命令,还可以接收异步 NFC 相关事件。在 NFC netlink 套接字上侦听广播 NFC 事件的应用将收到以下通知:

  • 检测到 NFC 标签和设备
  • 发现的安全元素
  • 安全元素交易状态
  • LLCP 服务名称查找回复

整个 netlink API(包括命令和事件)以及 socket API 通过内核头文件导出,并在标准 Linux 发行版上的/usr/include/linux/nfc.h处安装。

NFC 初始化

NFC 初始化通过nfc_init()方法完成:

static int __init nfc_init(void)
{
        int rc;
        . . .

注册通用 netlink NFC 系列和 NFC 通知程序回调,nfc_genl_rcv_nl_event()方法:

        rc = nfc_genl_init();
        if (rc)
                goto err_genl;

        /* the first generation must not be 0 */
        nfc_devlist_generation = 1;

初始化 NFC 原始套接字:

        rc = rawsock_init();
        if (rc)
                goto err_rawsock;

初始化 NFC LLCP 套接字:

        rc = nfc_llcp_init();
        if (rc)
                goto err_llcp_sock;

初始化 AF_NFC 协议:

        rc = af_nfc_init();
        if (rc)
                goto err_af_nfc;

        return 0;
        . . .
}
(net/nfc/core.c)

驱动程序 API

如前所述,如今大多数 NFC 控制器使用 HCI 或 NCI 作为其主机控制器接口。其他人通过 USB 定义他们的专有接口,例如,像大多数 PC 兼容的 NFC 阅读器。还有一些“软”NFC 控制器期望主机平台实现 NFC 论坛数字层,并与仅支持模拟的固件对话。为了支持各种硬件控制器,NFC 内核实现了 NFC NCI、HCI 和数字层。根据他们打算支持的 NFC 硬件,设备驱动程序开发人员将需要在模块探测时针对这些堆栈之一进行注册,或者直接针对纯专有协议的 NFC 核心实现进行注册。注册时,它们通常提供堆栈操作数实现,这是 NFC 内核驱动程序和 NFC 堆栈核心部分之间的实际硬件抽象层。NFC 驱动程序注册 API 和操作数原型在内核include/net/nfc/目录中定义。

图 14-8 显示了 NFC Linux 架构的框图。

9781430261964_Fig14-08.jpg

图 14-8 。NFC Linux 内核架构。(注意 NFC 数字层不在内核 3.9 中。它将被集成到内核 3.13 中。)

通过查看将 NFC 设备驱动程序直接注册到 NFC 核心并针对 HCI 和 NCI 层的实现细节,可以更好地理解该图中所示的层级:

  • 直接针对 NFC 核心的注册通常在驱动程序probe()回调中完成。注册通过以下步骤完成:

  • 通过调用nfc_allocate_device()方法创建一个nfc_dev对象。

  • 调用nfc_register_device()方法,将在上一步中创建的nfc_dev对象作为单个参数传递。

  • 参见:drivers/nfc/pn533.c

  • 针对 HCI 层的注册通常也在驱动程序的probe()回调中完成;在内核 3.9 中仅有的 HCI 驱动程序pn544microread NFC 设备驱动程序的情况下,这个probe()方法由 I2C 子系统调用。注册通过以下步骤完成:

  • 通过调用nfc_hci_allocate_device()方法创建一个nfc_hci_dev对象。

  • nfc_hci_dev结构在include/net/nfc/hci.h中定义。

  • 调用nfc_hci_register_device()方法,将在上一步中创建的nfc_hci_dev对象作为单个参数传递。通过调用nfc_register_device()方法,nfc_hci_register_device()方法反过来执行对 NFC 核心的注册。

  • 参见drivers/nfc/pn544/pn544.cdrivers/nfc/microread/microread.c

  • 针对 NCI 层的注册通常也在驱动程序的probe()回调中完成,例如在nfcwilink驱动程序中。注册通过以下步骤完成:

  • 通过调用nci_allocate_device()方法创建一个nci_dev对象。

  • nci_dev结构在include/net/nfc/nci_core.h.中定义

  • 调用nci_register_device()方法,将在上一步中创建的nci_dev对象作为单个参数传递。通过调用nfc_register_device()方法,nci_register_device()方法反过来执行针对 NFC 核心的注册,类似于您在本节前面看到的针对 HCI 层的注册。

  • 参见drivers/nfc/nfcwilink.c

当直接针对 NFC 内核工作时,驱动程序必须在nfs_ops对象中定义五个回调(该对象作为nfc_allocate_device()方法):的第一个参数传递)

  • start_poll:设置驱动程序在轮询模式下工作。
  • stop_poll:停止轮询。
  • 激活选定的目标。
  • 取消激活一个选定的目标。
  • im_transceive:收发操作。

当使用 HCI 时,hci_nfc_ops对象(它是nfs_ops,的一个实例)定义了这五个回调,当用nfc_hci_allocate_device()方法分配一个 HCI 对象时,nfc_allocate_device()方法被调用,这个hci_nfc_ops对象作为第一个参数。

与 NCI,有一些非常相似的东西,与nci_nfc_ops对象;参见:net/nfc/nci/core.c

用户空间架构

neard ( http://git.kernel.org/?p=network/nfc/neard.git;a=summary)是运行在内核 NFC APIs 之上的 Linux NFC 守护进程。它是一个单线程、基于 GLib 的进程,实现了 NFC 点对点堆栈的更高层,以及用于读取和写入 NFC 标签的四种标签类型特定命令。NDEF 推协议(NPP)、SNEP、PHDC 和连接切换规范都是通过neard插件实现的。neard 的主要设计目标之一是为愿意提供高级 NFC 服务的基于 Linux 的应用提供一个小型、简单和统一的 NFC API。这是通过一个小型的 D-Bus API 实现的,它抽象了标签和设备接口和方法,对应用开发人员隐藏了 NFC 的复杂性。此 API 与 freedesktop D-Bus ObjectManager 兼容,并提供以下接口:

  • org.neard.Adapter:用于检测新的 NFC 控制器、开启和关闭控制器以及启动 NFC 轮询。
  • org.neard.Device, org.neard.Tag:用于表示检测到的 NFC 标签和设备。呼叫设备。Push 方法将在标记时向对等设备发送 NDEFs。Write 会将它们写入选定的标签。
  • org.neard.Record:表示人类可读和可理解的 NDEF 记录有效载荷和属性。根据org.neard.NDEF代理接口注册代理将使应用能够访问 NDEF 原始负载。

您可以在这里找到关于neard用户空间守护进程的更多信息:http://git.kernel.org/cgit/network/nfc/neard.git/tree/doc

Android 上的 NFC

最初的 NFC 支持是在 2010 年 12 月随着 2.3(姜饼)正式发布而添加到 Android 操作系统中的。Android 2.3 仅支持阅读器/写入器模式,但自那以后情况有了显著改善,最新的 Android 版本(Jelly Bean 4.3)提供了全功能的 NFC 支持。更多信息请参见 Android NFC 页面:http://developer.android.com/guide/topics/connectivity/nfc/index.html。遵循经典的 Android 架构,Java 特定的 NFC API 可用于应用来提供 NFC 服务和操作。通过本地硬件抽象层(HAL)实现这些 API 的任务留给了集成商。谷歌推出了 Broadcom NFC HAL,目前仅支持 Broadcom NFC 硬件。同样,Android OEMs 和集成商要么让 Broadcom NFC HAL 适应他们选择的 NFC 芯片组,要么实现他们自己的 HAL。值得注意的是,由于 Broadcom 堆栈实现了 NFC 控制器接口(NCI)规范,因此对其进行调整以支持任何 NCI 兼容的 NFC 控制器相对容易。Android NFC 架构可以称为用户空间 NFC 堆栈。事实上,整个 NFC 实现是通过 HAL 在用户空间中完成的。然后,NFC 帧通过内核驱动程序存根被下推到 NFC 控制器。驱动程序只是将这些帧封装到缓冲区中,准备发送到主机平台和 NFC 控制器之间的物理链路(例如,I2C、SPI、UART)。

image nfc-next git树的 Pull 请求发送到wireless-next树(除了 NFC 子系统,还有蓝牙子系统和 mac802.11 子系统 pull 请求由无线维护人员处理)。从wireless-next树,拉请求被发送到net-next树,并从那里发送到 Linus linux-next树。nfc-next树可用于:git://git.kernel.org/pub/scm/linux/kernel/git/sameo/nfc-next.git

还有一个nfc-fixes git存储库,其中包含当前版本的紧急和关键修复(-rc*)。nfc-fixesgit树出现在:git://git.kernel.org/pub/scm/linux/kernel/git/sameo/nfc-fixes.git/

NFC 邮件列表:linux-nfc@lists.01.org

NFC 邮件列表档案:https://lists.01.org/pipermail/linux-nfc/`。

在本节中,您了解了什么是 NFC,以及 Linux NFC 子系统实现和 Android NFC 子系统实现。在下一节中,我将讨论通知链机制,这是一种向网络设备通知各种事件的重要机制。

通知链

网络设备状态可以动态改变;有时,用户/管理员可以注册/取消注册网络设备、更改其 MAC 地址、更改其 MTU 等。网络堆栈和其他子系统和模块应该能够被通知这些事件并正确处理它们。网络通知链提供了一种处理这类事件的机制,我将在这一节描述它的 API 和它处理的可能的网络事件。关于事件的完整列表,见本章后面的表 14-1 。每个子系统和每个模块都可以向通知链注册自己。这通过定义一个notifier_block并注册它来完成。通知链注册和注销的核心方法分别是notifier_chain_register()notifier_chain_unregister()方法。通知事件的生成是通过调用notifier_call_chain()方法完成的。这三个方法不是直接用的(不导出;见kernel/notifier.c),而且他们没有使用任何锁定机制。以下方法是围绕notifier_chain_register()、的包装器,它们都在kernel/notifier.c中实现:

  • atomic_notifier_chain_register()
  • blocking_notifier_chain_register()
  • raw_notifier_chain_register()
  • srcu_notifier_chain_register()
  • register_die_notifier()

表 14-1。网络设备事件:

|

事件

|

意义

|
| --- | --- |
| NETDEV_UP | 设备启动事件 |
| 网络开发 _ 关闭 | 设备停机事件 |
| 网络开发 _ 重启 | 检测到硬件崩溃并重新启动设备 |
| 网络开发 _ 变更 | 设备状态变化 |
| NETDEV _ 寄存器 | 设备注册事件 |
| NETDEV_UNREGISTER | 设备注销事件 |
| NETDEV_CHANGEMTU(网络配置文件) | 设备 MTU 已更改 |
| NETDEV_CHANGEADDR | 设备 MAC 地址已更改 |
| NETDEV_GOING_DOWN | 设备正在关闭 |
| NETDEV_CHANGENAME | 设备已更改其名称 |
| 网络发展 _ 专长 _ 改变 | 设备功能已更改 |
| NETDEV_BONDING_FAILOVER | 绑定故障转移事件 |
| NETDEV_PRE_UP | 此事件允许否决将设备状态更改为启动;例如,在 cfg80211 中,如果已知设备已被射频识别,则拒绝设置接口。参见cfg80211_netdev_notifier_call() |
| 网络开发 _ 前期 _ 类型 _ 变更 | 该设备即将改变其类型。这是 NETDEV_BONDING_OLDTYPE 标志的推广,它被 NETDEV_PRE_TYPE_CHANGE 所取代 |
| 网络开发 _ 发布 _ 类型 _ 更改 | 设备改变了类型。这是 NETDEV_BONDING_NEWTYPE 标志的推广,它被 NETDEV_POST_TYPE_CHANGE 所取代 |
| 网络开发 _ 发布 _ 初始化 | 该事件在设备注册(register_netdevice())中生成,在netdev_register_kobject()创建网络设备对象之前;用于 cfg80211 (net/wireless/core.c) |
| NETDEV_UNREGISTER_FINAL | 为完成设备注销而生成的事件。 |
| 网络开发 _ 发布 | 释放绑定的最后一个从机(当通过绑定使用 netconsole 时)(在br_if.c中,该标志也曾用于网桥)。 |
| 网络开发 _ 通知 _ 对等方 | 通知网络对等体事件(即,设备想要通知网络的其余部分某种重新配置,例如故障转移事件或虚拟机迁移) |
| 网络开发 _ 加入 | 设备添加了一个从属设备。例如在绑定驱动程序中使用,在bond_enslave()方法中,我们添加了一个从机;参见drivers/net/bonding/bond_main.c |

还有相应的包装器方法,用于注销通知链和为每个包装器生成通知事件。例如,对于用atomic_notifier_chain_register()方法注册的通知链,atomic_notifier_chain_unregister()用于取消注册通知链,__atomic_notifier_call_chain()方法用于生成通知事件。这些包装器中的每一个都有相应的宏来定义通知链;对于atomic_notifier_chain_register()包装器,它是ATOMIC_NOTIFIER_HEAD(include/linux/notifier.h)

注册一个notifier_block对象后,当表 14-1 中显示的每一个事件发生时,调用一个notifier_block中指定的回调。通知链的基本数据结构是notifier_block结构;让我们来看看:

struct notifier_block {
        int (*notifier_call)(struct notifier_block *, unsigned long, void *);
        struct notifier_block __rcu *next;
        int priority;
};
(include/linux/notifier.h)

  • notifier_call:要调用的回调。
  • 首先执行具有较高优先级的notifier_block对象的priority:回调。

在网络子系统和其他子系统中有许多链。我们来提几个重要的:

  • netdev_chain:通过register_netdevice_notifier()方式注册,通过unregister_netdevice_notifier()方式取消注册(net/core/dev.c)。
  • inet6addr_chain:通过register_inet6addr_notifier()方式注册,通过unregister_inet6addr_notifier ()方式取消注册。通知由inet6addr_notifier_call_chain ()方法生成(net/ipv6/addrconf_core.c)。
  • netevent_notif_chain:通过register_netevent_notifier()方式注册,通过unregister_netevent_notifier()方式取消注册。通知由call_netevent_notifiers()方法生成(net/core/netevent.c)。
  • inetaddr_chain:通过register_inetaddr_notifier()方式注册,通过unregister_inetaddr_notifier()方式取消注册。通知是通过调用blocking_notifier_call_chain()方法生成的。

让我们来看一个使用netdev_chain的例子;您之前已经看到,使用netdev_chain,注册是通过register_netdevice_notifier()方法完成的,它是raw_notifier_chain_register()方法的包装器。下面是一个注册名为br_device_event的回调的例子;首先定义一个notifier_block对象,然后通过调用register_netdevice_notifier()方法注册它:

struct notifier_block br_device_notifier = {
        .notifier_call = br_device_event
};
(net/bridge/br_notify.c)
static int __init br_init(void)
{
        ...
        register_netdevice_notifier(&br_device_notifier);
        ...
}
(net/bridge/br.c)

通过调用call_netdevice_notifiers()方法来生成netdev_chain的通知。此方法的第一个参数是事件。call_netdevice_notifiers()方法:实际上是raw_notifier_call_chain()的包装器。

因此,当生成网络通知时,所有已注册的回调都被调用;在这个例子中,无论发生了哪个网络事件,都将调用br_device_event()回调;回调将决定如何处理通知,或者可能忽略它。我们来看看回调方法,br_device_event():

static int br_device_event(struct notifier_block *unused, unsigned long event, void *ptr)
{
        struct net_device *dev = ptr;
        struct net_bridge_port *p;
        struct net_bridge *br;
        bool changed_addr;
        int err;
        . . .

br_device_event()方法的第二个参数是事件(所有事件都在include/linux/netdevice.h中定义):

        switch (event) {
        case NETDEV_CHANGEMTU:
                dev_set_mtu(br->dev, br_min_mtu(br));
                break;
        . . .
}

image 注意通知链的注册不仅限于网络子系统。因此,例如,clockevents子系统定义了一个名为clockevents_chain的链并通过调用raw_notifier_chain_register()方法注册它,hung_task模块定义了一个名为panic_notifier_list的链并通过调用atomic_notifier_chain_register()方法注册它。

除了本节讨论的通知之外,还有另一种类型的通知,称为 RTNetlink 通知;这些通知通过rtmsg_ifinfo()方法发送。:这种类型的通知在第二章中讨论过,它处理 Netlink 套接字。

以下是联网支持的事件类型(注意:下表中提到的事件类型在include/linux/netdevice.h中定义):

我们现在已经介绍了通知事件,这是一种机制,它使网络设备能够获得关于 MTU 更改、MAC 地址更改等事件的通知。下一节将简要讨论 PCI 子系统,描述它的一些主要数据结构。

PCI 子系统

许多网络接口卡是外围组件互连(PCI)设备,应该与 Linux PCI 子系统协同工作。不是所有的网络接口都是 PCI 设备;有许多嵌入式设备的网络接口不在 PCI 总线上;这些设备的初始化和处理是以不同的方式完成的,下面的讨论与这些非 PCI 设备无关。新的 PCI 设备是 PCI Express (PCIe 或 PCIe)设备;该标准创建于 2004 年。它们有一个串行接口而不是并行接口,因此它们有更高的最大系统总线吞吐量。每个 PCI 设备都有一个只读配置空间;它至少有 256 个字节。PCI-X 2.0 和 PCI Express 总线中提供的扩展配置空间为 4096 字节。您可以通过lspci读取 PCI 配置空间和扩展 PCI 配置空间(该lspci工具属于pciutils包):

  • lspci  -xxx:显示 PCI 配置空间的十六进制转储。
  • lspci –xxxx:显示扩展 PCI 配置空间的十六进制转储。

Linux PCI API 提供了三种读取配置空间的方法,用于处理 8 位、16 位和 32 位粒度:

  • static inline int pci_read_config_byte(const struct pci_dev *dev, int where, u8 *val)
  • static inline int pci_read_config_word(const struct pci_dev *dev, int where, u16 *val)
  • static inline int pci_read_config_dword(const struct pci_dev *dev, int where, u32 *val)

写配置空间也有三种方法;同样,也可以处理 8 位、16 位和 32 位粒度:

  • static inline int pci_write_config_byte(const struct pci_dev *dev, int where, u8 val)
  • static inline int pci_write_config_word(const struct pci_dev *dev, int where, u16 val)
  • static inline int pci_write_config_dword(const struct pci_dev *dev, int where, u32 val)

每个 PCI 制造商至少给 PCI 设备的配置空间中的供应商、设备和类别字段赋值。Linux PCI 子系统通过一个pci_device_id对象来识别 PCI 设备。pci_device_id structinclude/linux/mod_devicetable.h中定义:

struct pci_device_id {
        __u32 vendor, device;           /* Vendor and device ID or PCI_ANY_ID*/
        __u32 subvendor, subdevice;     /* Subsystem ID's or PCI_ANY_ID */
        __u32 class, class_mask;        /* (class,subclass,prog-if) triplet */
        kernel_ulong_t driver_data;     /* Data private to the driver */
};
(include/linux/mod_devicetable.h)

pci_device_id中的厂商、设备和类别字段标识一个 PCI 设备;大多数驱动程序不需要指定类别,因为供应商/设备通常就足够了。

每个 PCI 设备驱动程序声明一个pci_driver对象。我们来看看pci_driver的结构:

struct pci_driver {
    . . .
    const char *name;
    const struct pci_device_id *id_table;   /* must be non-NULL for probe to be called */
    int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);   /* New device inserted */
    void (*remove) (struct pci_dev *dev);   /* Device removed (NULL if not a hot-plug capable driver) */
    int  (*suspend) (struct pci_dev *dev, pm_message_t state);      /* Device suspended */
    . . .
    int  (*resume) (struct pci_dev *dev);                   /* Device woken up */
    . . .
};
(include/linux/pci.h)

下面是对pci_driver结构成员的简短描述:

  • name:PCI 设备的名称。
  • id_table:它支持的pci_device_id对象的数组。初始化id_table通常用 DEFINE_PCI_DEVICE_TABLE 宏来完成。
  • probe:设备初始化的一种方法。
  • remove:释放设备的方法。remove()方法通常会释放在probe()方法中分配的所有资源。
  • 对于支持电源管理的设备,将设备置于低功耗状态的电源管理回调。
  • 对于支持电源管理的设备,从低功耗状态唤醒设备的电源管理回调。

PCI 设备由struct pci_dev表示。它是一个大的结构;让我们来看看它的一些成员(他们不言自明):

struct pci_dev {
        . . .
        unsigned short  vendor;
        unsigned short  device;
        unsigned short  subsystem_vendor;
        unsigned short  subsystem_device;
        . . .
        struct pci_driver *driver;      /* which driver has allocated this device */
        . . .
        pci_power_t     current_state;  /* Current operating state. In ACPI-speak,
                                           this is D0-D3, D0 being fully functional,
                                           and D3 being off. */
        struct  device  dev;            /* Generic device interface */

        int             cfg_size;       /* Size of configuration space */

        unsigned int    irq;
};
(include/linux/pci.h)

向 PCI 子系统注册 PCI 网络设备是通过定义一个pci_driver对象并调用pci_register_driver()宏来完成的,这个宏得到一个pci_driver对象作为它的单个参数。为了在使用前初始化 PCI 设备,驱动程序应该调用pci_enable_device()方法。如果设备挂起,此方法会唤醒设备,并分配所需的 I/O 资源和内存资源。注销 PCI 驱动程序是通过pci_unregister_driver()方法.完成的。通常在驱动程序module_init()方法中调用pci_register_driver()宏,在驱动程序module_exit()方法中调用pci_unregister_driver()方法。每个驱动程序应该在设备启动时调用指定 IRQ 处理程序的request_irq()方法,在设备关闭时调用free_irq()

当使用未缓存的内存缓冲区时,DMA(直接内存访问)内存的分配和释放通常通过dma_alloc_coherent()/dma_free_coherent()完成。使用dma_alloc_coherent(),我们不需要担心缓存一致性,因为这个方法的映射是缓存一致的。参见e1000_alloc_ring_dma(), drivers/net/ethernet/intel/e1000e/netdev.c中的例子。Linux DMA API 在文档/DMA-API.txt 中有描述。

image 注意单根 I/O 虚拟化(SR-IOV)是一种 PCI 特性,它使一个物理设备表现为几个虚拟设备。SR-IOV 规范是由 PCI SIG 创建的。参见http://www.pcisig.com/specifications/iov/single_root/。更多信息见Documentation/PCI/pci-iov-howto.txt

关于 PCI 的更多信息可以在 Jonathan Corbet、Alessandro Rubini 和 Greg Kroah-Hartman 所著的第三版“Linux 设备驱动程序”中找到,可以通过以下网址获得(在知识共享许可下): http://lwn.net/Kernel/LDD3/

局域网唤醒(WOL)

LAN 唤醒是一种标准,它允许已被软断电的设备被网络数据包通电或唤醒。默认情况下,局域网唤醒是禁用的。有一些网络设备驱动程序允许系统管理员启用局域网唤醒功能,通常是通过从用户空间运行ethtool命令。为了支持这一点,网络设备驱动程序应该在ethtool_ops对象中定义一个set_wol()回调。例如,RealTek ( net/ethernet/realtek/8139cp.c)的8139cp驱动程序。运行ethtool <networkDeviceName>显示网络设备是否支持局域网唤醒。ethtool还让系统管理员定义哪些包应该唤醒设备;例如,ethtool -s eth1 wol g将启用 MagicPacket 帧的 LAN 唤醒(MagicPacket 是 AMD 的一个标准)。您可以使用net-tools包的ether-wake实用程序来发送 LAN 唤醒 MagicPacket 帧。

分组网络设备

虚拟分组网络设备驱动程序旨在替代绑定网络设备(drivers/net/bonding)。绑定网络设备提供链路聚合解决方案(也称为:“链路捆绑”或“中继”)。参见Documentation/networking/bonding.txt。绑定驱动程序完全在内核中实现,众所周知,它非常大,容易出问题。与绑定网络驱动程序相反,成组网络驱动程序由用户空间控制。用户空间守护进程被称为teamd,它通过库名libteam与内核团队驱动程序通信。libteam 库基于通用的 netlink 套接字(参见第二章)。

分组驱动程序有四种模式:

  • loadbalance: Used in Link Aggregation Control Protocol (LACP), which is part of the 802.3ad standard.

    net/team/team_mode_loadbalance.c

  • activebackup: Only one port is active at a given time. This port can transmit and receive SKBs. The other ports are backup ports. A userspace application can specify which port to use as the active port.

    net/team/team_mode_activebackup.c

  • broadcast: All packets are sent by all ports.

    net/team/team_mode_broadcast.c

  • roundrobin: Selection of ports is done by a round robin algorithm. No need for interaction with userspace for this mode.

    net/team/team_mode_roundrobin.c

image 注意组队网络驱动程序位于drivers/net/team下,由 Jiri Pirko 开发。

更多信息见http://libteam.org/

``libteam地点:https://github.com/jpirko/libteam 。`

`我们对团队驱动因素的简要概述到此结束。许多读者在网上冲浪时使用 PPPoE 服务。以下简短部分介绍了 PPPoE 协议。

PPPoE 协议

PPPoE 是一种将多个客户端连接到远程站点的规范。DSL 提供商通常使用 PPPoE 来处理 IP 地址和鉴定用户。PPPoE 协议提供了对以太网数据包使用 PPP 封装的能力。PPPoE 协议在 1999 年的 RFC 2516 中指定,PPP 协议在 1994 年的 RFC 1661 中指定。PPPoE 分为两个阶段:

  • PPPoE 发现阶段。发现是在客户端-服务器会话中完成的。该服务器被称为接入集中器,并且可以有多个。这些接入集中器通常由互联网服务提供商(ISP) 部署。这是发现阶段的四个步骤:

  • PPPoE 主动发现发起(PADI) 。从主机发送广播数据包。PPPoE 报头中的code为 0x 09(PADI _ 码),PPPoE 报头中的会话 id ( sid)必须为 0。

  • PPPoE 主动发现要约(PADO) 。接入集线器用 PADO 回复来回复 PADI 请求。目的地址是发送 PADI 的主机的地址。PPPoE 报头中的code是 0x07 (PADO 代码)。PPPoE 报头中的会话 id ( sid)必须再次为 0。

  • PPPoE 主动发现请求(PADR) 。主机收到 PADO 回复后,会向接入集线器发送 PADR 数据包。PPPoE 报头中的code是 0x19 (PADR 代码)。PPPoE 报头中的会话 id ( sid)必须再次为 0。

  • PPPoE 主动发现会话-确认(PADS)。当接入集中器收到 PADR 请求时,它会生成一个唯一的会话 id,并发送一个 PADS 数据包作为回复。PPPoE 报头中的code是 0x65 (PADS_CODE)。PPPoE 报头中的会话 id ( sid)是它生成的会话 id。数据包的目的地是发送 PADR 请求的主机的 IP 地址。

  • 通过发送 PPPoE 主动发现终止(PADT) 数据包来终止会话。PPPoE 报头中的code是 0xa7 (PADT 代码)。PADT 可以由接入集线器或主机发送,并且可以在会话建立后的任何时间发送。目的地址是单播地址。所有五个发现数据包(PADI、PADO、PADR、PADS 和 PADT)的以太网报头的以太网类型是 0x8863 (ETH_P_PPP_DISC)。

  • PPPoE 会话阶段。一旦 PPPoE 发现阶段成功完成,就使用 PPP 封装发送数据包,这意味着添加两个字节的 PPP 报头。使用 PPP 可以使用 PPP 子协议进行注册和认证,如密码认证协议 (PAP)或挑战握手认证协议(CHAP),以及称为链路控制协议(LCP) 的 PPP 子协议,它负责建立和测试数据链路连接。以太网报头的以太类型是 0x8864 (ETH_P_PPP_SES)。

每个 PPPoE 数据包都以 6 字节的 PPPoE 报头开始,为了更好地理解 PPPoE 协议,您必须了解 PPPoE 报头。

PPPoE 报头

我将首先展示 Linux 内核中的 PPPoE 头定义:

struct pppoe_hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8 ver : 4;
        __u8 type : 4;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u8 type : 4;
        __u8 ver : 4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif
        __u8 code;
        __be16 sid;
        __be16 length;
        struct pppoe_tag tag[0];
} __packed;
(include/uapi/linux/if_pppox.h)

以下是对pppoe_hdr结构成员的描述:

  • ver:ver字段是一个 4 位字段,根据 RFC 2516 的第四部分,它必须设置为 0x1。

  • type:type字段是一个 4 位字段,根据 RFC 2516 的第四部分,它也必须设置为 0x1。

  • code:代码字段为 8 位字段,可以是前面提到的常量之一:PADI 代码、PADO 代码、PADR 代码、PADS 代码、PADT 代码。

  • sid:会话 ID (16 位)。

  • length:length为 16 位字段,表示 PPPoE 净荷的长度,不含 PPPoE 头的长度,也不含以太网头的长度。

  • tag[0]:PPPoE 有效载荷可以包含零个或多个标签,采用类型-长度-值(TLV)格式。标签由 3 个字段组成:

  • TAG_TYPE: 16 位(例如 AC-Name、Service-Name、Generic-Error 等)。

  • TAG_LENGTH: 16 位。

  • TAG_VALUE:长度可变。

  • RFC 2516 的附录 A 列出了各种标签类型和标签值。

图 14-9 显示了一个 PPPoE 头:

9781430261964_Fig14-09.jpg

图 14-9 。PPPoE 报头

PPPoE 初始化

PPPoE 初始化由pppoe_init()方法drivers/net/ppp/pppoe.c完成。注册了两个 PPPoE 协议处理程序,一个用于 PPPoE 发现数据包,一个用于 PPPoE 会话数据包。让我们来看看 PPPoE 协议处理程序的注册:

static struct packet_type pppoes_ptype __read_mostly = {
        .type   = cpu_to_be16(ETH_P_PPP_SES),
        .func   = pppoe_rcv,
};

static struct packet_type pppoed_ptype __read_mostly = {
        .type   = cpu_to_be16(ETH_P_PPP_DISC),
        .func   = pppoe_disc_rcv,
};

static int __init pppoe_init(void)
{
        int err;

        dev_add_pack(&pppoes_ptype);
        dev_add_pack(&pppoed_ptype);
        . . .

        return 0;

}

dev_add_pack()方法是注册协议处理程序的通用方法,您在前面的章节中会遇到。通过pppoe_init()方法注册的协议处理程序有:

  • pppoe_disc_rcv()方法是 PPPoE 发现包的处理程序。
  • pppoe_rcv()方法是 PPPoE 会话数据包的处理程序。

PPPoE 模块将条目导出到procfs/proc/net/pppoe。该条目由会话 id、MAC 地址和当前 PPPoE 会话的设备组成。运行cat /proc/net/pppoepppoe_seq_show()方法处理。通过调用register_netdevice_notifier(&pppoe_notifier),由pppoe_init()方法注册一个通知链。

PPPoX 套接字

PPPoX 套接字由pppox_sock结构(include/linux/if_pppox.h)表示,并在net/ppp/pppox.c中实现。这些套接字实现了一个通用的 PPP 封装套接字系列。除了 PPPoE,PPP 上的第 2 层隧道协议(L2TP)也使用它们。通过调用pppoe_init()方法中的register_pppox_proto(PX_PROTO_OE, &pppoe_proto)来注册 PPPoX 套接字。让我们来看看pppox_sock结构的定义:

struct pppox_sock {
        /* struct sock must be the first member of pppox_sock */
        struct sock sk;
        struct ppp_channel chan;
        struct pppox_sock       *next;    /* for hash table */
        union {
                struct pppoe_opt pppoe;
                struct pptp_opt  pptp;
        } proto;
        __be16                  num;
};
(include/linux/if_pppox.h)

当 PPPoE 使用 PPPoX 套接字时,使用pppox_sock对象的proto联合的pppoe_optpppoe_opt结构包含一个名为pa的成员,它是pppoe_addr结构的一个实例。pppoe_addr结构表示 PPPoE 会话的参数:会话 id、对等体的远程 MAC 地址以及所使用的网络设备的名称:

struct pppoe_addr {
        sid_t         sid;                    /* Session identifier */
        unsigned char remote[ETH_ALEN];       /* Remote address */
        char          dev[IFNAMSIZ];          /* Local device to use */
};
(include/uapi/linux/if_pppox.h)

image 注意对嵌入在proto联合中的pppoe_opt结构的pa成员的访问在大多数情况下是在 PPPoE 模块中使用pppoe_pa宏完成的:

#define pppoe_pa        proto.pppoe.pa

(include/linux/if_pppox.h)

使用 PPPoE 发送和接收数据包

如前所述,一旦发现阶段完成,就必须使用 PPP 协议来支持两个对等体之间的流量。当通过运行pppd eth0启动 PPP 连接时(参见本节后面的例子),用户空间pppd守护进程通过调用socket(AF_PPPOX, SOCK_STREAM, PX_PROTO_OE)创建一个 PPPoE 套接字;这是在pppd守护进程的rp-pppoe插件中,在pppd/plugins/rp-pppoe/plugin.cPPPOEConnectDevice()方法中完成的。这个socket()系统调用通过 PPPoE 内核模块的pppoe_create()方法创建一个 PPPoE 套接字。PPPoE 会话完成后释放套接字是由 PPPoE 内核模块的pppoe_release()方法完成的。我们来看看pppoe_create()方法:

static const struct proto_ops pppoe_ops = {
        .family         = AF_PPPOX,
        .owner          = THIS_MODULE,
        .release        = pppoe_release,
        .bind           = sock_no_bind,
        .connect        = pppoe_connect,
        . . .
        .sendmsg        = pppoe_sendmsg,
        .recvmsg        = pppoe_recvmsg,
        . . .
        .ioctl          = pppox_ioctl,
};

static int pppoe_create(struct net *net, struct socket *sock)
{
        struct sock *sk;

        sk = sk_alloc(net, PF_PPPOX, GFP_KERNEL, &pppoe_sk_proto);
        if (!sk)
                return -ENOMEM;

        sock_init_data(sock, sk);

        sock->state     = SS_UNCONNECTED;
        sock->ops       = &pppoe_ops;

        sk->sk_backlog_rcv      = pppoe_rcv_core;
        sk->sk_state            = PPPOX_NONE;
        sk->sk_type             = SOCK_STREAM;
        sk->sk_family           = PF_PPPOX;
        sk->sk_protocol         = PX_PROTO_OE;

        return 0;
}
(drivers/net/ppp/pppoe.c)

通过定义pppoe_ops,我们为这个套接字设置了回调。所以从用户空间调用 AF_PPPOX 套接字上的connect()系统调用将由内核中 PPPoE 模块的pppoe_connect()方法处理。创建 PPPoE 套接字后,PPPOEConnectDevice()方法调用connect()。让我们来看看pppoe_connect()方法:

static int pppoe_connect(struct socket *sock, struct sockaddr *uservaddr,
                  int sockaddr_len, int flags)
{
        struct sock *sk = sock->sk;
        struct sockaddr_pppox *sp = (struct sockaddr_pppox *)uservaddr;
        struct pppox_sock *po = pppox_sk(sk);
        struct net_device *dev = NULL;
        struct pppoe_net *pn;
        struct net *net = NULL;
        int error;

        lock_sock(sk);

        error = -EINVAL;
        if (sp->sa_protocol != PX_PROTO_OE)
                goto end;

        /* Check for already bound sockets */
        error = -EBUSY;

当会话 id 不为 0 时,stage_session()方法返回true(如前所述,会话 id 仅在发现阶段为 0)。如果套接字已连接并且处于会话阶段,则套接字已经被绑定,因此我们退出:

        if ((sk->sk_state & PPPOX_CONNECTED) &&
             stage_session(sp->sa_addr.pppoe.sid))
                goto end;

到达这里意味着套接字没有连接(它的sk_state不是 PPPOX_CONNECTED)我们需要注册一个 PPP 通道:

        . . .
        /* Re-bind in session stage only */
        if (stage_session(sp->sa_addr.pppoe.sid)) {
                error = -ENODEV;
                net = sock_net(sk);
                dev = dev_get_by_name(net, sp->sa_addr.pppoe.dev);
                if (!dev)
                        goto err_put;

                po->pppoe_dev = dev;
                po->pppoe_ifindex = dev->ifindex;
                pn = pppoe_pernet(net);

网络设备必须启动:

                if (!(dev->flags & IFF_UP)) {
                        goto err_put;
                }

                memcpy(&po->pppoe_pa,
                       &sp->sa_addr.pppoe,
                       sizeof(struct pppoe_addr));

                write_lock_bh(&pn->hash_lock);

__set_item()方法将pppox_sock对象po插入到 PPPoE 套接字哈希表中;哈希密钥是根据会话 id 和远程对等 MAC 地址通过hash_item()方法生成的。远程对等 MAC 地址是po->pppoe_pa.remote。如果哈希表中有一个条目具有相同的会话 id、相同的远程 MAC 地址和相同的网络设备的ifindex__set_item()方法将返回错误–eal ready:

                error = __set_item(pn, po);
                write_unlock_bh(&pn->hash_lock);

                if (error < 0)
                        goto err_put;

po->chan是一个ppp_channel对象,参见前面的pppox_sock结构定义。在用ppp_register_net_channel()方法注册它之前,它的一些成员应该被初始化:

                po->chan.hdrlen = (sizeof(struct pppoe_hdr) +
                                   dev->hard_header_len);

                po->chan.mtu = dev->mtu - sizeof(struct pppoe_hdr);
                po->chan.private = sk;
                po->chan.ops = &pppoe_chan_ops;

                error = ppp_register_net_channel(dev_net(dev), &po->chan);
                if (error) {

delete_item()方法从 PPPoE 套接字哈希表中删除一个pppox_sock对象。

                        delete_item(pn, po->pppoe_pa.sid,
                                    po->pppoe_pa.remote, po->pppoe_ifindex);
                        goto err_put;
                }

设置要连接的套接字状态:

                sk->sk_state = PPPOX_CONNECTED;
        }

        po->num = sp->sa_addr.pppoe.sid;

end:
        release_sock(sk);
        return error;
err_put:
        if (po->pppoe_dev) {
                dev_put(po->pppoe_dev);
                po->pppoe_dev = NULL;
        }
        goto end;
}

通过注册 PPP 频道,我们可以使用 PPP 服务。我们能够通过从pppoe_rcv_core()方法调用通用 PPP 方法ppp_input()来处理 PPPoE 会话数据包。PPPoE 会话数据包的传输采用通用的ppp_start_xmit()方法。

RP-PPPoE 是一个开源项目,它为 Linux: http://www.roaringpenguin.com/products/pppoe提供了 PPPoE 客户端和 PPPoE 服务器。运行 PPPoE 服务器的一个简单示例是:

pppoe-server -I  p3p1 -R 192.168.3.101  -L 192.168.3.210 -N 200

本例中使用的选项有:

  • -I:接口名称(p3p1)
  • -L:设置本地 IP 地址(192.168.3.210)
  • -R:设置起始远程 IP 地址(192.168.3.101)
  • -N:并发 PPPoE 会话的最大数量(本例中为 200)

有关其他选项,请参见man 8 pppoe-server

同一个局域网上的客户端可以使用rp-pppoe插件,通过pppd守护进程创建到该服务器的 PPPoE 连接。

作为智能手机和平板电脑的移动操作系统,Android 的受欢迎程度正在稳步增长。我将用一小段关于 Android 的内容来结束这本书,简要讨论 Android 开发模型,并展示四个关于 Android 网络的例子。

安卓

近年来,Android 操作系统被证明是一个非常可靠和成功的移动操作系统。Android 操作系统基于 Linux 内核,谷歌开发人员对其进行了修改。Android 运行在数百种移动设备上,这些设备大多基于 ARM 处理器。(我要提一下,有一个把 Android 移植到 Intel x86 处理器的项目,http://www.android-x86.org/ ). The first generation of Google TV devices is based on x86 processors by Intel, but the second generation of Google TV devices are based on ARM. Originally Android was developed by “Android Inc.”, a company that was founded in California in 2003 by Andy Rubin and others. Google bought this company in 2005\. The Open Handset Alliance (OHA), a consortium of over 80 companies, announced Android in 2007\. Android is an open source operating system, and its source code is released under the Apache License. Unlike Linux, most of the development is done by Google employees behind closed doors. As opposed to Linux, there is no public mailing list where developers are sending and discussing patches. One can, however, send patches to public Gerrit (see http://source.android.com/source/submit-patches.html )。但是他们是否会被包含在 Android 系统中只能由谷歌来决定。``

``谷歌开发者对 Linux 内核贡献良多。在本章的前面,您已经了解到 cgroup 子系统是由 Google 开发人员启动的。我还将提到两个 Linux 内核网络补丁,Google 的 Tom Herbert 开发的接收包控制(RPS)补丁和接收流控制(RFS)补丁(参见http://lwn.net/Articles/362339/http://lwn.net/Articles/382428/,它们被集成到内核 2.6.35 中。当使用多核平台时,RPS 和 RFS 允许您根据有效负载的散列将数据包导向特定的 CPU。还有很多其他 Google 对 Linux 内核做出贡献的例子,看起来将来你也会遇到 Google 对 Linux 内核做出的许多重要贡献。在 Linux 内核的 staging tree 中可以找到很多来自 Android 内核的代码。不过,Android 内核是否会完全合并到 Linux 内核中还不好说;很可能它的很大一部分会进入 Linux 内核。更多关于 Android 主流化的信息,请看这个维基:http://elinux.org/Android_Mainlining_Project。过去有许多障碍,因为谷歌实施了独特的机制,如唤醒锁、替代电源管理、自己的 IPC(称为 Binder),它基于轻量级远程过程调用(RPC)、Android 共享内存驱动程序(Ashmem)、低内存黑仔等等。事实上,内核社区在 2010 年就拒绝了谷歌电源管理 wakelocks 补丁。但从那时起,这些功能中的一些被合并,情况发生了变化。(参见“Autosleep and Wake Locks”,https://lwn.net/Articles/479841/,以及“LPC Android microconference”,https://lwn.net/Articles/570406/ ). Linaro (www.linaro.org/)是一家非营利性组织,由 ARM、飞思卡尔、IBM、三星、意法爱立信和德州仪器(TI)等领先的大公司于 2010 年成立。它的工程团队开发 Linux ARM 内核,并为 GCC 工具链进行优化。Linaro 团队在协调和推动/调整上游变化方面做得非常出色。深入研究 Android 内核实现和主线的细节超出了本书的范围。

`安卓联网

然而,Android 的主要网络问题不是由于 Linux 内核,而是由于 Android 用户空间。Android 严重依赖 HAL,即使是联网,以及系统框架。最初(例如,直到 4.2),在框架级别根本没有以太网支持。如果驱动程序在内核中编译,TCP/IP 堆栈仍然允许基本的以太网连接用于 Android 调试桥(ADB)调试,但仅此而已。从 4.0 开始,Android-x86 project fork 在框架层增加了一个以太网的早期实现(设计很差,但还能工作)。从 4.2 开始,官方上游源支持以太网,但没有办法实际配置它(它检测以太网插入/拔出,如果有 DHCP 服务器,它向接口提供 IP 地址)。应用实际上可以通过框架使用这个接口,但大多数情况下没有人这样做。如果您需要真正的以太网支持(例如,能够配置您的接口,静态/DHCP 配置它,设置代理,确保所有应用都使用该接口),那么仍然需要大量的黑客攻击(参见www.slideshare.net/gxben/abs-2013-dive-into-android-networking-adding-ethernet-connectivity ))。在所有情况下,一次只支持一个接口(仅支持eth0,即使您有eth0eth1,所以不要期望充当任何类型的路由器)。我将在这里展示四个简短的例子来说明 Android 网络与 Linux 内核网络的不同之处:

  • 安全特权和联网:Android 在 Linux 内核中增加了一个安全特性(名为“偏执网络”),根据调用进程的组来限制对一些联网特性的访问。在标准的 Linux 内核中,任何应用都可以打开一个套接字并使用它进行传输/接收,而在 Android 中,对网络资源的访问是由 Gid(组 ID)过滤的。网络安全的部分可能很难合并到主线内核中,因为它包括许多 Android 独有的功能。有关 Android 网络安全的更多信息,请参见http://elinux.org/Android_Security#Paranoid_network-ing
  • 蓝牙:Bluedroid 是由 Broadcom 开发的基于代码的蓝牙协议栈。它取代了 Android 4.2 中基于 BlueZ 的堆栈。2013 年 7 月,Android 4.3 (API 级别 18)引入了对蓝牙低能耗(BLE 或蓝牙 LE)设备(也称为蓝牙智能和智能就绪设备)的支持。在此之前,Android 开源项目(AOSP)不支持 BLE 设备,但有一些供应商向 BLE 提供了 API。
  • Netfilter:谷歌有一个有趣的项目,在 Android 上提供了更好的网络统计。这是由一个 netfilter 模块xt_qtaguid实现的,它使用户空间应用能够标记它们的套接字。这个项目需要对 Linux 内核 netfilter 子系统进行一些修改。这些变化的补丁也被发送到 Linux 内核邮件列表(LKML);参见http://lwn.net/Articles/517358/。详情请见http://www.linuxplumbersconf.org/2013/ocw/sessions/1491 .的“Android netfilter 变化”
  • NFC:正如本章前面的近场通信(NFC)部分所述,Android NFC 架构是一个用户空间 NFC 堆栈:通过 Broadcom 或 Android OEMs 提供的 HAL 在用户空间中实现。

Android 内部:资源

虽然有很多关于为 Android 开发应用的资源(无论是书籍、邮件列表、论坛、课程等。),关于 Android 内部的资源很少。对于那些有兴趣了解更多信息的读者,我推荐这些资源:

  • 卡里姆·亚格穆尔的《嵌入式 Android:移植、扩展和定制》一书
  • 幻灯片:Maxime Ripard,Alexandre Belloni 的 Android 系统开发(超过 400 张幻灯片);http://free-electrons.com/doc/training/android/
  • 幻灯片:Benjamin Zores 的 Android 平台剖析(59 张幻灯片);http://www.slideshare.net/gxben/droidcon-2013-france-android-platform-anatomy
  • 幻灯片:Benjamin Zores 的 Jelly Bean 设备移植(127 张幻灯片);http://www.slideshare.net/gxben/as-2013-jelly-bean-device-porting-walkthrough
  • 网址:http://developer.android.com/index.html
  • Android 平台内部论坛-档案:http://news.gmane.org/gmane.comp.handhelds.android.platform
  • 一年一度的 Android 构建者峰会(ABS)正在举行。首届 ABS 于 2011 年在旧金山举行。建议看幻灯片,看视频,或者参加。
  • XDA 开发者大会:http://xda-devcon.com/http://xda-devcon.com/presentations/中的幻灯片和视频
  • 幻灯片:Android 内部,Marko Gargenta: http://www.scandevconf.se/db/Marakana-Android-Internals.pdf

image 注意 Android git 库在https://android.googlesource.com/中可用

注意,Android 使用一个基于python的特殊工具repo来管理数百个git库,这使得使用git更加容易。```sh````````

十五、总结

我在本章中讨论了 Linux 中的名称空间,重点是网络名称空间。我还描述了 cgroups 子系统及其实现;此外,我描述了它的两个网络模块,net_priocls_cgroup。Linux Bluetooth 子系统及其实现、IEEE 802.15.4 Linux 子系统和 6LoWPAN 以及 NFC 子系统都包括在内。本章还讨论了通过低延迟套接字轮询实现的优化,以及在内核网络堆栈中广泛使用的通知链机制(您会在浏览源代码时遇到)。简要讨论的另一个主题是 PCI 子系统,以便提供一些关于 PCI 设备的背景知识,因为许多网络设备都是 PCI 设备。这一章以三个简短的部分结束,分别是关于网络团队驱动程序(旨在取代绑定驱动程序)、PPPoE 实现和 Android。

虽然我们已经到了这本书的结尾,但是关于 Linux 内核网络还有很多要学习的,因为它是一个巨大的细节海洋,并且它正在以如此快的速度动态地发展。新功能和新补丁不断增加。我希望你喜欢这本书,并且你学到了一些东西!

快速参考

我将以本章中提到的一系列方法和宏来结束。

方法

以下列表包含本章中涉及的几种方法的原型和描述。

void switch _ task _ namespaces(struct task _ struct * p,struct n proxy * new);

该方法将指定的nsproxy对象分配给指定的进程描述符(task_struct对象)。

struct n proxy * create _ n proxy(void);

该方法分配一个nsproxy对象,并将其引用计数器初始化为 1。

S7-1200 可编程控制器:

这个方法释放了指定的nsproxy对象的资源。

结构网开发网(const 结构网 _ 设备开发);

此方法返回与指定网络设备关联的网络名称空间对象(nd_net)。

void dev _ net _ set(struct net _ device * dev,struct net * net);

该方法通过设置net_device对象的nd_net成员将指定的网络名称空间与指定的网络设备相关联。

void sock_net_set(struct sock *sk,struct net * net);

该方法将指定的网络名称空间与指定的sock对象相关联。

struct net * sock _ net(const struct sock * sk);

该方法返回与指定 sock 对象关联的网络名称空间对象(sk_net)。

int net_eq(const struct net *net1,const struct net * net 2);

如果第一个指定的网络命名空间指针等于第二个指定的网络命名空间指针,则此方法返回 1,否则返回 0。

net *net_alloc 结构(请参阅):

此方法分配网络命名空间。它是从copy_net_ns()方法调用的。

struct net *copy_net_ns(无符号长标志,struct user_namespace *user_ns,struct net * old _ net);

如果在其第一个参数flags中设置了 CLONE_NEWNET 标志,此方法将创建一个新的网络名称空间。它创建新的网络名称空间,首先调用net_alloc()方法来分配它, then it i通过调用setup_net()方法来初始化它,最后将它添加到所有名称空间的全局列表net_namespace_list。如果在其第一个参数flags中设置了 CLONE_NEWNET 标志,则不需要创建新的名称空间,并且返回指定的旧网络名称空间old_net。注意,copy_net_ns()方法的描述是指 CONFIG_NET_NS 被设置的情况。当没有设置 CONFIG_NET_NS 时,还有第二个copy_net_ns()的实现,它唯一做的事情就是首先验证在指定的标志中设置了 CLONE_NEWNET,如果设置了,则返回指定的旧网络命名空间(old_net);参见include/net/net_namespace.h

`int setup_net(struct net *net,struct user _ namespace * user _ ns);

此方法初始化指定的网络命名空间对象。它将网络名称空间user_ns成员指定为指定的user_ns,它将指定网络名称空间的引用计数器(count)初始化为 1,并执行更多初始化。它是从copy_net_ns()方法和net_ns_init()方法中调用的。

int proc_alloc_inum(未签名的 int *inum):

这个方法分配一个 proc inode,并将* inum设置为生成的 proc inode 号(0xf0000000 和 0xffffffff 之间的整数)。如果成功,它返回 0。

struct n proxy * task _ n proxy(struct task _ struct * tsk);

该方法返回附加到指定流程描述符(tsk)的nsproxy对象。

struct new_utsname *utsname(void);

该方法返回与当前运行的进程(current)相关联的new_utsname对象。

struct uts _ namespace * clone _ uts _ ns(struct user _ namespace * user _ ns,struct uts _ namespace * old _ ns);

该方法通过调用create_uts_ns()方法创建一个新的 UTS 名称空间对象,并将指定的old_ns UTS 名称空间的new_utsname对象复制到新创建的 UTS 名称空间的new_utsname中。

struct uts _ namespace * copy _ utsname(无符号长标志,struct user_namespace *user_ns,struct uts _ namespace * old _ ns);

如果在其第一个参数flags中设置了 CLONE_NEWUTS 标志,此方法将创建一个新的 UTS 命名空间。它通过调用clone_uts_ns()方法创建新的 UTS 名称空间,并返回新创建的 UTS 名称空间。如果在其第一个参数中设置了 CLONE_NEWUTS 标志,则不需要创建新的名称空间,而是返回指定的旧 UTS 名称空间(old_ns)。

struct net * sock _ net(const struct sock * sk);

该方法返回与指定 sock 对象关联的网络名称空间对象(sk_net)。

void sock_net_set(struct sock *sk,struct net * net);

该方法将指定的网络名称空间分配给指定的sock对象。

int dev _ change _ net _ namespace(struct net _ device * dev,struct net *net,const char * pat);

此方法将指定网络设备的网络命名空间更改为指定的网络命名空间。如果成功,则返回 0;如果失败,则返回–errno。调用者必须持有rtnl信号量。如果在网络设备的功能中设置了 NETIF_F_NETNS_LOCAL 标志,则返回错误–EINVAL。

void put _ net(struct net * net);

此方法递减指定网络命名空间的引用计数器。如果它达到零,它调用__put_net()方法来释放它的资源。

struct net * get _ net(struct net * net);

此方法在递增其引用计数器后返回指定的网络命名空间对象。

void get _ n proxy(struct n proxy * ns);

该方法递增指定nsproxy对象的引用计数器。

struct net * get _ net _ ns _ by _ PID(PID _ t PID);

此方法获取一个进程 id (PID)作为参数,并返回此进程附加到的网络命名空间对象。

struct net * get _ net _ ns _ by _ FD(int FD);

该方法获取一个文件描述符作为参数,并返回与指定文件描述符对应的 inode 相关联的网络名称空间。

struct PID _ namespace * ns _ of _ PID(struct PID * PID);

该方法返回创建指定的pid的 PID 名称空间。

void put _ n proxy(struct n proxy * ns);

此方法递减指定的nsproxy对象的引用计数器;如果达到 0,通过调用free_nsproxy()方法释放指定的nsproxy

int register _ pernet _ device(struct pernet _ operations * ops);

此方法注册网络命名空间设备。

void unregister _ pernet _ device(struct pernet _ operations * ops);

此方法注销网络命名空间设备。

int register _ pernet _ subsys(struct pernet _ operations * ops);

此方法注册网络命名空间子系统。

void unregister _ pernet _ subsys(struct pernet _ operations * ops);

此方法注销网络命名空间子系统。

static int register _ VLAN _ device(struct net _ device * real _ dev,u16 VLAN _ id);

该方法注册与指定物理设备(real_dev)相关联的 VLAN 设备。

void cgroup _ release _ agent(struct work _ struct * work);

当一个 cgroup 被释放时,这个方法被调用。它通过调用call_usermodehelper()方法创建一个用户空间进程。

int call _ user mode helper(char * path,char ** argv,char ** envp,int wait);

该方法准备并启动用户空间应用。

int bacmp(bdaddr_t *ba1,bdaddr _ t * ba 2);

这个方法比较两个蓝牙地址。如果它们相等,则返回 0。

请参见 bacpy(bdadd _ t * dst,bdadd _ t * src);

该方法将指定的源蓝牙地址(src)复制到指定的目的蓝牙地址(dst)。

int HCI _ send _ frame(struct sk _ buff * skb);

这种方法是传输 skb(命令和数据)的主要蓝牙方法。

int HCI _ register _ dev(struct HCI _ dev * hdev);

此方法注册指定的 HCI 设备。它是从蓝牙设备驱动程序调用的。如果未定义指定的hci_dev对象的open()close()回调,该方法将失败并返回–EINVAL。该方法在指定 HCI 设备的dev_flags成员中设置 HCI_SETUP 标志;它还为设备创建了一个sysfs条目。

请参阅 HCI _ unregister _ dev(struct HCI _ dev * hdev);

此方法注销指定的 HCI 设备。它是从蓝牙设备驱动程序调用的。它在指定 HCI 设备的dev_flags成员中设置 HCI_UNREGISTER 标志;它还会删除设备的sysfs条目。

void HCI _ event _ packet(struct HCI _ dev * hdev,struct sk _ buff * skb);

这个方法处理通过hci_rx_work()方法从 HCI 层接收的事件。

int low pan _ rcv(struct sk _ buff * skb,struct net_device *dev,struct packet_type *pt,struct net _ device * orig _ dev);

该方法是 6LoWPAN 数据包的主要接收处理程序。6LoWPAN 数据包的ethertype为 0x00F6。

void PCI _ unregister _ driver(struct PCI _ driver * dev);

这个方法注销 PCI 驱动程序。通常在网络驱动module_exit()方法中调用。

int PCI _ enable _ device(struct PCI _ dev * dev);

这个方法在驱动程序使用 PCI 设备之前初始化它。

int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void * dev);

该方法将指定的handler注册为指定irq的中断服务程序。

void free_irq(无符号 int irq,void * dev _ id);

该方法释放一个用request_irq()方法分配的中断。

int NFC _ init(void);

该方法通过注册通用 netlink NFC 系列、初始化 NFC 原始套接字和 NFC LLCP 套接字以及初始化 AF_NFC 协议来执行 NFC 子系统的初始化。

int NFC _ register _ device(struct NFC _ dev * dev);

该方法向 NFC 核心注册一个 NFC 设备(一个nfc_dev对象)。

int NFC _ HCI _ register _ device(struct NFC _ HCI _ dev * hdev);

该方法根据 NFC HCI 层注册一个 NFC HCI 设备(一个nfc_hci_dev对象)。

int NCI _ register _ device(struct NCI _ dev * ndev);

该方法针对 NFC NCI 层注册 NFC NCI 设备(一个nci_dev对象)。

static int _ _ init PPPoE _ init(void);

该方法初始化 PPPoE 层(PPPoE 协议处理程序、PPPoE 使用的套接字、网络通知处理程序、PPPoE procfs条目等等)。

struct PPPoE _ HDR * PPPoE _ HDR(const struct sk _ buf * skb);

该方法返回与指定的skb相关联的 PPPoE 头。

static int PPPoE _ create(struct net * net,struct socket * sock);

此方法创建 PPPoE 套接字。如果成功,返回 0;如果通过sk_alloc()方法分配套接字失败,返回–eno mem。

int _ _ set _ item(struct PPPoE _ net * pn,struct pppox _ sock * po);

这个方法将指定的pppox_sock对象插入到 PPPoE 套接字哈希表中。哈希密钥是根据会话 id 和远程对等 MAC 地址通过hash_item()方法计算出来的。

void delete _ item(struct PPPoE _ net * pn,__be16 sid,char *addr,int ifindex);

该方法删除 PPPoE 套接字哈希表条目,该条目具有指定的会话 id、指定的 MAC 地址和指定的网络接口索引(ifindex))。

bool stage _ session(_ _ be16 sid);

当指定的会话 id 不为 0 时,该方法返回true

int notifier _ chain _ register(struct notifier _ block * * nl,struct notifier _ block * n);

该方法将指定的notifier_block对象(n)注册到指定的通告程序链(nl)。注意,这个方法不是直接使用的,它周围有几个包装器。

int notifier _ chain _ unregister(struct notifier _ block * * nl,struct notifier _ block * n);

该方法从指定的通告程序链(nl)中注销指定的notifier_block对象(n)。注意,这个方法也不是直接使用的,它周围有几个包装器。

int register _ net device _ notifier(struct notifier _ block * nb);

该方法通过调用raw_notifier_chain_register()方法将指定的notifier_block对象注册到netdev_chain中。

int unregister _ net device _ notifier(struct notifier _ block * nb);

该方法通过调用raw_notifier_chain_unregister()方法从netdev_chain中注销指定的notifier_block对象。

int register _ inet 6 addr _ notifier(struct notifier _ block * nb);

该方法通过调用atomic_notifier_chain_register()方法将指定的notifier_block对象注册到inet6addr_chain中。

int unregister _ inet 6 addr _ notification(struct notification _ block * nb);

该方法通过调用atomic_notifier_chain_unregister()方法从inet6addr_chain中注销指定的notifier_block对象。

int register _ net event _ notifier(struct notifier _ block * nb);

该方法通过调用atomic_notifier_chain_register()方法将指定的notifier_block对象注册到netevent_notif_chain中。

int unregister _ net event _ notification(struct notification _ block * nb):

该方法通过调用atomic_notifier_chain_unregister()方法从netevent_notif_chain中注销指定的notifier_block对象。

int _ _ k probes notifier _ call _ chain(struct notifier _ block * * nl,unsigned long val,void *v,int nr_to_call,int * NR _ calls);

此方法用于生成通知事件。注意,这个方法也不是直接使用的,它周围有几个包装器。

int call_netdevice_notifiers(无符号 long val,struct net _ device * dev);

该方法用于通过调用raw_notifier_call_chain()方法在netdev_chain,上生成通知事件。

int blocking _ notifier _ call _ chain(struct blocking _ notifier _ head * NH,unsigned long val,void * v);

此方法用于生成通知事件;最终,在使用锁定机制后,它调用notifier_call_chain()方法。

int _ _ atomic _ notifier _ call _ chain(struct atomic _ notifier _ head * NH,unsigned long val,void *v,int nr_to_call,int * NR _ calls);

此方法用于生成通知事件。最终,在使用锁定机制后,它调用notifier_call_chain()方法。

宏指令

在这里,您可以找到本章所涉及的宏的描述。

pci _ 寄存器 _ 驱动程序()

这个宏在 PCI 子系统中注册 PCI 驱动程序。它获取一个pci_driver对象作为参数。通常在网络驱动module_init()方法中调用。`

十六、附录 A:Linux API

在这个附录中,我将介绍 Linux 内核网络栈中两个最基本的数据结构:sk_buffnet_device。这是可以帮助你阅读本书其余部分的参考资料,因为你可能在几乎每一章都会遇到这两种结构。熟悉并了解这两种数据结构对于理解 Linux 内核网络栈是必不可少的。随后,有一节是关于远程 DMA (RDMA)的,这是第十三章的进一步参考资料。它详细描述了 RDMA 使用的主要方法和主要数据结构。这个附录是一个很好的地方,尤其是在寻找基本术语的定义时。

sk_buff 结构

sk_buff结构代表一个数据包。SKB 代表套接字缓冲器。包可以由本地机器中的本地套接字生成,该本地套接字由用户空间应用创建;数据包可以被发送到外部或同一台机器上的另一个套接字。内核套接字也可以创建数据包;您可以从网络设备(第 2 层)接收物理帧,将其附加到sk_buff并将其传递到第 3 层。当数据包的目的地是您的本地机器时,它将继续到第 4 层。如果数据包不是给你的机器的,它将根据你的路由表规则被转发,如果你的机器支持转发。如果数据包由于任何原因被损坏,它将被丢弃。sk_buff是一个非常大的建筑;我在这一节提到了它的大多数成员。sk_buff结构在include/linux/skbuff.h中定义。以下是对其大多数成员的描述:

  • ktime_t tstamp

    数据包到达的时间戳。时间戳作为对基本时间戳的偏移量存储在 SKB 中。注意:不要把 SKB 的tstamp和硬件时间戳混淆,硬件时间戳是用skb_shared_infohwtstamps实现的。我将在后面的 appenidx 中描述skb_shared_info对象。

    助手方法:

  • skb_get_ktime(const struct sk_buff *skb):返回指定skbtstamp

  • skb_get_timestamp(const struct sk_buff *skb, struct timeval *stamp):将偏移量转换回struct timeval

  • net_timestamp_set(struct sk_buff *skb):为指定的skb设置时间戳。时间戳的计算是用ktime_get_real()方法完成的,该方法以ktime_t格式返回时间。

  • net_enable_timestamp():应该调用这个方法来启用 SKB 时间戳。

  • 应该调用此方法来禁用 SKB 时间戳。

  • struct sock *sk

    拥有 SKB 的套接字,用于本地生成的流量和发往本地主机的流量。对于正在转发的数据包,sk为空。通常,当谈到套接字时,您处理的是通过从用户空间调用socket()系统调用而创建的套接字。应该提到的是,还有内核套接字,它们是通过调用sock_create_kern()方法创建的。参见 VXLAN 驱动程序drivers/net/vxlan.c中的vxlan_init_net()示例。

    助手方法:

  • skb_orphan(struct sk_buff *skb):如果指定的skb有析构函数,调用这个析构函数;将指定skb的 sock 对象(sk)设置为 NULL,将指定skb的析构函数设置为 NULL。

  • struct net_device *dev

    dev成员是一个net_device对象,代表与 SKB 关联的网络接口设备;你有时会遇到 NIC(网络接口卡)这个术语来描述这样的网络设备。它可以是数据包到达的网络设备,也可以是数据包将要发送到的网络设备。net_device结构将在下一节深入讨论。

  • char cb[48]

    这是控制缓冲区。任何层都可以免费使用。这是一个用来存储私人信息的不透明区域。例如,TCP 协议将其用于 TCP 控制缓冲区:

    #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))
    (include/net/tcp.h)
    

    蓝牙协议也使用控制块:

    #define bt_cb(skb) ((struct bt_skb_cb *)((skb)->cb))
        (include/net/bluetooth/bluetooth.h)
    
  • unsigned long _skb_refdst

    目的地条目(dst_entry)地址。dst_entry struct 表示给定目的地的路由条目。对于每个数据包,无论是传入的还是传出的,都要在路由表中进行查找。有时这种查找被称为 FIB 查找。这个查找的结果决定您应该如何处理这个数据包;比如是否应该转发,如果应该,应该在哪个接口上传输;或者是否应该抛出,是否应该发送 ICMP 错误消息,等等。dst_entry对象有一个引用计数器(__refcnt字段)。有些情况下您会使用此引用计数,有些情况下您不会使用它。在第四章的中更详细地讨论了dst_entry对象和 FIB 中的查找。

    助手方法:

  • skb_dst_set(struct sk_buff *skb, struct dst_entry *dst):设置skb dst,假设对dst进行了引用,并且应该由dst_release()方法释放(由skb_dst_drop()方法调用)。

  • skb_dst_set_noref(struct sk_buff *skb, struct dst_entry *dst):设置skb dst,假设没有对dst进行参考。在这种情况下,skb_dst_drop()方法不会为dst调用dst_release()方法。

image SKB 可能有一个dst_entry指针附在上面;它可以被引用计数,也可以不被引用计数。如果没有取参考计数器,则设置_skb_refdst的低位。

  • struct sec_path *sp

    安全路径指针。它包括一组 IPsec XFRM 转换状态(xfrm_state对象)。IPsec (IP 安全)是第 3 层协议,主要用于 VPN。它在 IPv6 中是强制的,在 IPv4 中是可选的。像许多其他操作系统一样,Linux 为 IPv4 和 IPv6 实现了 IPsec。sec_path结构在include/net/xfrm.h中定义。详见第十章,其中讨论了 IPsec 子系统。

    助手方法:

  • struct sec_path *skb_sec_path(struct sk_buff *skb):返回与指定的skb关联的sec_path对象(sp)。

  • unsigned int len

    数据包字节总数。

  • unsigned int data_len

    数据长度。该字段仅在数据包包含非线性数据(分页数据)时使用。

    助手方法:

  • 当指定的skbdata_len大于 0 时,skb_is_nonlinear(const struct sk_buff *skb):返回true

  • __u16 mac_len

    MAC(第 2 层)报头的长度。

  • __wsum csum

    校验和。

  • __u32 priority

    数据包的排队优先级。在 Tx 路径中,根据套接字优先级(套接字的sk_priority字段)设置 SKB 的优先级。套接字优先级可以通过调用带有 SO_PRIORITY 套接字选项的setsockopt()系统 c 来设置。使用net_prio cgroup内核模块,您可以定义一个规则来设置 SKB 的优先级;参见本章后面对sk_buff netprio_map字段的描述,以及Documentation/cgroup/netprio.txt。对于转发的数据包,优先级是根据 IP 报头中的 TOS(服务类型)字段设置的。有一个名为ip_tos2prio的表格,由 16 个元素组成。根据 IP 报头的 TOS 字段,通过rt_tos2priority()方法完成从 TOS 到优先级的映射;参见net/ipv4/ip_forward.c中的ip_forward()方法和include/net/route.h中的ip_tos2prio定义。

  • __u8 local_df:1

    允许本地碎片标志。如果发送分组的套接字的pmtudisc字段的值是 IP_PMTUDISC_DONT 或 IP_PMTUDISC_WANT,local_df被设置为 1;如果套接字的pmtudisc字段的值是 IP_PMTUDISC_DO 或 IP_PMTUDISC_PROBE,local_df被设置为 0。参见net/ipv4/ip_output.c__ip_make_skb()方法的实现。只有当数据包local_df为 0 时,才设置 IP 头不分片标志 IP _ DF 参见net/ipv4/ip_output.c中的ip_queue_xmit()方法:

    . . .
            if (ip_dont_fragment(sk, &rt->dst) && !skb->local_df)
                   iph->frag_off = htons(IP_DF);
               else
                 iph->frag_off = 0;
    . . .
    

    IP 报头中的frag_off字段是一个 16 位的字段,它表示片段的偏移量和标志。最左边的 13 位(MSB)是偏移量(偏移量单位为 8 字节),最右边的 3 位(LSB)是标志。这些标志可以是 IP_MF(有更多分段)、IP_DF(不分段)、IP_CE(用于拥塞)或 IP_OFFSET(偏移部分)。

    这背后的原因是,有时您不希望允许 IP 碎片。例如,在路径 MTU 发现(PMTUD)中,您设置了 IP 报头的 DF(不分段)标志。因此,您不会对传出的数据包进行分段。路径上任何 MTU 小于数据包的网络设备都会丢弃该数据包,并发回一个 ICMP 数据包(“需要分段”)。为了确定路径 MTU,需要获取这些 ICMP“需要分段”数据包。详见第三章。从用户空间,设置 IP_PMTUDISC_DO 是这样完成的,例如,这样(下面的代码片段摘自iputils包中tracepath实用程序的源代码;tracepath实用程序找到路径 MTU):

    . . .
          int on = IP_PMTUDISC_DO;
          setsockopt(fd, SOL_IP, IP_MTU_DISCOVER, &on, sizeof(on));
    . . .
    
    
  • __u8 cloned:1

    当使用__skb_clone()方法克隆数据包时,该字段在克隆数据包和主数据包中都被设置为 1。克隆 SKB 意味着创造一个sk_buff struct的私人副本;该数据块在克隆和主 SKB 之间共享。

  • __u8 ip_summed:2

    IP(第三层)校验和的指示器;可以是下列值之一:

  • CHECKSUM_NONE:当设备驱动程序不支持硬件校验和时,它将ip_summed字段设置为 CHECKSUM_NONE。这表明校验和应该在软件中完成。

  • 校验和 _ 不必要:不需要任何校验和。

  • CHECKSUM_COMPLETE:硬件完成了对传入数据包的校验和计算。

  • CHECKSUM_PARTIAL:为传出数据包计算了部分校验和;硬件应该完成校验和计算。CHECKSUM_COMPLETE 和 CHECKSUM_PARTIAL 取代了 CHECKSUM_HW 标志,该标志现已弃用。

  • __u8 nohdr:1

    仅有效负载引用,不得修改标头。有些情况下,SKB 的所有者不再需要访问报头。在这种情况下,可以调用skb_header_release()方法,该方法设置 SKB 的nohdr字段;这表明该 SKB 的报头不应被修改。

  • __u8 nfctinfo:3

    连接跟踪信息。连接跟踪允许内核跟踪所有逻辑网络连接或会话。NAT 依靠连接跟踪信息进行转换。nfctinfo字段的值对应于ip_conntrack_info enum的值。因此,例如,当一个新的连接开始被跟踪时,nfctinfo的值是 IP_CT_NEW。当连接建立后,nfctinfo的值为 IP_CT_ESTABLISHED。当数据包与现有连接相关时,例如,当流量是某个 FTP 会话或 SIP 会话的一部分时,则nfctinfo的值可以更改为 IP_CT_RELATED。关于ip_conntrack_info enum值的完整列表,请参见include/uapi/linux/netfilter/nf_conntrack_common.h。SKB 的nfctinfo字段在resolve_normal_ct()方法net/netfilter/nf_conntrack_core.c中设置。此方法执行连接跟踪查找,如果有遗漏,它将创建一个新的连接跟踪条目。连接跟踪将在第九章中深入讨论,该章涉及 netfilter 子系统。

  • __u8 pkt_type:3

    对于以太网,数据包类型取决于以太网报头中的目的 MAC 地址,并由eth_type_trans()方法确定:

  • 用于广播的数据包 _ 广播

  • 用于多播的数据包多播

  • 如果目的 MAC 地址是作为参数传递的设备的 MAC 地址,则为 PACKET_HOST

  • PACKET_OTHERHOST 如果不满足这些条件

参见include/uapi/linux/if_packet.h中数据包类型的定义。

  • __u8 ipvs_property:1

此标志指示 SKB 是否归ipvs (IP 虚拟服务器)所有,这是一种基于内核的传输层负载平衡解决方案。在ipvs ( net/netfilter/ipvs/ip_vs_xmit.c)的发送方法中,该字段被设置为 1。

  • __u8 peeked:1

这个包已经被看过了,所以已经对它做了统计—所以不要再做了。

  • __u8 nf_trace:1

netfilter 数据包跟踪标志。该标志由跟踪 netfilter 模块 xt_TRACE 模块的包流设置,用于标记要跟踪的包(net/netfilter/xt_TRACE.c)。

助手方法:

  • nf_reset_trace(struct sk_buff *skb):将指定skbnf_trace设置为 0。

  • __be16 protocol

使用以太网和 IP 时,eth_type_trans()方法将 Rx 路径中的协议字段初始化为 ETH_P_IP。

  • void (*destructor)(struct sk_buff *skb)

通过调用kfree_skb()方法释放 SKB 时调用的回调。

  • struct nf_conntrack *nfct

关联的连接跟踪对象(如果存在)。与nfctinfo字段一样,nfct字段也是在resolve_normal_ct()方法中设置的。在第九章的中深入讨论了连接跟踪层,它处理 netfilter 子系统。

  • int skb_iif

数据包到达的网络设备的ifindex

  • __u32 rxhash

根据 IP 报头的源和目的地址以及传输报头的端口,在接收路径中计算 SKB 的rxhash。零值表示哈希无效。rxhash用于确保在使用对称多处理(SMP)时,具有相同流的数据包将由相同的 CPU 处理。这减少了缓存未命中的数量,并提高了网络性能。rxhash是接收数据包导向(RPS)特性的一部分,由 Google 开发者(Tom Herbert 和其他人)贡献。RPS 功能提高了 SMP 环境中的性能。详见Documentation/networking/scaling.txt

  • __be16 vlan_proto

    使用的 VLAN 协议——通常是802.1q协议。最近增加了对802.1ad协议(也称为堆叠 VLAN)的支持。

    下面是一个使用iproute2包的ip命令在用户空间创建802.1q802.1ad VLAN 设备的例子:

    ip link add link eth0 eth0.1000 type vlan proto 802.1ad id 1000
    ip link add link eth0.1000 eth0.1000.1000 type vlan proto 802.1q id 100
    

    注意:内核 3.10 及更高版本支持该特性。

  • __u16 vlan_tci

VLAN 标签控制信息(2 字节),由 ID 和优先级组成。

助手方法:

  • vlan_tx_tag_present(__skb):该宏检查在指定的__skbvlan_tci域中是否设置了 VLAN _ 标签 _ 存在标志。

  • __u16 queue_mapping

多队列设备的队列映射。

助手方法:

  • skb_set_queue_mapping (struct sk_buff *skb, u16 queue_mapping):为指定的skb设置指定的queue_mapping

  • skb_get_queue_mapping(const struct sk_buff *skb):返回指定skbqueue_mapping

  • __u8 pfmemalloc

从 PFMEMALLOC 储备中分配 SKB。

助手方法:

  • skb_pfmemalloc() : 如果 SKB 是从 PFMEMALLOC 储备中分配的,则返回true

  • __u8 ooo_okay:1

设置ooo_okay标志以避免ooo(无序)数据包。

  • __u8 l4_rxhash:1

使用传输端口上的规范 4 元组哈希时设置的标志。

参见net/core/flow_dissector.c中的__skb_get_rxhash()方法。

  • __u8 no_fcs:1

当您请求 NIC 将最后 4 个字节视为以太网帧校验序列(FCS)时设置的标志。

  • __u8 encapsulation:1

封装字段表示 SKB 用于封装。例如,它用于 VXLAN 驱动程序。VXLAN 是通过 UDP 内核套接字传输第 2 层以太网数据包的标准协议。当有防火墙阻止隧道,例如只允许 TCP 或 UDP 流量时,它可以用作一种解决方案。VXLAN 驱动程序使用 UDP 封装,并在vxlan_init_net()方法中将 SKB 封装设置为 1。此外,ip_gre模块和ipip隧道模块使用封装,并将 SKB 封装设置为 1。

  • __u32 secmark

    安全标志字段。secmark字段由iptables SECMARK 目标设置,该目标用任何有效的安全上下文标记数据包。例如:

    iptables -t mangle -A INPUT -p tcp --dport 80 -j SECMARK --selctx system_u:object_r:httpd_packet_t:s0
    iptables -t mangle -A OUTPUT -p tcp --sport 80 -j SECMARK --selctx system_u:object_r:httpd_packet_t:s0
    

    在前面的规则中,您将到达和离开端口 80 的数据包静态标记为httpd_packet_t。参见:netfilter/xt_SECMARK.c

    助手方法:

  • void skb_copy_secmark(struct sk_buff *to, const struct sk_buff *from):将第一个指定的 SKB ( to)的secmark字段的值设置为等于第二个指定的 SKB ( from)的secmark字段的值。

  • void skb_init_secmark(struct sk_buff *skb):将指定skbsecmark初始化为 0。

接下来的三个字段:markdropcountreserved_tailroom出现在一个联合中。

  • __u32 mark

此字段可通过标记来识别 SKB。

您可以设置 SKB 的mark字段,例如,使用 mangle 表的iptables预路由规则中的iptables标记目标。

  • iptables -A PREROUTING -t mangle -i eth1 -j  MARK  --set-mark   0x1234

该规则将在执行路由查找之前,为eth1上的传入流量的每个 SKB mark字段分配值 0x1234。您还可以运行一个iptables规则,该规则将检查每个 SKB 的mark字段,以匹配指定的值并对其采取行动。Netfilter 目标和iptables在第九章的中讨论,它涉及 netfilter 子系统。

  • __u32 dropcount

dropcount 计数器表示被分配的sock对象(sk)的sk_receive_queue的丢包数(sk_drops)。参见net/core/sock.c中的sock_queue_rcv_skb()方法。

  • _u32 reserved_tailroom:用于sk_stream_alloc_skb()方式。
  • sk_buff_data_t transport_header

传输层(L4)报头。

助手方法:

  • skb_transport_header(const struct sk_buff *skb):返回指定skb的传输头。

  • skb_transport_header_was_set(const struct sk_buff *skb):如果设置了指定的skbtransport_header,则返回 1。

  • sk_buff_data_t network_header

网络层(L3)报头。

助手方法:

  • skb_network_header(const struct sk_buff *skb):返回指定skb的网络头。

  • sk_buff_data_t mac_header

链路层(L2)报头。

助手方法:

  • skb_mac_header(const struct sk_buff *skb):返回指定skb的 MAC 头。

  • skb_mac_header_was_set(const struct sk_buff *skb):如果设置了指定skbmac_header,则返回 1。

  • sk_buff_data_t tail

数据的尾部。

  • sk_buff_data_t end

缓冲区的结尾。tail不能超过end

  • unsigned char head

缓冲区的头。

  • unsigned char data

    数据头。数据块的分配独立于sk_buff分配。

    参见,在_alloc_skb()net/core/skbuff.c:

    data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
    

    助手方法:

  • skb_headroom(const struct sk_buff *skb):该方法返回 headroom,即指定 skb 头部的空闲空间的字节数(skb->data – skb->head)。见图 A-1 。

  • skb_tailroom(const struct sk_buff *skb):该方法返回 tailroom,即指定的skb ( skb->end – skb->tail)尾部的空闲空间的字节数。见图 A-1 。

图 A-1 显示了 SKB 的顶部空间和尾部空间。

9781430261964_AppA-01.jpg

图 A-1 。SKB 的净空高度和净空高度

以下是处理缓冲液的一些方法:

  • skb_put(struct sk_buff *skb, unsigned int len):将数据添加到缓冲区:该方法将len字节添加到指定skb的缓冲区中,并将指定skb的长度增加指定的len
  • skb_push(struct sk_buff *skb, unsigned int len):将数据添加到缓冲区的开头;该方法将指定的skb的数据指针递减指定的len,并将指定的skb的长度递增指定的len
  • skb_pull(struct sk_buff *skb, unsigned int len):从缓冲区的开始处删除数据;该方法将指定的skb的数据指针递增指定的len,并将指定的skb的长度递减指定的len
  • skb_reserve(struct sk_buff *skb, int len):通过减少尾部来增加空skb的净空高度。

在描述了一些处理缓冲区的方法之后,我继续列出sk_buff结构的成员:

  • unsigned int truesize

为 SKB 分配的总内存(包括 SKB 结构本身和分配的数据块的大小)。

  • atomic_t users

初始化为 1 的参考计数器;通过skb_get()方法递增,通过kfree_skb()方法或consume_skb()方法递减;kfree_skb()方法递减使用计数器;如果达到 0,该方法将释放 SKB,否则,该方法将返回而不释放它。

助手方法:

  • skb_get(struct sk_buff *skb):将users参考计数器加 1。
  • skb_shared(const struct sk_buff *skb):如果users的个数不为 1,则返回true
  • skb_share_check(struct sk_buff *skb, gfp_t pri):如果缓冲区不共享,则返回原来的缓冲区。如果该缓冲区是共享的,则该缓冲区将被克隆,旧副本将删除一个引用。返回具有单个引用的新克隆。当从中断上下文调用或持有自旋锁时,pri参数(优先级)必须是 GFP_ATOMIC。如果内存分配失败,则返回 NULL。
  • consume_skb(struct sk_buff *skb):递减users参考计数器,如果users参考计数器为零,则释放 SKB。

结构 skb_shared_info

skb_shared_info struct位于数据块(skb_end_pointer(SKB))的末尾。它只包含几个字段。让我们来看看:

struct skb_shared_info {
    unsigned char         nr_frags;
    __u8                  tx_flags;
    unsigned short        gso_size;
    unsigned short        gso_segs;
    unsigned short        gso_type;
    struct sk_buff        *frag_list;
    struct skb_shared_hwtstamps hwtstamps;
    __be32                ip6_frag_id;
    atomic_t              dataref;
    void *                destructor_arg;
    skb_frag_t            frags[MAX_SKB_FRAGS];
};

以下是对skb_shared_info结构中一些重要成员的描述:

  • nr_frags:表示frags数组中元素的个数。

  • tx_flags可以是:

  • SKBTX_HW_TSTAMP:生成硬件时间戳。

  • 生成一个软件时间戳。

  • SKBTX_IN_PROGRESS:设备驱动程序将提供硬件时间戳。

  • SKBTX_DEV_ZEROCOPY:设备驱动程序支持 TX 零拷贝缓冲区。

  • SKBTX_WIFI_STATUS:生成 WIFI 状态信息。

  • SKBTX_SHARED_FRAG:表示至少有一个片段可能被覆盖。

  • 在处理碎片时,有些情况下你会处理一个sk_buffs ( frag_list)列表,有些情况下你会处理frags数组。这主要取决于是否设置了分散/聚集模式。

助手方法:

  • skb_is_gso(const struct sk_buff *skb):如果与指定的skb关联的skb_shared_infogso_size不为 0,则返回true
  • skb_is_gso_v6(const struct sk_buff *skb):如果skb关联的skb_shared_infogso_type为 SKB_GSO_TCPV6,则返回true
  • skb_shinfo(skb):返回与指定的skb关联的skb_shinfo的宏。
  • skb_has_frag_list(const struct sk_buff *skb):如果指定的skbskb_shared_infofrag_list不为空,则返回true
  • dataref:一个skb_shared_info struct的参考计数器。它在方法中被设置为 1,分配skb并初始化skb_shared_info(__alloc_skb()方法)。

net_device 结构

net_device struct代表网络设备。它可以是物理设备,如以太网设备,也可以是软件设备,如网桥设备或 VLAN 设备。与sk_buff结构一样,我将列出它的重要成员。net_device structinclude/linux/netdevice.h : 中定义

  • char name[IFNAMSIZ]

网络设备的名称。这是您使用ifconfigip命令看到的名称(例如eth0eth1等等)。接口名称的最大长度为 16 个字符。在支持biosdevname的新版本中,命名方案对应于网络设备的物理位置。因此,根据机箱标签,PCI 网络设备被命名为p<slot>p<port>,嵌入式端口(在主板接口上)被命名为em<port>——例如em1em2等等。支持 SR-IOV 设备和网络分区(NPAR)的设备有一个特殊的后缀—。Biosdevname 由戴尔开发:http://linux.dell.com/biosdevname。另见本白皮书:http://linux.dell.com/files/whitepapers/consistent_network_device_naming_in_linux.pdf

助手方法:

  • dev_valid_name(const char *name):检查指定网络设备名称的有效性。网络设备名称必须遵守某些限制,以便能够创建相应的sysfs条目。例如,它不能是“.”还是“..”;其长度不应超过 16 个字符。改变界面名称可以这样做,例如:ip link set <oldDeviceName> p2p1 <newDeviceName>。例如,ip link set p2p1 name a12345678901234567将失败,并显示以下消息:Error: argument "a12345678901234567" is wrong: "name" too long。原因是您试图设置超过 16 个字符的设备名称。并且运行ip link set p2p1 name.将会失败,出现RTNETLINK answers: Invalid argument,因为您试图将设备名称设置为“.”,这是一个无效值。参见net/core/dev.c中的dev_valid_name()

  • struct hlist_node name_hlist

这是网络设备的哈希表,由网络设备名称索引。由dev_get_by_name()在该哈希表中执行查找。通过list_netdevice()方法插入哈希表,通过unlist_netdevice()方法移除哈希表。

  • char *ifalias

    SNMP 别名接口名称。其长度可达 256 (IFALIASZ)。

    您可以使用以下命令行创建网络设备的别名:

    ip link set <devName> alias myalias
    

    ifalias名称由/sys/class/net/<devName>/ifalias通过sysfs导出。

    助手方法:

  • dev_set_alias(struct net_device *dev, const char *alias, size_t len):为指定的网络设备设置指定的别名。指定的len参数是指定的alias要复制的字节数;如果指定的len大于 256 (IFALIASZ),该方法将失败,并返回-EINVAL。

  • unsigned int irq

设备的中断请求(IRQ)号。网络驱动程序应该调用request_irq()用这个 IRQ 号注册它自己。通常这是在网络设备驱动程序的probe()回调中完成的。request_irq()方法的原型是:int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)。第一个参数是 IRQ 号。指定的handler是中断服务程序(ISR)。当网络驱动不再使用这个irq时,它应该调用free_irq()方法。在许多情况下,这个irq是共享的(使用 IRQF_SHARED 标志调用request_irq()方法)。您可以通过运行cat /proc/interrupts来查看每个内核上发生的中断数量。您可以通过echo irqMask > /proc/irq/<irqNumber>/smp_affinity设置irq的 SMP 关联。

在 SMP 机器中,设置中断的 SMP 关联性意味着设置允许哪些内核处理中断。一些 PCI 网络接口使用消息信号中断(MSI)。 PCI MSI 中断从不共享,所以在这些网络驱动中调用request_irq()方法时,IRQF_SHARED 标志不设置。在Documentation/PCI/MSI-HOWTO.txt中查看更多信息。

  • unsigned long state

可以是下列值之一的标志:

  • __LINK_STATE_START:该标志在设备启动时由dev_open()方法设置,在设备关闭时清除。
  • __LINK_STATE_PRESENT:该标志通过register_netdevice()方法在设备注册中设置,并通过netif_device_detach()方法清除。
  • __LINK_STATE_NOCARRIER:此标志显示设备是否检测到载波丢失。它由netif_carrier_off()方法设置,由netif_carrier_on()方法清除。由sysfs通过/sys/class/net/<devName>/carrier出口。
  • __LINK_STATE_LINKWATCH_PENDING:该标志由linkwatch_fire_event()方法设置,并由linkwatch_do_dev()方法清除。
  • __LINK_STATE_DORMANT:休眠状态表示接口无法传递数据包(也就是说,它不是“up”);然而,这是一个“未决”状态,等待一些外部事件。请参见 RFC 2863“接口组 MIB”中的第 3.1.12 节“IfOperStatus 的新状态”

可以用通用的set_bit()方法设置state标志。

助手方法:

  • netif_running(const struct net_device *dev):如果设置了指定设备的 STATE 字段的 __LINK_STATE_START 标志,则返回true
  • netif_device_present(struct net_device *dev):如果设置了指定设备的 STATE 字段的 __LINK_STATE_PRESENT 标志,则返回true
  • netif_carrier_ok (const struct net_device *dev):如果指定设备的 STATE 字段的 __LINK_STATE_NOCARRIER 标志没有置位,则返回true

这三种方法在include/linux/netdevice.h中有定义。

  • netdev_features_t features

当前活动的设备功能集。这些特征应该仅由网络核心或在ndo_set_features()回调的错误路径中改变。网络驱动程序开发人员负责设置设备功能的初始设置。有时他们会使用错误的功能组合。网络核心通过移除netdev_fix_features()方法中的违规特征来解决这个问题,该方法在注册网络接口时被调用(在register_netdevice()方法中);内核日志中也会写入一条适当的消息。

我将在这里提到一些net_device特性并进行讨论。关于net_device特性的完整列表,请查看include/linux/netdev_features.h

  • NETIF_F_IP_CSUM 意味着网络设备可以校验和 L4 IPv4 TCP/UDP 数据包。
  • NETIF_F_IPV6_CSUM 意味着网络设备可以校验和 L4 IPv6 TCP/UDP 数据包。
  • NETIF_F_HW_CSUM 意味着设备可以在硬件中对所有 L4 数据包进行校验和检查。您不能将 NETIF_F_HW_CSUM 与 NETIF_F_IP_CSUM 或 NETIF_F_IPV6_CSUM 一起激活,因为这将导致重复的校验和检查。

如果驱动程序功能集包括 NETIF_F_HW_CSUM 和 NETIF_F_IP_CSUM 功能,那么您将收到一条内核消息,提示“混合的硬件和 IP 校验和设置”在这种情况下,netdev_fix_features()方法删除了 NETIF_F_IP_CSUM 特性。如果驱动程序功能集包括 NETIF_F_HW_CSUM 和 NETIF_F_IPV6_CSUM 功能,您将再次得到与前一种情况相同的消息。这一次,NETIF_F_IPV6_CSUM 功能是被netdev_fix_features()方法删除的功能。为了使设备支持 TSO (TCP 分段卸载),它还需要支持分散/收集和 TCP 校验和;这意味着必须设置 NETIF_F_SG 和 NETIF_F_IP_CSUM 功能。如果驱动程序功能集不包括 NETIF_F_SG 功能,那么您将收到一条内核消息,说明“由于没有 SG 功能,正在删除 TSO 功能”,并且 NETIF_F_ALL_TSO 功能将被删除。如果驱动程序功能集不包括 NETIF_F_IP_CSUM 功能,也不包括 NETIF_F_HW_CSUM 功能,那么您将收到一条内核消息,提示“由于没有 CSUM 功能,正在删除 TSO 功能”,并且 NETIF_F_TSO 将被删除。

image 注意在最近的内核中,如果设置了 CONFIG_DYNAMIC_DEBUG 内核配置项,可能需要通过<debugfs>/dynamic_debug/control接口显式启用一些消息的打印。参见Documentation/dynamic-debug-howto.txt

  • NETIF_F_LLTX is the LockLess TX flag and is considered deprecated. When it is set, you don’t use the generic Tx lock (This is why it is called LockLess TX). See the following macro (HARD_TX_LOCK) from net/core/dev.c:

    #define HARD_TX_LOCK(dev, txq, cpu) { \ if ((dev->features & NETIF_F_LLTX) == 0) { \
       __netif_tx_lock(txq, cpu); \
      } \
        }
    

    NETIF_F_LLTX 用于 VXLAN、VETH 等隧道驱动程序,以及 IP over IP (IPIP)隧道驱动程序。例如,在 IPIP 隧道模块中,您在ipip_tunnel_setup()方法(net/ipv4/ipip.c)中设置 NETIF_F_LLTX 标志。

    NETIF_F_LLTX 标志也用于一些实现了自己的 TX 锁的驱动程序中,如 cxgb 网络驱动程序。

    drivers/net/ethernet/chelsio/cxgb/cxgb2.c中,您有:

       static int __devinit init_one(struct pci_dev *pdev,
       const struct pci_device_id *ent)
       {
           . . .
           netdev->features |= NETIF_F_SG | NETIF_F_IP_CSUM |
                               NETIF_F_RXCSUM | NETIF_F_LLTX;
           . . .
       }
    
    
  • NETIF _ F _ GRO 用于表示设备支持 GRO(通用接收卸载)。使用 GRO,传入的数据包在接收时被合并。GRO 功能提高了网络性能。GRO 取代了仅限于 TCP/IPv4 的 LRO(大型接收卸载)。该标志在dev_gro_receive()方法开始时被检查;没有设置该标志的设备将不会执行该方法中的 GRO 处理部分。想要使用 GRO 的驱动程序应该在驱动程序的 Rx 路径中调用napi_gro_receive()方法。您可以分别通过ethtool -K <deviceName> gro on / ethtool -K <deviceName> gro off使用 ethtool 启用/禁用 GRO。您可以通过运行ethtool –k <deviceName>并查看gro字段来检查 GRO 是否已设置。

  • NETIF_F_GSO 置位表示器件支持通用分段卸载(GSO)。GSO 是以前一个名为 TSO (TCP 分段卸载)的解决方案的推广,它只处理 IPv4 中的 TCP。GSO 还可以处理 IPv6、UDP 和其他协议。GSO 是一种性能优化,它基于对大数据包遍历网络堆栈一次而不是多次。因此,我们的想法是避免在第 4 层分段,并尽可能推迟分段。系统管理员可以分别通过ethtool -K <driverName> gso on / ethtool -K <driverName> gso offethtool使能/禁用 GSO。您可以通过运行ethtool –k <deviceName>并查看gso字段来检查是否设置了 GSO。要使用 GSO,您应该在分散/聚集模式下工作。必须设置 NETIF_F_SG 标志。

  • NETIF_F_NETNS_LOCAL 是为网络命名空间本地设备设置的。这些是不允许在网络名称空间之间移动的网络设备。环回、VXLAN 和 PPP 网络设备是命名空间本地设备的示例。所有这些设备都设置了 NETIF_F_NETNS_LOCAL 标志。系统管理员可以通过ethtool -k <deviceName>检查接口是否设置了 NETIF_F_NETNS_LOCAL 标志。该特性是固定的,不能通过ethtool改变。尝试将这种类型的网络设备移动到不同的名称空间会导致错误(-EINVAL)。详细信息,请查看dev_change_net_namespace()方法(net/core/dev.c)。删除网络名称空间时,未设置 NETIF_F_NETNS_LOCAL 标志的设备将被移动到默认的初始网络名称空间(init_net)。网络命名空间设置了 NETIF_F_NETNS_LOCAL 标志的本地设备不会移动到默认的初始网络命名空间(init_net),而是会被删除。

  • NETIF_F_HW_VLAN_CTAG_RX 为,供支持 VLAN Rx 硬件加速的设备使用。它以前被称为 NETIF_F_HW_VLAN_RX,并在内核 3.10 中被重命名,当时增加了对802.1ad的支持。添加了“CTAG”以表示此设备不同于“STAG”设备(服务提供商标记)。设置 NETIF_F_HW_VLAN_RX 特性的设备驱动程序还必须定义ndo_vlan_rx_add_vid()ndo_vlan_rx_kill_vid()回调。不这样做将避免设备注册,并导致“驱动程序中的错误 VLAN 加速”内核错误消息。

  • NETIF_F_HW_VLAN_CTAG_TX 用于支持 VLAN Tx 硬件加速的设备。它以前被称为 NETIF_F_HW_VLAN_TX,在内核 3.10 中添加了对802.1ad的支持后被重命名。

  • NETIF_F_VLAN_CHALLENGED is set for devices that can’t handle VLAN packets. Setting this feature avoids registration of a VLAN device. Let’s take a look at the VLAN registration method:

    static int register_vlan_device(struct net_device *real_dev, u16 vlan_id) {
        int err;
        . . .
        err = vlan_check_real_dev(real_dev, vlan_id);
    

    vlan_check_real_dev()方法做的第一件事是检查网络设备特性,如果设置了 NETIF _ F _ VLAN _ 挑战特性,则返回一个错误:

    int vlan_check_real_dev(struct net_device *real_dev, u16 vlan_id)
    {
            const char *name = real_dev->name;
    
            if (real_dev->features & NETIF_F_VLAN_CHALLENGED) {
                    pr_info("VLANs not supported on %s\n", name);
                    return -EOPNOTSUPP;
            }
                    . . .
    }
    

例如,某些类型的英特尔 e100 网络设备驱动程序设置了 NETIF _ F _ VLAN _ 挑战功能(参见drivers/net/ethernet/intel/e100.c中的e100_probe())。

您可以通过运行ethtool –k <deviceName>并查看vlan-challenged字段来检查 NETIF_F_VLAN_CHALLENGED 是否已设置。这是一个固定值,不能用ethtool命令改变。

  • 当网络接口支持分散/聚集 IO 时,设置 NETIF_F_SG。您可以通过ethtool -K <deviceName> sg on / ethtool -K <deviceName> sg off分别使用ethtool启用和禁用分散/聚集。您可以通过运行ethtool –k <deviceName>并查看sg字段来检查是否设置了分散/聚集。
  • 如果设备可以通过 DMA 对高位存储器进行访问,则 NETIF_F_HIGHDMA 设置为。设置这个特性的实际含义是,net_device_ops对象的ndo_start_xmit()回调可以管理 skb,skb 在高内存中有frags元素。您可以通过运行ethtool –k <deviceName>并查看highdma字段来检查 NETIF_F_HIGHDMA 是否已设置。这是一个固定值,不能用ethtool命令改变。
  • netdev_features_t hw_features

可变特征的特征集。这意味着它们的状态可能会因用户的请求而改变(启用或禁用)特定设备。这个设置应该在ndo_init()回调中初始化,以后不能更改。

  • netdev_features_t wanted_features

用户请求的功能集。用户可以请求更改各种卸载功能,例如通过运行ethtool -K eth1 rx on。这会生成一个特性变更事件通知(NETDEV_FEAT_CHANGE ),由netdev_features_change()方法发送。

  • netdev_features_t vlan_features

    子 VLAN 设备继承其状态的功能集。比如我们来看看rtl_init_one()方法,就是r8169网络设备驱动的probe回调(见第十四章):

    int rtl_init_one(struct pci_dev *pdev, const struct pci_device_id *ent)
    
    {
       . . .
       dev->vlan_features=NETIF_F_SG|NETIF_F_IP_CSUM|NETIF_F_TSO|   NETIF_F_HIGHDMA;
       . . .
    }
    

    (drivers/net/ethernet/realtek/r8169.c)

    这种初始化意味着所有子 VLAN 设备都将具有这些功能。例如,假设您的eth0设备是一个r8169设备,您因此添加了一个 VLAN 设备:vconfig add eth0 100。然后,在 VLAN 模块的初始化中,有这个与vlan_features相关的代码:

    static int vlan_dev_init(struct net_device *dev)
    {
        . . .
        dev->features |= real_dev->vlan_features | NETIF_F_LLTX;
        . . .
    }
    
    (net/8021q/vlan_dev.c)
    

    这意味着它将 VLAN 子设备的特性设置为真实设备的vlan_features(在本例中是eth0),这些特性是根据您之前在rtl_init_one()方法中看到的内容设置的。

  • netdev_features_t hw_enc_features

封装设备继承的特征掩码。该字段指示硬件能够做什么封装卸载,驱动程序需要适当地设置它们。有关网络设备功能的更多信息,请参见Documentation/networking/netdev-features.txt

  • ifindex

ifindex(接口索引)是唯一的设备标识符。通过dev_new_index()方法,每当您创建一个新的网络设备时,该索引就增加 1。您创建的第一个网络设备(几乎总是环回设备)的ifindex为 1。循环整数溢出由处理ifindex数赋值的方法处理。ifindexsysfs通过/sys/class/net/<devName>/ifindex输出。

  • struct net_device_stats stats

作为遗产留下的统计数据struct,包括像rx_packets的数量或者tx_packets的数量这样的字段。新的设备驱动使用rtnl_link_stats64 struct(在include/uapi/linux/if_link.h中定义)代替net_device_stats struct。大多数网络驱动程序实现了net_device_opsndo_get_stats64()回调(或net_device_opsndo_get_stats()回调,当使用旧的 API 时)。

统计数据通过/sys/class/net/<deviceName>/statistics导出。

有些驱动实现了get_ethtool_stats()回调。这些驱动程序按ethtool -S <deviceName>显示统计数据

例如,参见drivers/net/ethernet/realtek/r8169.c中的rtl8169_get_ethtool_stats()方法。

  • atomic_long_t rx_dropped

核心网络堆栈在 RX 路径中丢弃的数据包数量的计数器。驱动程序不应使用此计数器。不要混淆sk_buffrx_dropped字段和softnet_data structdropped字段。softnet_data struct代表一个每 CPU 的对象。它们是不等价的,因为sk_buffrx_dropped可能以几种方法递增,而softnet_datadropped计数器仅通过enqueue_to_backlog()方法(net/core/dev.c)递增。softnet_data丢弃的计数器由/proc/net/softnet_stat导出。在/proc/net/softnet_stat中,每个 CPU 有一行。第一列是总数据包计数器,第二列是丢弃的数据包计数器。

例如:

cat /proc/net/softnet_stat
00000076 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000005 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

你在这里看到每个 CPU 一行(你有两个 CPU);对于第一个 CPU,您看到总共 118 个数据包(十六进制 0x76),其中一个数据包被丢弃。对于第二个 CPU,您看到总共有 5 个数据包,0 个被丢弃。

  • struct net_device_ops *netdev_ops

netdev_ops结构包含了几个回调方法的指针,如果你想覆盖默认行为,就要定义这些回调方法。下面是netdev_ops的一些回调:

  • 注册网络设备时调用ndo_init()回调。
  • 当网络设备未注册或注册失败时,调用ndo_uninit()回调。
  • 当网络设备状态从停机状态改变到运行状态时,ndo_open()回调处理设备状态的改变。
  • 当网络设备的状态被更改为 down 时,调用ndo_stop()回调。
  • 调用ndo_validate_addr()回调来检查 MAC 是否有效。许多网络驱动程序将通用的eth_validate_addr()方法设置为ndo_validate_addr()回调。如果 MAC 地址不是多播地址并且不全是零,则通用eth_validate_addr()方法返回true
  • ndo_set_mac_address()回调设置 MAC 地址。许多网络驱动程序将通用的eth_mac_addr()方法设置为struct net_device_opsndo_set_mac_address()回调,用于设置它们的 MAC 地址。比如 VETH 驱动(drivers/net/veth.c)或者 VXLAN 驱动(drivers/nets/vxlan.c)。
  • ndo_start_xmit()回调处理数据包传输。它不能为空。
  • 使用多队列时,ndo_select_queue()回调用于选择发送队列。如果没有设置ndo_select_queue()回调,那么调用__netdev_pick_tx()。参见net/core/flow_dissector.cnetdev_pick_tx()方法的实现。
  • ndo_change_mtu()回调处理修改 MTU。它应该检查指定的 MTU 不小于 68,这是最小的 MTU。在许多情况下,网络驱动程序将ndo_change_mtu()回调设置为通用的eth_change_mtu()方法。如果支持巨型帧,应该覆盖eth_change_mtu()方法。
  • 当获得非通用接口代码处理的 IOCTL 请求时,调用ndo_do_ioctl()回调。
  • 当发送器长时间空闲时,调用ndo_tx_timeout()回调(用于看门狗)。
  • 调用ndo_add_slave()回调将指定的网络设备设置为指定网络设备的从设备。例如,它用在组网络驱动程序和绑定网络驱动程序中。
  • 调用ndo_del_slave()回调来移除先前被奴役的网络设备。
  • 调用ndo_set_features()回调来用新特性更新网络设备的配置。
  • 如果网络设备支持 VLAN 过滤(NETIF _ F _ HW _ VLAN _ 过滤器标志在设备特性中设置),则在注册 VLAN id 时调用ndo_vlan_rx_add_vid()回调。
  • 如果网络设备支持 VLAN 过滤(在设备特性中设置了 NETIF _ F _ HW _ VLAN _ 过滤器标志),则在注销 VLAN id 时调用ndo_vlan_rx_kill_vid()回调。

image 注意从内核 3.10 开始,NETIF _ F _ HW _ VLAN _ 过滤器标志被重命名为 NETIF _ F _ HW _ VLAN _ CTAG _ 过滤器。

  • 还有几个处理 SR-IOV 设备的回调函数,例如,ndo_set_vf_mac()ndo_set_vf_vlan()

在内核 2.6.29 之前,有一个名为set_multicast_list()的用于添加组播地址的回调,它被dev_set_rx_mode()方法所取代。每当单播或多播地址列表或网络接口标志被更新时,就主要调用dev_set_rx_mode()回调。

  • struct ethtool_ops *ethtool_ops

ethtool_ops结构包括几个回调的指针,用于处理卸载、获取和设置各种设备设置、读取寄存器、获取统计数据、读取 RX 流哈希间接表、WakeOnLAN 参数等等。如果网络驱动程序没有初始化ethtool_ops对象,网络核心提供一个名为default_ethtool_ops的默认空ethtool_ops对象。ethtool_ops的管理在net/core/ethtool.c完成。

助手方法:

  • SET_ETHTOOL_OPS (netdev,OPS):为指定的net_device设置指定的ethtool_ops的宏。

您可以通过运行ethtool –k <deviceName>来查看网络接口设备的卸载参数。您可以通过运行ethtool –K <deviceName> offloadParameter off/on来设置网络接口设备的一些卸载参数。参见man 8 ethtool

  • const struct header_ops *header_ops

header_ops struct 包括创建第 2 层报头、解析它、重建它等等的回调函数。对于以太网,它是在net/ethernet/eth.c中定义的eth_header_ops

  • unsigned int flags

您可以从用户空间看到的网络设备的接口标志。以下是一些标志(完整列表见include/uapi/linux/if.h):

  • 当接口状态从关闭变为打开时,IFF_UP 标志被置位。
  • 当接口处于混杂模式(接收所有数据包)时,IFF_PROMISC 置位。当运行类似wiresharktcpdump的嗅探器时,网络接口处于混杂模式。
  • 为环回设备设置 IFF_LOOPBACK。
  • IFF_NOARP 是为不使用 ARP 协议的设备设置的。例如,在隧道设备中设置 IFF_NOARP(例如,参见ipip_tunnel_setup()方法中的net/ipv4/ipip.c)。
  • IFF_POINTOPOINT 是为 PPP 设备设置的。例如,参见ppp_setup()方法、drivers/net/ppp/ppp_generic.c
  • IFF_MASTER 是为主设备设置的。例如,参见drivers/net/bonding/bond_main.c中的bond_setup()方法。
  • IFF_LIVE_ADDR_CHANGE 标志表示设备在运行时支持硬件地址修改。参见net/ethernet/eth.c中的eth_mac_addr()方法。
  • 当网络驱动程序处理单播地址过滤时,IFF_UNICAST_FLT 标志被设置。
  • IFF_BONDING 是为绑定主设备或绑定从设备设置的。绑定驱动程序提供了一种将多个网络接口聚合成单个逻辑接口的方法。
  • IFF_TEAM_PORT 是为用作组端口的设备设置的。分组驱动程序是一个负载平衡网络软件驱动程序,旨在取代绑定驱动程序。
  • IFF_MACVLAN_PORT 是为用作 MACVLAN 端口的设备设置的。
  • IFF_EBRIDGE 是为以太网桥接设备设置的。

sysfs通过/sys/class/net/<devName>/flags导出flags字段。

其中一些标志可以由用户空间工具设置。例如,ifconfig <deviceName> -arp会设置 IFF_NOARP 网络接口标志,ifconfig <deviceName> arp会清除 IFF_NOARP 标志。注意,你可以用iproute2 ip命令:ip link set dev <deviceName> arp onip link set dev <deviceName> arp off做同样的事情。

  • unsigned int priv_flags

接口标志,从用户空间看不到。例如,桥接口的 IFF_EBRIDGE 或绑定接口的 IFF_BONDING 或支持发送自定义 FCS 的接口的 IFF_SUPP_NOFCS。

助手方法:

  • netif_supports_nofcs():如果在指定设备的priv_flags中设置了 IFF_SUPP_NOFCS,则返回true
  • is_vlan_dev(struct net_device *dev):如果在指定网络设备的priv_flags中设置了 IFF_802_1Q_VLAN 标志,则返回 1。
  • unsigned short gflags

全局标志(作为遗产保存)。

  • unsigned short padded

通过alloc_netdev()方法添加了多少填充。

  • unsigned char operstate

RFC 2863 操作状态。

  • unsigned char link_mode

将策略映射到 operstate。

  • unsigned int mtu

网络接口 MTU(最大传输单位)值。设备可以处理的最大帧尺寸。RFC 791 将 68 设置为最小 MTU。每个协议都有自己的 MTU。以太网的默认 MTU 是 1,500 字节。在ether_setup()方法net/ethernet/eth.c中设置。大小超过 1,500 字节,最高可达 9,000 字节的以太网数据包称为巨型帧。网络接口 MTU 由sysfs通过/sys/class/net/<devName>/mtu.导出

助手方法:

  • dev_set_mtu(struct net_device *dev, int new_mtu): Changes the MTU of the specified device to a new value, specified by the mtu parameter.

    例如,系统管理员可以通过以下方式之一将网络接口的 MTU 更改为 1,400:

    ifconfig <netDevice> mtu 1400
    ip link set <netDevice> mtu 1400
    echo 1400 > /sys/class/net/<netDevice>/mtu
    

    许多驱动程序实现了ndo_change_mtu()回调来改变 MTU,以执行驱动程序特定的所需动作(比如重置网卡)。

  • unsigned short type

网络接口硬件类型。例如,对于以太网,它是 ARPHRD_ETHER,并在net/ethernet/eth.cether_setup()中设置。对于 PPP 接口,为 ARPHRD_PPP,在drivers/net/ppp/ppp_generic.cppp_setup()方法中设置。该类型由sysfs通过/sys/class/net/<devName>/type输出。

  • unsigned short hard_header_len

硬件头长度。例如,以太网报头由 MAC 源地址、MAC 目的地址和类型组成。MAC 源地址和目的地址各为 6 个字节,类型为 2 个字节。因此以太网报头长度为 14 个字节。在ether_setup()方法net/ethernet/eth.c中,以太网报头长度被设置为 14 (ETH_HLEN)。ether_setup()方法负责初始化一些以太网设备的默认值,比如 hard header len、Tx queue len、MTU、type 等等。

  • unsigned char perm_addr[MAX_ADDR_LEN]

设备的永久硬件地址(MAC 地址)。

  • unsigned char addr_assign_type

硬件地址分配类型,可以是下列之一:

  • 网 _ ADDR _ 彼尔姆

  • 网 _ ADDR _ 随机

  • NET _ ADDR _ 被盗

  • NET_ADDR_SET

    默认情况下,MAC 地址是永久的(NET_ADDR_PERM)。如果 MAC 地址是用名为eth_hw_addr_random()的帮助器方法生成的,那么 MAC 地址的类型就是 NET_ADD_RANDOM。MAC 地址的类型存储在net_deviceaddr_assign_type成员中。同样在改变设备的 MAC 地址的时候,用eth_mac_addr(),用∾NET _ ADDR _ 随机(如果之前标记为 NET _ ADDR _ 随机)重置addr_assign_type。当注册网络设备时(通过register_netdevice()方法),如果addr_assign_type等于 NET_ADDR_PERM,dev->perm_addr被设置为dev->dev_addr。当您设置 MAC 地址时,您将addr_assign_type设置为 NET_ADDR_SET。这表示设备的 MAC 地址已经由dev_set_mac_address()方法设置。addr_assign_typ e 由sysfs通过/sys/class/net/<devName>/addr_assign_type输出。

  • unsigned char addr_len

以八位字节表示的硬件地址长度。对于以太网地址,它是 6 (ETH_ALEN)字节,在ether_setup()方法中设置。addr_lensysfs通过/sys/class/net/<deviceName>/addr_len出口。

  • unsigned char neigh_priv_len

neigh_alloc()法中使用了、net/core/neighbour.cneigh_priv_len仅在 ATM 代码中初始化(atm/clip.c)。

  • struct netdev_hw_addr_list uc

单播 MAC 地址列表,由dev_uc_init()方法初始化。以太网中有三种类型的数据包:单播、组播和广播。单播的目的地是一台机器,多播的目的地是一组机器,广播的目的地是局域网中的所有机器。

助手方法:

  • netdev_uc_empty(dev):如果指定设备的单播列表为空(其count字段为 0),则返回 1。

  • dev_uc_flush(struct net_device *dev):刷新指定网络设备的单播地址,并置零count

  • struct netdev_hw_addr_list mc

多播 MAC 地址列表,由dev_mc_init()方法初始化。

助手方法:

  • netdev_mc_empty(dev):如果指定设备的组播列表为空(其count字段为 0),则返回 1。
  • dev_mc_flush(struct net_device *dev):刷新指定网络设备的组播地址,并将计数字段置零。
  • unsigned int promiscuity

网络接口卡被告知以混杂模式工作的次数计数器。在混杂模式下,MAC 目的地址不同于接口 MAC 地址的数据包不会被拒绝。promiscuity计数器用于例如启用一个以上的嗅探客户端;所以当打开一些嗅探客户端(比如wireshark)的时候,你每打开一个客户端,这个计数器就加 1,关闭那个客户端就会递减滥交计数器。当嗅探客户端的最后一个实例关闭时,promiscuity将被设置为 0,设备将退出混杂模式。它也用于桥接子系统,因为桥接接口需要在混杂模式下工作。因此,当添加网桥接口时,网络接口卡被设置为以混杂模式工作。参见br_add_if()net/bridge/br_if.c中对dev_set_promiscuity()方法的调用。

助手方法:

  • dev_set_promiscuity(struct net_device *dev, int inc):按照指定的增量递增/递减指定网络设备的promiscuity计数器。dev_set_promiscuity()方法可以得到一个正增量或负增量参数。只要混杂计数器保持大于零,接口就保持混杂模式。一旦达到零,该装置就回复到正常的过滤操作。因为混杂是一个整数,dev_set_promiscuity()方法考虑了整数的循环溢出,这意味着它处理当promiscuity计数器达到无符号整数所能达到的最大正值时递增的情况。

  • unsigned int allmulti

    网络设备的allmulti计数器启用或禁用 allmulticast 模式。选中时,接口将接收网络上的所有多播数据包。您可以通过ifconfig eth0 allmulti设置网络设备在 allmulticast 模式下工作。你通过ifconfig eth0 –allmulti禁用allmulti标志。

    也可以使用ip命令启用/禁用 allmulticast 模式:

    ip link set p2p1 allmulticast on
    ip link set p2p1 allmulticast off
    

    您还可以通过检查由ip命令显示的标志来查看 allmulticast 状态:

    ip addr show
    flags=4610<BROADCAST,ALLMULTI,MULTICAST>  mtu 1500
    

    助手方法:

  • dev_set_allmulti(struct net_device *dev, int inc):按照指定的增量(可以是正整数,也可以是负整数)递增/递减指定网络设备的allmulti计数器。dev_set_allmulti()方法还在设置 allmulticast 模式时设置网络设备的 IFF_ALLMULTI 标志,并在禁用 allmulticast 模式时删除该标志。

接下来的三个字段是特定于协议的指针:

  • struct in_device __rcu *ip_ptr

inetdev_init()net/ipv4/devinet.c中,该指针被分配给指向struct in_device的指针,其代表 IPv4 特定数据。

  • struct inet6_dev __rcu *ip6_ptr

ipv6_add_dev()net/ipv6/addrconf.c中,该指针被分配给指向代表 IPv6 特定数据的struct inet6_dev的指针。

  • struct wireless_dev *ieee80211_ptr

这是无线设备的指针,在ieee80211_if_add()方法net/mac80211/iface.c中分配。

  • unsigned long last_rx

上次接收的时间。除非确实需要,否则不应由网络设备驱动程序设置。例如,在绑定驱动程序代码中使用。

  • struct list_head dev_list

网络设备的全局列表。当注册网络设备时,使用list_netdevice()方法插入列表。当网络设备未注册时,使用unlist_netdevice()方法从列表中删除。

  • struct list_head napi_list

NAPI 代表新 API,这是一种网络驱动程序在高流量下工作在轮询模式而不是中断驱动模式的技术。事实证明,在高流量下使用 NAPI 可以提高性能。当使用 NAPI 时,网络堆栈会缓冲数据包,并不时触发驱动程序使用netif_napi_add()方法注册的轮询方法,而不是为每个收到的数据包中断。当使用轮询模式时,驱动程序开始以中断驱动模式工作。当收到的第一个包发生中断时,您将到达中断服务程序(ISR),这是用request_irq()注册的方法。然后驱动程序禁用中断并通知 NAPI 取得控制权,通常是通过从 ISR 调用__napi_schedule()方法。例如,参见drivers/net/ethernet/ti/cpsw中的cpsw_interrupt()方法。

当流量较低时,网络驱动程序切换到中断驱动模式下工作。现在,大多数网络司机都和 NAPI 一起工作。napi_list对象是napi_struct对象的列表;netif_napi_add()方法将napi_struct对象添加到这个列表中,netif_napi_del()方法从这个列表中删除napi_struct对象。当调用netif_napi_add()方法时,驱动程序应该指定它的轮询方法和一个权重参数。权重是对每个轮询周期中驱动程序将传递到堆栈的数据包数量的限制。建议使用 64 的重量。如果一个驱动程序试图调用权重大于 64 的netif_napi_add()(NAPI _ 投票 _ 权重),会有一个内核错误消息。NAPI _ 投票 _ 权重在include/linux/netdevice.h中定义。

网络驱动应该调用napi_enable()来启用 NAPI 调度。通常这是在net_device_ops对象的ndo_open()回调中完成的。网络驱动应该调用napi_disable()来禁用 NAPI 调度。通常这是在net_device_opsndo_stop()回调中完成的。NAPI 是使用softirqs实现的。这个softirq处理程序是net_rx_action()方法,通过net/core/dev.c中的net_dev_init()方法调用open_softirq(NET_RX_SOFTIRQ, net_rx_action)来注册。net_rx_action()方法调用向 NAPI 注册的网络驱动程序的轮询方法。默认情况下,一个轮询周期(NAPI 轮询)中的最大数据包数量(取自注册轮询的所有接口)为 300。它是在net/core/dev.c中定义的netdev_budget变量,可以通过procfs条目/proc/sys/net/core/netdev_budget进行修改。过去,您可以通过将值写入procfs条目来更改每台设备的重量,但目前,/sys/class/net/<device>/weight sysfs条目已被删除。参见Documentation/sysctl/net.txt。我还应该提到的是,napi_complete()方法从轮询列表中删除了一个设备。当一个网络驱动程序想要回到中断驱动模式下工作时,它应该调用napi_complete()方法将自己从轮询列表中删除。

  • struct list_head unreg_list

未注册的网络设备列表。设备在取消注册时会添加到此列表中。

  • unsigned char *dev_addr

网络接口的 MAC 地址。有时你想分配一个随机的 MAC 地址。你可以通过调用eth_hw_addr_random()方法来实现,该方法也将addr_assign_type设置为 NET _ ADDR _ 随机。

sysfs通过/sys/class/net/<devName>/address导出dev_addr字段。

你可以用用户空间工具改变dev_addr,比如ifconfig或者iproute2ip

助手方法:通常在以太网地址上,特别是在网络设备的dev_addr字段上,您会多次调用以下助手方法:

  • is_zero_ether_addr(const u8 *addr):如果地址全为零,则返回true

  • is_multicast_ether_addr(const u8 *addr):如果地址是组播地址,则返回true。根据定义,广播地址也是组播地址。

  • is_valid_ether_addr (const u8 *addr):如果指定的 MAC 地址不是 00:00:00:00:00:00:00,不是组播地址,也不是广播地址(FF:FF:FF:FF:FF:FF)。

  • struct netdev_hw_addr_list dev_addrs

设备硬件地址列表。

  • unsigned char broadcast[MAX_ADDR_LEN]

硬件广播地址。对于以太网设备,广播地址在ether_setup()方法net/ethernet/eth.c中初始化为 0XFFFFFF。广播地址由sysfs通过/sys/class/net/<devName>/broadcast输出。

  • struct kset *queues_kset

一个kset是一组特定类型的kobjects,属于一个特定的子系统。

kobject结构是设备模型的基本类型。Tx 队列由结构netdev_queue表示,Rx 队列由struct netdev_rx_queue表示。他们每个人都拿着一个kobject指针。queues_kset对象是 Tx 队列和 Rx 队列的所有kobjects的集合。每个 Rx 队列都有sysfs条目/sys/class/net/<deviceName>/queues/<rx-queueNumber>,每个 Tx 队列都有sysfs条目/sys/class/net/<deviceName>/queues/<tx-queueNumber>。在net/core/net-sysfs.c中,这些条目分别用rx_queue_add_kobject()方法和netdev_queue_add_kobject()方法添加。有关kobject和设备型号的更多信息,请参见Documentation/kobject.txt

  • struct netdev_rx_queue *_rx

Rx 队列(netdev_rx_queue对象)的数组,由netif_alloc_rx_queues()方法初始化。在get_rps_cpu()方法中确定要使用的接收队列。在前面的sk_buff部分的rxhash字段的描述中可以看到更多关于 RPS 的信息。

  • unsigned int num_rx_queues

register_netdev()方法中分配的接收队列的数量。

  • unsigned int real_num_rx_queues

设备中当前活动的接收队列数量。

助手方法:

  • netif_set_real_num_rx_queues (struct net_device *dev, unsigned int rxq):根据指定的接收队列数量,设置指定设备使用的接收队列实际数量。更新相关的sysfs条目(/sys/class/net/<devName>/queues/*)(仅在设备状态为 NETREG_REGISTERED 或 NETREG_UNREGISTERING 的情况下)。注意alloc_netdev_mq()num_rx_queuesreal_num_rx_queuesnum_tx_queuesreal_num_tx_queues初始化为相同的值。添加设备时,可以使用ip link设置发送队列和接收队列的数量。例如,如果您想要创建一个具有 6 个 Tx 队列和 7 个 Rx 队列的 VLAN 设备,您可以运行以下命令:

    ip link add link p2p1 name p2p1.1 numtxqueues 6 numrxqueues 7 type vlan id 8
    
    
  • rx_handler_func_t __rcu *rx_handler

助手方法:

  • netdev_rx_handler_register(struct net_device *dev, rx_handler_func_t *rx_handler  void *rx_handler_data)

通过调用netdev_rx_handler_register()方法来设置rx_handler回调。例如,它用于绑定、群组、openvswitch、macvlan 和桥接设备。

  • netdev_rx_handler_unregister(struct net_device *dev):注销指定网络设备的接收处理程序。

  • void __rcu *rx_handler_data

当一个非空值被传递给netdev_rx_handler_register()方法时,rx_handler_data字段也被netdev_rx_handler_register()方法设置。

  • struct netdev_queue __rcu *ingress_queue

助手方法:

  • struct netdev_queue *dev_ingress_queue(struct net_device *dev):返回指定net_device ( include/linux/rtnetlink.h)的ingress_queue

  • struct netdev_queue *_tx

Tx 队列(netdev_queue对象)的数组,由netif_alloc_netdev_queues()方法初始化。

助手方法:

  • netdev_get_tx_queue(const struct net_device *dev,unsigned int index):返回 Tx 队列(netdev_queue对象),在指定的index返回指定网络设备的_tx数组的一个元素。

  • unsigned int num_tx_queues

通过alloc_netdev_mq()方法分配的发送队列数量。

  • unsigned int real_num_tx_queues

设备中当前活动的发送队列数量。

助手方法:

  • netif_set_real_num_tx_queues(struct net_device *dev, unsigned int txq):设置实际使用的发送队列数量。

  • struct Qdisc *qdisc

    每个设备维护一个名为qdisc的待传输数据包队列。Qdisc (排队规则)层实现了 Linux 内核流量管理。默认的qdiscpfifo_fast。您可以使用iproute2包的流量控制工具tc设置不同的qdisc。您可以使用ip命令查看网络设备的qdisc:

    ip addr show <deviceName>
    

    比如跑步

    ip addr show eth1
    

    可以给:

    2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:e0:4c:53:44:58 brd ff:ff:ff:ff:ff:ff
    inet 192.168.2.200/24 brd 192.168.2.255 scope global eth1
    inet6 fe80::2e0:4cff:fe53:4458/64 scope link
    valid_lft forever preferred_lft forever
    

    在本例中,您可以看到使用了pfifo_fast中的一个qdisc,这是默认设置。

  • unsigned long tx_queue_len

    每个队列允许的最大数据包数。每个硬件层都有自己的tx_queue_len默认值。对于以太网设备,tx_queue_len默认设置为 1000(参见ether_setup()方法)。对于 FDDI,tx_queue_len默认设置为 100(参见net/802/fddi.c中的fddi_setup()方法)。

    对于虚拟设备(如 VLAN 设备),tx_queue_len字段设置为 0,因为数据包的实际传输是由这些虚拟设备所基于的真实设备完成的。您可以使用命令ifconfig(该选项称为txqueuelen)或使用命令ip link show(称为qlen)来设置设备的发送队列长度,例如:

    ifconfig  p2p1 txqueuelen 900
    ip link set txqueuelen 950 dev p2p1
    

    Tx 队列长度通过以下sysfs条目导出:/sys/class/net/<deviceName>/tx_queue_len

  • unsigned long trans_start

最后一次传输的时间(在jiffies)。

  • int watchdog_timeo

看门狗是一个定时器,当网络接口空闲并且在某个指定的超时间隔内没有执行传输时,它将调用回调。在这种情况下,驱动程序通常会定义一个看门狗回调函数来重置网络接口。net_device_opsndo_tx_timeout()回调作为看门狗回调。watchdog_timeo字段代表看门狗使用的超时。见dev_watchdog()法,net/sched/sch_generic.c

  • int __percpu *pcpu_refcnt

每 CPU 网络设备引用计数器。

助手方法:

  • dev_put(struct net_device *dev):递减参考计数。

  • dev_hold(struct net_device *dev):增加参考计数。

  • struct hlist_node index_hlist

这是网络设备的哈希表,由网络设备索引(ifindex字段)索引。通过dev_get_by_index()方法在该表中进行查找。通过list_netdevice()方法插入该表,通过unlist_netdevice()方法从该表中删除。

  • enum {...} reg_state

代表网络设备的各种注册状态的enum

可能的值:

  • NETREG _ UNINITIALIZED:分配设备内存时,在alloc_netdev_mqs()方法中。

  • NETREG_REGISTERED:当net_device被注册时,在register_netdevice()方法中。

  • NETREG_UNREGISTERING:当注销一个设备时,在rollback_registered_many()方法中。

  • NETREG_UNREGISTERED:网络设备被取消注册,但它还没有被释放。

  • NETREG_RELEASED:网络设备处于释放网络设备已分配内存的最后阶段,在free_netdev()方法中。

  • NETREG_DUMMY:在init_dummy_netdev()方法中的dummy设备中使用。参见drivers/net/dummy.c

  • bool dismantle

布尔标志,表示设备处于拆除阶段,这意味着它将被释放。

  • enum {...} rtnl_link_state

这是一个可以有两个值的enum,代表创建新链接的两个阶段:

  • RTNL_LINK_INITIALIZE:正在进行的状态,创建链接时仍未完成。
  • RTNL_LINK_INITIALIZING:工作完成时的最终状态。

参见net/core/rtnetlink.c中的rtnl_newlink()方法。

  • void (*destructor)(struct net_device *dev)

netdev_run_todo()方法中,当注销网络设备时调用这个析构函数回调。它使网络设备能够执行注销所需的额外任务。例如,回送设备析构函数回调loopback_dev_free(),调用free_percpu()来释放它的统计对象和free_netdev()。同样,组设备析构函数回调team_destructor()也调用free_percpu()来释放它的统计对象和free_netdev()。还有许多其他的网络设备驱动程序定义了一个destructor回调。

  • struct net *nd_net

    此网络设备所在的网络命名空间。网络名称空间支持是在 2.6.29 内核中添加的。这些特性提供了进程虚拟化,与 KVM 和 Xen 等其他虚拟化解决方案相比,这被认为是轻量级的。Linux 内核目前支持六种名称空间。为了支持网络名称空间,添加了一个名为net的结构。此结构表示网络命名空间。流程描述符(task_struct)通过一个名为nsproxy的新成员来处理网络名称空间和其他名称空间。这个nsproxy包括一个名为net_ns的网络名称空间对象,以及以下名称空间的四个其他名称空间对象:pid 名称空间、mount 名称空间、uts 名称空间和 ipc 名称空间;第六个名称空间,即用户名称空间,保存在 struct cred(凭证对象)中,它是流程描述符task_struct的成员。

    网络命名空间提供了一种分区和隔离机制,使一个进程或一组进程能够拥有自己的完整网络堆栈的私有视图。默认情况下,启动后所有网络接口都属于默认的网络名称空间init_net。您可以使用来自iproute2包的ip命令或util-linuxunshare命令,或者通过编写您自己的用户空间应用并调用带有 CLONE_NEWNET 标志的unshare()clone()系统调用,用用户空间工具创建一个网络名称空间。此外,您还可以通过调用setns()系统调用来更改进程的网络名称空间。这个setns()系统调用和unshare()系统调用是专门为支持名称空间而添加的。setns()系统调用可以将任何类型的现有名称空间(网络名称空间、pid 名称空间、挂载名称空间等)附加到调用进程。您需要 CAP_SYS_ADMIN 特权来为所有名称空间调用set_ns(),除了用户名称空间。参见man 2 setns

    在给定时刻,一个网络设备恰好属于一个网络命名空间。并且网络套接字在给定时刻恰好属于一个网络命名空间。名称空间没有名称,但是它们有一个惟一的索引节点来标识它们。这个惟一的索引节点是在创建名称空间时生成的,并且可以通过读取一个procfs条目来读取(命令ls –al /proc/<pid>/ns/显示了一个进程的所有惟一的索引节点号符号链接——您也可以使用readlink命令来读取这些符号链接)。

    例如,使用ip命令,创建一个名为ns1的新名称空间是这样完成的:

    ip netns add myns1
    

    每个新创建的网络命名空间只包括环回设备,不包括套接字。从运行在该名称空间中的进程(如 shell)创建的每个设备(如桥设备或 VLAN 设备)都属于该名称空间。

    使用以下命令可以删除命名空间:

    ip netns del myns1
    

    image 注意删除一个命名空间后,其所有的物理网络设备都被移动到默认的网络命名空间。本地设备(设置了 NETIF_F_NETNS_LOCAL 标志的命名空间本地设备,如 PPP 设备或 VXLAN 设备)不会移动到默认网络命名空间,但会被删除。

    使用以下命令可以显示系统上所有网络命名空间的列表:

    ip netns list
    

    将 p2p1 接口分配给myns1网络名称空间是通过以下命令完成的:

    ip link set p2p1 netns myns1
    

    myns1中打开 Shell 是这样完成的:

    ip netns exec myns1 bash
    

    使用unshare实用程序,创建一个新的名称空间并在其中启动一个 bash shell 是这样完成的:

    unshare --net bash
    

    两个网络名称空间可以通过使用一个特殊的虚拟以太网驱动程序veth. ( drivers/net/veth.c)进行通信。

    助手方法:

  • dev_change_net_namespace(struct net_device *dev, struct net *net, const char *pat):将网络设备移动到不同的网络名称空间,由net参数指定。不允许本地设备(设置了 NETIF_F_NETNS_LOCAL 功能的设备)更改其命名空间。对于这种类型的设备,此方法返回-EINVAL。当pat参数不为空时,它是一个名称模式,如果当前的设备名称已经在目标网络名称空间中被使用,就尝试使用这个模式。该方法还发送一个 KOBJ_REMOVE uevent,用于从sysfs中删除旧的名称空间条目,并发送一个 KOBJ_ADD uevent,用于将sysfs条目添加到新的名称空间中。这是通过调用指定相应 uevent 的kobject_uevent()方法来完成的。

  • dev_net(const struct net_device *dev):返回指定网络设备的网络名称空间。

  • dev_net_set(struct net_device *dev, struct net *net):递减指定设备的nd_net(名称空间对象)的引用计数,并为其分配指定的网络名称空间。

以下四个字段是联合中的成员:

  • struct pcpu_lstats __percpu *lstats

    环回网络设备统计。

  • struct pcpu_tstats __percpu *tstats

    隧道统计。

  • struct pcpu_dstats __percpu *dstats

    虚拟网络设备统计。

  • struct pcpu_vstats __percpu *vstats

    虚拟以太网统计。

  • struct device dev

与网络设备相关联的device对象。Linux 内核中的每个设备都与一个设备对象相关联,该设备对象是device结构的一个实例。关于device结构的更多信息,我建议你阅读 Linux 设备驱动第三版(O'Reilly,2005)的第十四章中的“设备”部分和Documentation/driver-model/overview.txt

助手方法:

  • to_net_dev(d):返回包含指定设备作为其设备对象的net_device对象。

  • SET_NETDEV_DEV (net, pdev): Sets the parent of the dev member of the specified network device to be that specified device (the second argument, pdev).

    对于虚拟设备,不要调用 SET_NETDEV_DEV()宏。因此,这些虚拟设备的条目在/sys/devices/virtual/net下创建。

    应该在调用register_netdev()方法之前调用 SET_NETDEV_DEV()宏。

  • SET_NETDEV_DEVTYPE(net, devtype): Sets the type of the dev member of the specified network device to be the specified type. The type is a device_type object.

    例如,SET_NETDEV_DEVTYPE()用于br_dev_setup()方法中,用于net/bridge/br_device.c:

    static struct device_type br_type = {
    .name = "bridge",
    };
    
    void br_dev_setup(struct net_device *dev)
    {
        . . .
        SET_NETDEV_DEVTYPE(dev, &br_type);
        . . .
    
    }
    

    使用udevadm工具(udev管理工具),您可以找到设备类型,例如,一个名为mybr : 的桥接设备

    udevadm info -q all -p /sys/devices/virtual/net/mybr
    
    P: /devices/virtual/net/mybr
    
    E: DEVPATH=/devices/virtual/net/mybr
    
    E: DEVTYPE=bridge
    
    E: ID_MM_CANDIDATE=1
    
    E: IFINDEX=7
    
    E: INTERFACE=mybr
    
    E: SUBSYSTEM=net
    
    
  • const struct attribute_group *sysfs_groups[4]

联网使用sysfs

  • struct rtnl_link_ops *rtnl_link_ops

rtnetlink 链接操作对象。它由处理网络设备的各种回调组成,例如:

  • newlink()用于配置和注册新设备。
  • changelink()用于改变现有设备的参数。
  • dellink()用于移除设备。
  • get_num_tx_queues()用于获取发送队列的数量。
  • get_num_rx_queues()用于获取接收队列的数量。

分别使用rtnl_link_register()方法和rtnl_link_unregister()方法注册和注销rtnl_link_ops对象。

  • unsigned int gso_max_size

助手方法:

  • netif_set_gso_max_size(struct net_device *dev, unsigned int size):为指定的网络设备设置指定的gso_max_size

  • u8 num_tc

网络设备中流量类别的数量。

助手方法:

  • netdev_set_num_tc(struct net_device *dev, u8 num_tc):设置指定网络设备的num_tc(num_tc的最大值可以是 TC_MAX_QUEUE,为 16)。

  • int netdev_get_num_tc(struct net_device *dev):返回指定网络设备的num_tc值。

  • struct netdev_tc_txq tc_to_txq[TC_MAX_QUEUE]

  • u8 prio_tc_map[TC_BITMASK + 1];

  • struct netprio_map __rcu *priomap

网络优先级cgroup模块提供了设置网络流量优先级的接口。cgroups 层是一个 Linux 内核层,支持进程资源管理和进程隔离。它支持将一个或多个任务分配给系统资源,如网络资源、内存资源、CPU 资源等。cgroups 层实现了一个虚拟文件系统(VFS) ,并由文件系统操作管理,如安装/卸载、创建文件和目录、写入 cgroup VFS 控制文件等等。cgroup 项目由谷歌的开发人员(Paul Manage、Rohit Seth 和其他人)于 2005 年启动。一些项目是基于 cgroups 用法的,比如systemdlxc (Linux 容器)。Google 有自己的容器实现,基于 cgroups。cgroup 实现和名称空间实现之间没有关系。过去,cgroups 中有一个名称空间控制器,但它被删除了。没有为 cgroup 实现添加新的系统调用,c group 代码的添加对于性能来说并不重要。有两个联网 cgroups 模块:net_prionet_cls。这两个cgroup模块比较短,比较简单。

使用netprio cgroup模块设置网络流量的优先级是通过向cgroup控制文件 /sys/fs/cgroup/net_prio/<group>/net_prio.ifpriomap写入一个条目来完成的。该条目的格式为“设备名称优先级”确实,应用可以通过使用 SO_PRIORITY 的setsockopt()系统调用来设置其流量的优先级,但这并不总是可行的。有时你不能改变某些应用的代码。此外,您希望让系统管理员根据站点特定的设置来决定优先级。当使用带有 SO_PRIORITY 的setsockopt()系统调用不可行时,netprio内核模块是一个解决方案。netprio模块还导出另一个/sys/fs/cgroup/netprio条目net_prio.prioidxnet_prio.prioidx条目是一个只读文件,包含一个惟一的整数值,内核使用它作为这个 cgroup 的内部表示。

netprionet/core/netprio_cgroup.c中实现。

net_clsnet/sched/cls_cgroup.c中实现。

网络分类器 cgroup 提供了一个用类标识符(classid)标记网络数据包的接口。创建一个net_cls cgroups 实例会创建一个net_cls.classid控制文件。该net_cls.classid值被初始化为 0。您可以使用iproute2的流量控制命令tc为该 classid 设置规则。

更多信息,参见Documentation/cgroups/net_cls.txt

  • struct phy_device *phydev

相关的 PHY 设备。phy_device是第 1 层(物理层)设备。它在include/linux/phy.h中定义。对于许多设备,PHY 流量控制参数如自动协商、速度或双工可以通过 PHY 设备用ethtool命令配置。更多信息见man 8 ethtool

  • int group

网络设备所属的组。默认情况下,它用 INIT_NETDEV_GROUP (0)初始化。组由sysfs通过/sys/class/net/<devName>/netdev_group导出。网络设备组过滤器用于例如net/netfilter/xt_devgroup.c的 netfilter。

助手方法:

  • void dev_set_group(struct net_device *dev, int new_group):将指定设备的组更改为指定组。

  • struct pm_qos_request pm_qos_req

    电源管理服务质量请求对象,在include/linux/pm_qos.h中定义。

    有关 PM QoS 的更多详细信息,请参见Documentation/power/pm_qos_interface.txt

    接下来我将描述 netdev_priv()方法和 alloc_netdev()宏,它们在网络驱动程序中被大量使用。

    netdev_priv(struct net_device *netdev)方法返回一个指向net_device末尾的指针。该区域由驱动程序使用,驱动程序定义了一个专用网络接口结构来存储专用数据。例如,在drivers/net/ethernet/intel/e1000e/netdev.c:

    static int e1000_open(struct net_device *netdev)
    {
        struct e1000_adapter *adapter = netdev_priv(netdev);
        . . .
    }
    

    netdev_priv()方法也用于软件设备,如 VLAN 设备。所以你有:

    static inline struct vlan_dev_priv *vlan_dev_priv(const struct net_device *dev)
    {
        return netdev_priv(dev);
    }
    
    (net/8021q/vlan.h)
    
    
  • alloc_netdev(sizeof_priv, name, setup)宏用于网络设备的分配和初始化。它实际上是围绕alloc_netdev_mqs()的包装器,有一个 Tx 队列和一个 Rx 队列。sizeof_priv是要分配空间的私有数据的大小。setup方法是一个回调来初始化网络设备。对于以太网设备,通常是ether_setup()

对于以太网设备,可以使用alloc_etherdev()alloc_etherdev_mq()宏,它们最终会调用alloc_etherdev_mqs()alloc_etherdev_mqs()也是alloc_netdev_mqs()的包装器,用ether_setup()作为设置回调方法。

  • 软件设备通常定义自己的设置方法。因此,在 PPP 中,您可以在drivers/net/ppp/ppp_generic.c中使用ppp_setup()方法,对于 VLAN,您可以在net/8021q/vlan.h中使用vlan_setup(struct net_device *dev)方法。

RDMA(远程 DMA)

以下部分描述了用于以下数据结构的 RDMA API:

  • RDMA 装置
  • 保护域
  • 扩展可靠连接(XRC)
  • 共享接收队列(SRQ)
  • 地址句柄(AH)
  • 多播组
  • 完成队列(CQ)
  • 队列对(QP)
  • 存储窗口(MW)
  • 存储区

RDMA 装置

以下方法与 RDMA 装置相关。

ib_register_client()方法

ib_register_client()方法注册一个想要使用 RDMA 堆栈的内核客户端。将为系统中当前存在的每个 RDMA 设备以及系统将检测到或移除的每个新设备(使用热插拔)调用指定的回调。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_register_client(struct ib_client *client);

  • client:描述注册属性的结构。

ib_client 结构:

设备注册属性由struct ib_client : 表示

struct ib_client {
        char  *name;
        void (*add)   (struct ib_device *);
        void (*remove)(struct ib_device *);

        struct list_head list;
};

  • name:要注册的内核模块的名称。
  • add:为系统中存在的每个 RDMA 设备和内核将检测到的每个新 RDMA 设备调用回调。
  • remove:为内核移除的每个 RDMA 设备调用的回调。

ib_unregister_client()方法

方法注销一个想要停止使用 RDMA 堆栈的内核模块。

void ib_unregister_client(struct ib_client *client);

  • device:描述注销属性的结构。
  • client:应该是调用ib_register_client()时使用的同一个对象。

ib _ get _ 客户端 _ 数据()方法

ib_get_client_data()方法使用ib_set_client_data()方法返回与 RDMA 设备相关联的客户端上下文。

void *ib_get_client_data(struct ib_device *device, struct ib_client *client);

  • device:从中获取客户端上下文的 RDMA 设备。
  • client:描述注册/注销属性的对象。

ib _ set _ 客户端 _ 数据()方法

ib_set_client_data()方法设置与 RDMA 设备相关联的客户端上下文。

void  ib_set_client_data(struct ib_device *device, struct ib_client *client,
             void *data);

  • device:用来设置客户端上下文的 RDMA 设备。
  • client:描述注册/注销属性的对象。
  • data:要关联的客户端上下文。

INIT _ IB _ 事件处理程序宏

INIT_IB_EVENT_HANDLER 宏为 RDMA 设备可能发生的异步事件初始化一个事件处理程序。这个宏应该在调用ib_register_event_handler()方法之前使用:

#define INIT_IB_EVENT_HANDLER(_ptr, _device, _handler)        \
    do {                            \
        (_ptr)->device  = _device;            \
        (_ptr)->handler = _handler;            \
        INIT_LIST_HEAD(&(_ptr)->list);            \
    } while (0)

  • _ptr:指向将提供给ib_register_event_handler()方法的事件处理程序的指针。
  • _device:RDMA 设备上下文;在事件发生时,回调将被调用。
  • _handler:每个异步事件都会调用的回调。

ib 注册事件处理程序()方法

ib_register_event_handler()方法注册一个 RDMA 事件,该事件将被每个处理程序异步事件调用。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_register_event_handler  (struct ib_event_handler *event_handler);

  • event_handler:用宏 INIT_IB_EVENT_HANDLER 初始化的事件处理程序。这个回调可能发生在中断上下文中。

ib_event_handler 结构:

RDMA 事件处理程序由struct ib_event_handler : 表示

struct ib_event_handler {
    struct ib_device *device;
    void            (*handler)(struct ib_event_handler *, struct ib_event *);
    struct list_head  list;
};

ib_event 结构

事件回调是用发生在 RDMA 设备上的新事件调用的。这个事件用struct ib_event来表示。

struct ib_event {
    struct ib_device    *device;
    union {
        struct ib_cq    *cq;
        struct ib_qp    *qp;
        struct ib_srq    *srq;
        u8        port_num;
    } element;
    enum ib_event_type    event;
};

  • device:发生异步事件的 RDMA 设备。

  • element.cq:如果这是一个 CQ 事件,异步事件发生的 CQ。

  • element.qp:如果这是一个 QP 事件,异步事件发生的 QP。

  • element.srq:如果这是一个 SRQ 事件,异步事件发生的 SRQ。

  • element.port_num:如果是端口事件,则异步事件发生的端口号。

  • event:发生的异步事件的类型。它可以是:

  • CQ 事件:CQ 事件。CQ 出错,将不再为其生成工作完成。

  • IB _ 事件 _ QP _ 致命:QP 事件。QP 出错,无法通过工作完成来报告错误。

  • QP 事件。传入的 RDMA 请求在目标 QP 中导致传输错误违规。

  • QP 事件。传入的 RDMA 请求导致目标 QP 中的请求错误违规。

  • QP 事件。发生了通信建立事件。当 QP 处于 RTR 状态时,它收到了传入的消息。

  • IB_EVENT_SQ_DRAINED: QP 事件。发送队列清空事件。QP 的发送队列已耗尽。

  • QP 事件。路径迁移已成功完成,主路径已更改。

  • IB_EVENT_PATH_MIG_ERR: QP 事件尝试执行路径迁移时出错。

  • IB _ 事件 _ 设备 _ 致命:设备事件。RDMA 设备出错。

  • IB_EVENT_PORT_ACTIVE:端口事件端口状态已变为活动状态。

  • IB_EVENT_PORT_ERR:端口事件端口状态为活动,现在不再活动。

  • IB_EVENT_LID_CHANGE:端口事件。端口的盖子被换了。

  • IB_EVENT_PKEY_CHANGE:端口事件。端口的 P_Key 表中的 P_Key 条目已被更改。

  • IB_EVENT_SM_CHANGE:端口事件。管理此端口的子网管理器已更改。

  • SRQ 事件:SRQ 事件。SRQ 出现错误。

  • IB _ EVENT _ SRQ _ LIMIT _ reated:SRQ 事件/SRQ 极限事件。SRQ 中的接收请求数低于请求的水位线。

  • IB _ EVENT _ QP _ LAST _ WQE _ 到达:QP 事件。来自 SRQ 的最后一个接收请求,它不会再消耗来自它的任何接收请求。

  • IB_EVENT_CLIENT_REREGISTER:端口事件。客户端应该向子网管理员的所有服务重新注册。

  • IB_EVENT_GID_CHANGE:端口事件。端口的 GID 表中的 GID 条目已被更改。

ib_unregister_event_handler()方法

方法注销一个 RDMA 事件处理程序。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_unregister_event_handler(struct ib_event_handler *event_handler);

  • event_handler:要注销的事件处理程序。它应该是用ib_register_event_handler()注册的同一个对象。

ib 查询设备()方法

方法 q 查询 RDMA 设备的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_device(struct ib_device *device,
        struct ib_device_attr *device_attr);

  • device:要查询的 RDMA 设备。
  • device_attr:指向将被填充的 RDMA 设备属性的结构的指针。

ib_device_attr 结构:

RDMA 设备属性由struct ib_device_attr : 表示

struct ib_device_attr {
    u64            fw_ver;
    __be64         sys_image_guid;
    u64            max_mr_size;
    u64            page_size_cap;
    u32            vendor_id;
    u32            vendor_part_id;
    u32            hw_ver;
    int            max_qp;
    int            max_qp_wr;
    int            device_cap_flags;
    int            max_sge;
    int            max_sge_rd;
    int            max_cq;
    int            max_cqe;
    int            max_mr;
    int            max_pd;
    int            max_qp_rd_atom;
    int            max_ee_rd_atom;
    int            max_res_rd_atom;
    int            max_qp_init_rd_atom;
    int            max_ee_init_rd_atom;
    enum ib_atomic_cap    atomic_cap;
    enum ib_atomic_cap    masked_atomic_cap;
    int            max_ee;
    int            max_rdd;
    int            max_mw;
    int            max_raw_ipv6_qp;
    int            max_raw_ethy_qp;
    int            max_mcast_grp;
    int            max_mcast_qp_attach;
    int            max_total_mcast_qp_attach;
    int            max_ah;
    int            max_fmr;
    int            max_map_per_fmr;
    int            max_srq;
    int            max_srq_wr;
    int            max_srq_sge;
    unsigned int   max_fast_reg_page_list_len;
    u16            max_pkeys;
    u8             local_ca_ack_delay;
};

  • fw_ver:代表 RDMA 设备固件版本的数字。可以评估为 ZZZZYYXX: Zs 是主版本号,Ys 是次版本号,Xs 是内部版本号。

  • sys_image_guid:系统映像 GUID:对于每个系统都有唯一的值。

  • max_mr_size:支持的最大 MR 尺寸。

  • page_size_cap:对所有支持的内存页面移位进行按位或运算。

  • vendor_id:IEEE 厂商 ID。

  • vendor_part_id:设备的零件 ID,由供应商提供。

  • hw_ver:设备的硬件版本,由供应商提供。

  • max_qp:qp 支持的最大数量。

  • max_qp_wr:每个非 RD QP 支持的最大工作请求数。

  • device_cap_flags:RDMA 设备支持的能力。它是掩码的按位“或”运算:

  • IB _ DEVICE _ RESIZE _ MAX _ WR:RDMA 设备支持 QP 中工作请求数量的调整。

  • IB _ DEVICE _ BAD _ PKEY _ CNTR:RDMA 设备支持计算坏 P_Keys 数量的能力。

  • IB _ DEVICE _ BAD _ QKEY _ CNTR:RDMA 设备支持计数坏的 Q _ Keys 的数量的能力。

  • IB _ DEVICE _ RAW _ MULTI:RDMA 设备支持原始数据包多播。

  • IB _ DEVICE _ AUTO _ PATH _ MIG:RDMA 设备支持自动路径迁移。

  • IB _ DEVICE _ CHANGE _ PHY _ PORT:RDMA 设备支持更改 QP 主端口号。

  • IB _ DEVICE _ UD _ AV _ PORT _ ENFORCE:RDMA 设备支持 UD QP 端口号和地址句柄的强制执行。

  • IB _ DEVICE _ CURR _ QP _ STATE _ MOD:RDMA 设备调用ib_modify_qp()时支持当前 QP 修改器。

  • IB 设备关闭端口:RDMA 设备支持端口关闭。

  • IB _ DEVICE _ INIT _ TYPE:RDMA 设备支持设置 InitType 和 InitTypeReply。

  • IB _ DEVICE _ PORT _ ACTIVE _ EVENT:RDMA 设备支持端口主动异步事件的生成。

  • IB _ DEVICE _ SYS _ IMAGE _ GUID:RDMA 设备支持系统映像 GUID。

  • IB _ DEVICE _ RC _ RNR _ NAK _ GEN:RDMA 设备支持 RC QPs 的 RNR-NAK 生成。

  • IB _ DEVICE _ SRQ _ RESIZE:RDMA 设备支持 SRQ 的大小调整。

  • IB_DEVICE_N_NOTIFY_CQ:当 CQ 中存在 N 个工作完成时,RDMA 设备支持通知。

  • IB _ DEVICE _ LOCAL _ DMA _ LKEY:RDMA 设备支持零 Stag(在 iWARP 中)和保留 LKEY(在 InfiniBand 中)。

  • IB_DEVICE_RESERVED:保留位。

  • IB _ DEVICE _ MEM _ WINDOW:RDMA 设备支持内存窗口。

  • IB _ DEVICE _ UD _ IP _ CSUM:RDMA 设备支持在输出的 UD IPoIB 消息上插入 UDP 和 TCP 校验和,并且可以验证输入消息的这些校验和的有效性。

  • IB _ DEVICE _ UD _ TSO:RDMA 设备支持 TCP 分段卸载。

  • IB _ DEVICE _ XRC:RDMA 设备支持扩展的可靠连接传输。

  • IB _ DEVICE _ MEM _ MGT _ 扩展:RDMA 设备支持内存管理扩展。

  • IB _ DEVICE _ BLOCK _ MULTICAST _ LOOPBACK:RDMA 设备支持阻塞式多播环回。

  • IB _ DEVICE _ MEM _ WINDOW _ TYPE _ 2A:RDMA 设备支持内存窗口类型 2A:与 QP 号码关联。

  • IB _ DEVICE _ MEM _ WINDOW _ TYPE _ 2B:RDMA 设备支持内存窗口类型 2B:与 QP 号码和 PD 关联。

  • max_sge:非 RD QP 中每个工作请求支持的最大分散/收集元素数。

  • max_sge_rd:RD QP 中每个工作请求支持的最大分散/收集元素数。

  • max_cq:支持的最大 CQ 数。

  • max_cqe:每个 CQ 支持的最大条目数。

  • max_mr:支持的最大 MRs 数量

  • max_pd:支持的最大 PD 数。

  • max_qp_rd_atom:可以发送到作为操作目标的 QP 的 RDMA 读取和原子操作的最大数量。

  • max_ee_rd_atom:可以发送到 EE 上下文作为操作目标的 RDMA 读取和原子操作的最大数量。

  • max_res_rd_atom:可发送到此 RDMA 设备作为操作目标的传入 RDMA 读取和原子操作的最大数量。

  • max_qp_init_rd_atom:可从作为操作发起者的 QP 发送的 RDMA 读取和原子操作的最大数量。

  • max_ee_init_rd_atom:作为操作的发起者,可以从 EE 上下文发送的 RDMA 读取和原子操作的最大数量。

  • atomic_cap:设备支持原子操作的能力。可以是:

  • IB _ ATOMIC _ NONE:RDMA 设备根本不能保证任何原子性。

  • IB _ ATOMIC _ HCA:RDMA 设备保证同一设备中 qp 之间的原子性。

  • IB _ ATOMIC _ GLOB:RDMA 设备保证该设备和任何其他组件之间的原子性。

  • masked_atomic_cap:设备支持屏蔽原子操作的能力。前面atomic_cap中描述的可能值。

  • max_ee:EE 上下文支持的最大数量。

  • max_rdd:rdd 支持的最大数量。

  • max_mw:支持的最大 MWs 数。

  • max_raw_ipv6_qp:原始 IPv6 数据报 qp 的最大支持数量。

  • max_raw_ethy_qp:支持的原始以太类型数据报 qp 的最大数量。

  • max_mcast_grp:支持的最大组播组数。

  • max_mcast_qp_attach:可以连接到每个多播组的 qp 的最大支持数量。

  • max_total_mcast_qp_attach:可以连接到任何多播组的 qp 总数的最大值。

  • max_ah:AHs 支持的最大数量。

  • max_fmr:fmr 支持的最大数量。

  • max_map_per_fmr:每个 FMR 允许的地图操作的最大支持数量。

  • max_srq:srq 支持的最大数量。

  • max_srq_wr:每个 SRQ 支持的最大工作请求数。

  • max_srq_sge:SRQ 中每个工作请求支持的最大分散/聚集元素数。

  • max_fast_reg_page_list_len:使用工作请求注册 FMR 时,可使用的最大页面列表数。

  • max_pkeys😛 _ Keys 支持的最大数量。

  • local_ca_ack_delay:本地确认延迟。该值指定本地设备接收消息和发送相关 ACK 或 NAK 之间的最大预期时间间隔。

ib_query_port()方法

ib_query_port()方法查询 RDMA 设备端口的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_port(struct ib_device *device,
         u8 port_num, struct ib_port_attr *port_attr);

  • device:要查询的 RDMA 设备。
  • port_num:要查询的端口号。
  • port_attr:指向将被填充的 RDMA 端口属性的结构的指针。

ib_port_attr 结构

RDMA 端口属性用struct ib_port_attr : 表示

struct ib_port_attr {
    enum ib_port_state    state;
    enum ib_mtu   max_mtu;
    enum ib_mtu   active_mtu;
    int           gid_tbl_len;
    u32           port_cap_flags;
    u32           max_msg_sz;
    u32           bad_pkey_cntr;
    u32           qkey_viol_cntr;
    u16           pkey_tbl_len;
    u16           lid;
    u16           sm_lid;
    u8            lmc;
    u8            max_vl_num;
    u8            sm_sl;
    u8            subnet_timeout;
    u8            init_type_reply;
    u8            active_width;
    u8            active_speed;
    u8            phys_state;
};

  • state:逻辑端口状态。可以是:

  • IB_PORT_NOP:保留值。

  • IB_PORT_DOWN:逻辑链路关闭。

  • IB_PORT_INIT:逻辑链路已初始化。物理链路已建立,但子网管理器尚未开始配置端口。

  • IB_PORT_ARMED:逻辑链路处于待命状态。物理链路已建立,但子网管理器已开始配置端口,但尚未完成。

  • IB_PORT_ACTIVE:逻辑链路处于活动状态。

  • IB_PORT_ACTIVE_DEFER:逻辑链路处于活动状态,但物理链路已关闭。链路试图从这种状态中恢复。

  • max_mtu:该端口支持的最大 MTU。可以是:

  • IB_MTU_256:256 字节。

  • IB_MTU_512:512 字节。

  • IB_MTU_1024:1,024 字节。

  • IB_MTU_2048:2,048 字节。

  • IB_MTU_4096:4,096 字节。

  • active_mtu:该端口配置的实际 MTU。可以作为前面提到的max_mtu

  • gid_tbl_len:端口的 GID 表中的条目数。

  • port_cap_flags:端口支持的能力。它是掩码的按位“或”运算:

  • IB_PORT_SM:表示管理子网的 SM 正在从该端口发送数据包。

  • IB_PORT_NOTICE_SUP:表示该端口支持通知。

  • IB_PORT_TRAP_SUP:表示该端口支持陷阱。

  • IB_PORT_OPT_IPD_SUP:表示该端口支持数据包间延迟可选值。

  • IB_PORT_AUTO_MIGR_SUP:表示该端口支持自动路径迁移。

  • IB_PORT_SL_MAP_SUP:表示该端口支持 SL 2 VL 映射表。

  • IB_PORT_MKEY_NVRAM:表示该端口支持将 M_Key 属性保存在非易失性 RAM 中。

  • IB_PORT_PKEY_NVRAM:表示该端口支持将 P_Key 表保存在非易失性 RAM 中。

  • IB_PORT_LED_INFO_SUP:表示该端口支持使用管理包打开和关闭 LED。

  • IB_PORT_SM_DISABLED:表示该端口中有一个不活动的 SM。

  • IB_PORT_SYS_IMAGE_GUID_SUP:表示端口支持系统映像 GUID。

  • IB _ PORT _ PKEY _ SW _ EXT _ PORT _ TRAP _ SUP:表示交换机管理端口上的 SMA 将监控每个交换机外部端口上的 P_Key 不匹配。

  • IB_PORT_EXTENDED_SPEEDS_SUP:表示端口支持扩展速度(FDR 和 EDR)。

  • IB_PORT_CM_SUP:表示该端口支持 CM。

  • IB_PORT_SNMP_TUNNEL_SUP:表示 SNMP 隧道代理正在侦听此端口。

  • IB_PORT_REINIT_SUP:表示该端口支持节点的重新初始化。

  • IB_PORT_DEVICE_MGMT_SUP:表示该端口支持设备管理。

  • IB_PORT_VENDOR_CLASS_SUP:表示特定于供应商的代理正在侦听此端口。

  • IB_PORT_DR_NOTICE_SUP:表示该端口支持直接路由通知。

  • IB_PORT_CAP_MASK_NOTICE_SUP:表示如果端口的port_cap_flags发生变化,该端口支持发送通知。

  • IB_PORT_BOOT_MGMT_SUP:表示引导管理器代理正在监听此端口。

  • IB_PORT_LINK_LATENCY_SUP:表示该端口支持链路往返延迟测量。

  • IB_PORT_CLIENT_REG_SUP:表示该端口能够生成 IB_EVENT_CLIENT_REREGISTER 异步事件。

  • max_msg_sz:此端口支持的最大消息大小。

  • bad_pkey_cntr:此端口收到的错误 P_Key 消息数的计数器。

  • qkey_viol_cntr:此端口接收的消息中 Q_Key 违例数的计数器。

  • pkey_tbl_len:端口的 P_Key 表中的条目数。

  • lid:端口的本地标识符(LID),由 SM 分配。

  • sm_lid:SM 的盖子。

  • lmc:该端口的盖罩。

  • max_vl_num:该端口支持的最大虚拟通道数。可以是:

  • 支持 1:1 VL:VL0

  • 支持 2:2 VL:VL0–VL1

  • 支持 3:4 VL:VL0–VL3

  • 支持 4: 8 个 VL:VL0–VL7

  • 支持 5: 15 个 VL:VL0–VL14

  • sm_sl:向 SM 发送消息时使用的 SL。

  • subnet_timeout:最大预期子网传播延迟。这个持续时间的计算是 4.094*2^subnet_timeout.

  • init_type_reply:SM 在将端口状态改为 IB_PORT_ARMED 或 IB_PORT_ACTIVE 之前配置的值,以指定所执行的初始化类型。

  • active_width:端口的活动宽度。可以是:

  • IB _ WIDTH _ 1X:1 的倍数。

  • IB _ WIDTH _ 4X:4 的倍数。

  • IB _ WIDTH _ 8X:8 的倍数。

  • IB _ WIDTH _ 12X:12 的倍数。

  • active_speed:端口的活动速度。可以是:

  • IB_SPEED_SDR:单数据速率(SDR):2.5 Gb/秒,8/10 位编码。

  • IB_SPEED_DDR:双倍数据速率(DDR):5 Gb/秒,8/10 位编码。

  • IB_SPEED_QDR:四倍数据速率(DDR):10 Gb/秒,8/10 位编码。

  • IB _ SPEED _ FD r10:14 10 数据速率(FD r10):10.3125 Gb/秒,64/66 位编码。

  • IB_SPEED_FDR:十四数据速率(FDR):14.0625 Gb/秒,64/66 位编码。

  • IB_SPEED_EDR:增强型数据速率(EDR):25.78125 Gb/秒。

  • phys_state:物理端口状态。这个值没有任何枚举。

rdma_port_get_link_layer()方法

rdma_port_get_link_layer()方法返回 RDMA 设备端口的链路层。它将返回以下值:

  • IB_LINK_LAYER_UNSPECIFIED:未指定的值,通常是指示这是 InfiniBand 链路层的传统值。

  • IB_LINK_LAYER_INFINIBAND:链路层是 INFINIBAND。

  • IB_LINK_LAYER_ETHERNET:链路层是以太网。这表明该端口支持聚合以太网 RDMA(RoCE)。

    enum rdma_link_layer rdma_port_get_link_layer(struct ib_device *device, u8 port_num);
    
    
  • device:要查询的 RDMA 设备。

  • port_num:要查询的端口号。

ib_query_gid()方法

ib_query_gid()方法查询 RDMA 设备端口的 GID 表。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_gid(struct ib_device *device, u8 port_num, int index, union ib_gid *gid);

  • device:要查询的 RDMA 设备。
  • port_num:要查询的端口号。
  • index:要查询的 GID 表中的索引。
  • gid:指向要填充的 GID 联合的指针。

ib_query_pkey()方法

ib_query_pkey()方法查询 RDMA 设备端口的 P_Key 表。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_pkey(struct ib_device *device,
        u8 port_num, u16 index, u16 *pkey);

  • device:要查询的 RDMA 设备。
  • port_num:要查询的端口号。
  • index:要查询的 P_Key 表中的索引。
  • pkey:指向要填充的 P_Key 的指针。

ib 修改设备()方法

ib_modify_device()方法修改 RDMA 设备的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_modify_device(struct ib_device *device,
            int device_modify_mask,
            struct ib_device_modify *device_modify);

  • device:要修改的 RDMA 装置。

  • device_modify_mask:要更改的设备属性。它是掩码的按位“或”运算:

  • IB _ DEVICE _ MODIFY _ SYS _ IMAGE _ GUID:修改系统映像 GUID。

  • IB _ 设备 _ 修改 _ 节点 _DESC:修改节点描述。

  • device_modify:要修改的 RDMA 属性,如前所述。

ib_device_modify 结构

RDMA 设备属性由struct ib_device_modify表示:

struct ib_device_modify {
    u64    sys_image_guid;
    char    node_desc[64];
};

  • sys_image_guid:系统镜像 GUID 的 64 位值。
  • node_desc:描述节点描述的空终止字符串。

ib_modify_port()方法

ib_modify_port()方法修改 RDMA 设备端口的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_modify_port(struct ib_device *device,
           u8 port_num, int port_modify_mask,
           struct ib_port_modify *port_modify);

  • device:要修改的 RDMA 装置。

  • port_num:要修改的端口号。

  • port_modify_mask:要更改的端口属性。它是掩码的按位“或”运算:

  • IB_PORT_SHUTDOWN:将端口状态移至 IB_PORT_DOWN。

  • IB_PORT_INIT_TYPE:设置端口 InitType 值。

  • IB_PORT_RESET_QKEY_CNTR:重置端口的 Q_Key 违例计数器。

  • port_modify:要修改的端口属性,如下一节所述。

ib_port_modify 结构:

RDMA 设备属性由struct ib_port_modify:表示

struct ib_port_modify {
    u32    set_port_cap_mask;
    u32    clr_port_cap_mask;
    u8    init_type;
};

  • set_port_cap_mask:要设置的端口能力位。
  • clr_port_cap_mask:要清除的端口能力位。
  • init_type:要设置的 InitType 值。

IB find GID()方法

ib_find_gid()方法在 GID 表中找到特定 GID 值所在的端口号和索引。如果成功,它将返回 0 或 errno 值,并说明失败的原因。

int ib_find_gid(struct ib_device *device, union ib_gid *gid,
        u8 *port_num, u16 *index);

  • device:要查询的 RDMA 设备。
  • gid:要搜索的 GID 的指针。
  • port_num:将用该 GID 所在的端口号填充。
  • index:将用该 GID 所在的 GID 表中的索引填充。

ib_find_pkey()方法

ib_find_pkey()方法在特定端口号的 P_Key 表中找到特定 P_Key 值存在的索引。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_find_pkey(struct ib_device *device,
         u8 port_num, u16 pkey, u16 *index);

  • device:要查询的 RDMA 设备。
  • port_num:搜索 P_Key 的端口号。
  • pkey:要搜索的 P_Key 值。
  • index:该 P_Key 所在的 P_Key 表中的索引。

rdma 节点获取传输()方法

rdma_node_get_transport()方法返回特定节点类型的 RDMA 传输类型。可用的传输类型可以是:

  • RDMA _ 运输 _IB:运输是无限带宽。
  • RDMA _ 运输 _IWARP:运输就是 IWARP。

rdma 节点获取传输()方法

enum rdma_transport_type
rdma_node_get_transport(enum rdma_node_type node_type) __attribute_const__;

  • node_type:节点类型。可以:
  • RDMA _ 节点 _IB_CA:节点类型是 InfiniBand 通道适配器。
  • RDMA 节点 IB 交换机:节点类型是 InfiniBand 交换机。
  • RDMA 节点 IB 路由器:节点类型是 InfiniBand 路由器。
  • RDMA 节点网卡:节点类型是 RDMA 网卡。

ib_mtu_to_int()方法

ib_mtu_to_int()方法返回 MTU 枚举的整数字节数。如果成功,它将返回正值;如果失败,它将返回-1。

static inline int ib_mtu_enum_to_int(enum ib_mtu mtu);

  • mtu:可以是 MTU 枚举,如前所述。

方法的作用是

ib_width_enum_to_int()方法返回 IB 端口枚举的整数倍宽度。如果成功,它将返回正值;如果失败,它将返回-1。

static inline int ib_width_enum_to_int(enum ib_port_width width);

  • width:可以是端口宽度枚举,如前所述。

IB rate to mult()方法

对于 IB 速率枚举,ib_rate_to_mult()方法以整数形式返回 2.5 Gbit/sec 基本速率的倍数。如果成功,它将返回正值;如果失败,它将返回-1。

int ib_rate_to_mult(enum ib_rate rate) __attribute_const__;

  • rate:要转换的速率枚举。可以是:

  • IB_RATE_PORT_CURRENT:当前端口速率。

  • IB _ RATE _ 2 _ 5 _ GBPS:2.5 Gbit/秒的速率。

  • IB _ RATE _ 5 _ GBPS:5 Gbit/秒的速率。

  • IB _ RATE _ 10 _ GBPS:10 Gbit/秒的速率。

  • IB _ RATE _ 20 _ GBPS:20 Gbit/秒的速率。

  • IB _ RATE _ 30 _ GBPS:30 Gbit/秒的速率。

  • IB _ RATE _ 40 _ GBPS:40 Gbit/秒的速率。

  • IB _ RATE _ 60 _ GBPS:60 Gbit/秒的速率。

  • IB _ RATE _ 80 _ GBPS:80 Gbit/秒的速率。

  • IB _ RATE _ 120 _ GBPS:120 Gbit/秒的速率。

  • IB _ RATE _ 14 _ GBPS:14 Gbit/秒的速率。

  • IB_RATE_56_GBPS:速率为 56 Gbit/秒。

  • IB _ RATE _ 112 _ GBPS:112 Gbit/秒的速率。

  • IB _ RATE _ 168 _ GBPS:168 Gbit/秒的速率。

  • IB _ RATE _ 25 _ GBPS:25 Gbit/秒的速率。

  • IB _ RATE _ 100 _ GBPS:100 Gbit/秒的速率。

  • IB _ RATE _ 200 _ GBPS:200 Gbit/秒的速率。

  • IB _ RATE _ 300 _ GBPS:300 Gbit/秒的速率。

ib_rate_to_mbps()方法

对于 IB 速率枚举,ib_rate_to_mbps()方法以整数形式返回 Mbit/sec 的数量。如果成功,它将返回正值;如果失败,它将返回-1。

int ib_rate_to_mbps(enum ib_rate rate) __attribute_const__;

  • rate:要转换的速率枚举,如前所述。

ib_rate_to_mbps()方法

ib_rate_to_mbps()方法返回 2.5 Gbit/sec 基本速率倍数的 IB 速率枚举。如果成功,它将返回正值;如果失败,它将返回-1。

enum ib_rate mult_to_ib_rate(int mult) __attribute_const__;

  • mult:要转换的汇率倍数,如前所述。

保护域(PD)

PD 是一种 RDMA 资源,将 qp 和 srq 与 MRs 相关联,将 AHs 与 qp 相关联。人们可以将 PD 视为一种颜色,例如:红色 MR 可以与红色 QP 搭配,而红色 AH 可以与红色 QP 搭配。使用绿色 AH 和红色 QP 会导致错误。

ib_alloc_pd()方法

ib_alloc_pd()方法分配一个 PD。如果成功,它将返回一个指向新分配的 PD 的指针,或者返回一个 ERR_PTR(),指明失败的原因。

struct ib_pd *ib_alloc_pd(struct ib_device *device);

  • device:PD 将要关联的 RDMA 设备。

ib_dealloc_pd()方法

ib_dealloc_pd() 方法释放一个 PD。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_dealloc_pd(struct ib_pd *pd);

  • pd:要解除分配的 PD。

扩展可靠连接(XRC)

XRC 是 IB 传输扩展,它在发送端为可靠连接的 qp 提供了比原始可靠传输更好的可伸缩性。使用 XRC 将减少两个特定内核之间的 QP 数量:当使用 RC QPs 时,对于每台机器中的每个内核,都有一个 QP。使用 XRC 时,每个主机中会有一个 XRC·QP。发送消息时,发送者需要指定将接收消息的远程 SRQ 号码。

ib_alloc_xrcd()方法

ib_alloc_xrcd()方法分配一个 XRC 域。如果成功,它将返回一个指向新创建的 XRC 域的指针,或者返回一个 ERR_PTR(),指明失败的原因。

struct ib_xrcd *ib_alloc_xrcd(struct ib_device *device);

  • device:将分配此 XRC 域的 RDMA 设备。

ib_dealloc_xrcd_cq()方法

方法释放一个 XRC 域。如果成功,它将返回 0,或者返回 errno 值,并说明失败的原因:

int ib_dealloc_xrcd(struct ib_xrcd *xrcd);

  • xrcd:要解除分配的 XRC 域。

共享接收队列(SRQ)

SRQ 是一种资源,有助于 RDMA 变得更加可扩展。代替在许多 qp 的接收队列中管理接收请求,可以在所有 qp 共享的单个接收队列中管理它们。这将消除 RC QPs 中的饥饿或不可靠传输类型中的数据包丢失,并有助于减少总的发送接收请求,从而减少消耗的内存。此外,与 QP 不同,SRQ 可以具有水印,以允许在 SRQ 中的 RRs 数量下降到指定值以下时进行通知。

ib_srq_attr 结构

SRQ 属性由struct ib_srq_attr : 表示

struct ib_srq_attr {
    u32    max_wr;
    u32    max_sge;
    u32    srq_limit;
};

  • max_wr:此 SRQ 可以持有的未完成 RR 的最大数量。
  • max_sge:SRQ 中每个 RR 可以容纳的最大分散/聚集元素数。
  • srq_limit:当 SRQ 中的 RRs 数量低于此值时,创建异步事件的水位线限制。

ib_create_srq 方法

ib_create_srq()方法创建一个 SRQ。如果成功,它将返回一个指向新创建的 SRQ 的指针,或者返回一个 ERR_PTR(),指明失败的原因:

struct ib_srq *ib_create_srq(struct ib_pd *pd, struct ib_srq_init_attr *srq_init_attr);

  • pd:与此 SRQ 相关联的 PD。
  • srq_init_attr:这个 SRQ 将被创建的属性。

ib_srq_init_attr 结构

创建的 SRQ 属性由struct ib_srq_init_attr表示:

struct ib_srq_init_attr {
    void              (*event_handler)(struct ib_event *, void *);
    void               *srq_context;
    struct ib_srq_attr    attr;
    enum ib_srq_type    srq_type;

    union {
        struct {
            struct ib_xrcd *xrcd;
            struct ib_cq   *cq;
        } xrc;
    } ext;
};

  • event_handler:一个指针,指向一个回调函数,当一个附属的异步事件指向 SRQ 时,这个回调函数将被调用。

  • srq_context:可以与 SRQ 关联的自定义上下文。

  • attr:SRQ 属性,如前所述。

  • srq_type:SRQ 的型号。可以是:

  • IB_SRQT_BASIC:用于常规 SRQ。

  • IB _ SRQT:去 XRC SRQ。

  • ext:如果srq_type是 IB_SRQT_XRC,指定这个 SRQ 关联的 XRC 域或者 CQ。

ib_modify_srq()方法

ib_modify_srq()方法修改 SRQ 的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_modify_srq(struct ib_srq *srq, struct ib_srq_attr *srq_attr, enum ib_srq_attr_mask srq_attr_mask);

  • srq:要修改的 SRQ。

  • srq_attr:SRQ 属性,如前所述。

  • srq_attr_mask:要更改的 SRQ 属性。它是掩码的按位“或”运算:

  • IB _ SRQ _ 马克斯 _WR:修改 SRQ 的 RR 数量(也就是调整 SRQ 的大小)。只有当设备支持 SRQ 调整大小时,才能做到这一点,即在设备标志中设置 IB_DEVICE_SRQ_RESIZE。

  • IB_SRQ_LIMIT:设置 SRQ 水印限制的值。

ib_query_srq()方法

ib_query_srq()方法查询当前的 SRQ 属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_srq(struct ib_srq *srq, struct ib_srq_attr *srq_attr);

  • srq:要查询的 SRQ。
  • srq_attr:SRQ 属性,如前所述。

ib_destroy_srq_)方法

方法销毁一个 SRQ。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_destroy_srq(struct ib_srq *srq);

  • 被摧毁的 SRQ。

ib_post_srq_recv()方法

ib_post_srq_recv()方法获取一个接收请求的链表,并将它们添加到 SRQ 中以备将来处理。每个接收请求都被认为是未完成的,直到在其处理后生成工作完成。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

static inline int ib_post_srq_recv(struct ib_srq *srq, struct ib_recv_wr *recv_wr,
struct ib_recv_wr **bad_recv_wr);

  • srq:接收请求将被发送到的 SRQ。
  • recv_wr:待发布接收请求链表。
  • bad_recv_wr:如果接收请求的处理有错误,这个指针将被导致这个错误的接收请求的地址所填充。

ib_recv_wr 结构

接收请求由struct ib_recv_wr : 表示

struct ib_recv_wr {
    struct ib_recv_wr          *next;
    u64            wr_id;
    struct ib_sge        *sg_list;
    int            num_sge;
};

  • next:指向列表中下一个接收请求的指针,如果这是最后一个接收请求,则为空。
  • wr_id:一个 64 位值,与此接收请求相关联,将在相应的工作完成中可用。
  • sg_list:分散/聚集元素的数组,如下一节所述。
  • num_sge:在sg_list中的条目数。零值意味着可以保存的消息大小为零字节。

ib_sge 结构

分散/聚集元素由struct ib_sge : 表示

struct ib_sge {
    u64    addr;
    u32    length;
    u32    lkey;
};

  • addr:要访问的缓冲区的地址。
  • length:要访问的地址的长度。
  • lkey:该缓冲区注册的内存区域的本地键。

地址句柄(AH)

AH 是一种 RDMA 资源,描述从目的地的本地端口到远程端口的路径。它正被用于 UD QP 。

ib_ah_attr 结构

AH 属性由struct ib_ah_attr : 表示

struct ib_ah_attr {
    struct ib_global_route    grh;
    u16                dlid;
    u8                sl;
    u8                src_path_bits;
    u8                static_rate;
    u8                ah_flags;
    u8                port_num;
};

  • grh:全局路由头属性,用于向另一个子网或本地或远程子网中的组播组发送消息。

  • dlid:目的地 LID。

  • sl:该消息将使用的服务级别。

  • src_path_bits:使用的源路径位。如果 LMC 用于此端口,则相关。

  • static_rate:发送消息之间的延迟级别。当向支持比本地节点更慢的消息速率的远程节点发送消息时,使用它。

  • ah_flags:AH 标志。它是掩码的按位“或”运算:

  • IB_AH_GRH:这个 AH 用的是 GRH。

  • port_num:发送消息的本地端口号。

ib_create_ah 方法

方法创建了一个 AH。如果成功,它将返回一个指向新创建的 AH 的指针,或者返回一个 ERR_PTR(),指明失败的原因。

struct ib_ah *ib_create_ah(struct ib_pd *pd, struct ib_ah_attr *ah_attr);

  • pd:该 AH 关联的 PD。
  • ah_attr:将创建此 AH 的属性。

ib_init_ah_from_wc()方法

ib_init_ah_from_wc()方法从工作完成和 GRH 结构初始化 AH 属性结构。这样做是为了将消息返回给 UD·QP 的传入消息。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_init_ah_from_wc(struct ib_device *device, u8 port_num, struct ib_wc *wc,
        struct ib_grh *grh, struct ib_ah_attr *ah_attr);

  • device:工作完成来自的 RDMA 设备和要在其上创建的 AH。
  • port_num:工作完成来自的端口号,AH 将与之关联。
  • wc:传入消息的工作完成。
  • grh:传入消息的 GRH 缓冲区。
  • ah_attr:该 AH 需要填写的属性。

ib_create_ah_from_wc 方法

ib_create_ah_from_wc()方法从工作完成和 GRH 结构中创建 AH。这样做是为了将消息返回给 UD·QP 的传入消息。如果成功,它将返回一个指向新创建的 AH 的指针,或者返回一个 ERR_PTR(),指明失败的原因。

struct ib_ah *ib_create_ah_from_wc(struct ib_pd *pd, struct ib_wc *wc, struct ib_grh *grh, u8 port_num);

  • pd:该 AH 关联的 PD。
  • wc:传入消息的工作完成。
  • grh:传入消息的 GRH 缓冲区。
  • port_num:工作完成来自的端口号,AH 将与之关联。

ib_modify_ah()方法

ib_modify_ah()方法修改 AH 的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_modify_ah(struct ib_ah *ah, struct ib_ah_attr *ah_attr);

  • ah:要修改的 AH。
  • ah_attr:AH 属性,如前所述。

ib_query_ah()方法

ib_query_ah()方法查询当前 AH 属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_ah(struct ib_ah *ah, struct ib_ah_attr *ah_attr);

  • ah:要查询的 AH
  • ah_attr:AH 属性,如前所述。

ib_destroy_ah_)方法

ib_destory_ah()方法销毁一个 AH。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_destroy_ah(struct ib_ah *ah);

  • ah:要销毁的 AH。

多播组

多播组是从一个 UD QP 向许多 UD qp 发送消息的手段。想要得到这个消息的每一个 UD QP 需要被附加到一个多播组。

ib_attach_mcast()方法

ib_attach_mcast()方法将 UD QP 附加到 RDMA 设备内的多播组。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_attach_mcast(struct ib_qp *qp, union ib_gid *gid, u16 lid);

  • qp:要加入多播组的 UD QP 的处理程序。
  • gid:QP 将要加入的组播组的 GID。
  • lid:QP 将要加入的组播组的 LID。

ib_detach_mcast()方法

ib_detach_mcast()方法将 UD QP 从 RDMA 设备内的多播组中分离。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_detach_mcast(struct ib_qp *qp, union ib_gid *gid, u16 lid);

  • qp:要从组播组中分离的 UD QP 的处理程序。
  • gid:QP 将被移除的组播组的 GID。
  • lid:QP 将被移除的多播组的 LID。

完成队列(CQ)

工作完成指定相应的工作请求已完成,并提供一些信息

.

关于它:它的状态、使用的操作码、大小等等。CQ 是由工作完成组成的对象。

IB create CQ()方法

方法创建一个 CQ。如果成功,它将返回一个指向新创建的 CQ 的指针,或者返回一个指明失败原因的 ERR_PTR()。

struct ib_cq *ib_create_cq(struct ib_device *device, ib_comp_handler comp_handler, void (*event_handler)(struct ib_event *, void *), void *cq_context, int cqe, int comp_vector);

  • device:与此 CQ 相关联的 RDMA 设备。
  • comp_handler:一个指针,指向当 CQ 发生完成事件时将被调用的回调。
  • event_handler:一个指针,指向一个回调函数,当一个附属的异步事件指向 CQ 时,这个回调函数将被调用。
  • cq_context:可以与 CQ 关联的自定义上下文。
  • cqe:该 CQ 所能容纳的工作完成的请求数量。
  • comp_vector:要处理的 RDMA 设备的完成向量的索引。如果这些中断的 IRQ 关联掩码分布在内核上,则该值可用于将完成工作负载分布在所有内核上。

方法的作用是

ib_resize_cq()方法通过增加或减少 CQ 的大小来改变 CQ 的大小,以至少保持新的大小。即使用户要求调整 CQ 的大小,其大小也可能不会被调整。

int ib_resize_cq(struct ib_cq *cq, int cqe);

  • cq:要调整大小的 CQ。此值不能低于 CQ 中存在的工作完成数。
  • cqe:该 CQ 所能容纳的工作完成的请求数量。

ib_modify_cq()方法

ib_modify_cq()方法改变 CQ 的调节参数。如果至少特定数量的工作完成将进入 CQ,或者超时将过期,将生成完成事件。使用它可能有助于减少 RDMA 设备发生中断的次数。如果成功,它将返回 0,或者返回-errno 值以及失败的原因。

int ib_modify_cq(structib_cq *cq, u16 cq_count, u16 cq_period);

  • cq:要修改的 CQ。
  • cq_count:自触发 CQ 事件的最后一个完成事件以来,将被添加到 CQ 的工作完成数。
  • cq_period:从触发 CQ 事件的最后一个完成事件开始,经过的微秒数。

ib_peek_cq()方法

ib_peek_cq()方法返回 CQ 中可用工作完成的数量。如果 CQ 中的工作完成数等于或大于wc_cnt,则返回wc_cnt。否则,它将返回 CQ 中工作完成的实际数量。如果出现错误,它将返回 errno 值以及失败的原因。

int ib_peek_cq(structib_cq *cq, intwc_cnt);

  • cq:偷窥的 CQ。
  • cq_count:自触发 CQ 事件的最后一个完成事件以来,将添加到 CQ 的工作完成数。

ib _ req _ 通知 _cq()方法

ib_req_notify_cq() 方法请求创建一个完成事件通知。它的返回值可以是:

  • 0:这意味着通知请求成功。如果使用了 IB _ CQ _ 报告 _ 错过的事件,那么返回值 0 意味着没有任何错过的事件。
  • 仅当使用 IB _ CQ _ 报告 _ 错过的事件并且存在错过的事件时,才返回正值。用户应该调用ib_poll_cq()方法来读取 CQ 中存在的工作完成。
  • 发生错误时,返回负值。返回–errno 值,指定失败的原因。
static inline int ib_req_notify_cq(struct ib_cq *cq,
                      enum ib_cq_notify_flags flags);

  • cq:将为其生成此完成事件的 CQ。

  • flags:将导致创建完成事件通知的关于工作完成的信息。可以是下列之一:

  • IB_CQ_NEXT_COMP:调用此方法后,将添加到 CQ 的下一个工作完成将触发 CQ 事件。

  • IB _ CQ _ 被请求:在调用此方法后,将被添加到 CQ 的下一个被请求的工作完成将触发 CQ 事件。

这两个值都可以与 IB _ CQ _ 报告 _ 错过的事件进行位或运算,以请求关于错过的事件的提示(即,当调用此方法并且此 CQ 中已经有工作完成时)。

ib_req_ncomp_notif()方法

ib_req_ncomp_notif()方法要求当 CQ 中的工作完成数等于wc_cnt时,创建一个完成事件通知。如果成功,它将返回 0,或者返回包含失败原因的 errno 值。

static inline int ib_req_ncomp_notif(struct ib_cq *cq, int wc_cnt);

  • cq:将为其生成此完成事件的 CQ。
  • wc_cnt:在生成完成事件通知之前,CQ 将持有的工作完成数。

IB poll CQ()方法

ib_poll_cq()方法从 CQ 轮询工作完成。它从 CQ 中读取工作完成并删除它们。按照将工作完成添加到 CQ 的顺序读取工作完成。它将返回 0 或一个正数来指示已读取的工作完成数,或者返回-errno 值以及失败原因。

static inline int ib_poll_cq(struct ib_cq *cq, int num_entries,
               struct ib_wc *wc);

  • cq:要被轮询的 CQ。
  • num_entries:要轮询的工作完成的最大数量。
  • wc:轮询工作完成数将存储在其中的数组。

ib_wc 结构

每一项工作的完成都用struct ib_wc : 表示

struct ib_wc {
    u64            wr_id;
    enum ib_wc_status    status;
    enum ib_wc_opcode    opcode;
    u32            vendor_err;
    u32            byte_len;
    struct ib_qp           *qp;
    union {
        __be32        imm_data;
        u32        invalidate_rkey;
    } ex;
    u32            src_qp;
    int            wc_flags;
    u16            pkey_index;
    u16            slid;
    u8            sl;
    u8            dlid_path_bits;
    u8            port_num;
};

  • wr_id:与相应的工作请求相关联的 64 位值。

  • status:结束工作请求的状态。可以是:

  • IB_WC_SUCCESS:操作成功完成。

  • IB_WC_LOC_LEN_ERR:本地长度错误。发送的消息太大而无法处理,或者传入的消息大于可用的接收请求。

  • IB_WC_LOC_QP_OP_ERR:本地 QP 操作错误。处理工作请求时检测到内部 QP 一致性错误。

  • IB_WC_LOC_EEC_OP_ERR:本地 EE 上下文操作错误。已弃用,因为不支持 RD QPs。

  • IB_WC_LOC_PROT_ERR:本地保护错误。工作请求缓冲区的保护对请求的操作无效。

  • IB_WC_WR_FLUSH_ERR:工作请求刷新错误。当 QP 处于错误状态时,工作请求已完成。

  • IB_WC_MW_BIND_ERR:内存窗口绑定错误。内存窗口绑定操作失败。

  • IB_WC_BAD_RESP_ERR:错误响应错误。响应程序返回了意外的传输层操作码。

  • IB_WC_LOC_ACCESS_ERR:本地访问错误。在处理带有立即消息的 RDMA 写入过程中,本地缓冲区出现保护错误。

  • IB_WC_REM_INV_REQ_ERR:删除无效请求错误。传入消息无效。

  • IB_WC_REM_ACCESS_ERR:远程访问错误。传入的 RDMA 操作出现保护错误。

  • IB_WC_REM_OP_ERR:远程操作错误。传入操作无法成功完成。

  • IB_WC_RETRY_EXC_ERR:超过传输重试计数器。远程 QP 没有发送任何 Ack 或 Nack,并且消息重新传输后超时已过期。

  • IB _ WC _ RNR _ 重试 _ EXC _ 错误:超过 RNR 重试。超过了 RNR NACK 返回计数。

  • IB_WC_LOC_RDD_VIOL_ERR:本地 RDD 违例错误。已弃用,因为不支持 RD QPs。

  • IB_WC_REM_INV_RD_REQ_ERR:删除无效的 RD 请求。已弃用,因为不支持 RD QPs。

  • IB_WC_REM_ABORT_ERR:远程中止错误。响应程序中止了操作。

  • IB_WC_INV_EECN_ERR:无效的 EE 上下文编号。已弃用,因为不支持 RD QPs。

  • IB_WC_INV_EEC_STATE_ERR:无效的 EE 上下文状态错误。已弃用,因为不支持 RD QPs。

  • IB_WC_FATAL_ERR:致命错误。

  • IB _ WC _ RESP _ 超时 _ 错误:响应超时错误。

  • IB_WC_GENERAL_ERR:一般错误。早期错误中没有涵盖的其他错误。

  • opcode:对应工作请求的操作,该工作请求随着该工作的完成而结束。可以是:

  • IB_WC_SEND:发送操作已在发送方完成。

  • IB_WC_RDMA_WRITE: RDMA 写操作在发送端完成。

  • IB_WC_RDMA_READ: RDMA 读取操作在发送端完成。

  • IB_WC_COMP_SWAP:比较和交换操作在发送端完成。

  • IB_WC_FETCH_ADD:提取和添加操作已在发送方完成。

  • IB_WC_BIND_MW:内存绑定操作在发送端完成。

  • IB_WC_LSO:具有大量发送卸载(LSO)的发送操作已在发送方完成。

  • IB_WC_LOCAL_INV:本地无效操作在发送方完成。

  • IB_WC_FAST_REG_MR:快速注册操作已在发送方完成。

  • IB_WC_MASKED_COMP_SWAP:屏蔽比较和交换操作在发送端完成。

  • IB_WC_MASKED_FETCH_ADD:屏蔽的提取和添加操作在发送端完成。

  • IB_WC_RECV:接收方已完成传入发送操作的接收请求。

  • IB_WC_RECV_RDMA_WITH_IMM:在接收端完成了一个带有立即操作的 RDMA 写入的接收请求。

  • vendor_err:特定于供应商的值,提供关于错误原因的额外信息。

  • byte_len:如果这是一个从接收请求结束时创建的工作完成,byte_len值表示接收到的字节数。

  • qp:获得工作完成的 QP 的句柄。当 QP 与 SRQ 相关联时,这是很有用的—通过这种方式,您可以知道与 QP 相关联的句柄,它的传入消息消耗了来自 SRQ 的接收请求。

  • ex.imm_data:带外数据(32 位),按网络顺序,随消息一起发送。如果在wc_flags中设置了 IB_WC_WITH_IMM,则可用。

  • ex.invalidate_rkey:被作废的rkey。在wc_flags中设置 IB_WC_WITH_INVALIDATE 时可用。

  • src_qp:来源 QP 号。发送这条信息的 QP 号码。仅与 UD QPs 相关。

  • wc_flags:提供工作完成信息的标志。它是掩码的按位“或”运算:

  • IB_WC_GRH:指示消息被接收的指示器有一个 GRH,接收请求缓冲区的前 40 个字节包含它。仅与 UD QPs 相关。

  • IB_WC_WITH_IMM:指示收到的消息有即时数据。

  • IB_WC_WITH_INVALIDATE:指示接收到了一个带 INVALIDATE 消息的 Send。

  • IB_WC_IP_CSUM_OK:指示收到的消息通过了 RDMA 设备进行的 IP 校验和测试。仅当 RDMA 设备支持 IP 校验和卸载时,此选项才可用。如果在设备标志中设置了 IB_DEVICE_UD_IP_CSUM,则该选项可用。

  • pkey_index😛 _ Key 索引,仅与 GSI QPs 相关。

  • slid:消息的 source LID。仅与 UD QPs 相关。

  • sl:消息的服务级别。仅与 UD QPs 相关。

  • dlid_path_bits:目的 LID 路径位。仅与 UD QPs 相关。

  • port_num:报文进来的端口号。仅与交换机上的直接路由 SMP 相关。

ib_destroy_cq_)方法

ib_destory_cq()方法销毁一个 CQ。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_destroy_cq(struct ib_cq *cq);

  • 被摧毁的 CQ。

队列对(QP)

QP 是一种将两个工作队列结合在一起的资源:发送队列和接收队列。每个队列充当一个 FIFO。发布到每个工作队列的工作记录将按照其到达的顺序进行处理。但是,队列之间的顺序没有任何保证。该资源是发送和接收数据包的资源。

ib_qp_cap 结构

QP 的工作队列大小由struct ib_qp_cap : 表示

struct ib_qp_cap {
    u32    max_send_wr;
    u32    max_recv_wr;
    u32    max_send_sge;
    u32    max_recv_sge;
    u32    max_inline_data;
};

  • max_send_wr:此 QP 在发送队列中可以容纳的未完成工作请求的最大数量。
  • max_recv_wr:此 QP 在接收队列中可以容纳的未完成工作请求的最大数量。如果 QP 与 SRQ 相关联,则忽略该值。
  • max_send_sge:发送队列中每个工作请求能够容纳的分散/收集元素的最大数量。
  • max_recv_sge:接收队列中每个工作请求能够容纳的分散/收集元素的最大数量。
  • max_inline_data:可以内联发送的最大消息大小。

ib_create_qp 方法

方法创建一个 QP。如果成功,它将返回一个指向新创建的 QP 的指针,或者返回一个指明失败原因的 ERR_PTR()。

struct ib_qp *ib_create_qp(struct ib_pd *pd,
        struct ib_qp_init_attr *qp_init_attr);

  • pd:与此 QP 相关联的 PD。
  • qp_init_attr:这个 QP 将被创建的属性。

ib_qp_init_attr 结构

创建的 QP 属性由struct ib_qp_init_attr : 表示

struct ib_qp_init_attr {
    void                      (*event_handler)(struct ib_event *, void *);
    void                *qp_context;
    struct ib_cq            *send_cq;
    struct ib_cq            *recv_cq;
    struct ib_srq            *srq;
    struct ib_xrcd           *xrcd;     /* XRC TGT QPs only */
    struct ib_qp_cap        cap;
    enum ib_sig_type        sq_sig_type;
    enum ib_qp_type        qp_type;
    enum ib_qp_create_flags    create_flags;
    u8                port_num; /* special QP types only */
};

  • event_handler:一个指针,指向一个回调函数,当一个附属的异步事件指向 QP 时,这个回调函数将被调用。

  • qp_context:可以与 QP 关联的自定义上下文。

  • send_cq:与此 QP 的发送队列相关联的 CQ。

  • recv_cq:与此 QP 的接收队列相关联的 CQ。

  • srq:与此 QP 的接收队列相关联的 SRQ,如果 QP 与 SRQ 不相关联,则为空。

  • xrcd:此 QP 将与之关联的 XRC 域。仅当 qp_type 为 IB_QPT_XRC_TGT 时相关。

  • cap:描述发送和接收队列大小的结构。这个结构在前面已经描述过了。

  • sq_sig_type:发送队列的信令类型。它可以是:

  • IB_SIGNAL_ALL_WR:每个发送到发送队列的发送请求都会以一个工作完成结束。

  • IB_SIGNAL_REQ_WR:只有通过显式请求(即设置 IB_SEND_SIGNALED 标志)发送到发送队列的发送请求才会以工作完成结束。这被称为选择性信号

  • qp_type:QP 运输类型。可以是:

  • IB_QPT_SMI:一个子网管理接口 QP。

  • GSI QPT IB:QP 通用服务接口。

  • IB_QPT_RC:可靠的互联 QP。

  • IB_QPT_UC:一个不可靠的连接 QP。

  • UD QPT:一个不可靠的数据报 QP。

  • IB _ QPT _ 原始 _IPV6:一个 IPV6 原始数据报 QP。

  • IB _ QPT _ 原始 _ 以太类型:以太类型原始数据报 QP。

  • IB _ QPT _ 原始包:一个 QP 的原始包。

  • IB _ QPT _ XRC _ INI:XRC 发起者 QP。

  • TGT XRC QPT IB:一个 XRC 目标 QP。

  • create_flags : QP 属性旗帜。它是掩码的按位“或”运算:

  • IB _ QP _ CREATE _ IPOIB _ UD _ LSO:QP 将用于发送 IPoIB LSO 消息。

  • IB _ QP _ 创建 _ 阻塞 _ 多播 _ 回送:阻塞回送多播数据包。

  • port_num:该 QP 关联的 RDMA 设备端口号。仅当qp_type为 IB_QPT_SMI 或 IB_QPT_GS 时相关。

ib_modify_qp()方法

ib_modify_qp()方法修改 QP 的属性。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_modify_qp(struct ib_qp *qp,
    struct ib_qp_attr *qp_attr,
    int qp_attr_mask);

  • qp:要修改的 QP。

  • qp_attr:QP 属性,如前所述。

  • qp_attr_mask : 要更改的 QP 属性。每个掩码指定了将在这个 QP 转换中被修改的属性,例如指定将使用qp_attr中的哪些属性。它是掩码的按位“或”运算:

  • IB _ QP _ 州:修改在qp_state字段中指定的 QP 州。

  • IB _ QP _ 当前状态:修改在cur_qp_state字段中指定的假定的当前 QP 状态。

  • IB _ QP _ EN _ SQD _ 异步 _ 通知:当在en_sqd_async_notify字段中指定的 QP 状态为 SQD.drained 时,修改通知请求的状态。

  • IB _ QP _ 访问 _ 标志:修改在qp_access_flags字段中指定的允许的输入远程操作。

  • IB_QP_PKEY_INDEX:修改 P_Key 表中的索引,该索引在主路径中与该 QP 相关联,在pkey_index字段中指定。

  • IB _ QP _ 端口:修改在port_num字段中指定的与 QP 主路径相关联的 RDMA 设备的端口号。

  • IB_QP_QKEY:修改在qkey字段中指定的 QP 的 Q-Key。

  • IB_QP_AV:修改在ah_attr字段中指定的 QP 的地址向量属性。

  • IB _ QP _ 路径 _MTU:修改路径的 MTU,在path_mtu字段中指定。

  • IB _ QP _ 超时:修改超时字段中指定的重新传输前等待的超时时间。

  • IB _ QP _ 重试 _ 计数:修改在retry_cnt字段中指定的 QP 因缺少 Ack/Nack 而重试的次数。

  • IB _ QP _ RNR _ 重试:修改在rq_psn字段中指定的 QP 的 RNR 重试次数。

  • IB_QP_RQ_PSN:修改在rnr_retry字段中指定的接收包的起始 PSN。

  • IB _ QP _ 最大 _ QP _ RD _ 原子:修改此 QP 作为启动器可以并行处理的 RDMA 读取和原子操作的数量,在max_rd_atomic字段中指定。

  • IB_QP_ALT_PATH:修改在alt_ah_attralt_pkey_indexalt_port_numalt_timeout字段中指定的 QP 的备用路径。

  • IB _ QP _ 最小 _ RNR _ 定时器:修改最小 RNR 定时器,QP 将在 RNR Nak 中报告给远端,在min_rnr_timer字段中指定。

  • IB_QP_SQ_PSN:修改发送包的起始 PSN,在sq_psn字段中指定。

  • IB _ QP _ 最大 _ DEST _ RD _ 原子:修改此 QP 作为启动器可以并行处理的 RDMA 读取和原子操作的数量,在max_dest_rd_atomic字段中指定。

  • IB _ QP _ 路径 _ MIG _ 状态:修改在path_mig_state字段中指定的路径迁移状态机的状态。

  • IB_QP_CAP:修改 QP 中工作队列(发送和接收队列)的大小,在cap字段中指定。

  • IB_QP_DEST_QPN:修改在dest_qp_num字段中指定的目的地 QP 号码。

ib_qp_attr 结构

QP 属性由struct ib_qp_attr表示:

struct ib_qp_attr {
    enum ib_qp_state    qp_state;
    enum ib_qp_state    cur_qp_state;
    enum ib_mtu        path_mtu;
    enum ib_mig_state    path_mig_state;
    u32            qkey;
    u32            rq_psn;
    u32            sq_psn;
    u32            dest_qp_num;
    int            qp_access_flags;
    struct ib_qp_cap    cap;
    struct ib_ah_attr    ah_attr;
    struct ib_ah_attr    alt_ah_attr;
    u16            pkey_index;
    u16            alt_pkey_index;
    u8            en_sqd_async_notify;
    u8            sq_draining;
    u8            max_rd_atomic;
    u8            max_dest_rd_atomic;
    u8            min_rnr_timer;
    u8            port_num;
    u8            timeout;
    u8            retry_cnt;
    u8            rnr_retry;
    u8            alt_port_num;
    u8            alt_timeout;
};

  • qp_state:QP 要移动到的状态。可以是:

  • IB _ QPS _ 复位:复位状态。

  • IB _ QPS _ 初始化:初始化状态。

  • IB_QPS_RTR:准备接收状态。

  • IB_QPS_RTS:准备发送状态。

  • IB_QPS_SQD:发送队列清空状态。

  • IB_QPS_SQE:发送队列错误状态。

  • IB_QPS_ERR:错误状态。

  • cur_qp_state:假定的 QP 当前状态。可以像qp_state一样。

  • path_mtu:路径中 MTU 的大小。可以是:

  • IB_MTU_256:256 字节。

  • IB_MTU_512:512 字节。

  • IB_MTU_1024:1,024 字节。

  • IB_MTU_2048:2,048 字节。

  • IB_MTU_4096:4,096 字节。

  • path_mig_state:路径迁移状态机,用于 APM(自动路径迁移)。可以是:

  • IB_MIG_MIGRATED:已迁移。路径迁移的状态机被迁移(迁移的初始状态已完成)。

  • IB_MIG_REARM:重新武装。路径迁移的状态机被重新装备(试图协调远程 RC QP 将本地和远程 QP 都移动到装备状态)。

  • IB_MIG_ARMED:武装。路径迁移的状态机被装备(本地和远程 qp 都准备好执行路径迁移)。

  • qkey:QP 的 Q _ 键。

  • rq_psn:接收队列中第一个数据包的预期 PSN。该值为 24 位。

  • sq_psn:发送队列中第一个包使用的 PSN。该值为 24 位。

  • dest_qp_num:远端(目的地)侧的 QP 号码。该值为 24 位。

  • qp_access_flags:允许的传入 RDMA 和原子操作。它是掩码的按位“或”运算:

  • IB_ACCESS_REMOTE_WRITE:允许传入 RDMA 写操作。

  • IB_ACCESS_REMOTE_READ:允许传入 RDMA 读取操作。

  • IB_ACCESS_REMOTE_ATOMIC:允许引入原子操作。

  • cap:QP 大小。接收和发送队列中的工作请求数。只有当设备支持 QP 调整大小时,才能做到这一点,即在设备标志中设置 IB_DEVICE_RESIZE_MAX_WR。这个结构在前面已经描述过了。

  • ah_attr:QP 主路径的地址向量。这个结构在前面已经描述过了。

  • alt_ah_attr:QP 备用路径的地址向量。这个结构在前面已经描述过了。

  • pkey_index:该 QP 关联的主路径的 P_Key 索引。

  • alt_pkey_index:该 QP 关联的备用路径的 P_Key 索引。

  • en_sqd_async_notify:如果值不为零,请求在 QP 进入 SQE.drained 状态时调用异步事件回调。

  • sq_draining:仅与ib_query_qp()相关。如果值不为零,则 QP 处于 SQD.drainning 状态(而不是 SQD.drainning)。

  • max_rd_atomic:该 QP 作为启动器可以并行处理的 RDMA 读取和原子操作的数量。

  • max_dest_rd_atomic:作为目的地,该 QP 可以并行处理的 RDMA 读取和原子操作的数量。

  • min_rnr_timer:如果远程端用 RNR Nack 响应,则在重新发送消息之前等待的超时时间。

  • port_num:该 QP 在主路径中关联的 RDMA 设备的端口号。

  • timeout:如果远程端在主路径中没有任何 Ack 或 Nack 响应,则在重新发送消息之前等待的超时时间。timeout是一个 5 位值,0 是无限时间,任何其他值意味着超时将是 4.096 * 2 ^ timeout usec。

  • retry_cnt:如果远端没有任何 Ack 或 Nack 响应,发送(重新)消息的次数。

  • rnr_retry:如果远端用 RNR Nack 应答,则发送(重新发送)消息的次数。3 位值,7 表示无限重试。该值可以是:

  • IB _ RNR _ 定时器 _655_36:延迟 655.36 毫秒。

  • IB _ RNR _ 定时器 _000_01:延迟 0.01 毫秒。

  • IB _ RNR _ 定时器 _000_02:延迟 0.02 毫秒。

  • IB _ RNR _ 定时器 _000_03:延迟 0.03 毫秒。

  • IB _ RNR _ 定时器 _000_04:延迟 0.04 毫秒。

  • IB _ RNR _ 定时器 _000_06:延迟 0.06 毫秒。

  • IB _ RNR _ 定时器 _000_08:延迟 0.08 毫秒。

  • IB _ RNR _ 定时器 _000_12:延迟 0.12 毫秒。

  • IB _ RNR _ 定时器 _000_16:延迟 0.16 毫秒。

  • IB _ RNR _ 定时器 _000_24:延迟 0.24 毫秒。

  • IB _ RNR _ 定时器 _000_32:延迟 0.32 毫秒。

  • IB _ RNR _ 定时器 _000_48:延迟 0.48 毫秒。

  • IB _ RNR _ 定时器 _000_64:延迟 0.64 毫秒。

  • IB _ RNR _ 定时器 _000_96:延迟 0.96 毫秒。

  • IB _ RNR _ 定时器 _001_28:延迟 1.28 毫秒。

  • IB _ RNR _ 定时器 _001_92:延迟 1.92 毫秒。

  • IB _ RNR _ 定时器 _002_56:延迟 2.56 毫秒。

  • IB _ RNR _ 定时器 _003_84:延时 3.84 毫秒。

  • IB _ RNR _ 定时器 _005_12:延迟 5.12 毫秒。

  • IB _ RNR _ 定时器 _007_68:延迟 7.68 毫秒。

  • IB _ RNR _ 定时器 _010_24:延迟 10.24 毫秒。

  • IB _ RNR _ 定时器 _015_36:延迟 15.36 毫秒。

  • IB _ RNR _ 定时器 _020_48:延时 20.48 毫秒。

  • IB _ RNR _ 定时器 _030_72:延时 30.72 毫秒。

  • IB _ RNR _ 定时器 _040_96:延迟 40.96 毫秒。

  • IB _ RNR _ 定时器 _061_44:延迟 61.44 毫秒。

  • IB _ RNR _ 定时器 _081_92:延迟 81.92 毫秒。

  • IB _ RNR _ 定时器 _122_88:延迟 122.88 毫秒。

  • IB _ RNR _ 定时器 _163_84:延迟 163.84 毫秒。

  • IB _ RNR _ 定时器 _245_76:延时 245.76 毫秒。

  • IB _ RNR _ 定时器 _327_68:延时 327.86 毫秒。

  • IB _ RNR _ 定时器 _491_52:延时 391.52 毫秒。

  • alt_port_num:备用路径中与该 QP 关联的 RDMA 设备的端口号。

  • alt_timeout:如果远程端在备用路径中没有任何 Ack 或 Nack 响应,则在重新发送消息之前等待的超时时间。5 位值,0 是无限时间,其他任何值都意味着超时将是 4.096 * 2 ^ timeout usec。

ib_query_qp()方法

ib_query_qp()方法查询当前的 QP 属性。在对状态字段ib_query_qp()的后续调用中,qp_attr中的一些属性可能会改变。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_query_qp(struct ib_qp *qp, struct ib_qp_attr *qp_attr, int qp_attr_mask, struct ib_qp_init_attr *qp_init_attr);

  • qp:要查询的 QP。
  • qp_attr:QP 属性,如前所述。
  • qp_attr_mask:需要查询的强制属性的掩码。低级驱动程序可以使用它作为要查询的字段的提示,但他们也可能忽略它并填充整个结构。
  • qp_init_attr:QP 初始化属性,如前所述。

方法销毁了一个 QP。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_destroy_qp(struct ib_qp *qp);

  • 被摧毁的 QP。

ib_open_qp()方法

ib_open_qp()方法获取对多个进程间现有的可共享 QP 的引用。创建 QP 的进程可能会退出,从而允许将 QP 的所有权转移给另一个进程。如果成功,它将返回一个指向可共享 QP 的指针,或者返回一个指明失败原因的 ERR_PTR()。

struct ib_qp *ib_open_qp(struct ib_xrcd *xrcd, struct ib_qp_open_attr *qp_open_attr);

  • xrcd:QP 将要关联的 XRC 域。
  • qp_open_attr:要打开的已有 QP 的属性。

ib_qp_open_attr 结构

共享的 QP 属性由struct ib_qp_open_attr : 表示

struct ib_qp_open_attr {
    void                (*event_handler)(struct ib_event *, void *);
    void               *qp_context;
    u32            qp_num;
    enum ib_qp_type    qp_type;
};

  • event_handler:一个指针,指向一个回调函数,当一个附属的异步事件指向 QP 时,这个回调函数将被调用。
  • qp_context:可以与 QP 关联的自定义上下文。
  • qp_num:这个 QP 要开的 QP 号。
  • qp_type : QP 运输类型。仅支持 QPT XRC TGT IB。

IB close qp()方法

方法释放一个对 QP 的外部引用。直到通过ib_open_qp()方法获得的所有内部引用都被释放后,底层的共享 QP 才会被销毁。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

int ib_close_qp(struct ib_qp *qp);

  • qp:即将关闭的 QP。

ib_post_recv()方法

ib_post_recv()方法获取一个接收请求的链表,并将它们添加到接收队列中以备将来处理。每个接收请求都被认为是未完成的,直到在其处理后生成工作完成。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

static inline int ib_post_recv(struct ib_qp *qp, struct ib_recv_wr *recv_wr, struct ib_recv_wr **bad_recv_wr);

  • qp:接收请求将被发送到的 QP。
  • recv_wr:待发布接收请求链表。
  • bad_recv_wr:如果接收请求的处理有错误,这个指针将被导致这个错误的接收请求的地址所填充。

ib_post_send()方法

ib_post_send()方法将发送请求的链表作为一个参数,并将它们添加到发送队列中以供将来处理。每个发送请求都被认为是未完成的,直到在其处理后生成工作完成。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

static inline int ib_post_send(struct ib_qp *qp, struct ib_send_wr *send_wr, struct ib_send_wr **bad_send_wr);

  • qp:发送请求将被发送到的 QP。
  • send_wr:待发布的发送请求链表。
  • bad_send_wr:如果发送请求的处理有错误,这个指针将被导致这个错误的发送请求的地址所填充。

ib_send_wr 结构

发送请求由struct ib_send_wr : 表示

struct ib_send_wr {
    struct ib_send_wr          *next;
    u64            wr_id;
    struct ib_sge        *sg_list;
    int            num_sge;
    enum ib_wr_opcode    opcode;
    int            send_flags;
    union {
        __be32        imm_data;
        u32        invalidate_rkey;
    } ex;
    union {
        struct {
            u64    remote_addr;
            u32    rkey;
        } rdma;
        struct {
            u64    remote_addr;
            u64    compare_add;
            u64    swap;
            u64    compare_add_mask;
            u64    swap_mask;
            u32    rkey;
        } atomic;
        struct {
            struct ib_ah     *ah;
            void           *header;
            int             hlen;
            int             mss;
            u32        remote_qpn;
            u32        remote_qkey;
            u16        pkey_index; /* valid for GSI only */
            u8        port_num;   /* valid for DR SMPs on switch only */
        } ud;
        struct {
            u64                iova_start;
            struct ib_fast_reg_page_list       *page_list;
            unsigned int            page_shift;
            unsigned int            page_list_len;
            u32                length;
            int                access_flags;
            u32                rkey;
        } fast_reg;
        struct {
            struct ib_mw                    *mw;
            /* The new rkey for the memory window. */
            u32                              rkey;
            struct ib_mw_bind_info           bind_info;
        } bind_mw;
    } wr;
    u32            xrc_remote_srq_num;    /* XRC TGT QPs only */
};

  • next:指向列表中下一个发送请求的指针,如果这是最后一个发送请求,则为空。

  • wr_id:与此发送请求相关联的 64 位值,将在相应的工作完成中可用。

  • sg_list:分散/聚集元素的数组。如前所述。

  • num_sge:在sg_list中的条目数。零值意味着消息大小为零字节。

  • opcode:要执行的操作。这会影响数据传输的方式、方向,以及接收请求是否会在远程端使用,以及发送请求(send_wr)中的哪些字段会被使用。可以是:

  • IB _ WR _ RDMA _ 写:RDMA 写操作。

  • IB _ WR _ RDMA _ 写 _ 与 _IMM: RDMA 写与立即操作。

  • IB _ WR _ 发送:发送操作。

  • IB _ WR _ IMM 发送:立即操作发送。

  • IB _ WR _ RDMA _ 读:RDMA 读操作。

  • IB _ WR _ 原子 _ CMP _ 和 _SWP:比较和交换操作。

  • IB _ WR _ 原子 _ 提取和添加:提取和添加操作。

  • IB_WR_LSO:用 LSO 发送一个 IPoIB 消息(让 RDMA 设备把大的 skb 分割成多个 MSS 大小的包)。LSO 是一个优化特性,它允许通过减少 CPU 开销来使用大数据包。

  • IB_WR_SEND_WITH_INV:带无效操作的发送。

  • IB_WR_RDMA_READ_WITH_INV: RDMA 带无效操作的读取。

  • IB _ WR _ 本地 _INV:本地无效操作。

  • IB _ WR _ 快速 _ 注册 _MR:快速 MR 注册操作。

  • IB _ WR _ 掩码 _ 原子 _ CMP _ 和 _SWP:掩码比较和交换操作。

  • IB _ WR _ 掩码 _ 原子 _ 提取和添加:掩码提取和添加操作。

  • IB_WR_BIND_MW:内存绑定操作。

  • send_flags:发送请求的额外属性。它是掩码的按位“或”运算:

  • IB_SEND_FENCE:在执行此操作之前,请等待之前的发送请求处理结束。

  • IB _ SEND _ SIGNALED:如果 QP 是用选择性信令创建的,当这个发送请求的处理结束时,将生成一个工作完成。

  • IB_SEND_SOLICITED:标记将在远程端创建一个请求事件。

  • IB_SEND_INLINE:将这个发送请求作为内联发送——也就是让底层驱动读取 if sg_list中的内存缓冲区,而不是 RDMA 设备;这可能会增加延迟。

  • IB_SEND_IP_CSUM:发送一个 IPoIB 消息,并在 HW(校验和卸载)中计算 IP 校验和。

  • ex.imm_data:要发送的即时数据。如果opcode是 IB_WR_SEND_WITH_IMM 或 IB_WR_RDMA_WRITE_WITH_IMM,则该值是相关的。

  • ex.invalidate_rkey:要作废的rkey。如果操作码是 IB_WR_SEND_WITH_INV,则此值是相关的。

如果操作码是 IB _ WR _ RDMA _ 写入、IB _ WR _ RDMA _ 写入 _WITH_IMM 或 IB _ WR _ RDMA _ 读取,则以下联合是相关的:

  • wr.rdma.remote_addr:这个发送请求将要访问的远程地址。
  • wr.rdma.rkey:该发送请求将要访问的 MR 的远程键(rkey)。

如果opcode是 IB _ WR _ 原子 _CMP_AND_SWP,IB _ WR _ 原子 _FETCH_AND_ADD,IB _ WR _ 屏蔽 _ 原子 _CMP_AND_SWP,或 IB _ WR _ 屏蔽 _ 原子 _FETCH_AND_ADD,则以下联合是相关的:

  • wr.atomic.remote_addr:这个发送请求将要访问的远程地址。
  • wr.atomic.compare_add:如果 opcode 是 IB _ WR _ 原子 _ 提取 _ 和 _ 添加*,这是添加到remote_addr内容的值。否则,这是与remote_addr的内容进行比较的值。
  • wr.atomic.swap:当remote_addr中的值等于compare_add时,放入其中的值。如果操作码是 IB _ WR _ 原子 _CMP_AND_SWP 或 IB _ WR _ 掩码 _ 原子 _CMP_AND_SWP,则此值是相关的。
  • wr.atomic.compare_add_mask:如果 opcode 是 IB _ WR _ 掩码 _ 原子 _ 取 _ 加,这是把compare_add的值加到remote_addr的内容时要改变的值的掩码。否则,这就是在与 swap 进行比较时对remote_addr的内容使用的掩码。
  • wr.atomic.swap_mask:这是remote_addr内容中要更改的值的掩码。仅当操作码为 IB _ WR _ 掩码 _ 原子 _ CMP _ 和 _SWP 时相关。
  • wr.atomic.rkey:该发送请求要访问的 MR 的rkey

如果此发送请求要发送到的 QP 类型是 UD,则以下工会是相关的:

  • wr.ud.ah:描述目标节点路径的地址句柄。
  • wr.ud.header:包含表头的指针。如果操作码是 IB_WR_LSO,则相关。
  • wr.ud.hlen:长度wr.ud.header。如果操作码是 IB_WR_LSO,则相关。
  • wr.ud.mss:消息分段的最大分段大小。如果操作码是 IB_WR_LSO,则相关。
  • wr.ud.remote_qpn:发送消息的远程 QP 号码。如果向多播组发送此消息,应使用枚举 IB_MULTICAST_QPN。
  • wr.ud.remote_qkey:要使用的远程 Q_Key 值。如果设置了该值的 MSB,则 Q_Key 的值将取自 QP 属性。
  • wr.ud.pkey_index:发送消息时使用的 P_Key 索引。如果 QP 类型是 is IB,则相关。
  • wr.ud.port_num:发送消息的端口号。与交换机上的直接路由 SMP 相关。

如果操作码是 IB_WR_FAST_REG_MR,则以下联合是相关的:

  • wr.fast_reg.iova_start:新创建的 FMR 的 I/O 虚拟地址。
  • wr.fast_reg.page_list:FMR 中要分配给地图的页面列表。
  • wr.fast_reg.page_shift:要映射的“页面”大小的 Log 2。
  • wr.fast_reg.page_list_len:在page_list中的页数。
  • wr.fast_reg.length:FMR 的大小,以字节为单位。
  • wr.fast_reg.access_flags:该 FMR 允许的操作。
  • wr.fast_reg.rkey:要分配给 FMR 的远程键值。

如果操作码是 IB _ WR _ 绑定 _MW,则以下联合是相关的:

  • wr.bind_mw.mw:要有界的 MW。
  • wr.bind_mw.rkey:分配给 MW 的远程键的值。
  • wr.bind_mw.bind_info:绑定属性,将在下一节中解释。

如果此发送请求发送到的 QP 类型是 XRCTGT,则以下成员是相关的:

  • xrc_remote_srq_num:将接收消息的远程 SRQ。

ib_mw_bind_info 结构

MW 类型 1 和类型 2 的 MW 绑定属性由struct ib_mw_bind_info表示。

struct ib_mw_bind_info {
    struct ib_mr       *mr;
    u64        addr;
    u64        length;
    int        mw_access_flags;
};

  • mr:该内存窗口将要绑定到的内存区域。

  • addr:存储窗口从.开始的地址

  • length:内存窗口的长度,以字节为单位。

  • mw_access_flags:允许的传入 RDMA 和原子操作。它是掩码的按位“或”运算:

  • IB_ACCESS_REMOTE_WRITE:允许传入 RDMA 写操作。

  • IB_ACCESS_REMOTE_READ:允许传入 RDMA 读取操作。

  • IB_ACCESS_REMOTE_ATOMIC:允许引入原子操作。

记忆窗(毫瓦)

内存窗口被用作一种轻量级操作,以改变传入远程操作的允许权限并使其无效。

ib_alloc_mw()方法

ib_alloc_mw()方法分配一个内存窗口。如果成功,它将返回一个指向新分配的 MW 的指针,或者返回一个 ERR_PTR(),指明失败的原因。

struct ib_mw *ib_alloc_mw(struct ib_pd *pd, enum ib_mw_type type);

  • pd:该 MW 关联的 PD。

  • type:记忆窗口的类型。可以是:

  • IB_MW_TYPE_1: MW,可以使用动词进行绑定,并且只支持 PD 的关联。

  • IB_MW_TYPE_2:可以使用工作请求绑定的 MW,支持仅 QP 号码或 QP 号码与 PD 的关联。

ib_bind_mw()方法

ib_bind_mw()方法将内存 窗口绑定到具有特定地址、大小和远程权限的指定内存区域。如果没有任何直接的错误,MW 的rkey将被更新为新值,但是绑定操作仍然可能异步失败(并以错误完成结束)。如果成功,它将返回 0,或者返回 errno 值以及失败的原因。

static inline int ib_bind_mw(struct ib_qp *qp, struct ib_mw *mw, struct ib_mw_bind *mw_bind);

  • qp:绑定 WR 将被发送到的 QP。
  • mw:要绑定的 MW。
  • mw_bind:绑定属性,如下所述。

ib_mw_bind 结构

类型 1 MW 的 MW 绑定属性由struct ib_mw_bind表示。

struct ib_mw_bind {
    u64                    wr_id;
    int                    send_flags;
    struct ib_mw_bind_info bind_info;
};

  • wr_id:与该绑定发送请求相关联的 64 位值,工作请求 id ( wr_id)的值将在相应的工作完成中可用。
  • send_flags:绑定发送请求的额外属性,如前所述。这里只支持 IB_SEND_FENCE 和 IB_SEND_SIGNALED。
  • bind_info:绑定操作的更多属性。如前所述。

ib_dealloc_mw()方法

方法释放一个 MW。 如果成功将返回 0,如果失败将返回 errno 值。

int ib_dealloc_mw(struct ib_mw *mw);

  • mw:要解除分配的 MW。

存储区

RDMA 设备访问的每个内存缓冲区都需要注册。在注册过程中,内存将被锁定(防止被换出),内存转换信息(来自虚拟地址arrow.jpg物理地址)将保存在 RDMA 设备中。注册后,每个内存区域都有两个密钥:一个用于本地访问,一个用于远程访问。当在工作请求中指定这些内存缓冲区时,将使用这些键。

方法的作用是

ib_get_dma_mr()方法返回可用于 DMA 的系统内存区域。创建这个 MR 是不够的,还需要下面的ib_dma_*()方法来创建或销毁这个 MR 的lkeyrkey将要使用的地址。如果成功,它将返回一个指向新分配的 MR 的指针,或者返回一个 ERR_PTR(),指明失败的原因。

struct ib_mr *ib_get_dma_mr(struct ib_pd *pd, int mr_access_flags);

  • pd:与该 MR 关联的 PD。

  • mr_access_flags:该 MR 本地写入允许的操作在该 MR 中始终受支持。它是掩码的按位或:

  • IB_ACCESS_LOCAL_WRITE:允许对该存储区进行本地写操作。

  • IB_ACCESS_REMOTE_WRITE:允许对该内存区域进行 RDMA 写入操作。

  • IB_ACCESS_REMOTE_READ:允许对这个内存区域进行 RDMA 读操作。

  • IB_ACCESS_REMOTE_ATOMIC:允许对这个内存区域进行原子操作。

  • IB_ACCESS_MW_BIND:允许对该内存区域进行 MW 绑定。

  • IB_ZERO_BASED:表示虚拟地址是从零开始的。

ib_dma_mapping_error()方法

ib_dma_mapping_error()方法检查从ib_dma_*()返回的 DMA 地址是否失败。如果有任何失败,它将返回一个非零值,如果操作成功完成,则返回零。

static inline int ib_dma_mapping_error(struct ib_device *dev, u64 dma_addr);

  • dev:使用ib_dma_*()方法为其创建 DMA 地址的 RDMA 设备。
  • dma_addr:要验证的 DMA 地址。

IB DMA map single()方法

ib_dma_map_single()方法将内核虚拟地址映射到 DMA 地址。它将返回一个需要用ib_dma_mapping_error()方法检查错误的 DMA 地址:

static inline u64 ib_dma_map_single(struct ib_device *dev, void *cpu_addr, size_t size, enum dma_data_direction direction);

  • dev:将在其上创建 DMA 地址的 RDMA 设备。

  • cpu_addr:DMA 要映射的内核虚拟地址。

  • size:要映射的区域的大小,以字节为单位。

  • direction:DMA 的方向。可以是:

  • DMA_TO_DEVICE:从主存储器到设备的 DMA。

  • DMA_FROM_DEVICE:从设备到主存储器的 DMA。

  • DMA_BIDIRECTIONAL:从主存储器到设备或从设备到主存储器的 DMA。

IB _ DMA _ unmapsingle()方法

ib_dma_unmap_single()方法取消了使用ib_dma_map_single() : 分配的 DMA 映射

static inline void ib_dma_unmap_single(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction direction);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • addr:要取消映射的 DMA 地址。
  • size:要取消映射的区域的大小,以字节为单位。该值必须与在ib_dma_map_single()方法中使用的值相同。
  • direction:DMA 的方向。该值必须与在ib_dma_map_single()方法中使用的值相同。

ib_dma_map_single_attrs()方法

ib_dma_map_single_attrs()方法根据 DMA 属性将内核虚拟地址映射到 DMA 地址。它将返回一个需要用ib_dma_mapping_error()方法检查错误的 DMA 地址。

static inline u64 ib_dma_map_single_attrs(struct ib_device *dev, void *cpu_addr, size_t size, enum dma_data_direction direction, struct dma_attrs *attrs);

  • dev:将在其上创建 DMA 地址的 RDMA 设备。
  • cpu_addr:DMA 要映射的内核虚拟地址。
  • size:要映射的区域的大小,以字节为单位。
  • direction:DMA 的方向。如前所述。
  • attrs:映射的 DMA 属性。如果该值为 NULL,该方法的行为类似于ib_dma_map_single()方法。

IB _ DMA _ unmapsingle _ attrs()方法

ib_dma_unmap_single_attrs()方法取消了使用ib_dma_map_single_attrs()方法分配的 DMA 映射:

static inline void ib_dma_unmap_single_attrs(struct ib_device *dev, u64 addr, size_t size,
enum dma_data_direction direction, struct dma_attrs *attrs);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • addr:要取消映射的 DMA 地址。
  • size:要取消映射的区域的大小,以字节为单位。该值必须与在ib_dma_map_single_attrs()方法中使用的值相同。
  • direction:DMA 的方向。该值必须与在ib_dma_map_single_attrs()方法中使用的值相同。
  • attrs:映射的 DMA 属性。该值必须与在ib_dma_map_single_attrs()方法中使用的值相同。如果该值为空,该方法的行为类似于ib_dma_unmap_single()方法。

IB DMA map page()方法

ib_dma_map_page()方法将物理页面映射到 DMA 地址。它将返回一个需要用ib_dma_mapping_error()方法检查错误的 DMA 地址:

static inline u64 ib_dma_map_page(struct ib_device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction);

  • dev:将在其上创建 DMA 地址的 RDMA 设备。
  • page:DMA 要映射的物理页面地址。
  • offset:页面内开始注册的偏移量。
  • size:区域的大小,以字节为单位。
  • direction:DMA 的方向。如前所述。

ib_dma_unmap_page()方法

ib_dma_unmap_page()方法取消了使用ib_dma_map_page()方法分配的 DMA 映射:

static inline void ib_dma_unmap_page(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction direction);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • addr:要取消映射的 DMA 地址。
  • size:要取消映射的区域的大小,以字节为单位。该值必须与在ib_dma_map_page()方法中使用的值相同。
  • direction:DMA 的方向。该值必须与在ib_dma_map_page()方法中使用的值相同。

IB DMA map SG()方法

ib_dma_map_sg()方法将一个分散/聚集列表映射到一个 DMA 地址。它将在成功时返回一个非零值,在失败时返回 0。

static inline int ib_dma_map_sg(struct ib_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

  • dev:将在其上创建 DMA 地址的 RDMA 设备。
  • sg:要映射的分散/聚集条目的数组。
  • nents:在sg中分散/聚集条目的数量。
  • direction:DMA 的方向。如前所述。

ib_dma_unmap_sg()方法

ib_dma_unmap_sg()方法取消了使用ib_dma_map_sg()方法分配的 DMA 映射:

static inline void ib_dma_unmap_sg(struct ib_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • sg:要取消映射的分散/聚集条目的数组。该值必须与在ib_dma_map_sg()方法中使用的值相同。
  • nents:在sg中分散/聚集条目的数量。该值必须与在ib_dma_map_sg()方法中使用的值相同。
  • direction:DMA 的方向。该值必须与在ib_dma_map_sg()方法中使用的值相同。

ib_dma_map_sg_attr()方法

ib_dma_map_sg_attr()方法根据 DMA 属性将分散/聚集列表映射到 DMA 地址。如果成功,它将返回一个非零值,如果失败,将返回 0。

static inline int ib_dma_map_sg_attrs(struct ib_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction, struct dma_attrs *attrs);

  • dev:将在其上创建 DMA 地址的 RDMA 设备。
  • sg:要映射的分散/聚集条目的数组。
  • nents:在sg中分散/聚集条目的数量。
  • direction:DMA 的方向。如前所述。
  • attrs:映射的 DMA 属性。如果该值为 NULL,该方法的行为类似于ib_dma_map_sg()方法。

ib_dma_unmap_sg()方法

ib_dma_unmap_sg()方法取消了使用ib_dma_map_sg()方法完成的 DMA 映射:

static inline void ib_dma_unmap_sg_attrs(struct ib_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction, struct dma_attrs *attrs);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • sg:要取消映射的分散/聚集条目的数组。该值必须与在ib_dma_map_sg_attrs()方法中使用的值相同。
  • nents:在sg中分散/聚集条目的数量。该值必须与在ib_dma_map_sg_attrs()方法中使用的值相同。
  • direction:DMA 的方向。该值必须与在ib_dma_map_sg_attrs()方法中使用的值相同。
  • attrs:映射的 DMA 属性。该值必须与在ib_dma_map_sg_attrs()方法中使用的值相同。如果该值为空,该方法的行为类似于ib_dma_unmap_sg()方法。

ib _ sg _ dma _ 地址()方法

ib_sg_dma_address()方法从分散/聚集条目返回 DMA 地址。

static inline u64 ib_sg_dma_address(struct ib_device *dev, struct scatterlist *sg);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • sg:分散/聚集条目。

ib_sg_dma_len 方法

ib_sg_dma_len()方法从分散/聚集条目返回 DMA 长度。

static inline unsigned int ib_sg_dma_len(struct ib_device *dev, struct scatterlist *sg);

  • dev:创建 DMA 地址的 RDMA 设备。
  • sg:分散/聚集条目。

ib_dma_sync_single_for_cpu()方法

ib_dma_sync_single_for_cpu()方法将 DMA 区域所有权转移给 CPU。该方法必须在 CPU 访问 DMA 映射缓冲区之前调用,以便读取或修改其内容,并防止设备访问该缓冲区:

static inline void ib_dma_sync_single_for_cpu(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction dir);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • addr:要同步的 DMA 地址。
  • size:区域的大小,以字节为单位。
  • direction:DMA 的方向。如前所述。

ib_dma_sync_single_for_device 方法

ib_dma_sync_single_for_device()方法将 DMA 区域所有权转移给设备。在调用ib_dma_sync_single_for_cpu()方法之后,设备可以再次访问 DMA 映射的缓冲区之前,必须调用该方法。

static inline void ib_dma_sync_single_for_device(struct ib_device *dev, u64 addr, size_t size, enum dma_data_direction dir);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • addr:要同步的 DMA 地址。
  • size:区域的大小,以字节为单位。
  • direction:DMA 的方向。如前所述。

IB DMA alloc coherent()方法

ib_dma_alloc_coherent()方法分配一个可以被 CPU 访问的内存块,并将其映射到 DMA。如果成功,它将返回 CPU 可以访问的虚拟地址,如果失败,则返回 NULL:

static inline void *ib_dma_alloc_coherent(struct ib_device *dev, size_t size, u64 *dma_handle, gfp_t flag);

  • dev:将在其上创建 DMA 地址的 RDMA 设备。

  • size:要分配和映射的内存大小,以字节为单位。

  • direction:DMA 的方向。如前所述。

  • dma_handle:如果分配成功,将被该区域的 DMA 地址填充的指针。

  • flag:内存分配标志。可以是:

  • GFP_KERNEL:允许阻塞(不在中断中,不持有 SMP 锁)。

  • GFP_ATOMIC:防止阻塞。

ib_dma_free_coherent 方法

ib_dma_free_coherent()方法释放使用ib_dma_alloc_coherent()方法分配的内存块:

static inline void ib_dma_free_coherent(struct ib_device *dev, size_t size, void *cpu_addr, u64 dma_handle);

  • dev:在其上创建 DMA 地址的 RDMA 设备。
  • size:内存区域的大小,以字节为单位。该值必须与在ib_dma_alloc_coherent()方法中使用的值相同。
  • cpu_addr:要释放的 CPU 内存地址。这个值必须是由ib_dma_alloc_coherent()方法返回的值。
  • dma_handle:要释放的 DMA 地址。这个值必须是由ib_dma_alloc_coherent()方法返回的值。

方法的作用是

ib_reg_phys_mr()方法获取一组物理页面,注册它们并准备一个可以被 RDMA 设备访问的虚拟地址。如果成功,它将返回一个指向新分配的 MR 的指针,或者一个 ERR_PTR(),指明失败的原因。

struct ib_mr *ib_reg_phys_mr(struct ib_pd *pd, struct ib_phys_buf *phys_buf_array, int num_phys_buf, int mr_access_flags, u64 *iova_start);

  • pd:与该 MR 关联的 PD。
  • phys_buf_array:内存区域使用的物理缓冲区数组。
  • num_phys_buf:在phys_buf_array中物理缓冲区的数量。
  • mr_access_flags:之前指定的该先生允许的操作。
  • iova_start:指向与该区域相关联的所请求的 I/O 虚拟地址的指针,其被允许在第一物理缓冲器内的任何地方开始。RDMA 设备将使用该区域的实际 I/O 虚拟地址来设置该值。该值可能与请求的值不同。

ib_phys_buf 结构

物理缓冲区由struct ib_phys_buf表示。

struct ib_phys_buf {
    u64      addr;
    u64      size;
};

  • addr:缓冲区的物理地址。
  • size:缓冲区的大小。

ib_rereg_phys_mr()方法

ib_rereg_phys_mr()方法修改现有内存区域的属性。这个方法可以被认为是对ib_dereg_mr()方法的调用,随后是对ib_reg_phys_mr()方法的调用。在可能的情况下,资源被重用,而不是被释放和重新分配。如果成功,它将返回 0,或者返回 errno 值,并说明失败的原因:

int ib_rereg_phys_mr(struct ib_mr *mr, int mr_rereg_mask, struct ib_pd *pd, struct ib_phys_buf *phys_buf_array, int num_phys_buf, int mr_access_flags, u64 *iova_start);

  • mr:要重新注册的内存区域。

  • mr_rereg_mask:要更改的存储区属性。它是掩码的按位“或”运算:

  • IB_MR_REREG_TRANS:修改这个内存区域的内存页面。

  • IB_MR_REREG_PD:修改该存储区的 PD。

  • IB_MR_REREG_ACCESS:修改该存储区允许的操作。

  • pd:该存储区将关联的新保护域。

  • phys_buf_array:要使用的新物理页面。

  • num_phys_buf:要使用的物理页数。

  • mr_access_flags:该存储区新允许的操作。

  • iova_start:该内存区域的新 I/O 虚拟地址。

ib 查询 mr()方法

ib_query_mr()方法检索特定 MR 的属性。如果成功,它将返回 0,如果失败,则返回 errno 值。

int ib_query_mr(struct ib_mr *mr, struct ib_mr_attr *mr_attr);

  • mr:要查询的 MR。
  • mr_attr:MR 属性,如下一节所述。

MR 属性由struct ib_mr_attr表示。

ib_mr_attr 结构

struct ib_mr_attr {
    struct ib_pd    *pd;
    u64        device_virt_addr;
    u64        size;
    int        mr_access_flags;
    u32        lkey;
    u32        rkey;
};

  • pd:MR 关联的 PD。
  • device_virt_addr:该 MR 覆盖的虚拟块的地址。
  • size:内存区域的大小,以字节为单位。
  • mr_access_flags:该内存区域的访问权限。
  • lkey:该内存区域的本地键。
  • rkey:该存储区的遥控键。

ib_dereg_mr 方法

ib_dereg_mr()方法取消一个 MR 的注册。如果一个内存窗口绑定到它,这个方法可能会失败。如果成功,它将返回 0,或者返回 errno 值,并说明失败的原因:

int ib_dereg_mr(struct ib_mr *mr);

  • mr:要注销的 MR。

十七、附录 B:网络管理

本附录回顾了一些最流行的网络管理和调试工具。这些工具在寻找常见问题的解决方案以及开发、调试、基准测试、分析、故障排除和研究网络项目方面帮助很大。这些工具中的大多数都有非常好的文档资源,或者是手册页,或者是 wiki 页,互联网上还有很多关于它们的其他信息资源。他们中的许多人都有活跃的邮件列表(针对用户和开发人员)和错误报告系统。这里描述了一些最常用的工具,具体说明了它们的用途和相关链接,并附有几个例子。本附录中提到的工具按字母顺序排列。

arp

该命令用于 ARP 表管理。用法示例:

您可以通过从命令行运行arp来显示 ARP 表。arp –n将显示没有名称解析的 ARP 表。

您可以通过以下方式向 ARP 表添加静态条目:

arp –s 192.168.2.10 00:e0:4c:11:22:33

arp实用程序属于net-tools包。网址:http://net-tools.sourceforge.net

解析

发送 ARP 请求的实用程序。–D标志用于重复地址检测(DAD)。arping实用程序属于iputils包。网址:http://www.skbuff.net/iputils/

可接受

一个用户空间工具,用于为基于 Linux 的 ARP 规则防火墙配置规则。网址:http://ebtables.sourceforge.net/

arpwatch

一个用于监控 ARP 流量的用户空间工具。网址:http://ee.lbl.gov/

ApacheBench (ab)

用于测量 HTTP web 服务器性能的命令行实用程序。ApacheBench 工具是 Apache 开源项目的一部分。在许多发行版中(例如 Ubuntu ),它是apache2-utils包的一部分。用法示例:

ab -n 100  http://www.google.com/

-n选项是为基准测试会话执行的请求数量。

brctl

用于管理以太网网桥的命令行实用程序,支持网桥配置的设置。brctl实用程序属于bridge-utils包。用法示例:

  • brctl addbr mybr:添加一个名为mybr的桥。
  • brctl delbr mybr:删除名为mybr的桥。
  • brctl addif mybr eth1:添加eth1接口到桥上。
  • brctl delif mybr eth1:从桥上删除eth1接口。
  • brctl show:显示网桥及其连接端口的信息。

这个bridge-utils包的维护者是 Stephen Hemminger。获取git存储库可以通过以下方式完成:

git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/bridge-utils.git

网址:http://www.linuxfoundation.org/collaborate/workgroups/networking/bridge

连接跟踪工具

一组用于管理 netfilter 连接跟踪的用户空间工具。它由一个用户空间守护进程conntrackd和一个命令行工具conntrack组成。网址:http://conntrack-tools.netfilter.org/

crtools

用于进程检查点/恢复的实用程序。网址:http://criu.org/Installation

ebtables

一个用户空间工具,用于为基于 Linux 的桥接防火墙配置规则。网址:http://ebtables.sourceforge.net/

以太尾流

一个实用程序发送唤醒局域网魔术包。ether-wake实用程序属于net-tools包。

ethtool实用程序提供了一种查询或控制网络驱动程序和硬件设置、获取统计数据、获取诊断信息等的方法。使用ethtool您可以控制以太网设备的参数,如速度、双工、自动协商和流量控制。ethtool的许多特性需要网络驱动程序代码的支持。

示例:

  • ethtool eth0:的输出

    Settings for eth0:
            Supported ports: [ TP MII ]
            Supported link modes:   10baseT/Half 10baseT/Full
                                    100baseT/Half 100baseT/Full
                                    1000baseT/Half 1000baseT/Full
            Supported pause frame use: No
            Supports auto-negotiation: Yes
            Advertised link modes:  10baseT/Half 10baseT/Full
                                    100baseT/Half 100baseT/Full
                                    1000baseT/Half 1000baseT/Full
            Advertised pause frame use: Symmetric Receive-only
            Advertised auto-negotiation: Yes
            Speed: 10Mb/s
            Duplex: Half
            Port: MII
            PHYAD: 0
            Transceiver: internal
            Auto-negotiation: on
            Supports Wake-on: pumbg
            Wake-on: g
            Current message level: 0x00000033 (51)
                                   drv probe ifdown ifup
            Link detected: no
    
    
  • 获取卸载参数由:ethtool –k eth1完成。

  • 卸载参数的设置由ethtool –K eth1 offLoadParamater完成。

  • 通过ethtool -i eth1向网络设备查询相关的驱动程序信息。

  • 显示统计数据是由:ethtool -S eth1完成的(注意,并不是所有的网络设备驱动程序都实现了这个特性)。

  • 显示永久硬件(MAC)地址:ethtool -P eth0

通过向netdev邮件列表发送补丁来完成ethtool的开发。撰写本文时,ethtool的维护者是本·休金斯。ethtool项目是在git存储库上开发的。可以通过:git clone git://git.kernel.org/pub/scm/network/ethtool/ethtool.git下载。

网址:www.kernel.org/pub/software/network/ethtool/

由 Linus Torvalds 发起的分布式版本控制系统。Linux 内核开发,以及很多 Linux 相关的项目都是由git管理的。也可以使用git send-email命令通过邮件发送补丁。网站:??。

hciconfig

用于配置蓝牙设备的命令行工具。使用hciconfig,您可以显示蓝牙接口类型(BR/EDR 或 AMP)、蓝牙地址、标志等信息。hciconfig工具属于bluez包。示例:

hciconfig
hci0:   Type: BR/EDR  Bus: USB
        BD Address: 00:02:72:AA:FB:94  ACL MTU: 1021:7  SCO MTU: 64:1
        UP RUNNING PSCAN
        RX bytes:964 acl:0 sco:0 events:41 errors:0
        TX bytes:903 acl:0 sco:0 commands:41 errors:0

网址:http://www.bluez.org/

hcidump

一个命令行实用程序,用于转储来自和去往蓝牙设备的原始 HCI 数据。hcidump实用程序属于bluez-hcidump包。网址:http://www.bluez.org/

六氯苯酚

一个命令行实用程序,用于配置蓝牙连接和向蓝牙设备发送一些特殊命令。例如,您可以通过hcitool scan扫描附近的蓝牙设备。hcitool实用程序属于bluez-hcidump包。

ifconifg

ifconfig命令允许您配置各种网络接口参数,包括设备的 IP 地址、MTU、MAC 地址、Tx 队列长度(txqueuelen)、标志等等。ifconfig工具属于net-tools包,它比iproute2包更老(将在本附录后面讨论)。下面是三个用法示例:

  • ifconfig eth0 mtu 1300:将 MTU 改为 1300。
  • ifconfig eth0 txqueuelen 1100:将发送队列长度改为 1100。
  • ifconfig eth0 –arp:禁用eth0上的 ARP 协议。

网址:http://net-tools.sourceforge.net

ifnslave

用于将从属网络设备连接到绑定设备和从绑定设备分离的实用程序。绑定是将多个物理以太网设备放入一个逻辑设备中,这通常被称为链路聚合/中继/链路捆绑。源文件在Documentation/networking/ifenslave.c里。例如,您可以通过以下方式将eth0连接到焊接设备bond0:

ifenslave bond0 eth0

ifenslave实用程序属于iputils包,由 Yoshifuji Hideaki 维护。网址:www.skbuff.net/iputils/

iperf

iperf项目是一个开源项目,它提供了一个基准测试工具来测量 TCP 和 UDP 带宽性能。它允许您调整各种参数。iperf工具报告带宽、延迟抖动和数据报丢失。它最初是由应用网络研究国家实验室的分布式应用支持小组(DAST)用 C++开发的。它以客户机-服务器模式工作。从零开始的一个新实现iperf3,不向后兼容原来的iperf,可从https://code.google.com/p/iperf/获得。据说iperf3有一个更简单的代码库。iperf3工具还可以报告客户机和服务器的平均 CPU 利用率。

使用 iperf

下面是一个使用iperf测量 TCP 性能的简单例子。在一台设备(IP 地址为 192.168.2.104)上,运行下一个命令,启动服务器端(默认情况下,它是端口 5001 上的 TCP 套接字):

iperf -s

在第二台设备上,运行iperf TCP 客户端以连接到iperf服务器:

iperf -c 192.168.2.104

在客户端,您将看到以下内容:

------------------------------------------------------------
Client connecting to 192.168.2.104, TCP port 5001
TCP window size: 22.9 KByte (default)
------------------------------------------------------------
[  3] local 192.168.2.200 port 35146 connected with 192.168.2.104 port 5001

默认时间间隔是 10 秒。10 秒钟后,客户端将断开连接,您将在终端上看到如下消息:

[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.3 sec  7.62 MBytes  6.20 Mbits/sec

您可以调整iperf的许多参数,例如:

  • –u:用于使用 UDP 套接字。
  • -t:使用不同的时间间隔,以秒为单位,而不是默认的 10 秒。
  • -T:设置组播的 TTL(默认为 1)。
  • -B:绑定到主机、接口或组播地址。

参见man iperf。网址:http://iperf.sourceforge.net/

iproute2

iproute2包为用户空间和内核网络子系统之间的交互提供了许多工具。最广为人知的是ip命令。它基于 netlink 套接字(在第二章的中讨论)。使用ip命令,你可以在广泛的网络区域进行各种操作,它有无数的选项;参见man 8 ip。以下是几个使用ip命令完成各种任务的例子:

  • 使用ip addr配置网络设备:

  • ip addr add 192.168.0.10/24 dev eth0:在eth0上设置一个 IP 地址。

  • ip addr show:显示所有网络接口的地址(IPv4 和 IPv6)。

参见man ip address

  • 使用ip link配置网络设备:

  • ip link add mybr type bridge:创建一个名为mybr的桥。

  • ip link add name myteam type team:创建一个名为myteam的分组设备。(分组设备驱动程序将多个物理以太网设备聚合成一个逻辑设备,实际上是新的绑定设备。组队驱动在第十四章中讨论。)

  • ip link set eth1 mtu 1450:设置eth1的 MTU 为 1450。

参见man ip link

  • ARP 表(IPv4)和 NDISC (IPv6)表的管理:

  • ip neigh show:显示 IPv4 邻居表(ARP 表)和 IPv6 邻居表。

  • ip -6 neigh show:仅显示 IPv6 邻居表。

  • ip neigh flush dev eth0:从与eth0相关联的相邻表中删除所有条目。

  • ip neigh add 192.168.2.20 dev eth2 lladdr 00:11:22:33:44:55 nud permanent:添加永久邻居条目(与在 ARP 表中添加静态条目并行)。

  • ip neigh change 192.168.2.20 dev eth2 lladdr 55:44:33:22:11:00 nud permanent:更新邻居条目。

参见man ip neighbour

  • 邻居表参数的管理:

  • ip ntable show:显示邻居表参数。

  • ip ntable change name arp_cache locktime 1200 dev eth0:改变与eth0关联的 IPv4 邻居表的locktime参数。

参见man ip ntable

  • 网络命名空间管理:

  • ip netns add myNamespace:添加一个名为myNamespace的网络名称空间。

  • ip netns del myNamespace:删除名为myNamespace的网络名称空间。

  • ip netns list:显示主机上的所有网络名称空间。

  • ip netns monitor:为通过ip netns命令添加或删除的每个网络名称空间显示一行屏幕。

参见man ip netns

  • 多播地址的配置:

  • ip maddr show:显示主机上的所有组播地址(IPv4 和 IPv6)。

  • ip maddr add 00:10:02:03:04:05 dev eth1:在eth1上添加组播地址。

参见man ip maddress

  • 监控 netlink 消息。例如:

  • ip monitor route在屏幕上显示关于各种网络事件的消息,如添加或删除路由。

参见man ip monitor

  • 路由表的管理:

  • ip route show:显示路由表。

  • ip route flush dev eth1:从路由表中删除与eth1相关的路由条目。

  • ip route add default via 192.168.2.1:添加 192.168.2.1 作为默认网关.

  • ip route get 192.168.2.10:获取到 192.168.2.10 的路由并显示。

参见man ip route

  • RPDB(路由策略数据库)中的规则管理。例如:

  • ip rule add tos 0x02 table 200:添加一个规则,设置路由子系统在路由表 252 中查找 TOS 值为 0x02 的数据包(TOS 是 IPv4 报头中的一个字段)。

  • ip rule del tos 0x02 table 200:从 RPDB 中删除指定的规则。

  • ip rule show:显示 RPDB 中的规则。

参见man ip rule

  • 调谐器/TAP 设备管理:

  • ip tuntap add tun1 mode tun:创建一个名为tun1的调谐器设备。

  • ip tuntap del tun1 mode tun:删除名为tun1的调谐器设备。

  • ip tuntap add tap1 mode tap:创建一个名为tap1的点击设备。

  • ip tuntap del tap1 mode tap:删除名为tap1的 TAP 设备。

  • IPsec 策略的管理:

  • ip xfrm policy show:显示 IPsec 策略。

  • ip xfrm state show:显示 IPsec 状态。

参见man ip xfrm

ss工具用于转储套接字统计数据。比如跑步

ss -t –a

将显示所有 TCP 套接字:

State       Recv-Q Send-Q          Local Address:Port              Peer Address:Port
LISTEN      0      32                          *:ftp                          *:*
LISTEN      0      128                         *:ssh                          *:*
LISTEN      0      128                 127.0.0.1:ipp                          *:*
ESTAB       0      0               192.168.2.200:ssh              192.168.2.104:52089
ESTAB       0      52              192.168.2.200:ssh              192.168.2.104:51352
ESTAB       0      0               192.168.2.200:ssh              192.168.2.104:51523
ESTAB       0      0               192.168.2.200:59532           107.21.231.190:http
LISTEN      0      128                        :::ssh                         :::*
LISTEN      0      128                       ::1:ipp                         :::*
CLOSE-WAIT  1      0                         ::1:48723                      ::1:ipp

iproute2还有其他工具:

  • bridge:显示/操作网桥地址和设备。例如:

  • bridge fdb show:显示转发条目。

参见man bridge

  • genl:获取注册的通用 netlink 系列的信息(如 id、标题大小、最大属性等)。例如,运行genl ctrl list可以得到这样的结果:

    Name: nlctrl
            ID: 0x10  Version: 0x2  header size: 0  max attribs: 7
            commands supported:
                    #1:  ID-0x3
                    Capabilities (0xe):
                      can doit; can dumpit; has policy
    
            multicast groups:
                    #1:  ID-0x10  name: notify
    
    
  • lnstat:显示 Linux 网络统计。

  • rtmon:监控 Rtnetlink 套接字。

  • tc:显示/操作交通控制设置。例如:

  • tc qdisc show:运行该命令显示安装了哪个队列规程(qdisc)条目,例如:

    qdisc pfifo_fast 0: dev eth1 root refcnt 2 bands 3 priomap  1 2 . . .
    
    
  • 这表明pfifo_fast qdisceth1网络设备相关联。?? 是一个无类排队规则,是 Linux 中默认的 ??。

  • tc -s qdisc show dev eth1: Shows statistics of the qdisc associated to eth1.

    参见man tc

    参见:Linux 高级路由和流量控制 HOWTO: www.lartc.org/howto/

通过向netdev邮件列表发送补丁来完成iproute2的开发。撰写本文时,ethtool的维护者是斯蒂芬·海明格。iproute2是在git资源库上开发的,可以通过git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git下载。

iptables 和 iptables6

iptablesiptables6分别是用于 IPv4 和 IPv6 的包过滤和 NAT 管理的管理工具。使用iptables / iptables6,您可以定义规则列表。每一条这样的规则都告诉我们应该对数据包做什么(例如,丢弃它还是接受它)。每个规则为数据包指定一些匹配条件,例如,它将是 UDP 数据包。以下是使用iptables命令的一些例子:

  • iptables -A INPUT -p tcp --dport=80 -j LOG --log-level 1:该规则的意思是,目的端口为 80 的传入 TCP 数据包将被转储到系统日志中。
  • iptables –L:列出过滤表中的所有规则。(命令中没有提到表,所以它访问过滤器表,这是默认的表。)
  • iptables –t nat –L:列出 NAT 表中的所有规则。
  • iptables –F:刷新选中的表格。
  • iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE:设置一个伪装规则。

网址:www.netfilter.org/

ipvsadm

Linux 虚拟服务器管理工具。网址:www.linuxvirtualserver.org/software/ipvs.html

iw

显示/操作无线设备及其配置。iw包基于通用的 netlink 套接字(见第二章)。例如,您可以执行以下操作:

  • iw dev wlan0 scan:扫描附近的无线设备。
  • iw wlan0 station dump:显示电台的统计数据。
  • iw list:获取关于无线设备的信息(例如频带信息和 802.11n 信息)。
  • iw dev wlan0 get power_save–进入省电模式。
  • iw dev wlan0 set type ibss:将无线接口模式更改为 ibss (Ad-Hoc)。
  • iw dev wlan0 set type mesh:将无线接口模式改为网状模式。
  • iw dev wlan0 set type monitor:将无线接口模式改为监控模式。
  • iw dev wlan0 set type managed:将无线接口模式改为管理模式。

参见man iw

Gitweb: http://git.kernel.org/cgit/linux/kernel/git/jberg/iw.git

网址:http://wireless.kernel.org/en/users/Documentation/iw

尤克里里琴

管理无线设备的旧工具。iwconfig属于wireless-tools包,基于 IOCTLs。网址:www.hpl.hp.com/personal/Jean_Tourrilhes/Linux/Tools.html

libreswan 项目

openswan版本 2.6.38 派生出来的 IPsec 软件解决方案。网址:http://libreswan.org/

l2ping

一个命令行实用程序,用于通过蓝牙设备发送 L2CAP 回应请求和接收应答。l2ping实用程序属于bluez包。网址:www.bluez.org/

低盘工具

一组管理 Linux LoWPAN 堆栈的实用程序。网址:http://sourceforge.net/projects/linux-zigbee/files/linux-zigbee-sources/0.3/

lshw

显示机器硬件配置信息的实用程序。网址:http://ezix.org/project/wiki/HardwareLiSter

lscpu

用于显示系统上 CPU 信息的实用程序。它基于来自/proc/cpuinfosysfs的信息。lscpu属于util-linux包。

LSPCI

一个实用程序,用于显示系统中 PCI 总线以及与之相连的设备的信息。有时你需要用lspci命令获得一些关于 PCI 网络设备的信息。lspci实用程序属于pciutils包。网址:http://mj.ucw.cz/sw/pciutils/

mrouted

多播路由守护程序,实现 IPv4 距离矢量多播路由协议(DVMRP ),该协议在 1988 年的 RFC 1075 中指定。网址:http://troglobit.com/mrouted.html

nc

跨网络读写数据的命令行实用程序。nc属于nmap-ncat包。网址:http://nmap.org/

一个命令行工具,基于著名的grep命令,允许您指定扩展表达式来匹配数据包的数据负载。它可以识别以太网、PPP、SLIP、FDDI 和空接口上的 TCP、UDP 和 ICMP。网址:http://ngrep.sourceforge.net/

netperf

Netperf 是一个网络基准测试工具。网址:www.netperf.org/netperf/

网络嗅探

netsniff-ng是一个开源的项目网络工具包,它可以帮助分析网络流量、执行压力测试、以非常高的速度生成数据包等等。它使用 PF_PACKET 零拷贝环(TX 和 RX)。它提供的工具如下:

  • netsniff-ng是一个快速的零拷贝分析器,pcap捕获和重放工具。与本附录中提到的许多工具不同,netsniff-ng工具是特定于 Linux 的,不支持其他操作系统。示例:运行netsniff-ng --in eth1 --out dump.pcap -s -b 0会创建一个可以被wiresharktcpdump读取的pcap文件。–s标志用于静音,–b 0 用于绑定到 CPU 0。参见man netsniff-ng
  • trafgen是一个零拷贝高性能网络数据包流量生成器实用程序。
  • ifpps是一个小工具,定期从内核提供类似 top 的网络和系统统计数据。ifpps直接从procfs文件中收集数据。
  • bpfc是一个小型的 Berkeley 包过滤汇编器和编译器。

正在获取git存储库:git clone git://github.com/borkmann/netsniff-ng.git。网址:http://netsniff-ng.org/

网表

netstat工具使您能够打印多播成员、路由表、网络连接、接口统计、套接字状态等等。netstat工具属于net-tools包。有用的标志:

  • netstat –s:显示每个协议的汇总统计数据。
  • netstat –g:显示 IPv4 和 IPv6 的组播组成员信息。
  • netstat -r:显示内核 IP 路由表。
  • netstat –nl:显示监听套接字(-n标志用于显示数字地址,而不是试图确定符号主机、端口或用户名)。
  • netstat –aw:显示所有原始套接字。
  • netstat –ax:显示所有 Unix 套接字。
  • netstat –at:显示所有 TCP 套接字。
  • netstat –au:显示所有 UDP 套接字。

网址:http://net-tools.sourceforge.net

nmap(网络映射器)

Nmap 是一个开源安全项目,它提供了一个网络探索和探测工具以及一个安全/端口扫描器。它具有端口扫描(检测目标主机上开放的端口)、操作系统检测、检测 MAC 地址等功能。例如,

nmapwww.google.com

可以给出如下输出:

Starting Nmap 6.00 (http://nmap.org) at 2013-09-26 16:37 IDT
Nmap scan report forwww.google.com(212.179.154.227)
Host is up (0.013s latency).
Other addresses forwww.google.com(not scanned): 212.179.154.221 212.179.154.251 212.179.154.232 212.179.154.237 212.179.154.216 212.179.154.231 212.179.154.241 212.179.154.247 212.179.154.222 212.179.154.226 212.179.154.236 212.179.154.246 212.179.154.212 212.179.154.217 212.179.154.242
Not shown: 998 filtered ports
PORT    STATE SERVICE
80/tcp  open  http
443/tcp open  https
Nmap done: 1 IP address (1 host up) scanned in 5.24 seconds

nmapnping实用程序可用于生成 ARP 中毒、网络压力测试和拒绝服务攻击的原始数据包,以及像普通的ping实用程序一样测试连通性。您可以使用nping实用程序在生成的流量中设置 IP 选项。参见http://nmap.org/book/nping-man-ip-options.html。网址:http://nmap.org/

open wan

一个实现基于 IPsec 的 VPN 解决方案的开源项目。它基于 FreeS/WAN 项目。网址:www.openswan.org/projects/openswan

openvpn

一个基于 SSL/TLS 实现 VPN 的开源项目。网址:www.openvpn.net/

包装

一个基于以太网的包生成器工具。该工具同时具有 GUI 和 CLI。网址:http://packeth.sourceforge.net/packeth/Home.html

通过发送 ICMP 回应请求消息来测试连通性的著名实用程序。以下是本书中提到的四个有用的选项:

  • -Q tos:允许设置 ICMP 数据包中的服务质量位。本附录中提到的关于tshark过滤器的解释。
  • -R:设置记录路由 IP 选项(在第四章中讨论)。
  • -T:设置时间戳 IP 选项(在第四章中讨论)。
  • -f:洪水平。
  • 更多命令行选项见man ping

ping实用程序属于iputils包。网站:。www.skbuff.net/iputils/

pipd

一个开源的轻量级独立协议独立多播-稀疏模式(PIM-SM) v2 多播守护程序。由约阿希姆·尼尔森维护。参见http://troglobit.com/pimd.htmlgit储存库:https://github.com/troglobit/pimd/

弹出型按钮

PPTP Linux 服务器。网址:http://poptop.sourceforge.net/dox/

购买力平价

一个开源的 PPP 守护进程。git储存库:git://ozlabs.org/∼paulus/ppp.git。网址:http://ppp.samba.org/download.html

PK gen

pktgen内核模块(net/core/pktgen.c)可以以非常高的速度生成数据包。监视和控制是通过写入/proc/net/pktgen条目完成的。关于“如何使用 linux 包生成器”,请参见Documentation/networking/pktgen.txt

rad VD

这是 IPv6 的路由器广告守护程序。这是一个由 Reuben Hawkins 维护的开源项目。它可用于 IPv6 无状态自动配置和重新编号。网址:www.litech.org/radvd/git储存库:https://github.com/reubenhwk/radvd

路线

用于路由表管理的命令行工具。它属于net-tools包,该包基于 IOCTLs,比iproute2包更老。示例:

  • route –n:显示没有名称解析的路由表。
  • route add default gateway 192.168.1.1:添加 192.168.1.1 作为默认网关。
  • route –C:显示路由缓存(请记住,内核 3.6 中删除了 IPv4 路由缓存;参见第五章中的“IPv4 路由缓存”部分。

参见man route

RP-PPPoE

适用于 Linux 和 Solaris 系统的开源以太网 PPP(PPPoE)客户端。网址:www.roaringpenguin.com/products/pppoe

sar

一个命令行工具,用于收集和报告有关系统活动的统计信息。它是sysstat包的一部分。例如,运行以下命令将显示四次 CPU 统计信息,时间间隔为 1 秒,最后显示平均值:

sar 1 4
Linux 3.6.10-4.fc18.x86_64 (a)  10/22/2013      _x86_64_        (2 CPU)

07:47:10 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
07:47:11 PM     all      0.00      0.00      0.00      0.00      0.00    100.00
07:47:12 PM     all      0.00      0.00      0.00      0.00      0.00    100.00
07:47:13 PM     all      0.00      0.00      0.00      0.00      0.00    100.00
07:47:14 PM     all      0.00      0.00      0.50      0.00      0.00     99.50
Average:        all      0.00      0.00      0.13      0.00      0.00     99.87

网址:http://sebastien.godard.pagesperso-orange.fr/

sm croute

用于多播路由操作的命令行工具。网址:www.cschill.de/smcroute/

放风

一个开源项目,提供网络入侵检测系统(IDS)和网络入侵防御系统(IPS)。网址:www.snort.org/

苏里南

提供 IDS/IPS 和网络安全监控引擎的开源项目。网址:http://suricata-ids.org/

strongSwan 天鹅

一个为 Linux、Android 和其他操作系统实现 IPsec 解决方案的开源项目。IKEv1 和 IKEv2 都已实现。维护者是 Andreas Steffen 教授。网址:www.strongswan.org/

系统

sysctl实用程序在运行时显示内核参数(包括网络参数)。它还可以设置内核参数。例如,sysctl –a显示所有内核参数。sysctl实用程序属于procps-ng包。

任务集

用于设置或检索进程的 CPU 关联性的命令行实用程序。taskset实用程序来自util-linux包。

`tcpdump

Tcpdump是一个开源的命令行协议分析器,可以从www.tcpdump.org获得。它基于一个名为libpcap的 C/C++网络流量捕获库。像wireshark一样,它可以将其结果写入一个文件,并从一个文件中读取它们,它支持过滤。与wireshark不同,它没有前端 GUI。但是它的输出文件可以被wireshark读取。使用tcpdump进行嗅探的示例:

tcpdump -i eth1

网址:www.tcpdump.org

顶部

top实用程序提供了系统的实时视图(内存使用、CPU 使用等参数)和系统摘要。这个实用程序是procps-ng包的一部分。网址:https://gitorious.org/procps

tracepath

tracepath命令跟踪到目的地址的路径,沿着这条路径发现 MTU。对于 IPv6 目的地址,可以使用tracepath6tracepath实用程序属于iputils包。网址:www.skbuff.net/iputils/

示踪路线

打印数据包到达某个目的地的路径。traceroute实用程序使用 IP 协议的生存时间(TTL)字段使数据包路径上的主机返回 ICMP 超时响应。在第三章的中讨论了traceroute实用程序,它处理 ICMP 协议。网址:http://traceroute.sourceforge.net

tshark

tshark实用程序提供了一个命令行包分析器。它是wireshark包的一部分。它有许多命令行选项。例如,您可以使用–w选项将输出写入文件。您可以使用tshark为包过滤设置各种过滤器,其中一些可以是复杂的过滤器(您很快就会看到)。设置过滤器以仅捕获 ICMPv4 数据包的示例:

tshark -R icmp
Capturing on eth1
17.609101 192.168.2.200 -> 81.218.16.241 ICMP 98 Echo (ping) request  id=0x0dc6, seq=1/256, ttl=64
17.617101 81.218.16.241 -> 192.168.2.200 ICMP 98 Echo (ping) reply    id=0x0dc6, seq=1/256, ttl=58

您还可以根据 IPv4 报头中的字段值设置过滤器。例如,以下命令对 IPv4 标头中的 DS 字段设置筛选器:

tshark -R "ip.dsfield==0x2"

如果您从第二个终端发送 IPv4 报头中 DS 字段为 0x2 的流量(例如,这种流量可以通过ping –Q 0x2 destinationAdderss发送),它将通过tshark显示在屏幕上。

按源 MAC 地址过滤的示例:

tshark ether src host 00:e0:4c:11:22:33

端口范围为 6000–8000 的 UDP 数据包过滤示例:

tshark -R udp portrange 6000-8000

设置过滤器以捕获源 IP 地址为 192.168.2.200 且端口为 80 的流量的示例(不一定是 TCP 流量,因为这里没有在某些指定协议上设置过滤器):

tshark -i eth1 -f "src host 192.168.2.200 and port 80"

tunctl

tunctl是一个用于创建调谐器/TAP 设备的旧工具。从http://tunctl.sourceforge.net开始提供。请注意,您也可以使用ip命令(参见本附录前面的iproute2部分)和openvpn包的openvpn命令行工具创建或删除调谐器/TAP 设备:

openvpn --mktun --dev tun1
openvpn --rmtun --dev tun1

udevadm】

您可以通过在它的sysfs条目上运行udevadm来获得网络设备类型。例如,如果设备在sysfs下有此条目:

/sys/devices/virtual/net/eth1.100

然后你可以发现它的 DEVTYPE 是 VLAN:

udevadm info -q all -p  /sys/devices/virtual/net/eth1.100/

P: /devices/virtual/net/eth1.100
E: COMMENT=net device ()
E: DEVPATH=/devices/virtual/net/eth1.100
E: DEVTYPE=vlan
E: IFINDEX=4
E: INTERFACE=eth1.100
E: MATCHADDR=00:e0:4c:53:44:58
E: MATCHDEVID=0x0
E: MATCHIFTYPE=1
E: SUBSYSTEM=net
E: UDEV_LOG=3
E: USEC_INITIALIZED=28392625695

udevadm属于udev包。网址:www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html

取消共享

unshare实用程序使您能够创建一个名称空间,并在该名称空间内运行一个程序,该程序不与其父级共享。unsare实用程序属于util-linux包。对于unshare实用程序的各种命令行选项,参见man unshare,用法示例:

unshare -u /bin/bash

这将创建一个 UTS 命名空间。

unshare --net /bin/bash

这将创建一个新的网络名称空间,其中将启动一个bash进程。Gitweb: http://git.kernel.org/cgit/utils/util-linux/util-linux.git。网址:http://userweb.kernel.org/∼kzak/util-linux/

vconfig

vconfig实用程序使您能够配置 VLAN (802.1q)接口。用法示例:

  • vconfig add eth2 100:增加一个 VLAN 接口。这将创建一个 VLAN 接口eth2.100
  • vconfig rem eth2.100:移除eth2.100 VLAN 接口。
  • 请注意,您还可以使用ip命令添加和删除 VLAN 接口,例如,如下所示:
  • ip link add link eth0 name eth0.100 type vlan id 100
  • vconfig set_egress_map eth2.100 0 4:将 SKB 优先级 0 映射到 VLAN 优先级 4,这样 SKB 优先级为 0 的出站数据包将被标记为 4 作为 VLAN 优先级。默认的 VLAN 优先级为 0。
  • vconfig set_ingress_map eth2.100 1 5:将 VLAN 优先级 5 映射到 SKB 优先级 1,这样 VLAN 优先级为 5 的传入数据包将与 SKB 优先级 1 一起排队。默认的 SKB 优先级为 0。

参见man vconfig

注意,如果 VLAN 支持被编译成一个内核模块,那么在试图添加 VLAN 接口之前,您必须通过modprobe 8021q加载 VLAN 内核模块。网址:www.candelatech.com/∼greear/vlan.html

wpa _ 恳求者

为 Linux 和其他操作系统提供无线请求程序的开源软件。它支持 WPA 和 WPA2。网址:http://hostap.epitest.fi/wpa_supplicant/

wireshark

项目提供了一个免费的开源分析器(“嗅探器”)。它有两种风格:基于 GTK+的前端 GUI 和命令行,即tshark实用程序(在本附录前面提到过)。它可以在许多操作系统上使用,并且是动态发展的:当新的特性被添加到现有协议和新的协议中时,新的解析器(“解析器”)被修改或添加。Wireshark 有许多功能:

  • 支持定义各种过滤器(端口、目的地或源地址、协议标识符、报头中的字段等)。
  • 允许根据各种参数(协议类型、时间等)对结果进行排序。
  • 将嗅探器输出保存到文件中/从文件中读取嗅探器输出。
  • 读取/写入许多不同的捕获文件格式:tcpdump ( libpcap)、Pcap NG 等等。
  • 捕获过滤器和显示过滤器。

激活wiresharkthsark嗅探器会将网络接口置于混杂模式,使其能够处理目的地不是本地主机的数据包。手册页中提供了很多信息:man wiresharkman tshark。你可以在http://wiki.wireshark.org/SampleCaptures找到超过 75 种不同协议的嗅探样本。Wireshark 用户邮件列表:www.wireshark.org/mailman/listinfo/wireshark-users。网址:www.wireshark.org。维基:http://wiki.wireshark.org/

索普

一个开源项目,实现了各种路由协议,如 BGP、IGMP、OLSR、OSPF、PIM 和 RIP。XORP 这个名字来源于可扩展的开放式路由器平台。网址:www.xorp.org/。`

十八、附录 C:词汇表

本书涵盖了以下词汇表中的术语。

ACL—异步面向连接的链路。蓝牙协议。

亚行 —安卓调试桥。

av DTP—音视频分发传输协议。蓝牙协议。

AEAD—关联数据的认证加密。

AES-NI —AES 指令集。

AH—认证头协议。用于 IPsec,协议编号为 51。

援助— 关联 ID 。无线客户端在关联到接入点时获得的唯一号码。它由接入点分配,范围为 1–2007。

AMP—候补麦克/PHY 。

AMPDU—聚合 Mac 协议数据单元。IEEE 802.11n 中的一种分组聚合

AMSDU—聚合 Mac 业务数据单元。IEEE 802.11n 中的一种分组聚合

AOSP —Android 开源项目。

接入点—接入点。在无线网络中,无线客户端关联的无线设备,它使客户端能够连接到有线网络。

API—应用编程接口。定义软件层接口的一组方法和数据结构,例如库的接口。

ABRO—权威的边界路由器选项。为 IPv6 的邻居发现优化添加。参见 RFC 6775。

ABS— Android Builders 峰会。

ARO—地址注册选项。为 IPv6 的邻居发现优化添加。参见 RFC 6775。

ARP—地址解析协议。一种协议,用于查找网络地址(如 IPv4 地址)到链路层地址(如 48 位以太网地址)之间的映射。

ARPD —ARP 守护进程。实现 ARP 功能的用户空间守护进程。

Ashmem****——安卓共享内存。

ASM—任意源组播。在任意源模型中,您不需要指定从单个特定源地址或一组地址接收多播流量。

IEEE 802.11n 中使用的 BA— 块确认机制

BGP—边界网关协议。核心路由协议。

BLE—蓝牙低能耗。

BNEP—蓝牙网络封装协议。

BTH—基地运输头。12 字节的 InfiniBand 报头。它指定了源和目的 QPs、操作、包序列号和分区。

InfiniBand 堆栈中的 CM— 通信管理器。

CIDR—无类域间路由。一种分配用于域间路由的 Internet 地址的方式。

CQ—完成队列 (InfiniBand)。

CRIU —用户空间中的检查点/恢复。CRIU 是一个软件工具,主要在用户空间中实现,使用它你可以冻结一个正在运行的进程,并把它作为一个文件集合检查到一个文件系统中。然后,您可以使用这些文件从应用冻结的地方恢复和运行应用。参见http://criu.org/Main_Page

CSMA/CD—载波侦听多路访问/冲突检测。一种用于以太网的媒体访问控制方法。

CSMA/加州— 载波侦听多路访问/冲突避免。一种用于无线网络的媒体访问控制方法。

CT—连接追踪。作为 NAT 基础的网络过滤层。

爸爸—重复地址检测。DAD 是一种机制,有助于检测 LAN 中不同主机上是否存在双 L3 地址。

DAC—重复地址确认。RFC 6775 中添加的 ICMPv6 类型,数值为 158。

DAR—重复地址请求。RFC 6775 中添加的 ICMPv6 类型,数值为 157。

DCCP—数据报拥塞控制协议。一种不可靠的拥塞控制传输层协议。例如,在要求低延迟且允许少量数据丢失的应用(如电话和流媒体应用)中,使用 DCCP 是有意义的。

DHCP—动态主机配置协议。一种协议,用于配置网络设备参数,如 IP 地址、默认路由以及一个或多个 DNS 服务器地址。

DMA—直接内存访问。

DNAT—目的地 NAT 。改变目的地址的 NAT。

DNS—域名系统。一种将域名转换成 IP 地址的系统。

DSCP—区分服务码点。分类机制。

DVMRP—距离矢量组播路由协议。路由多播数据报的协议。适合在自治系统中使用。在 1988 年的 RFC 1075 中定义。

ECN—显式拥塞通知。请参见 RFC 3168,“在 IP 中添加显式拥塞通知(ECN)”

EDR—增强型数据速率。

EGP—外部网关协议。现在被认为已经过时的路由协议。它于 1982 年在 RFC 827 中首次被正式化。

ERTM—增强型重传模式。蓝牙中使用的一种可靠的错误和流量控制协议。

ESP—封装安全载荷。在 IPsec 中使用,协议编号为 50。

ETH—扩展传输头:InfiniBand 头,大小从 4 到 28 字节。该报头代表额外的报头族,其可以根据服务的类别和所使用的操作而存在。

ETSI— 欧洲电信标准协会。

FCS— 帧检查序列

FIB—转发信息库。包含路由表信息的数据库。

FMR—快速内存区 (InfiniBand)。

FSF—自由软件基金会。

FTP— 文件传输协议。基于 TCP 在两台主机之间传输文件的协议。

GCC —GNU 编译器集合。

GID—全局标识符。

GMP—集团管理协议。指 IGMP 和马丁路德金。参见 RFC 4604,第一部分。

GRE—通用路由封装。隧道协议。

GRH—全局路由头 ??。40 字节的 InfiniBand 报头。它使用 GIDs 描述源端口和目的端口,其格式与 IPv6 报头相同。

GRO—通用接收卸载。一种技术,在接收时将传入的数据包合并成一个更大的数据包,以提高性能。

GSO—通用分段卸载。一种技术,不在传输层而是尽可能靠近网络驱动程序或在网络驱动程序本身对传出数据包进行分段。

GUID—全球唯一标识符。

HAL—硬件抽象层。

HCA—主机通道适配器。

HCI—主机控制器接口。例如,用于蓝牙、PCI 等。

HDP—健康设备简介。由蓝牙使用。

HFP—免提模式。由蓝牙使用。

HoL 阻塞—行首阻塞是一种性能限制现象,发生在一行数据包被第一个数据包阻塞时,例如,在 HTTP 流水线中的多个请求中。

HPC—高性能计算。对计算机资源的管理方式,可为繁重的任务(如解决科学、工程或经济中的大规模问题)提供高性能。

HS—高速。

HTTP—超文本传输协议。访问万维网的基本协议。

HWMP —混合无线网状协议。一种用于无线网状网络的路由协议,包括两种类型的路由:按需路由和主动路由。

I warp—互联网广域 RDMA 协议。

伊瑟尔—RDMA 的 iSCSI 分机。

IANA— 互联网号码分配机构。负责 IP 寻址、DNS 根的全球协调以及其他与 IP 相关的符号和数字。由互联网名称与数字地址分配机构(ICANN)运营。

IBTA —InfiniBand 贸易协会。

ICMP—互联网控制消息协议。用于控制和信息消息的 IP 协议。众所周知的ping实用程序基于 ICMP。众所周知,ICMP 协议被用于各种类型的安全 DoS 攻击,如 Smurf 攻击。

ICE—交互连接建立。在 RFC 5245 中规定。一种穿越 NAT 的协议。

红十字国际委员会(ICRC)—不变 CRC。4 字节的 InfiniBand 报头。涵盖所有字段,当数据包在子网中传输时,这些字段不应更改。

IDS—入侵检测系统。

IoT—物联网。日常物品的网络化。

IEEE—电气和电子工程师协会。

IGMP—互联网群组管理协议。多播组成员身份协议。

IKE—互联网密钥交换。设置 IPsec 安全关联的协议。

IOMMU —I/O 内存管理单元。

IP—互联网协议。互联网的主要寻址和路由协议。1981 年,RFC 791 首次规定了 IPv4,1995 年,RFC 1883 首次规定了 IPv6。

IPoIB—InfiniBand 上的 IP。

IPS—入侵防御系统。

ISAKMP—互联网安全协会&密钥管理协议。

IOCTL—输入/输出控制。提供从用户空间到内核的访问的系统调用。

IPC—进程间通信。IPC 有许多不同的机制,比如共享内存信号量、消息队列等等。

IPCOMP— IP 有效载荷压缩协议。一种压缩协议,旨在减少通过慢速网络连接发送的数据量。使用 IPComp 可以提高两个网络节点之间的整体通信性能。

IPsec —IP 安全。IETF 开发的一组协议,用于在 IP 协议上安全交换数据包。根据 IPv6 规范,IPsec 在 IPv6 中是强制的,在 IPv4 中是可选的,尽管许多操作系统也在 IPv4 中实现了 IPsec。IPsec 使用两种加密模式:传输和隧道。

IPVS —IP 虚拟服务器。Linux 内核负载平衡基础设施,支持 IPv4 和 IPv6。参见http://www.linuxvirtualserver.org/software/ipvs.html

中断服务程序。接收到中断时调用的中断处理程序。

ISM—工业、科学和医疗无线电波段。

巨型帧—大小高达 9K 的数据包。一些网络接口允许使用高达 9K 的 MTU。在某些情况下,使用巨型帧可以提高网络性能,例如在批量数据传输中。

KVM—基于内核的虚拟机。Linux 虚拟化项目。

LACP—链路聚合控制协议。

LAN—局域网。连接有限区域(如办公楼)的网络。

LID—本地标识符。由子网管理器(InfiniBand)分配给每个子网端口的 16 位值。

L2CAP—逻辑链路控制和适配协议。用于蓝牙。

L2TP—VPN 使用的第二层隧道协议。L2TPv3 是在 RFC 3931 中指定的(RFC 5641 有一些更新)。

LKML— Linux 内核邮件列表。

LLCP—逻辑链路控制协议。由 NFC 使用。

LLN—低功耗和有损网络。

LoWPAN— 低功耗无线个域网。

LMP—链路管理协议。控制两个蓝牙设备之间的无线电链接。

LPM—最长前缀匹配。路由子系统使用的算法。

LRH—本地路由头。8 字节的 InfiniBand 报头。它标识数据包的本地源端口和目的端口。它还指定消息的请求 QoS 属性(SL 和 VL)。

LRO—大型接收卸载。

LR-WPAN— 低速无线个人局域网。在 IEEE 802.15.4 中使用。

LSB—最低有效位。

LSRR—散源记录路线。

LTE—长期演进。

MAC—媒体访问控制。OSI 模型的数据链路层(L2)的子层。

MAD—管理数据报(InfiniBand)。

MFC—组播转发缓存。内核中的一种数据结构,由多播转发表项组成。

MIB—管理信息库。

MLD—多播监听器发现协议。使每个 IPv6 路由器能够发现多播侦听器的存在。在 2004 年的 RFC 3810 中规定了 MLD 协议。

MLME— MAC 层管理实体。IEEE 802.11 管理层中的一个组件,负责扫描、身份验证、关联和重新关联等操作。

MR—内存区(InfiniBand)。

MSF—组播源过滤。这是设置过滤器的功能,以便来自非预期源的多播流量将被丢弃。

MSI—消息发出中断信号。

MSS—最大分段尺寸。TCP 协议的一个参数。

MTU— 最大传输单位。网络协议可以传输的最大数据包的大小。

MW—内存窗口(InfiniBand)。

NAP—网络接入点。

NAPI— 新的 API。一种技术,网络驱动程序不是中断驱动的,而是使用轮询。在第一章中讨论了 NAPI。

NAT—网络地址转换。负责修改 IP 报头的层。在 Linux 中,对 IPv6 NAT 的支持合并到了内核 3.7 中。

NAT-T —NAT 穿越。

NCI —NFC 控制器接口。

ND/NDISC—邻居发现协议。在 IPv6 中使用。它的任务包括:发现同一链路上的网络节点,自动配置地址,查找其他节点的链路层地址,以及维护其他节点的可达性信息。

NFC— 近场通信。

NDEF —NFC 数据交换格式。

NIC—网络接口卡,又称网络接口控制器或网络适配器。硬件网络设备。

NUMA—非一致性内存访问。

NDEF 推送协议。

NPAR—网卡分区。一种使您能够将网卡(NIC)流量划分为多个分区的技术。

NUD—网络不可达检测。一种负责确定是否可以到达邻居的机制。

OBEX—物品交换。蓝牙中使用的设备间交换二进制对象的协议。

OEM— 原始设备制造商。

OFA —OpenFabrics 联盟。

OCF—开放的加密框架。

OHA—开放手机联盟。

OOTB——突如其来的数据包(《SCTP 协议》的一个术语)。如果数据包的格式正确(即没有校验和错误),则该数据包是 OOTB 数据包,但是接收方无法识别该数据包所属的 SCTP 协会(参见 RFC 4960 中的 8.4 节)。

OPP—对象推送模式。由蓝牙使用。

OSI 模型— 开放系统互连。

OSPF—首先打开最短路径。为 IP 网络开发的内部网关路由协议。

PADI— PPPoE 主动发现发起。

PADO— PPPoE 主动发现服务。

PADR— PPPoE 主动发现请求。

PADS— PPPoE 主动发现会话。

PADT— PPPoE 主动发现终止。

—个人区域联网。蓝牙中使用的描述文件。

PCI—外围组件互连。用于连接设备的总线。许多网络接口卡是 PCI 设备。

PD—保护域。

PHDC—个人健康设备通信。由 NFC 使用。

PID— 过程标识符。

PIM—协议独立组播协议。多播路由协议。

PIM-SM—协议独立组播—稀疏模式。

PLME—IEEE 802.11 中的物理层管理实体。

PM—电源管理。

PPP—点对点数据链路协议。两台主机之间直接通信的协议。

PPPoE—以太网上的 PPP。PPPoE 协议是在 1999 年的 RFC 2516 中指定的。

PERR—路径错误。通知无线网状网络路由中某些故障的消息。

准备—路径回复。在无线网状网络中作为对 PREQ 消息的回复而发送的单播包。

PREQ—路径请求。在无线网状网络中查找某个地址时发送的广播数据包。

PSK—预共享密钥。

Qdisc—排队纪律。

QP—队列对(内场带)。

RA—路由器警报。IPv4 选项之一。它通知中转路由器更仔细地检查 IP 数据包的内容。它被许多协议使用,如 IGMP、MLD 等。

RANN— 根公告。由无线网状网络中的根网状点周期性发送的广播包。

RARP—反向地址解析协议。一种协议,用于查找链路层地址(如 48 位以太网地址)到网络地址(如 IPv4 地址)之间的映射。

RC—InfiniBand 中的一种 QP 传输类型。

RDMA— 远程直接内存访问。从一台主机到另一台主机的直接内存访问。

RDS— 可靠数据报套接字。Oracle 开发的一种可靠的无连接协议。

RFC—征求意见。指定互联网规范、通信协议、程序和事件的文档。RFC 的标准化流程记录在http://tools.ietf.org/html/rfc2026“互联网标准流程”中。

RFID—射频识别。

RFCOMM—射频通信协议。用于蓝牙。

RFS—接收流量导向。

RIP—路由信息协议:一种距离矢量路由协议。

RoCE— 融合以太网上的 RDMA。

RP—会合点。

RPL—适用于低功耗和有损网络的 IPv6 路由协议。RFC 6550 中规定了 RPL 协议。

RPDB—路由策略数据库。

RPF—反向路径滤波器。一种旨在防止源地址欺骗的技术。

RPC— 远程过程调用。

RPS— 接收数据包导向。

RS—路由器招标。

RSA—一种密码算法。RSA 代表 Ron Rivest、阿迪·萨莫尔和 Leonard Adleman,他们是 RSA 的开发者。

RTP—实时传输协议。通过 IP 网络传输音频和视频的协议。

RTR—准备接收。无限带宽 QP 状态机中的一个状态。

RTS—准备发送。无限带宽 QP 状态机中的一个状态。

SA—保安协会。两台主机之间的逻辑关系,由各种参数组成,如加密密钥、加密算法、SPI 等。

SACK— 选择性确认。参见 RFC 2018,“TCP 选择性确认选项”,1996 年。

SAD— 安全关联数据库。

SAR—分割和重组。

SBC—会话边界控制器。

SCO— 面向同步连接的链路。蓝牙协议。

SDP— 服务发现协议。用于蓝牙。

SCTP— 流控传输协议。兼具 UDP 和 TCP 功能的传输协议。

SE—安全元素(NFC)。

SIG—特殊利益集团。

SIP—会话发起协议。VoIP 的一种信令协议,用于创建和修改 VoIP 会话。

SLAAC—无状态地址自动配置。在 RFC 4862 中规定。

SKB—套接字缓冲器。表示网络数据包的内核数据结构(由sk_buff结构、include/linux/skbuff.h实现)。

SL—服务水平。InfiniBand 中的 QoS 是使用 S1 到 VL 的映射和每个 VL 的资源来实现的。

SLAAC—无状态地址自动配置。

SM—子网管理器。

SMA—子网管理代理。

SME—IEEE 802.11 中的系统管理实体。

SMP—对称多处理。一种架构,其中两个或多个相同的处理器连接到一个共享的主存储器。

SNAT—源 NAT。改变源地址的 NAT。

SNEP—用于交换 NDEF 格式数据的简单 NDEF 交换协议(SNEP)。

SNMP—简单网络管理协议。

SPI—安全参数指标。由 IPsec 使用。

SPD—安全策略数据库。

SQD—发送队列清空。无限带宽 QP 状态机中的一个状态。

SQE—发送队列错误。无限带宽 QP 状态机中的一个状态。

SRP —SCSI RDMA 协议。

SR-IOV—单根 I/O 虚拟化。一种规范,允许 PCIe 设备表现为多个独立的物理 PCIe 设备。

SRQ—共享接收队列(InfiniBand)。

SSM—源特定多播。

STUN—NAT 的会话遍历实用程序。

SSP—安全简单配对。蓝牙 2.1 版要求的安全特性。

TCP—传输控制协议。TCP 协议是当今互联网上最常用的传输协议。许多协议运行在 TCP 之上,包括 FTP、HTTP 等等。TCP 是在 1981 年的 RFC 793 中规定的,从那以后的几年中,对基本 TCP 协议进行了许多协议更新、变化和添加。

TIPC—透明的进程间通信协议。参见http://tipc.sourceforge.net/

TOS—服务类型。

TSO —TCP 分段卸载。

TTL—生存时间。IPv4 报头中的计数器(在 IPv6 中的对应部分称为跳数限制)在每个转发设备中递减。当此计数器达到 0 时,发送回超时 ICMP,并丢弃数据包。IPv4 报头的ttl成员和 IPv6 报头的hop_limit成员都是 8 位字段。

TURN—使用中继绕过 NAT 进行遍历。

UC—连接不可靠。无限带中的一种 QP 输运类型。

UD—不可靠的数据报。无限带中的一种 QP 输运类型。

UDP—用户数据报协议。UDP 是一种不可靠的协议,因为无法保证数据包会被传送到上层协议。与 TCP 不同,UDP 中没有握手阶段。UDP 报头很简单,只包含 4 个字段:源端口、目的端口、校验和以及长度。

USAGI—Ipv6 的全球游乐场。一个为 Linux 内核开发 IPv6 和 IPsec(针对 IPv4 和 IPv6)堆栈的项目。

UTS —Unix 分时系统。

VCRC—变体 CRC。2 字节的 InfiniBand 报头。覆盖数据包的所有字段。

VETH—虚拟以太网。一种网络驱动程序,支持不同网络名称空间中的两个网络设备之间的通信。

VoIP—网络电话。

VFS—虚拟文件系统。

VL—虚拟车道。一种通过单个物理链路创建多个虚拟链路的机制。

VLAN—虚拟局域网。

VPN—虚拟专用网络。

VXLAN—虚拟可扩展局域网。VXLAN 是通过 UDP 传输第 2 层以太网数据包的标准协议。VXLAN 是必需的,因为在某些情况下,防火墙会阻止隧道,例如,只允许 TCP/UDP 流量。

WDS—无线分布系统。

WLAN— 无线局域网。

WOL— 在兰上醒来。

WSN— 无线传感器网络。

XRC—扩展可靠连接。无限带中的一种 QP 输运类型。

XFRM —IPsec 变压器。用于处理 IPsec 转换的 Linux 内核框架。XFRM 框架的两个最基本的数据结构是 XFRM 策略和 XFRM 状态。

posted @ 2024-08-02 19:34  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报