1. 引言
Linux 内核的网络栈是操作系统中最复杂且至关重要的组件之一,负责处理从简单的网络配置到大规模数据中心和全球互联网流量的各种网络通信任务。其性能、灵活性和可扩展性在很大程度上取决于其核心数据结构的设计与实现。这些数据结构是内核表示和操作网络数据包、网络接口、套接字连接、路由信息以及防火墙状态的基础。理解这些结构不仅对于内核开发者至关重要,对于需要深入优化网络性能或排查复杂网络问题的系统管理员和网络工程师也大有裨益。
网络栈通常采用分层架构,类似于 TCP/IP 或 OSI 模型,其中每一层都依赖于下层提供的服务,并通过定义良好的接口进行交互。数据在各层之间传递时,会被封装或解封装,每一层都需要高效地访问和修改数据包信息。因此,核心数据结构的设计必须在信息表示的完整性、内存使用的效率、并发访问的安全性以及跨层传递的性能之间取得精妙的平衡。本报告旨在深入剖析 Linux 内核网络栈中的几个关键数据结构,包括 sk_buff
、net_device
、sock
及其相关结构、路由相关的 dst_entry
和 rtable
,以及 Netfilter 使用的 nf_hook_ops
和 nf_conn
。我们将基于现有研究资料,详细阐述这些结构的定义、关键字段的含义与用途、它们之间的相互关系,以及其设计选择对网络性能、可扩展性和整体功能的影响。
2. 核心网络子系统概述
Linux 网络栈遵循分层设计原则,将复杂的网络通信功能划分为多个相对独立的子系统,每个子系统负责特定的任务。这种分层不仅使得代码更易于管理和维护,也允许不同协议(如 IP、IPX)和硬件(如以太网、Wi-Fi)共存。主要子系统包括:
-
套接字层 (Socket Layer / BSD Socket Interface): 这是用户空间应用程序与内核网络栈交互的主要接口。它提供了标准的 BSD 套接字 API,包括
socket()
,bind()
,connect()
,listen()
,accept()
,send()
,recv()
等系统调用。套接字抽象了一个通信端点或通道,使得应用程序可以进行网络编程而无需关心底层协议和硬件的细节。在内核中,struct socket
结构体部分代表了这个接口层。套接字通过文件描述符在用户空间表示。 -
协议层 (Protocol Layer - Transport/Network): 这一层实现了具体的网络协议,主要是 TCP/IP 协议族,包括 TCP、UDP、IP、ICMP、ARP 等。它负责数据包的格式化、分段与重组、连接管理(TCP)、可靠性传输(TCP)、流量控制、拥塞控制以及路由查找等核心功能。内核通常处理 OSI 模型的第 2 层到第 4 层,而第 7 层及以上的应用层协议(如 HTTP, FTP, SSH)则通常在用户空间实现。
struct sock
及其特定于协议的衍生结构(如tcp_sock
,udp_sock
)是这一层的核心数据结构。 -
网络设备层 (Network Device Layer / Driver / MAC Layer): 该层负责与物理网络硬件(如网络接口卡 NIC)进行交互。它包含了各种设备的驱动程序,管理设备的注册、激活、数据发送和接收。核心数据结构是
struct net_device
,它代表了一个网络接口。为了提高效率,该层通常采用 NAPI (New API) 等机制来优化中断处理,减少 CPU 负载。驱动程序通过 DMA (Direct Memory Access) 与 NIC 共享环形缓冲区(Ring Buffer)来传输数据,以避免 CPU 介入每个数据包的复制。 -
路由子系统 (Routing Subsystem): 负责为出站和转发的数据包确定最佳传输路径。它维护着一个或多个路由表,通常以 FIB (Forwarding Information Base) 的形式存储在内核中。路由决策基于目的 IP 地址、源 IP 地址、服务类型 (TOS)、防火墙标记以及策略路由规则。关键数据结构包括
rtable
(IPv4)、fib_table
和dst_entry
(目标缓存条目)。 -
Netfilter 框架: 在网络栈的关键路径上提供了一系列钩子 (Hooks),允许内核模块注册回调函数来检查、修改、丢弃或排队数据包。这为实现防火墙、网络地址转换 (NAT)、连接跟踪、数据包日志记录等功能提供了基础。
struct nf_hook_ops
用于注册钩子函数,而struct nf_conn
则用于存储连接跟踪信息。
内核与用户空间的分界明确:内核处理 L2 到 L4 的协议栈逻辑,而 L7 以上的应用协议在用户空间实现。套接字是两者间最主要的通信桥梁。此外,Netlink 机制也提供了一种用户空间与内核空间进行双向通信的方式,常用于网络配置和管理。
这种严格的分层设计带来了显著的模块化优势和协议独立性,使得 Linux 网络栈能够灵活地支持各种网络技术。然而,这也对数据在层间传递的效率提出了极高要求。为了避免在每一层都进行数据复制所带来的巨大开销,内核必须依赖精心设计的数据结构(如 sk_buff
)和高效的传递机制。这构成了 Linux 网络数据结构设计的核心挑战和优化方向,贯穿于后续的分析之中。
3. sk_buff
(Socket Buffer) 深度分析
struct sk_buff
(Socket Buffer) 是 Linux 内核网络栈中最核心、最基础的数据结构之一。几乎所有网络相关的代码都会涉及对 sk_buff
的操作。它代表了一个网络数据包(或其片段)在内核中的生命周期,并承载着数据本身以及大量的元数据信息。
3.1. 核心作用与目的
sk_buff
的主要目的是作为一个统一的容器,在网络栈的不同层级(从设备驱动到协议层,再到套接字层)之间传递网络数据包。它不仅包含了数据包的实际内容(payload),还包含了处理该数据包所需的各种控制信息和状态。为了提高效率,sk_buff
的设计着重于最小化数据复制。它通过灵活的指针操作和预留空间(headroom/tailroom)来实现在添加或移除协议头时无需移动数据本身。此外,sk_buff
通常组织成双向链表,便于在各种队列(如设备接收队列、套接字发送/接收队列)中进行高效的插入和删除操作。这些队列通常由 struct sk_buff_head
结构来管理。
3.2. 关键字段与内存布局
sk_buff
结构体本身是一个元数据结构,它并不直接持有所有的数据。实际的数据存储在一个或多个关联的内存缓冲区中。理解其内存布局和关键字段对于掌握内核如何处理数据包至关重要。
内存布局与数据指针:
内核为每个 sk_buff
分配一块内存区域。sk_buff
结构中的几个关键指针定义了数据在这块内存中的位置和边界:
head
: 指向已分配内存区域的起始位置。这个指针在sk_buff
的生命周期内通常是固定的。data
: 指向当前有效数据载荷的起始位置。当协议层处理数据包并移除头部时(例如,IP 层处理完 IP 头后),data
指针会向前移动(通过skb_pull()
操作)。当添加头部时(例如,发送数据包时添加 TCP 头),data
指针会向后移动(通过skb_push()
操作)。tail
: 指向当前有效数据载荷的末尾(即最后一个数据字节之后的位置)。当向数据包末尾添加数据时(通过skb_put()
操作),tail
指针会向前移动。end
: 指向已分配内存区域的结束位置。这个指针通常也是固定的。
Headroom 与 Tailroom:
这种指针设计自然地形成了两个重要的空间:
- Headroom:
data
指针与head
指针之间的空间。这部分预留的空间用于在数据包向下层传递时添加协议头,而无需重新分配内存或复制数据。skb_reserve()
函数通常在分配sk_buff
后调用,用于创建初始的 headroom。 - Tailroom:
tail
指针与end
指针之间的空间。这部分空间允许向数据包末尾追加数据(如协议尾部或填充字节)而无需重新分配。
长度字段:
len
: 表示sk_buff
中当前有效数据的总长度,包括线性数据区(data
到tail
)和任何分片数据(data_len
)。计算方式通常是(skb->tail - skb->data) + skb->data_len
。data_len
: 表示存储在非线性区域(如frags
或frag_list
)中的数据长度。对于完全存储在线性区域的sk_buff
,此值为 0。mac_len
: MAC 头部(L2)的长度。truesize
: 表示此sk_buff
及其关联数据所占用的总内存大小,用于内存记账,特别是计算套接字缓冲区配额。
关联字段:
这些字段将 sk_buff
与网络栈的其他关键结构联系起来:
dev
: 指向与此sk_buff
关联的struct net_device
,表示接收或发送此数据包的网络接口。sk
: 指向拥有此sk_buff
的struct sock
(套接字结构),主要用于出站数据包或与特定套接字关联的入站数据包。也用于内存记账。dst
: 指向struct dst_entry
,包含了此数据包的路由和目标信息。在路由查找后设置。
协议与类型:
protocol
: 存储网络层协议标识符(例如,ETH_P_IP
表示 IPv4),通常由链路层驱动程序在接收时设置。pkt_type
: 指示数据包的链路层类型(例如,PACKET_HOST
表示发往本机,PACKET_BROADCAST
表示广播)。
头部指针:
为了方便访问各层协议头,sk_buff
内部维护了指向这些头部的指针:
transport_header
: 指向传输层头部(如 TCP, UDP)。network_header
: 指向网络层头部(如 IP, ARP)。mac_header
: 指向链路层头部(如 Ethernet)。 这些指针在数据包在协议栈中传递时被设置和更新。内核提供了skb_transport_header()
,skb_network_header()
,skb_mac_header()
等辅助函数来访问它们。
时间戳:
tstamp
或skb_mstamp_ns
: 记录数据包接收或发送时的软件时间戳。硬件时间戳信息则存储在skb_shared_info
中。
校验和相关字段:
ip_summed
: 指示校验和的状态。CHECKSUM_NONE
: 未执行校验和或校验和无效。CHECKSUM_PARTIAL
: 需要硬件或后续软件层计算校验和(用于校验和卸载)。csum_start
和csum_offset
指示需要计算的部分。CHECKSUM_COMPLETE
: 硬件计算了整个数据包的校验和,结果存储在csum
字段中。CHECKSUM_UNNECESSARY
: 校验和验证由硬件完成或不需要验证(如环回设备)。
csum
: 存储硬件计算的校验和值或部分校验和计算的伪头和。csum_level
: 用于CHECKSUM_UNNECESSARY
,指示已验证的连续校验和层数。csum_not_inet
: 指示CHECKSUM_PARTIAL
是否用于非 IP 校验和(如 SCTP CRC32c)。
控制缓冲区:
cb1
: 一个 48 字节的控制缓冲区,可供协议栈的各个层用作“暂存区”,存储与该sk_buff
相关的临时或层特定的信息。例如,IP 层用它来处理 IP 选项。
引用计数与共享信息:
users
:atomic_t
类型的引用计数器,跟踪有多少实体(代码路径)持有对struct sk_buff
元数据本身的引用。当users
计数降至 0 时,sk_buff
结构本身才会被释放。cloned
: 一个标志位,指示此sk_buff
的数据区是否与其他sk_buff
共享(即是否为克隆)。struct skb_shared_info
: 位于skb->end
指向的内存区域末尾的一个结构。它包含了与可能被多个sk_buff
共享的数据区相关的信息:dataref
:atomic_t
类型的引用计数器,跟踪有多少sk_buff
共享这个数据区。当dataref
降至 0 时,数据缓冲区才会被释放。dataref
被巧妙地分为两半,用于区分头部是否可写。nr_frags
:frags
数组中有效碎片的数量。frags
: 一个skb_frag_t
结构数组,每个元素描述一个存储在物理内存页(struct page
)中的数据片段。这常用于零拷贝(Zero-Copy)和分散/聚集(Scatter-Gather)DMA。frag_list
: 指向另一个sk_buff
的指针,形成一个sk_buff
链表,用于表示逻辑上属于同一个大数据包的多个片段(例如,IP 分片的重组)。- GSO/GRO 相关字段:
gso_size
,gso_segs
,gso_type
用于通用分段卸载 (Generic Segmentation Offload) 和通用接收卸载 (Generic Receive Offload)。
表 1: struct sk_buff
关键字段摘要
字段名 | 类型 (近似) | 描述 |
head | unsigned char * | 指向分配缓冲区的起始 |
data | unsigned char * | 指向当前有效数据的起始 |
tail | unsigned char * | 指向当前有效数据的末尾 |
end | unsigned char * | 指向分配缓冲区的末尾 |
len | unsigned int | 当前 sk_buff 中数据的总长度 (线性 + 分片) |
data_len | unsigned int | 分片区域 (frags /frag_list ) 中的数据长度 |
mac_len | unsigned int | MAC 头部长度 |
truesize | unsigned int | sk_buff 结构及关联数据占用的总内存大小 |
users | atomic_t | sk_buff 结构本身的引用计数 |
cloned | unsigned int:1 | 标记数据区是否被共享 (克隆) |
protocol | __be16 | 网络层协议类型 (Network Byte Order) |
dev | struct net_device * | 关联的网络设备 (接收或发送) |
sk | struct sock * | 关联的套接字 |
dst | struct dst_entry * | 目标路由缓存条目 |
ip_summed | __u8 | 校验和状态 (硬件卸载/验证状态) |
cb1 | char | 控制缓冲区 (层间暂存区) |
transport_header | sk_buff_data_t | 指向传输层头部的偏移量 |
network_header | sk_buff_data_t | 指向网络层头部的偏移量 |
mac_header | sk_buff_data_t | 指向链路层头部的偏移量 |
(in skb_shared_info) dataref | atomic_t | 共享数据区的引用计数 |
(in skb_shared_info) nr_frags | unsigned char | frags 数组中碎片的数量 |
(in skb_shared_info) frags | skb_frag_t | 指向存储在内存页中的数据碎片的数组 |
(in skb_shared_info) frag_list | struct sk_buff * | 指向分片链中的下一个 sk_buff |
3.3. 内存管理与性能考量
sk_buff
的内存管理机制是其设计的核心,直接关系到网络栈的性能和效率。
-
分配与释放: 内核提供了
alloc_skb()
和dev_alloc_skb()
函数来分配sk_buff
结构和初始的数据缓冲区。dev_alloc_skb()
是设备驱动常用的版本,它会自动预留一些 headroom。释放sk_buff
则通过kfree_skb()
、consume_skb()
或dev_kfree_skb()
。这些函数会处理引用计数,确保在没有引用时才真正释放内存。 -
引用计数: 这是
sk_buff
内存管理的关键。users
计数器保护sk_buff
结构本身,而dataref
计数器(位于skb_shared_info
中)保护共享的数据缓冲区。每当sk_buff
被传递、排队或克隆时,相应的引用计数会增加(通过skb_get()
或内部机制)。当处理完成或sk_buff
出队时,计数会减少。只有当计数器归零时,相应的内存(结构或数据区)才会被释放。这种机制对于在并发环境中安全地共享数据至关重要,尤其是在多核处理器上,不同的 CPU 可能同时访问同一个sk_buff
或其数据。然而,引用计数的增减需要原子操作,这会带来一定的性能开销,尤其是在高争用情况下,这与后面讨论的锁机制的可伸缩性问题相关联。 -
克隆 (Cloning):
skb_clone()
提供了一种极其高效的方式来“复制”sk_buff
。它只创建一个新的sk_buff
元数据结构,而让新旧sk_buff
共享同一个底层数据缓冲区。这通过增加dataref
引用计数来实现。相比于skb_copy()
(它会复制整个数据区),克隆操作的开销非常小,因为它避免了内存分配和大量数据的复制。这对于需要保留数据包副本(如 TCP 重传)或将数据包传递给多个处理路径(如tcpdump
抓包)的场景至关重要。skb_shared()
或skb_cloned()
可以检查sk_buff
是否是克隆或数据是否共享。如果需要修改共享数据,通常需要使用skb_unshare()
或pskb_copy()
来创建一个私有副本。 -
分片处理 (Fragmentation):
sk_buff
通过frags
数组和frag_list
链表支持非线性缓冲区。frags
数组存储指向内存页(struct page
)的片段描述符 (skb_frag_t
),常用于支持网络接口卡的 Scatter-Gather DMA 功能,实现数据直接在预分配的页面中接收或发送,是实现零拷贝(Zero-Copy)的关键。frag_list
则主要用于 IP 分片的重组,将属于同一个 IP 数据报的不同sk_buff
片段链接起来。虽然分片处理增加了数据访问和管理的复杂性(例如,计算校验和需要遍历所有片段),但它对于处理大于 MTU 的数据包以及实现高效的 I/O 是必不可少的。 -
Headroom/Tailroom 的效率: 这是
sk_buff
设计中避免数据复制的核心机制之一。通过预留空间,协议栈可以在向下(发送)或向上(接收)传递数据包时,直接在原地添加或移除协议头,而无需移动或复制主体数据。这极大地提升了数据包在协议栈中穿梭的效率。 -
性能影响:
sk_buff
的设计目标是高性能。最小化数据复制是首要原则,通过克隆、headroom/tailroom 和分片(零拷贝)等机制实现。克隆操作远快于数据复制。分片虽然增加了复杂性,但其带来的零拷贝收益在高负载下非常显著。然而,sk_buff
结构本身较大,其内存布局对缓存局部性有影响,不当的修改(如随意添加字段)可能导致缓存行失效增加,从而显著降低性能。truesize
字段用于内存记账,确保套接字缓冲区的使用不超过系统或用户设定的限制。
综合来看,sk_buff
的内存模型是在性能和复杂性之间精心权衡的结果。它通过多种机制避免数据复制,提高了数据包处理效率。引用计数是其在并发环境中安全管理共享数据的基础,尽管原子操作会引入一些开销。这种设计使得 Linux 网络栈能够在保持灵活性的同时处理高速网络流量。
3.4. sk_buff
的传递与操作
sk_buff
在网络栈的不同层之间传递,每一层都会对其进行相应的操作。
-
层间传递: 数据包从驱动程序接收后,封装在
sk_buff
中向上传递到网络层(IP)、传输层(TCP/UDP),最终可能到达套接字层。相反,应用程序通过套接字发送数据时,数据被放入sk_buff
,然后向下传递经过传输层、网络层,最终交给设备驱动程序发送。 -
操作函数: 内核提供了一系列内联函数或宏来高效地操作
sk_buff
的指针和长度字段:skb_put()
,__skb_put()
: 向sk_buff
的末尾添加数据,增加len
并前移tail
指针。skb_put
包含边界检查。skb_push()
,__skb_push()
: 在sk_buff
的开头添加数据(需要足够的 headroom),增加len
并后移data
指针。skb_push
包含边界检查。skb_pull()
,__skb_pull()
: 从sk_buff
的开头移除数据(通常用于剥离协议头),减少len
并前移data
指针。skb_trim()
,__skb_trim()
: 从sk_buff
的末尾移除数据,减少len
并后移tail
指针。skb_reserve()
: 在sk_buff
的开头预留 headroom,通过前移data
和tail
指针实现。pskb_expand_head()
: 扩展sk_buff
的 headroom 或 tailroom,如果需要,可能会重新分配内存并复制数据。skb_copy_bits()
: 从sk_buff
(可能包括非线性部分)中复制数据到指定的缓冲区。skb_store_bits()
: 将数据从指定缓冲区复制到sk_buff
中。
-
头部访问: 除了直接操作
data
指针,内核还提供了更抽象的函数来访问特定协议的头部,如skb_mac_header()
,skb_network_header()
,skb_transport_header()
。特定协议的辅助函数(如eth_hdr()
,ip_hdr()
,tcp_hdr()
,udp_hdr()
)通常封装了这些函数并进行类型转换,使代码更清晰。
通过这些精心设计的操作函数,内核可以在处理数据包时高效地添加、移除和访问各层协议头及数据,而无需进行昂贵的数据复制。
4. net_device
结构体分析
struct net_device
是 Linux 内核中代表网络接口的核心数据结构。无论是物理硬件(如以太网卡、无线网卡)还是虚拟接口(如 loopback、VLAN、bond、bridge、VRF),都通过一个 net_device
实例来表示。它充当了协议层与设备驱动程序之间的桥梁,封装了接口的属性、状态和操作方法。
4.1. 核心作用与目的
net_device
的主要作用是向内核的网络子系统提供一个统一的、抽象的接口视图,隐藏底层硬件的具体实现细节。它使得 IP 层、TCP/UDP 层等协议栈代码能够以一致的方式与各种不同的网络硬件进行交互,进行数据包的发送和接收。内核通过维护一个全局的网络设备列表来管理系统中所有的网络接口。
4.2. 关键字段
struct net_device
结构体非常庞大,包含了描述接口硬件特性、配置状态、统计信息以及操作方法的众多字段。以下是一些关键字段:
-
标识与索引:
name
: 接口的名称,如 "eth0", "wlan0", "lo"。用户空间工具(如ip
,ifconfig
)使用此名称来引用接口。ifindex
: 内核分配的唯一接口索引号。
-
硬件地址与属性:
dev_addr
: 接口的硬件地址(MAC 地址)。驱动程序负责从硬件读取并填充此字段。addr_len
: 硬件地址的长度(例如,以太网为 6 字节)。broadcast
: 链路层的广播地址。type
: 接口的硬件类型,如ARPHRD_ETHER
(以太网),ARPHRD_LOOPBACK
(环回)。mtu
: 最大传输单元 (Maximum Transmission Unit),指接口在链路层所能传输的最大数据载荷大小(不包括链路层头部)。上层协议(如 IP)需要根据 MTU 来决定是否分片。
-
状态与标志:
flags
: 接口标志位 (IFF_*
,定义在<linux/if.h>
),表示接口的当前状态和能力。常见的标志包括:IFF_UP
: 接口已启用(管理状态)。IFF_BROADCAST
: 支持广播。IFF_MULTICAST
: 支持多播。IFF_LOOPBACK
: 是一个环回接口。IFF_POINTOPOINT
: 是一个点对点接口。IFF_RUNNING
: 接口链路已连接,可以传输数据(物理/操作状态),通常由驱动根据链路状态更新。IFF_PROMISC
: 接口处于混杂模式,接收所有经过的数据包。IFF_NOARP
: 禁用 ARP 协议。IFF_ALLMULTI
: 接收所有多播数据包。
operstate
: 更细粒度的操作状态(如IF_OPER_UP
,IF_OPER_DOWN
,IF_OPER_LOWERLAYERDOWN
)。reg_state
: 接口的注册状态 (NETREG_*
枚举),如NETREG_REGISTERED
。state
: 包含设备内部状态标志(如__LINK_STATE_START
),通常由netif_*
函数管理,驱动不应直接修改。
-
驱动程序接口 (
netdev_ops
):netdev_ops
: 指向struct net_device_ops
的指针,该结构包含了一组由设备驱动程序实现的回调函数(方法)。内核通过这些函数来操作具体的硬件。关键操作包括:ndo_open()
: 启用接口时调用(对应ip link set dev up
),负责分配资源、初始化硬件。ndo_stop()
: 禁用接口时调用(对应ip link set dev down
),负责释放资源、关闭硬件。ndo_start_xmit()
: 核心的发送函数。当内核需要通过此接口发送数据包 (sk_buff
) 时调用此函数。驱动程序负责将sk_buff
中的数据交给硬件进行传输。ndo_get_stats()
: 获取接口的统计信息(收发包数、错误数等),返回struct net_device_stats *
。ndo_set_mac_address()
: 设置 MAC 地址。ndo_change_mtu()
: 更改 MTU。ndo_tx_timeout()
: 发送超时处理函数。ndo_init()
: 在设备注册期间、设备对用户可见之前调用,允许在持有 RTNL 锁的情况下进行初始化。ndo_uninit()
: 在设备注销期间、设备关闭之后调用,允许在持有 RTNL 锁的情况下进行清理。
ethtool_ops
: 指向struct ethtool_ops
,包含ethtool
工具所需的操作函数。
-
协议层接口:
ip_ptr
: 指向 IPv4 协议相关的私有数据结构 (struct in_device
)。ip6_ptr
: 指向 IPv6 协议相关的私有数据结构 (struct inet6_dev
)。dn_ptr
: 指向 DECnet 协议相关的私有数据。这些指针允许各网络协议栈将特定于接口的状态(如 IP 地址配置、路由参数)附加到net_device
上。header_ops
: 指向struct header_ops
,包含创建和解析链路层头部(如以太网头)的函数指针。
-
队列与 NAPI:
napi_list
: 用于 NAPI (New API) 调度的链表节点。NAPI 是一种中断缓解技术,用于在高流量下减少中断开销。_tx
: 指向传输队列数组 (struct netdev_queue *
)。现代网卡通常支持多队列以提高并发性。num_tx_queues
: 分配的传输队列数量。real_num_tx_queues
: 当前活动的传输队列数量。_rx
: 指向接收队列数组。num_rx_queues
: 接收队列数量。ingress_queue
: 指向入口队列 (struct netdev_queue
)。
-
驱动私有数据:
priv
: 一个void *
指针,但实际上指向由驱动程序定义的私有数据结构。内核在分配net_device
时会额外分配空间用于存储这个私有结构。驱动可以通过netdev_priv()
宏方便地访问这块内存,用于存储硬件寄存器地址、内部状态、配置等设备特定的信息。
-
生命周期管理:
priv_destructor
: 一个函数指针,当设备被释放时调用,用于清理priv
指向的私有数据,特别是在 RTNL 锁保护的上下文中。needs_free_netdev
: 一个标志,指示内核在设备注销且所有引用消失后自动调用free_netdev()
。destructor
: 另一个析构函数指针。
表 2: struct net_device
关键字段摘要
字段名 | 类型 (近似) | 描述 |
name | char | 接口名称 (e.g., "eth0") |
ifindex | int | 接口唯一索引 |
mtu | unsigned int | 最大传输单元 (字节) |
flags | unsigned int | 接口标志 (IFF_UP, IFF_RUNNING, etc.) |
dev_addr | unsigned char | 硬件 (MAC) 地址 |
netdev_ops | const struct net_device_ops * | 指向驱动程序操作函数集 |
ip_ptr | struct in_device __rcu * | 指向 IPv4 相关数据 |
ip6_ptr | struct inet6_dev __rcu * | 指向 IPv6 相关数据 |
priv | void * | (通过 netdev_priv() 访问) 指向驱动私有数据 |
state | unsigned long | 设备内部状态标志 (由 netif_* 管理) |
reg_state | enum net_reg_state | 设备注册状态 |
num_tx_queues | unsigned int | 分配的传输队列数量 |
napi_list | struct list_head | NAPI 调度链表节点 |
4.3. 生命周期、注册与交互
net_device
的生命周期管理和与内核其他部分的交互是驱动开发的核心。
-
分配与初始化: 驱动程序在探测到硬件时,必须使用
alloc_netdev_mqs()
或类似函数分配net_device
结构。这个函数会同时为net_device
本身和驱动的私有数据 (priv
) 分配内存。分配后,驱动程序负责填充结构的各个字段,包括设置名称、MTU、硬件地址、接口标志,最重要的是,将netdev_ops
指向驱动实现的具体操作函数集。对于以太网设备,ether_setup()
函数可以方便地初始化许多通用字段。 -
注册: 初始化完成后,驱动调用
register_netdev()
(或在持有 RTNL 锁时调用register_netdevice()
)将设备注册到内核。注册成功后,设备对内核网络栈和用户空间可见,可以开始收发数据。ndo_init
函数(如果提供)会在此时被调用。 -
注销与释放: 当设备移除或驱动卸载时,需要调用
unregister_netdev()
(或unregister_netdevice()
)来注销设备。此调用会关闭设备并等待所有使用者完成操作。ndo_uninit
函数(如果提供)会在注销过程中被调用。net_device
结构本身的内存通过free_netdev()
释放。驱动程序通常负责释放priv
指向的私有数据,除非在 RTNL 锁上下文中使用了priv_destructor
机制。needs_free_netdev
标志可以使内核在适当的时候自动调用free_netdev()
。 -
与驱动的交互: 内核通过调用
netdev_ops
中的ndo_*
函数来控制硬件。例如,ip link set eth0 up
会触发ndo_open
,发送数据包最终会调用ndo_start_xmit
。反过来,驱动程序在接收到数据包后,会将数据放入sk_buff
,并通过netif_rx()
或 NAPI 相关的函数(如napi_gro_receive()
)将sk_buff
提交给上层协议栈。驱动程序还负责根据物理链路状态更新IFF_RUNNING
标志,并通过netif_carrier_on()
/netif_carrier_off()
通知内核。 -
与协议层的交互: 协议层(如 IP)通过路由查找(结果存储在
dst_entry
中)找到目标net_device
(dst_entry->dev
)。发送数据时,协议层调用dev_queue_xmit()
,最终触发设备的ndo_start_xmit()
。协议层可以通过ip_ptr
、ip6_ptr
等指针在net_device
结构中存储和访问特定于该接口的协议状态信息。
net_device
的生命周期管理与 RTNL (Routing Netlink) 锁紧密相关。RTNL 锁用于序列化对网络配置(包括设备注册/注销、地址配置、路由更改等)的访问,确保在进行这些可能影响整个网络栈状态的操作时的一致性。区分 register_netdev
/unregister_netdev
和 register_netdevice
/unregister_netdevice
的原因就在于调用者是否已经持有 RTNL 锁,这表明对网络接口的动态管理需要严格的同步控制。
netdev_ops
结构体现了典型的策略(Strategy)设计模式。它定义了一套标准的操作接口 (ndo_*
函数),而具体的实现则由各个设备驱动程序提供。这使得内核的网络核心代码能够以统一的方式与功能各异的硬件设备进行交互,具有良好的可扩展性和硬件无关性。
5. sock
及相关协议特定结构分析
在 Linux 内核中,套接字是网络通信的核心抽象。与用户空间通过文件描述符操作套接字不同,内核内部使用更为复杂的结构来表示和管理套接字状态和数据流。其中,struct sock
是最核心的网络层套接字表示,而 struct socket
则更贴近用户空间的 BSD 套接字接口。此外,针对具体协议(如 TCP、UDP),还存在更专门化的结构,它们通常嵌套或包含了通用的 sock
结构。
5.1. struct sock
与 struct socket
的关系与作用
内核中使用两个主要的结构来代表一个打开的套接字:
-
struct socket
: 这个结构更接近用户空间的 BSD 套接字概念。它是执行socket()
、bind()
、connect()
等系统调用时内核直接操作的对象。它包含了一些通用的套接字属性,如状态 (state
)、类型 (type
),以及指向底层协议操作集 (ops
,即struct proto_ops *
) 和核心网络层套接字结构 (sk
,即struct sock *
) 的指针。struct socket
的操作函数(通常以sock_
前缀命名,定义在net/socket.c
)是协议无关的,它们通过ops
指针调用具体协议的实现。它还包含一个file
指针,用于关联 VFS 文件系统(在 Linux 中,套接字可以像文件一样被处理)。 -
struct sock
: 这是内核网络层对套接字的内部表示,有时被称为 "INET socket"。它包含了关于套接字连接状态、数据队列、内存管理、定时器、协议特定选项等大量详细信息。它是协议栈(如 TCP/IP)进行数据收发和状态管理的核心。struct sock
通过sk_socket
字段反向指向其所属的struct socket
。
两者通过指针相互关联 (socket->sk
和 sock->sk_socket
),但生命周期可能略有不同。struct socket
提供了通用的系统调用接口层,而 struct sock
则包含了协议栈内部所需的具体状态和数据。这种分离提供了一个清晰的抽象边界:socket
处理 VFS 和系统调用接口,而 sock
处理网络协议的复杂细节,从而在 VFS/系统调用层面实现了协议的独立性。
5.2. 通用 sock
结构的关键字段
struct sock
(通常在代码中别名为 sk
) 包含了所有类型套接字共有的核心字段:
__sk_common
: 这是一个嵌入的结构 (struct sock_common
),包含了最基础和通用的套接字信息,许多字段以skc_
前缀命名。关键成员包括:- 地址/端口信息:
skc_daddr
(目的 IP),skc_rcv_saddr
(接收源 IP),skc_dport
(目的端口),skc_num
(本地端口),skc_family
(地址族, 如AF_INET
)。 - 状态与类型:
skc_state
(连接状态),skc_reuse
,skc_reuseport
(对应SO_REUSEADDR
,SO_REUSEPORT
选项)。 - 协议与网络命名空间:
skc_prot
(指向struct proto
),skc_net
(指向所属的struct net
)。 - 引用计数:
skc_refcnt
。 - 哈希链表节点:
skc_node
,skc_bind_node
,skc_portaddr_node
用于将sock
链接到各种协议的查找哈希表中。
- 地址/端口信息:
sk_lock
: 套接字锁 (socket_lock_t
),用于保护sock
结构内部成员的并发访问。这是实现线程安全的关键。- 状态 (
sk_state
): 一个unsigned char
字段,表示套接字的具体状态。对于 TCP,这对应于标准的 TCP 状态(如TCP_ESTABLISHED
,TCP_LISTEN
,TCP_CLOSE
)。对于 UDP 等无连接协议,通常只使用少数几个状态(如TCP_ESTABLISHED
表示已绑定,TCP_CLOSE
表示未绑定)。状态由协议栈根据接收到的数据包和用户操作(如connect
,listen
,close
)进行管理。 - 协议与类型 (
sk_protocol
,sk_type
):sk_protocol
存储 IP 协议号(如IPPROTO_TCP
,IPPROTO_UDP
)。sk_type
存储套接字类型(如SOCK_STREAM
,SOCK_DGRAM
)。 - 数据队列:
sk_receive_queue
:struct sk_buff_head
类型的接收队列,存储已接收、等待应用程序读取的sk_buff
。sk_write_queue
:struct sk_buff_head
类型的发送队列,存储应用程序已写入、等待协议层处理和发送的sk_buff
。sk_backlog
: 一个特殊的接收队列,用于暂存那些在sk_receive_queue
被锁定时到达的数据包,防止数据丢失。
- 缓冲区管理:
sk_sndbuf
: 发送缓冲区大小限制(字节),对应SO_SNDBUF
选项。sk_rcvbuf
: 接收缓冲区大小限制(字节),对应SO_RCVBUF
选项。sk_wmem_alloc
:atomic_t
类型,记录当前发送队列 (sk_write_queue
) 中sk_buff
所占用的内存字节数。sk_rmem_alloc
:atomic_t
类型,记录当前接收队列 (sk_receive_queue
和sk_backlog
) 中sk_buff
所占用的内存字节数。sk_forward_alloc
: 用于记账转发数据包分配的内存。
- 协议操作 (
sk_prot
,sk_prot_creator
):sk_prot
指向struct proto
结构,该结构包含了一组特定协议的核心函数指针(如connect
,disconnect
,accept
,sendmsg
,recvmsg
,hash
,unhash
,get_port
等)。sk_prot_creator
指向创建此套接字的原始协议结构。 - 指针:
sk_socket
指向关联的struct socket
。sk_dst_cache
和sk_rx_dst
分别缓存用于发送和接收路径的dst_entry
(路由信息)。 - 回调函数:
sk_data_ready
,sk_state_change
,sk_write_space
,sk_error_report
等函数指针,当特定事件发生时(如数据到达、状态改变、发送缓冲区可用、错误发生),协议栈会调用这些回调函数来通知等待的进程或执行相应操作。 - 过滤 (
sk_filter
): 指向附加到此套接字的 BPF (Berkeley Packet Filter) 程序,用于自定义数据包过滤。
5.3. 协议特定结构 (inet_sock
, inet_connection_sock
, tcp_sock
, udp_sock
)
为了处理不同协议族的特定需求,Linux 内核在通用 struct sock
的基础上,通过 C 语言的结构体嵌套(第一个成员是父结构类型)实现了一种类似继承的层次结构。这允许代码重用,同时为特定协议添加所需字段。
-
层次结构:
- TCP:
sock
->inet_sock
->inet_connection_sock
->tcp_sock
- UDP:
sock
->inet_sock
->udp_sock
- IPv6: 通常有对应的
*6_sock
结构(如udp6_sock
,raw6_sock
),这些结构可能包含一个ipv6_pinfo
结构,并常常嵌套相应的 IPv4 结构。 这种嵌套允许通过简单的类型转换访问通用层的功能。例如,一个tcp_sock
指针可以安全地转换为inet_connection_sock*
,inet_sock*
, 或sock*
来访问相应层级的字段或调用通用函数。
- TCP:
-
struct inet_sock
: 这是所有基于 IPv4 协议的套接字(TCP, UDP, RAW)的通用基础结构,直接嵌套了struct sock sk
。它添加了 IPv4 特有的字段:- 地址/端口:
inet_dport
(目的端口),inet_sport
(源端口),inet_num
(本地端口),inet_saddr
(源 IP),inet_rcv_saddr
(绑定的本地 IP),inet_daddr
(目的 IP)。 - IP 选项:
inet_opt
(指向struct ip_options_rcu
)。 - IP 头字段:
tos
(服务类型),ttl
(uc_ttl
,mc_ttl
),hdrincl
(用户提供 IP 头标志),pmtudisc
(路径 MTU 发现设置),nodefrag
(不分片标志)。 - 套接字选项标志:
freebind
,transparent
,mc_loop
,mc_all
,recverr
。 - 索引:
uc_index
(单播出口设备索引),mc_index
(多播设备索引)。 - 多播相关:
mc_addr
(组地址),mc_list
(组成员列表)。 - Corking:
cork
(用于 TCP/UDP corking)。
- 地址/端口:
-
struct inet_connection_sock
(icsk): 用于面向连接的 INET 套接字(主要是 TCP),嵌套了struct inet_sock icsk_inet
。它添加了与连接建立、维护和拆除相关的字段:- 连接队列:
icsk_accept_queue
(存储已完成三次握手、等待accept()
的连接),request_sock
(指向半连接struct request_sock
队列,用于处理 SYN 包)。 - 定时器:
icsk_retransmit_timer
(重传定时器),icsk_delack_timer
(延迟 ACK 定时器)。 - 拥塞控制:
icsk_ca_ops
(指向拥塞控制算法操作集),icsk_ca_priv
(拥塞控制私有数据)。 - 其他: SYN Cookie 相关信息,bind 冲突处理逻辑。
- 连接队列:
-
struct tcp_sock
(tp): 这是 TCP 协议最具体的结构,嵌套了struct inet_connection_sock inet_conn
。它包含了 TCP 协议状态机、流量控制、拥塞控制、重传机制等所需的全部状态变量:- 序列号:
rcv_nxt
(期望接收的下一个序列号),copied_seq
(已拷贝给用户的最后一个字节的序列号),write_seq
(下一个要发送的字节的序列号),snd_una
(未确认的第一个字节序列号),snd_nxt
(下一个要发送的新数据的序列号),snd_sml
(上次发送的最后一个字节序列号)。 - 窗口:
snd_cwnd
(拥塞窗口),snd_ssthresh
(慢启动阈值),rcv_wnd
(接收窗口),window_clamp
(接收窗口上限)。 - 定时器: RTO (Retransmission Timeout), Probe Timer 等。
- 队列:
out_of_order_queue
(乱序到达的数据包队列),prequeue
(在用户读取前预处理的队列)。 - TCP 选项: SACK (Selective Acknowledgment) 信息, 时间戳选项 (
rx_opt.tstamp_ok
,rx_opt.rcv_tsval
,rx_opt.rcv_tsecr
), 窗口缩放 (rx_opt.wscale_ok
,rx_opt.snd_wscale
,rx_opt.rcv_wscale
)。 - 拥塞控制变量: 特定于算法的状态变量。
- 其他: TCP 头部长度 (
tcp_header_len
), 快速路径相关标志, 头部预测信息。
- 序列号:
-
struct udp_sock
: UDP 特有的字段相对较少,嵌套了struct inet_sock inet
。主要包括:- Corking 信息 (
cork
)。 - Pending 状态 (
pending
)。 - 封装类型 (
encap_type
),用于 UDP 封装隧道。
- Corking 信息 (
表 3: Socket 结构层次与关键字段示例
字段名 | 结构 | 描述 |
skc_family | sock_common | 地址族 (e.g., AF_INET) |
skc_state | sock_common | 连接状态 (e.g., TCP_ESTABLISHED) |
skc_daddr / skc_rcv_saddr | sock_common | 目的/源 IP 地址 |
skc_dport / skc_num | sock_common | 目的/本地 端口 |
skc_prot | sock_common | 指向协议操作结构 (struct proto * ) |
sk_receive_queue | sock | 接收队列 (sk_buff_head ) |
sk_write_queue | sock | 发送队列 (sk_buff_head ) |
sk_sndbuf / sk_rcvbuf | sock | 发送/接收缓冲区大小限制 |
sk_wmem_alloc / sk_rmem_alloc | sock | 已分配发送/接收内存 (atomic_t ) |
sk_socket | sock | 指向 struct socket |
inet_sport / inet_dport | inet_sock | 源/目的 端口 (Network Byte Order) |
inet_saddr / inet_daddr | inet_sock | 源/目的 IPv4 地址 (Network Byte Order) |
inet_opt | inet_sock | IP 选项 |
tos / ttl | inet_sock | 服务类型 / 生存时间 |
icsk_accept_queue | inet_connection_sock | 等待 accept() 的连接队列 (request_sock_queue ) |
icsk_retransmit_timer | inet_connection_sock | 重传定时器 |
icsk_ca_ops | inet_connection_sock | 拥塞控制算法操作 |
snd_cwnd | tcp_sock | 拥塞窗口大小 |
rcv_wnd | tcp_sock | 接收窗口大小 |
write_seq | tcp_sock | 下一个要写入的序列号 |
out_of_order_queue | tcp_sock | 乱序数据包队列 (sk_buff_head ) |
5.4. 套接字状态管理
套接字的状态管理是协议栈正确运行的基础,尤其对于 TCP 这样的面向连接的协议。
sk_state
字段:struct sock
中的sk_state
字段是状态管理的核心。它是一个枚举类型(对于 TCP,定义在include/net/tcp_states.h
),表示套接字在其生命周期中所处的不同阶段。- TCP 状态: TCP 协议具有复杂的状态机,
sk_state
反映了这些状态,包括:TCP_CLOSE
: 初始状态或完全关闭状态。TCP_LISTEN
: 服务器调用listen()
后等待连接的状态。TCP_SYN_SENT
: 客户端发送 SYN 后等待 SYN-ACK 的状态。TCP_SYN_RECV
: 服务器收到 SYN 并发送 SYN-ACK 后等待 ACK 的状态。TCP_ESTABLISHED
: 三次握手完成,连接已建立,可以进行数据传输的状态。TCP_FIN_WAIT1
,TCP_FIN_WAIT2
: 主动关闭方发送 FIN 后的状态。TCP_CLOSE_WAIT
: 被动关闭方收到 FIN 后的状态。TCP_LAST_ACK
: 被动关闭方发送 FIN 后等待 ACK 的状态。TCP_CLOSING
: 双方同时关闭连接时的状态。TCP_TIME_WAIT
: 主动关闭方在收到对方 FIN 的 ACK 后,等待一段时间以确保所有报文都已消失的状态。
- UDP 状态: UDP 是无连接的,其状态管理简单得多。
sk_state
通常只使用TCP_ESTABLISHED
(表示套接字已绑定到端口)和TCP_CLOSE
(表示未绑定或已关闭)。 - 状态转换: 内核协议栈代码(如
tcp_rcv_state_process
)负责根据接收到的数据包(如 SYN, ACK, FIN)和用户空间的操作(如connect()
,listen()
,close()
,shutdown()
)来更新sk_state
字段,驱动 TCP 状态机的转换。 - 锁与并发: 对
sk_state
的访问和修改通常需要获取套接字锁 (sk_lock
),以防止在多核环境下的并发访问冲突。
5.5. 内存管理与缓冲区
套接字需要管理发送和接收缓冲区,以平衡应用程序的读写速度和网络传输速度。
- 缓冲区大小 (
sk_sndbuf
,sk_rcvbuf
): 这些字段定义了套接字发送和接收缓冲区的最大允许大小(以字节为单位)。它们可以通过setsockopt()
系统调用(使用SO_SNDBUF
和SO_RCVBUF
选项)进行调整。内核通常会自动调整这些值(autotuning),但应用程序可以设置硬限制。 - 已分配内存 (
sk_wmem_alloc
,sk_rmem_alloc
): 这些atomic_t
类型的字段精确地跟踪当前在发送队列 (sk_write_queue
) 和接收队列 (sk_receive_queue
+sk_backlog
) 中sk_buff
所占用的实际内存量。内核使用这些值来判断是否可以接受更多的数据写入(发送)或是否需要通知对端减小发送窗口(接收)。 - 内存记账:
sk_buff
的truesize
字段用于计算其占用的内存。当sk_buff
被添加到套接字队列时,相应的sk_wmem_alloc
或sk_rmem_alloc
会增加truesize
的值。当sk_buff
被处理或发送并从队列中移除时,这些计数器会相应减少。 - 内存压力: 内核会监控全局和每个套接字的内存使用情况。如果
sk_wmem_alloc
接近sk_sndbuf
,应用程序的写入操作可能会被阻塞,直到发送队列有更多空间。如果sk_rmem_alloc
接近sk_rcvbuf
,TCP 可能会通告一个较小的接收窗口给对端,以减缓数据发送速度(流量控制)。全局 TCP 内存限制(通过sysctl tcp_mem
设置)也会影响单个套接字的内存分配。
6. 路由相关结构 (dst_entry
, rtable
)
路由子系统负责为网络数据包确定最佳的传输路径。struct dst_entry
和 struct rtable
是 IPv4 路由中的核心数据结构。
6.1. struct dst_entry
(Destination Cache Entry)
dst_entry
是一个通用的目标缓存条目结构,用于存储关于如何到达特定目的地的决策结果。它不仅仅用于 IP 路由,也可能用于其他协议。
- 核心作用: 封装路由查找的结果,包括下一跳信息、输出设备以及处理该数据包所需的输入/输出函数。它充当了网络层和链路层之间的接口。
- 关键字段:
dev
: 指向用于发送数据包的struct net_device
(输出接口)。input
: 函数指针,指向处理发往此目的地的入站数据包的函数。例如,对于本地主机,可能是ip_local_deliver
;对于需要转发的数据包,可能是ip_forward
。output
: 函数指针,指向处理发往此目的地的出站数据包的函数。通常是ip_output
或ip_mc_output
(多播)。ops
: 指向struct dst_ops
,包含一组与此目标类型相关的操作函数(如垃圾回收、更新 PMTU 等)。neighbour
: 指向struct neighbour
,表示下一跳的邻居信息(如 MAC 地址),由 ARP 或 NDP 解析。hh
: 指向struct hh_cache
,缓存下一跳的硬件头部(如以太网头),用于快速封装。expires
: 缓存条目的过期时间。flags
: 状态标志,如DST_HOST
(主机路由),DST_NOXFRM
(不进行 XFRM 处理)。error
: 缓存相关的错误代码。obsolete
: 指示条目是否过时或已被强制销毁。__refcnt
: 引用计数器,管理dst_entry
的生命周期。lastuse
: 上次使用的时间戳。header_len
,trailer_len
: 链路层头部和尾部的长度。_metrics
: 存储路由度量值(如 MTU)。
6.2. struct rtable
(IPv4 Route Table Entry)
rtable
是 IPv4 特有的路由表条目结构,它嵌入了一个 dst_entry
。
- 核心作用: 代表 IPv4 路由表中的一个具体条目,包含了
dst_entry
的通用信息,并添加了 IPv4 特定的路由属性。 - 关键字段:
dst
: 嵌入的struct dst_entry
。这是rtable
的第一个成员,允许将rtable
指针强制转换为dst_entry
指针。rt_flags
: 路由标志 (RTCF_*
),如RTCF_BROADCAST
,RTCF_MULTICAST
,RTCF_LOCAL
,RTCF_DOREDIRECT
。rt_type
: 路由类型 (RTN_*
),如RTN_UNICAST
,RTN_LOCAL
,RTN_BROADCAST
,RTN_MULTICAST
,RTN_BLACKHOLE
。rt_dst
: 目标 IPv4 地址。rt_src
: 首选源 IPv4 地址。rt_iif
: 输入接口索引(用于反向路径过滤等)。rt_gateway
: 下一跳网关的 IPv4 地址。
6.3. 路由查找与 dst_entry
的使用
- 查找触发: 当内核需要发送 IP 数据包(本地产生或转发)时,会触发路由查找过程。对于接收到的数据包,在
ip_rcv()
中进行查找;对于本地发送的数据包,在ip_queue_xmit()
或协议层(如udp_sendmsg
)中进行查找。 - FIB 查找 (
fib_lookup
): 核心的查找函数是fib_lookup()
。它使用数据包的五元组信息(源/目的 IP、源/目的端口、协议)以及其他信息(如接口、防火墙标记)作为键,在 FIB(通常是 LPC-trie 实现)中查找最长前缀匹配的路由条目。 fib_result
:fib_lookup()
返回一个fib_result
结构,其中包含了指向fib_info
的指针。fib_info
包含了路由的详细信息,如下一跳信息和输出设备。dst_entry
/rtable
创建/获取:- 缓存命中 (旧机制): 在 Linux 3.6 之前,内核会先查找路由缓存。如果命中,直接返回缓存的
rtable
。 - 缓存未命中/新机制: 如果缓存未命中(或在 3.6 之后),内核会调用
fib_lookup()
。根据fib_result
,内核会创建一个新的rtable
对象(包含dst_entry
),或者从与 FIB 条目关联的缓存中获取一个预先构建的dst_entry
(现代内核的优化)。rtable
中的dst.input
和dst.output
函数指针会根据路由类型(本地、转发、单播等)被设置为相应的处理函数(如ip_local_deliver
,ip_forward
,ip_output
)。rt_gateway
和dst.dev
等字段也会被填充。
- 缓存命中 (旧机制): 在 Linux 3.6 之前,内核会先查找路由缓存。如果命中,直接返回缓存的
- 附加到
sk_buff
: 最终得到的dst_entry
(或包含它的rtable
)会被附加到sk_buff
的dst
字段(通过skb_dst_set()
)。 - 数据包处理: 后续的网络层处理(如 Netfilter 钩子、IP 选项处理、分片)以及最终的发送过程会使用
skb->dst
中的信息:- 调用
dst->output()
函数将数据包传递给下一层或进行发送。 - 通过
dst->dev
确定输出的网络设备。 - 通过
dst->neighbour
获取下一跳的 MAC 地址信息(如果需要)。
- 调用
路由缓存的设计在 Linux 内核中经历了演变。早期的全流缓存(pre-3.6)虽然对命中流性能好,但存在扩展性和安全问题。现代内核(post-3.6)取消了全局流缓存,转而依赖于更高效的 FIB 查找(如 LPC-trie)和更细粒度的缓存,例如在 fib_info
中缓存 nexthop 对应的 dst_entry
,以及在 sock
结构中缓存 dst_entry
(sk_dst_cache
),从而在性能、可扩展性和安全性之间取得了更好的平衡。
7. Netfilter 相关结构 (nf_hook_ops
, nf_conn
)
Netfilter 是 Linux 内核中用于数据包过滤、网络地址转换 (NAT) 和其他数据包处理的框架。它在网络栈的关键位置提供了钩子 (hooks),允许内核模块注册回调函数来检查和操作数据包。
7.1. struct nf_hook_ops
(Netfilter Hook Operations)
-
核心作用: 定义了一个 Netfilter 钩子函数的注册信息。内核模块通过填充这个结构并将其实例注册到 Netfilter 核心,来将自己的函数挂载到特定的钩子点上。
-
关键字段:
hook
: 指向实际处理数据包的回调函数 (nf_hookfn
)。该函数接收sk_buff
和nf_hook_state
等参数,并返回一个 Netfilter 判决(verdict),如NF_ACCEPT
,NF_DROP
,NF_QUEUE
,NF_STOLEN
。pf
: 协议族 (Protocol Family),指定此钩子应用于哪个协议栈,如PF_INET
(IPv4),PF_INET6
(IPv6),PF_BRIDGE
。hooknum
: 钩子点编号,指定此钩子注册在哪个具体的 Netfilter 钩子上。对于 IPv4,这通常是NF_INET_PRE_ROUTING
,NF_INET_LOCAL_IN
,NF_INET_FORWARD
,NF_INET_LOCAL_OUT
,NF_INET_POST_ROUTING
之一。priority
: 钩子函数的优先级。在同一个钩子点上可以注册多个函数,它们会按照优先级的升序(值越小,优先级越高)依次调用。Netfilter 定义了一些标准的优先级(如NF_IP_PRI_FIRST
,NF_IP_PRI_CONNTRACK
,NF_IP_PRI_NAT_DST
,NF_IP_PRI_FILTER
,NF_IP_PRI_NAT_SRC
,NF_IP_PRI_LAST
),允许模块将自己的功能插入到处理流程的特定位置。priv
: 一个void *
指针,用于传递私有数据给钩子函数。注册钩子时设置,调用钩子函数时作为参数传入。
-
注册机制: 内核模块使用
nf_register_net_hook()
(或nf_register_net_hooks()
注册多个) 函数,传入一个指向已填充的nf_hook_ops
结构的指针,将其注册到指定的网络命名空间 (struct net
) 的 Netfilter 框架中。注销则使用nf_unregister_net_hook()
或nf_unregister_net_hooks()
。
7.2. struct nf_conn
(Netfilter Connection Tracking Entry)
-
核心作用: 代表一个被 Netfilter 连接跟踪子系统 (
nf_conntrack
) 跟踪的网络连接(或流)。它存储了连接的状态信息,使得 Netfilter 能够实现状态防火墙和 NAT 等功能。 -
关键字段:
tuplehash
: 一个包含两个nf_conntrack_tuple_hash
结构的数组,分别存储连接的原始方向 (original) 和回复方向 (reply) 的元组信息和哈希链表节点。nf_conntrack_tuple
: 嵌入在nf_conntrack_tuple_hash
中,定义了一个连接的唯一标识(元组),包含:src
:struct nf_conntrack_man
,包含源 IP 地址 (u3
)、协议特定信息(如端口号u.tcp.port
或 ICMP IDu.icmp.id
) 和 L3 协议号 (l3num
)。dst
: 包含目的 IP 地址 (u3
)、协议特定信息(如端口号u.tcp.port
或 ICMP 类型/代码u.icmp.type
/u.icmp.code
)、L4 协议号 (protonum
) 和方向 (dir
)。
status
: 一个unsigned long
位掩码,表示连接的当前状态 (IPS_*
枚举,定义在linux/netfilter/nf_conntrack_common.h
)。关键状态包括:IPS_EXPECTED
: 预期连接(由 helper 创建)。IPS_SEEN_REPLY
: 已看到回复方向的流量。IPS_ASSURED
: 连接已确认双向流量,不易被提前回收。IPS_CONFIRMED
: 连接已确认通过POST_ROUTING
或LOCAL_IN
钩子,正式加入主哈希表。IPS_SRC_NAT
,IPS_DST_NAT
: 已执行源或目的 NAT。IPS_DYING
: 连接正在被销毁。IPS_UNTRACKED
: 连接被明确标记为不跟踪。
timeout
: 指向struct nf_conntrack_timing
或类似结构的指针/值,表示此连接的超时时间(以 jiffies 或秒为单位)。超时值取决于协议和连接状态。proto
:union nf_conntrack_proto
,包含特定协议的私有数据和状态(如struct ip_ct_tcp
用于 TCP 状态跟踪)。counters
: 可选的扩展,用于存储连接的字节数和包计数 (struct nf_conn_counter
)。master
: 指向主连接(例如,FTP 控制连接对应的 FTP 数据连接)。mark
: 防火墙标记。secmark
: 安全标记。ct_net
: 指向所属的网络命名空间 (struct net
)。ct_general
: 嵌入的struct nf_conntrack
,包含引用计数 (use
) 等通用信息。
-
连接跟踪过程:
- 钩子点: 连接跟踪主要在
NF_INET_PRE_ROUTING
和NF_INET_LOCAL_OUT
钩子点进行查找和创建新连接。 - 查找: 当一个
sk_buff
到达这些钩子点时,nf_conntrack_in()
函数被调用。它从sk_buff
中提取元组信息,计算哈希值,并在连接跟踪哈希表中查找匹配的nf_conn
条目。 - 创建: 如果找不到匹配项,且协议是可跟踪的,则会创建一个新的
nf_conn
条目,并将其状态初始化(通常为IPS_EXPECTED
或未确认状态)。 - 确认: 新创建的连接在通过
NF_INET_POST_ROUTING
或NF_INET_LOCAL_IN
钩子点时,由nf_conntrack_confirm()
确认,状态置为IPS_CONFIRMED
,并正式加入主哈希表。 - 状态更新: 对于已存在的连接,Netfilter 会根据通过的数据包更新其状态 (
status
) 和超时时间。 - 附加到
sk_buff
: 找到或创建的nf_conn
指针会通过nf_ct_set()
附加到sk_buff
上,供后续的 Netfilter 模块(如 NAT、iptables/nftables 的state
或ct
模块)使用。可以通过nf_ct_get()
从sk_buff
获取连接跟踪信息。 - 超时与回收: 内核有垃圾回收机制,会定期清理超时的
nf_conn
条目。
- 钩子点: 连接跟踪主要在
Netfilter 框架通过 nf_hook_ops
结构实现了高度的模块化,允许各种网络功能(防火墙、NAT、连接跟踪等)以回调函数的形式插入到网络栈中。nf_conn
结构则是实现状态化网络处理(如状态防火墙和 NAT)的关键,它记录了网络连接的上下文信息。
8. 总结
Linux 内核网络栈是一个复杂而高效的系统,其性能和功能在很大程度上依赖于其核心数据结构的设计。
sk_buff
作为网络数据包在内核中的通用载体,通过精巧的指针操作、引用计数、克隆和分片机制,实现了高效的内存管理和跨层传递,最大限度地减少了数据复制开销。net_device
为各种物理和虚拟网络接口提供了一个统一的抽象视图,通过netdev_ops
回调函数集,将协议层与具体的设备驱动程序解耦,实现了良好的模块化和可扩展性。sock
及其协议特定衍生结构(如inet_sock
,tcp_sock
)构成了套接字层和传输/网络协议层的核心。它们通过层次化设计,在提供通用套接字接口的同时,精细地管理着连接状态、数据队列、缓冲区和协议特定的复杂逻辑。dst_entry
和rtable
是路由子系统的关键,它们缓存了路由查找的结果,包含了下一跳信息、输出设备以及关键的输入/输出处理函数指针,指导着数据包在网络中的转发路径。nf_hook_ops
和nf_conn
是 Netfilter 框架的基础。nf_hook_ops
允许内核模块在网络栈的关键点注册回调函数,而nf_conn
则通过连接跟踪,为状态防火墙和 NAT 等高级功能提供了必要的上下文信息。
这些数据结构相互关联,协同工作,共同构成了 Linux 网络栈的基础。理解它们的定义、关键字段、相互关系以及设计背后的考量,对于深入理解 Linux 网络行为、进行性能调优和开发网络相关内核模块至关重要。