前几篇我们拆解了 收包路径 和 发包路径
的全景。你会发现,两条路径在 IP 层交汇——收包时
ip_rcv() 决定这个包是本地投递还是转发,发包时
ip_queue_xmit() 查路由决定从哪个接口出去。IP
层是内核网络栈的中枢路由器 。
但如果你用 ip route show table all
看到几千条路由规则,你能回答这些问题吗?
FIB
路由表用的是什么数据结构?查找一条路由的时间复杂度是多少?
ip rule
策略路由是怎么选表的?性能开销有多大?
IP
分片重组的定时器什么时候触发?超时的分片怎么处理?
PMTU 发现失败时,内核是怎么回退到分片的?
Netfilter 的五个钩子点分别在 IP
处理的哪个位置被调用?
本文全部从内核源码出发回答这些问题。
一、ip_rcv():IP 层入口
所有 IPv4 包在 L2 协议分发后进入
ip_rcv()(include/net/ip.h:161):
int ip_rcv( struct sk_buff * skb, struct net_device * dev,
struct packet_type * pt, struct net_device * orig_dev);
ip_rcv()
做的事情很少但很关键——基本的合法性校验:
// net/ipv4/ip_input.c — ip_rcv() 简化
int ip_rcv( struct sk_buff * skb, ...)
{
struct iphdr * iph;
// 1. 只处理发给本机的包(pkt_type == PACKET_HOST 或 PACKET_BROADCAST 等)
// PACKET_OTHERHOST 的包在这里被丢弃
if ( skb-> pkt_type == PACKET_OTHERHOST)
goto drop;
// 2. 确保 IP 头可读(可能需要 pskb_may_pull)
if (! pskb_may_pull( skb, sizeof ( struct iphdr)))
goto inhdr_error;
iph = ip_hdr( skb);
// 3. 版本号必须是 4
if ( iph-> version != 4 )
goto inhdr_error;
// 4. 头长度至少 20 字节(ihl >= 5)
if ( iph-> ihl < 5 )
goto inhdr_error;
// 5. 校验和验证
if ( unlikely( ip_fast_csum(( u8 *) iph, iph-> ihl)))
goto csum_error;
// 6. 总长度合法性
if ( skb-> len < ntohs( iph-> tot_len))
goto drop;
// 统计计数
__IP_INC_STATS( net, IPSTATS_MIB_INPKTS);
// 7. 进入 Netfilter PRE_ROUTING 钩子
return NF_HOOK( NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
校验失败时递增
IPSTATS_MIB_INHDRERRORS——这个计数器可以在
/proc/net/netstat 中看到。
1.1 NF_HOOK 宏的语义
NF_HOOK 是 Netfilter
的核心宏(include/linux/netfilter.h):
static inline int
NF_HOOK( uint8_t pf, unsigned int hook, struct net * net, struct sock * sk,
struct sk_buff * skb, struct net_device * in, struct net_device * out,
int (* okfn)( struct net *, struct sock *, struct sk_buff *))
{
int ret = nf_hook( pf, hook, net, sk, skb, in, out, okfn);
if ( ret == 1 )
ret = okfn( net, sk, skb); // 所有钩子都放行 → 调用 okfn
return ret;
}
返回值语义:
返回值
含义
NF_ACCEPT (1)
放行,继续调用 okfn(如
ip_rcv_finish)
NF_DROP (0)
丢弃,kfree_skb()
NF_QUEUE (3)
排入用户态队列(NFQUEUE)
NF_STOLEN (4)
Netfilter 接管了这个包,不再继续
这意味着 ip_rcv() 最后的
NF_HOOK 调用实际上是:“先让 iptables/nftables
的 PREROUTING 链检查这个包,如果放行就调用
ip_rcv_finish()”。
二、ip_rcv_finish():路由决策
ip_rcv_finish() 是 IP
收包路径的核心——它决定这个包的命运:本地投递还是转发。
// net/ipv4/ip_input.c — ip_rcv_finish() 简化
static int ip_rcv_finish( struct net * net, struct sock * sk, struct sk_buff * skb)
{
struct iphdr * iph = ip_hdr( skb);
// 1. Early Demux:已建立连接的包跳过路由查找
if ( net-> ipv4. sysctl_ip_early_demux && ! skb_dst( skb)) {
// TCP: tcp_v4_early_demux()
// UDP: udp_v4_early_demux()
// 如果找到匹配的 socket,直接复用 socket 上缓存的 dst_entry
}
// 2. 如果 Early Demux 没命中,做完整路由查找
if (! skb_dst( skb)) {
int err = ip_route_input_noref( skb, iph-> daddr, iph-> saddr,
iph-> tos, skb-> dev);
if ( err)
goto drop;
}
// 3. IP 选项处理
if ( iph-> ihl > 5 ) {
// 处理 IP 选项(源路由、Record Route 等)
if ( ip_rcv_options( skb, net))
goto drop;
}
// 4. 根据路由结果跳转
// dst->input 被设置为:
// - ip_local_deliver() — 本地投递
// - ip_forward() — 转发
// - ip_error() — 错误(如目的不可达)
return dst_input( skb); // 等价于 skb_dst(skb)->input(skb)
}
2.1 Early Demux
的性能意义
Early Demux(早期解复用)是一个重要的优化。对于已建立的
TCP 连接,socket
上已经缓存了路由结果(dst_entry)。tcp_v4_early_demux()
通过四元组查找
socket,如果命中就直接复用缓存的路由——跳过整个 FIB
查找。
在连接数较多的服务器上,Early Demux 可以节省 30-50% 的 IP
层处理时间。但在纯路由/转发场景下(包不是发给本机的),它只是浪费
CPU 做无用的 socket 查找。所以转发网关通常关闭它:
# 在纯转发/路由器设备上关闭 Early Demux
sysctl -w net.ipv4.tcp_early_demux=0
sysctl -w net.ipv4.udp_early_demux=0
当 Early Demux 未命中时,内核调用
ip_route_input_noref()(include/net/route.h:201):
int ip_route_input_noref( struct sk_buff * skb, __be32 dst, __be32 src,
u8 tos, struct net_device * devin);
这个函数内部的关键步骤:
构建流键 (struct flowi4):目的
IP、源 IP、TOS、入接口
策略路由选表 :遍历 ip rule
规则链,选择匹配的 FIB 表
FIB 查找 :在选中的 FIB
表中做最长前缀匹配
构建 rtable :将 fib_result
转化为 struct rtable,设置
dst->input 和
dst->output
路由类型决定了 dst->input 的值:
fib_result.type
dst->input
含义
RTN_LOCAL
ip_local_deliver()
目的是本机地址
RTN_UNICAST
ip_forward()
需要转发
RTN_BROADCAST
ip_local_deliver()
广播包
RTN_MULTICAST
ip_mr_input()
组播
RTN_UNREACHABLE
ip_error()
不可达,回 ICMP
RTN_BLACKHOLE
dst_discard()
静默丢弃
三、FIB 路由表:LC-trie
实现
FIB(Forwarding Information
Base)是内核路由表的底层数据结构。理解它的实现对理解路由查找性能至关重要。
3.1 FIB 表组织
内核支持多个 FIB
表(include/net/ip_fib.h):
struct fib_table {
struct hlist_node tb_hlist; // 表链
u32 tb_id; // 表 ID(如 254=main, 255=local)
int tb_num_default;
struct rcu_head rcu;
unsigned long * tb_data; // 指向 trie 根节点
unsigned long __data[]; // 内联数据
};
默认有三张表:
表 ID
名称
用途
255
local
本机地址、广播地址(自动维护,不可手动修改)
254
main
普通路由(ip route add
默认写入这张表)
253
default
默认路由(通常为空)
策略路由可以创建编号 1-252 的自定义表。
3.2 LC-trie 数据结构
FIB 表内部使用 LC-trie (Level-Compressed
trie)实现最长前缀匹配。这是 Robert Olsson 在 2004 年为
Linux 2.6.13 引入的算法,替代了之前的哈希表实现。
LC-trie 的核心思想:
普通 trie(前缀树):
每个 IP 地址的 32 位逐 bit 构建树
问题:树高最大 32 层,稀疏节点浪费内存
LC-trie 优化:
1. Path Compression:跳过只有单子节点的路径
10.0.0.0/8 和 10.1.0.0/16 之间的中间节点被压缩
2. Level Compression:多 bit 步进
每个节点可以同时检查 N 位(而非 1 位)
减少树高,加速查找
查找复杂度:
操作
时间复杂度
说明
最长前缀匹配
O(log n) 摊销
n 为前缀数,实际 10-15 次内存访问
插入/删除
O(log n)
可能触发子树重组
全表扫描
O(n)
ip route show 遍历
对于 100 万条路由(如 BGP 全表),LC-trie 通常只需 8-12
次内存访问完成一次查找。这比之前的哈希表实现(最坏 O(32)
次哈希)有显著改善。
3.3 fib_lookup() 调用链
fib_lookup() 是 FIB
查找的入口(include/net/ip_fib.h:311):
static inline int fib_lookup( struct net * net, const struct flowi4 * flp,
struct fib_result * res, unsigned int flags)
{
struct fib_table * tb;
// 快速路径:没有策略路由时直接查 local + main 表
if (! net-> ipv4. fib_has_custom_rules) {
// 先查 local 表(本机地址最优先)
tb = rcu_dereference( net-> ipv4. fib_local);
if ( tb && ! fib_table_lookup( tb, flp, res, flags | FIB_LOOKUP_NOREF))
return 0 ;
// 再查 main 表
tb = rcu_dereference( net-> ipv4. fib_main);
if ( tb && ! fib_table_lookup( tb, flp, res, flags | FIB_LOOKUP_NOREF))
return 0 ;
return - ENETUNREACH;
}
// 慢速路径:策略路由 — 遍历 ip rule 规则链
return __fib_lookup( net, flp, res, flags);
}
性能关键点 :当没有自定义策略路由时(fib_has_custom_rules == false),内核跳过规则链遍历,直接查
local 和 main 两张表。这是大多数服务器的路径——只有两次 trie
查找。
3.4 fib_result 结构
查找成功后,结果存入
fib_result(include/net/ip_fib.h:169):
struct fib_result {
__be32 prefix; // 匹配的前缀
unsigned char prefixlen; // 前缀长度
unsigned char nh_sel; // 选中的下一跳索引(ECMP)
unsigned char type; // 路由类型(RTN_LOCAL/RTN_UNICAST/...)
unsigned char scope; // 范围(RT_SCOPE_HOST/LINK/UNIVERSE)
u32 tclassid; // TC 分类 ID
dscp_t dscp; // DSCP 值
struct fib_nh_common * nhc; // 下一跳公共信息
struct fib_info * fi; // 路由信息
struct fib_table * table; // 匹配的 FIB 表
};
3.5 ECMP 多路径路由
当一条路由有多个下一跳时(ip route add ... nexthop via A nexthop via B),fib_info
中包含多个
fib_nh(include/net/ip_fib.h:105):
struct fib_nh {
struct fib_nh_common nh_common; // 通用字段:dev, gw, flags
struct hlist_node nh_hash;
struct fib_info * nh_parent;
__be32 nh_saddr; // 首选源地址
int nh_saddr_genid;
};
ECMP 选择算法基于流哈希(源 IP + 目的 IP + 源端口 +
目的端口 +
协议号),同一条流始终选同一个下一跳——避免乱序。
四、策略路由:ip rule
选表机制
4.1 策略路由规则链
策略路由的核心是 ip rule
规则链。每条规则定义”在什么条件下查哪张 FIB
表”。默认规则:
$ ip rule show
0: from all lookup local # 优先级 0,查 local 表
32766: from all lookup main # 优先级 32766,查 main 表
32767: from all lookup default # 优先级 32767,查 default 表
4.2 规则匹配字段
fib_rule
结构(include/net/fib_rules.h:20)支持丰富的匹配条件:
struct fib_rule {
// 匹配条件
int iifindex; // 入接口
int oifindex; // 出接口
u32 mark; // fwmark
u32 mark_mask; // fwmark 掩码
u32 flags;
u32 table; // 目标 FIB 表
u8 action; // 动作(FR_ACT_TO_TBL/FR_ACT_UNREACHABLE/...)
u8 l3mdev;
u8 proto;
u8 ip_proto; // TCP/UDP/...
char iifname[ IFNAMSIZ];
char oifname[ IFNAMSIZ];
struct fib_kuid_range uid_range;
struct fib_rule_port_range sport_range;
struct fib_rule_port_range dport_range;
// ...
};
可以基于源 IP、目的
IP、入接口、fwmark、协议、端口范围、UID 等条件匹配。
4.3 策略路由的性能开销
__fib_lookup()
遍历规则链是线性的 ——每条规则逐一匹配。如果你有
100 条 ip rule,每个包都要检查最多 100
条规则才能找到匹配的 FIB 表。
性能影响:
无策略路由(默认): 2 次 trie 查找
10 条 ip rule: ~10 次规则匹配 + 若干次 trie 查找
100 条 ip rule: ~100 次规则匹配 + 若干次 trie 查找
在高 PPS 转发场景下,大量 ip rule
会成为性能瓶颈。优化方案:
减少规则数 :用 fwmark 合并规则(在
iptables/nftables 中打标,一条 ip rule fwmark
覆盖多种场景)
调整规则优先级 :把最频繁命中的规则放在最前面
使用
VRF :ip link add vrf-blue type vrf table 100——VRF
设备在路由查找前就确定了表,跳过规则链
五、本地投递路径:ip_local_deliver()
当路由决策为 RTN_LOCAL
时,dst->input 指向
ip_local_deliver()(include/net/ip.h:165)。
5.1 分片重组
ip_local_deliver()
的第一件事是检查是否需要分片重组:
// net/ipv4/ip_input.c — ip_local_deliver() 简化
int ip_local_deliver( struct sk_buff * skb)
{
struct iphdr * iph = ip_hdr( skb);
// MF (More Fragments) 标志或 fragment offset 非零 → 这是一个分片
if ( ip_is_fragment( iph)) {
// 送入分片重组队列
if ( ip_defrag( net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0 ; // 还没收齐,等待更多分片
// ip_defrag 返回 0 = 重组完成,skb 现在是完整包
}
// 进入 Netfilter LOCAL_IN 钩子
return NF_HOOK( NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb-> dev, NULL,
ip_local_deliver_finish);
}
5.2
ip_local_deliver_finish():协议分发
通过 Netfilter LOCAL_IN
后,ip_local_deliver_finish()
做最终的协议分发:
static int ip_local_deliver_finish( struct net * net, struct sock * sk,
struct sk_buff * skb)
{
skb_clear_delivery_time( skb);
__skb_pull( skb, skb_network_header_len( skb)); // 移动指针跳过 IP 头
rcu_read_lock();
ip_protocol_deliver_rcu( net, skb, ip_hdr( skb)-> protocol);
rcu_read_unlock();
return 0 ;
}
ip_protocol_deliver_rcu() 根据 IP 头的
protocol 字段查找注册的协议处理函数:
protocol 值
处理函数
含义
6 (IPPROTO_TCP)
tcp_v4_rcv()
TCP 协议
17 (IPPROTO_UDP)
udp_rcv()
UDP 协议
1 (IPPROTO_ICMP)
icmp_rcv()
ICMP 协议
47 (IPPROTO_GRE)
gre_rcv()
GRE 隧道
4 (IPPROTO_IPIP)
ipip_rcv()
IPIP 隧道
如果找不到注册的协议处理函数,递增
IPSTATS_MIB_INUNKNOWNPROTOS 并发送 ICMP
“协议不可达”。
六、转发路径:ip_forward()
当路由决策为 RTN_UNICAST
且包不是发给本机时,进入
ip_forward()(include/net/ip.h:749)。
6.1 ip_forward() 核心逻辑
// net/ipv4/ip_forward.c — ip_forward() 简化
int ip_forward( struct sk_buff * skb)
{
struct iphdr * iph = ip_hdr( skb);
struct rtable * rt = skb_rtable( skb);
// 1. TTL 检查
if ( iph-> ttl <= 1 ) {
// TTL 耗尽,发送 ICMP Time Exceeded
icmp_send( skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0 );
__IP_INC_STATS( net, IPSTATS_MIB_INHDRERRORS);
goto drop;
}
// 2. MTU 检查
if ( ip_exceeds_mtu( skb, mtu)) {
if ( iph-> frag_off & htons( IP_DF)) {
// DF 标志 + 包太大 → ICMP "Fragmentation Needed"
icmp_send( skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl( mtu));
__IP_INC_STATS( net, IPSTATS_MIB_FRAGFAILS);
goto drop;
}
}
// 3. 严格源路由检查(rp_filter)
if ( net-> ipv4. sysctl_ip_forward &&
IN_DEV_RPFILTER( in_dev)) {
// 反向路径过滤:检查源 IP 的路由是否指向入接口
}
// 4. TTL 减一
ip_decrease_ttl( iph);
// 5. Netfilter FORWARD 钩子
return NF_HOOK( NFPROTO_IPV4, NF_INET_FORWARD,
net, NULL, skb, skb-> dev, rt-> dst. dev,
ip_forward_finish);
}
6.2 ip_forward_finish()
→ ip_output()
static int ip_forward_finish( struct net * net, struct sock * sk, struct sk_buff * skb)
{
struct iphdr * iph = ip_hdr( skb);
// 处理 IP options(如 Record Route、Timestamp)
if ( unlikely( iph-> ihl > 5 ))
ip_forward_options( skb);
__IP_INC_STATS( net, IPSTATS_MIB_OUTFORWDATAGRAMS);
// 进入发送路径
return dst_output( net, sk, skb);
// dst->output 指向 ip_output()
}
6.3 转发路径的性能瓶颈
对于纯转发场景(路由器/网关),IP 层的关键瓶颈是:
阶段
开销
优化手段
FIB 查找
8-12 次内存访问
关闭 Early Demux 避免无用 socket 查找
Netfilter 钩子
与规则数线性
减少规则、用 nftables 替代 iptables
TTL 修改 + 校验和
固定
无(必须做)
策略路由
与规则数线性
减少 ip rule、用 VRF
conntrack
哈希表查找
转发流量可跳过 conntrack(-j NOTRACK)
高 PPS 转发场景的极限优化通常是完全绕过 IP
层 ——用 XDP 在驱动层直接转发,避免
ip_rcv→ip_forward→ip_output
的全部开销(详见 21-xdp )。
7.1 为什么会分片
当 IP 包大小超过出接口的 MTU 时,如果 DF(Don’t
Fragment)标志未设置,内核会对包进行分片。分片发生在
ip_finish_output() 中(发送路径)。
7.2
发送端分片:ip_do_fragment()
ip_do_fragment()(include/net/ip.h:170)处理发送端分片:
int ip_do_fragment( struct net * net, struct sock * sk, struct sk_buff * skb,
int (* output)( struct net *, struct sock *, struct sk_buff *));
分片过程使用 ip_frag_state
状态机(include/net/ip.h:194):
struct ip_frag_state {
bool DF; // Don't Fragment 标志
unsigned int hlen; // IP 头长度
unsigned int ll_rs; // 链路层预留空间
unsigned int mtu; // 出接口 MTU
unsigned int left; // 剩余数据长度
int offset; // 当前分片偏移
int ptr; // 当前分片数据指针
__be16 not_last_frag; // MF 标志
};
分片后每个片段都是独立的 sk_buff,各有自己的
IP 头,fragment offset 字段标记在原始包中的位置。
7.3
接收端重组:ip_defrag()
收包端在 ip_local_deliver() 中调用
ip_defrag()(include/net/ip.h:735):
int ip_defrag( struct net * net, struct sk_buff * skb, u32 user);
重组使用 inet_frag_queue
结构(include/net/inet_frag.h):
struct inet_frag_queue {
spinlock_t lock; // 队列锁
struct timer_list timer; // 重组超时定时器
struct hlist_node hash; // 哈希表节点
refcount_t refcnt;
struct rb_root rb_fragments; // 红黑树组织分片
struct sk_buff * fragments_tail;
ktime_t stamp; // 第一个分片到达时间
int len; // 已收到的总长度
int meat; // 已收到的数据长度
u8 flags;
// ...
};
重组流程:
1. 收到分片 → 用 (源IP, 目的IP, ID, 协议号) 做哈希查找
2. 找到现有队列 → 将分片插入红黑树(按 offset 排序)
3. 没有现有队列 → 创建新队列,启动重组定时器
4. 检查是否所有分片到齐(收到最后一片 + 数据连续)
5. 到齐 → 重组为完整 sk_buff,返回给调用者
6. 超时 → 丢弃所有分片,发送 ICMP "Fragment Reassembly Time Exceeded"
7.4 重组超时与资源限制
# 分片重组超时(秒)
sysctl net.ipv4.ipfrag_time # 默认 30 秒
# 分片队列内存上限
sysctl net.ipv4.ipfrag_high_thresh # 默认 4MB
sysctl net.ipv4.ipfrag_low_thresh # 默认 3MB
# 当总内存超过 high_thresh,开始丢弃最老的队列,直到降到 low_thresh
统计计数器在 /proc/net/snmp 中:
$ cat /proc/net/snmp | grep -A 1 Ip:
Ip: ... ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates
Ip: ... 0 1234 1230 4 5678 0 11356
计数器
含义
ReasmReqds
收到的分片总数
ReasmOKs
成功重组的包数
ReasmFails
重组失败的包数(超时或内存不足)
ReasmTimeout
因超时丢弃的重组队列数
FragOKs
成功分片的包数
FragFails
分片失败的包数(DF 标志 + 包太大)
FragCreates
创建的分片数
7.5 分片的安全风险
IP 分片长期以来是安全攻击的目标:
Tiny Fragment
Attack :构造极小的分片,让第一个分片的 TCP
头不完整,绕过状态less防火墙的端口检查
Overlapping Fragment
Attack :构造偏移重叠的分片,重组后覆盖已检查的头部字段
Fragment
Flood :大量发送不完整的分片,耗尽重组队列内存
现代内核的应对(net/ipv4/ip_fragment.c):
拒绝偏移重叠的分片
用红黑树替代链表,加速分片插入
ipfrag_high_thresh 限制总内存
超时机制回收僵尸队列
八、PMTU 发现与 FNHE
缓存
8.1 Path MTU Discovery
当发送的包因 DF 标志太大而被中间路由器丢弃时,路由器回复
ICMP “Fragmentation Needed” 并携带下一跳 MTU。内核收到这个
ICMP 后,更新该目的地的 PMTU 缓存。
8.2 FNHE(FIB Nexthop
Exception)
Linux 3.6
删除了全局路由缓存(rt_cache),改用
per-nexthop 的异常表(FNHE)来缓存 PMTU
等路径特定信息(include/net/ip_fib.h:59):
struct fib_nh_exception {
struct fib_nh_exception __rcu * fnhe_next;
int fnhe_genid;
__be32 fnhe_daddr; // 目的 IP
u32 fnhe_pmtu; // 缓存的 PMTU
bool fnhe_mtu_locked; // 手动锁定的 MTU
__be32 fnhe_gw; // 重定向的网关
unsigned long fnhe_expires; // 过期时间
struct rtable __rcu * fnhe_rth_input; // 缓存的入方向 rtable
struct rtable __rcu * fnhe_rth_output; // 缓存的出方向 rtable
unsigned long fnhe_stamp; // 更新时间戳
};
FNHE 的生命周期:
1. 收到 ICMP "Frag Needed" → 创建/更新 FNHE,设置 fnhe_pmtu
2. 后续发往该目的的包 → 路由查找命中 FNHE → 使用缓存的 PMTU
3. FNHE 过期(默认 10 分钟) → 回退到接口 MTU,重新发现
4. 如果 PMTU 持续减小 → FNHE 持续更新
8.3 dst_entry 与路由缓存
dst_entry(include/net/dst.h:26)是路由查找结果的抽象:
struct dst_entry {
struct net_device * dev; // 出接口
struct dst_ops * ops; // 操作函数表
unsigned long _metrics; // 路径度量(MTU 等)
unsigned long expires; // 过期时间
int (* input)( struct sk_buff *); // 收包处理函数
int (* output)( struct net *, struct sock *, struct sk_buff *); // 发包处理函数
unsigned short flags;
short obsolete; // 是否过期
rcuref_t __rcuref; // RCU 引用计数
// ...
};
dst_entry 的 input 和
output 函数指针是 IP
层处理的核心分发机制——路由查找的结果被编码为函数指针,后续处理只需调用
dst_input(skb) 或
dst_output(net, sk, skb) 即可。
九、Netfilter
五钩子详解
IP 层在五个位置调用 Netfilter
钩子。以下是每个钩子的精确调用位置和用途:
9.1 钩子位置总览
// include/uapi/linux/netfilter_ipv4.h
#define NF_IP_PRE_ROUTING 0 // ip_rcv() → ip_rcv_finish()
#define NF_IP_LOCAL_IN 1 // ip_local_deliver() → ip_local_deliver_finish()
#define NF_IP_FORWARD 2 // ip_forward() → ip_forward_finish()
#define NF_IP_LOCAL_OUT 3 // ip_local_out() → dst_output()
#define NF_IP_POST_ROUTING 4 // ip_output() → ip_finish_output()
9.2
数据流与钩子的对应关系
本地收包 :
PRE_ROUTING → 路由决策 → LOCAL_IN → 协议栈
转发 :
PRE_ROUTING → 路由决策 → FORWARD → POST_ROUTING → 出接口
本地发送 :
LOCAL_OUT → 路由决策 → POST_ROUTING → 出接口
9.3 conntrack 的影响
连接跟踪(nf_conntrack)在
PRE_ROUTING 和 LOCAL_OUT
处注册。对于转发流量,每个包都要经历两次 conntrack
查找(PRE_ROUTING 入 + POST_ROUTING 确认)。
在高 PPS 转发场景下,conntrack 的开销可能占 IP 层处理的
30-50%。如果不需要 NAT 和状态防火墙,可以跳过:
# 对转发流量跳过 conntrack(使用 nftables)
nft add rule inet filter prerouting iif eth0 oif eth1 notrack
# 或使用 iptables -j NOTRACK(raw 表)
iptables -t raw -A PREROUTING -i eth0 -o eth1 -j NOTRACK
十、IP 层统计与可观测性
10.1 /proc/net/snmp
与 /proc/net/netstat
# SNMP 标准计数器
cat /proc/net/snmp | grep Ip:
# 扩展计数器
cat /proc/net/netstat | grep IpExt:
关键计数器与内核对应关系:
计数器
内核常量
递增位置
InReceives
IPSTATS_MIB_INPKTS
ip_rcv() 入口
InHdrErrors
IPSTATS_MIB_INHDRERRORS
ip_rcv() 校验失败
InAddrErrors
IPSTATS_MIB_INADDRERRORS
路由查找失败(目的不可达)
InDelivers
IPSTATS_MIB_INDELIVERS
ip_local_deliver_finish()
ForwDatagrams
IPSTATS_MIB_OUTFORWDATAGRAMS
ip_forward_finish()
OutRequests
IPSTATS_MIB_OUTREQUESTS
ip_local_out()
OutDiscards
IPSTATS_MIB_OUTDISCARDS
发送路径丢弃
InNoRoutes
IPSTATS_MIB_INNOROUTES
路由查找无匹配
InDiscards
IPSTATS_MIB_INDISCARDS
内存不足等原因丢弃
10.2 bpftrace
追踪路由决策
# 追踪 ip_route_input_noref 的延迟和结果
bpftrace -e '
kprobe:ip_route_input_noref {
@start[tid] = nsecs;
}
kretprobe:ip_route_input_noref /@start[tid]/ {
$lat = (nsecs - @start[tid]) / 1000;
@route_lookup_us = hist($lat);
@route_result = lhist(retval, -10, 10, 1);
delete(@start[tid]);
}
'
10.3 追踪转发路径
# 追踪 ip_forward 的调用频率和 TTL
bpftrace -e '
kprobe:ip_forward {
$skb = (struct sk_buff *)arg0;
$iph = (struct iphdr *)($skb->head + $skb->network_header);
@forward_ttl = lhist($iph->ttl, 0, 256, 8);
@forward_count = count();
}
interval:s:1 {
printf("--- %d forwards/sec ---\n", @forward_count);
clear(@forward_count);
}
'
10.4 追踪分片重组
# 追踪 ip_defrag 调用和重组结果
bpftrace -e '
kprobe:ip_defrag {
@defrag_calls = count();
}
kretprobe:ip_defrag {
@defrag_result = lhist(retval, -10, 10, 1);
// 返回 0 = 重组完成,-EINPROGRESS = 等待更多分片
}
interval:s:5 {
printf("=== ip_defrag stats ===\n");
print(@defrag_calls);
print(@defrag_result);
clear(@defrag_calls);
clear(@defrag_result);
}
'
10.5 perf 定位 IP 层热点
# 采集 IP 层相关函数的 CPU 热点
perf top -g -e cycles:k -s comm --filter = 'ip_rcv or ip_forward or \
ip_local_deliver or fib_table_lookup or ip_output'
# 对比优化前后的 FIB 查找开销
perf stat -e cycles,instructions,cache-misses -a \
-p $( pgrep -f "ip_forward" ) -- sleep 10
十一、关键参数与调优
11.1 路由相关 sysctl
参数
默认值
作用
调优建议
net.ipv4.ip_forward
0
启用 IP 转发
路由器/网关设为 1
net.ipv4.tcp_early_demux
1
TCP Early Demux
纯转发设备关闭
net.ipv4.udp_early_demux
1
UDP Early Demux
纯转发设备关闭
net.ipv4.conf.all.rp_filter
0
反向路径过滤
1=严格,2=宽松
net.ipv4.conf.all.accept_redirects
1
接受 ICMP 重定向
安全环境设为 0
net.ipv4.conf.all.send_redirects
1
发送 ICMP 重定向
非路由器设为 0
net.ipv4.ip_forward_use_pmtu
0
转发时用 PMTU
特殊场景可开启
net.ipv4.ip_no_pmtu_disc
0
禁用 PMTU 发现
不建议修改
11.2 分片相关 sysctl
参数
默认值
作用
net.ipv4.ipfrag_time
30
重组超时(秒)
net.ipv4.ipfrag_high_thresh
4194304
重组队列内存上限(字节)
net.ipv4.ipfrag_low_thresh
3145728
重组队列内存下限(字节)
net.ipv4.ipfrag_max_dist
64
最大乱序分片距离
11.3 高 PPS
转发场景调优清单
# 1. 开启转发
sysctl -w net.ipv4.ip_forward=1
# 2. 关闭 Early Demux(转发流量不需要)
sysctl -w net.ipv4.tcp_early_demux=0
sysctl -w net.ipv4.udp_early_demux=0
# 3. 关闭不需要的 Netfilter 模块
# 如果不需要 NAT,卸载 conntrack
modprobe -r nf_conntrack
# 4. 减少策略路由规则
# 用 VRF 替代大量 ip rule
# 5. 调整 rp_filter(宽松模式比严格模式快)
sysctl -w net.ipv4.conf.all.rp_filter=2
# 6. 确保 RSS/RPS 均匀分布(减少单 CPU 瓶颈)
ethtool -L eth0 combined $( nproc )
十二、总结
IP
层是内核网络栈中逻辑最复杂的一层——路由查找、策略选表、Netfilter
钩子、分片重组、PMTU
缓存,每个子系统都有自己的数据结构和性能特征:
FIB 用 LC-trie :O(log n) 前缀匹配,10
万条路由也只需十几次内存访问
策略路由是线性遍历 :ip rule
越多越慢,高 PPS 场景用 VRF 替代
五个 Netfilter 钩子 :PRE_ROUTING →
LOCAL_IN / FORWARD → POST_ROUTING / LOCAL_OUT,conntrack
在高 PPS 转发时可能成为瓶颈
分片重组用红黑树 :超时 30 秒、内存限制
4MB,注意分片攻击防护
PMTU 用 FNHE 缓存 :per-nexthop
异常表替代了旧的全局路由缓存
Early Demux
是双刃剑 :加速已建立连接,但在转发场景是无用开销
下一篇我们深入 TCP 连接管理,拆解 SYN 队列、Accept 队列和
TCP 状态机的内核实现。
参考文献
Linux 内核源码,net/ipv4/ip_input.c,6.6
LTS / 6.8
Linux 内核源码,net/ipv4/ip_forward.c,6.6
LTS
Linux 内核源码,net/ipv4/fib_trie.c,6.6
LTS(LC-trie 实现)
Linux
内核源码,include/net/ip.h,6.8(ip_rcv
声明于第 161 行,ip_forward 声明于第 749
行,ip_defrag 声明于第 735 行)
Linux
内核源码,include/net/ip_fib.h,6.8(fib_result
定义于第 169 行,fib_nh 定义于第 105 行)
Linux
内核源码,include/net/route.h,6.8(rtable
定义于第 60 行)
Robert Olsson, “Trash-based Routing”, 2004, USENIX
Annual Technical Conference
Linux
内核文档,Documentation/networking/ip-sysctl.rst
上一篇 :软中断与
ksoftirqd:网络包处理的调度引擎
下一篇 :TCP
内核实现(上):连接管理与状态机
同主题继续阅读 把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-04-21 · linux / networking
IP 包到了 ip_rcv_finish(),下一跳怎么选?本文深入拆解 Linux 路由子系统的完整实现:fib_table 的 LC-trie 数据结构、fib_info/fib_nh 的内存布局、fib_rules 策略路由链、ECMP 多路径哈希选路、nexthop 对象 API、FNHE 异常缓存(路由缓存的替代品)、dst_entry 与 rtable 的关系、IPv6 fib6 差异,以及 bpftrace 追踪路由决策的实战方法。
2026-04-20 · linux / networking
从 sk_buff 到 XDP,从收包路径到 TC 框架——系统拆解 Linux 内核网络子系统的每一个核心模块。基于 Linux 6.6 LTS 源码,配合 bpftrace/perf 实测追踪。
2026-04-03 · linux / networking
从 NIC 驱动到用户态 read(),一个网络包在 Linux 内核中到底经历了什么?本文拆解 sk_buff、NAPI、softirq、netfilter 的完整收包路径,并用 bpftrace 实测追踪每一跳的延迟。
2026-04-03 · linux / networking
Linux 路由表、FIB 查找、策略路由,以及 VXLAN/Geneve/WireGuard 隧道技术深度拆解