惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
罗磊的独立博客
腾讯CDC
云风的 BLOG
云风的 BLOG
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
U
Unit 42
I
InfoQ
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
美团技术团队
IT之家
IT之家
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
GbyAI
GbyAI
S
SegmentFault 最新的问题

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计
【Linux 网络子系统深度拆解】IP 层内核实现:路由查找、分片...
2026-04-20 · via 土法炼钢兴趣小组的算法知识备份

前几篇我们拆解了 收包路径发包路径 的全景。你会发现,两条路径在 IP 层交汇——收包时 ip_rcv() 决定这个包是本地投递还是转发,发包时 ip_queue_xmit() 查路由决定从哪个接口出去。IP 层是内核网络栈的中枢路由器

但如果你用 ip route show table all 看到几千条路由规则,你能回答这些问题吗?

  • FIB 路由表用的是什么数据结构?查找一条路由的时间复杂度是多少?
  • ip rule 策略路由是怎么选表的?性能开销有多大?
  • IP 分片重组的定时器什么时候触发?超时的分片怎么处理?
  • PMTU 发现失败时,内核是怎么回退到分片的?
  • Netfilter 的五个钩子点分别在 IP 处理的哪个位置被调用?

本文全部从内核源码出发回答这些问题。

IP 层数据路径与 Netfilter 钩子

一、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

2.2 ip_route_input_noref():完整路由查找

当 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);

这个函数内部的关键步骤:

  1. 构建流键struct flowi4):目的 IP、源 IP、TOS、入接口
  2. 策略路由选表:遍历 ip rule 规则链,选择匹配的 FIB 表
  3. FIB 查找:在选中的 FIB 表中做最长前缀匹配
  4. 构建 rtable:将 fib_result 转化为 struct rtable,设置 dst->inputdst->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_resultinclude/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_nhinclude/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 会成为性能瓶颈。优化方案:

  1. 减少规则数:用 fwmark 合并规则(在 iptables/nftables 中打标,一条 ip rule fwmark 覆盖多种场景)
  2. 调整规则优先级:把最频繁命中的规则放在最前面
  3. 使用 VRFip 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_rcvip_forwardip_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 分片长期以来是安全攻击的目标:

  1. Tiny Fragment Attack:构造极小的分片,让第一个分片的 TCP 头不完整,绕过状态less防火墙的端口检查
  2. Overlapping Fragment Attack:构造偏移重叠的分片,重组后覆盖已检查的头部字段
  3. 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_entryinclude/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_entryinputoutput 函数指针是 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_ROUTINGLOCAL_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 缓存,每个子系统都有自己的数据结构和性能特征:

  1. FIB 用 LC-trie:O(log n) 前缀匹配,10 万条路由也只需十几次内存访问
  2. 策略路由是线性遍历ip rule 越多越慢,高 PPS 场景用 VRF 替代
  3. 五个 Netfilter 钩子:PRE_ROUTING → LOCAL_IN / FORWARD → POST_ROUTING / LOCAL_OUT,conntrack 在高 PPS 转发时可能成为瓶颈
  4. 分片重组用红黑树:超时 30 秒、内存限制 4MB,注意分片攻击防护
  5. PMTU 用 FNHE 缓存:per-nexthop 异常表替代了旧的全局路由缓存
  6. Early Demux 是双刃剑:加速已建立连接,但在转发场景是无用开销

下一篇我们深入 TCP 连接管理,拆解 SYN 队列、Accept 队列和 TCP 状态机的内核实现。


参考文献

  1. Linux 内核源码,net/ipv4/ip_input.c,6.6 LTS / 6.8
  2. Linux 内核源码,net/ipv4/ip_forward.c,6.6 LTS
  3. Linux 内核源码,net/ipv4/fib_trie.c,6.6 LTS(LC-trie 实现)
  4. Linux 内核源码,include/net/ip.h,6.8(ip_rcv 声明于第 161 行,ip_forward 声明于第 749 行,ip_defrag 声明于第 735 行)
  5. Linux 内核源码,include/net/ip_fib.h,6.8(fib_result 定义于第 169 行,fib_nh 定义于第 105 行)
  6. Linux 内核源码,include/net/route.h,6.8(rtable 定义于第 60 行)
  7. Robert Olsson, “Trash-based Routing”, 2004, USENIX Annual Technical Conference
  8. Linux 内核文档,Documentation/networking/ip-sysctl.rst

上一篇软中断与 ksoftirqd:网络包处理的调度引擎

下一篇TCP 内核实现(上):连接管理与状态机

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-21 · linux / networking

【Linux 网络子系统深度拆解】路由子系统深度拆解:FIB、策略路由与路由缓存

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

Linux 网络子系统深度拆解

从 sk_buff 到 XDP,从收包路径到 TC 框架——系统拆解 Linux 内核网络子系统的每一个核心模块。基于 Linux 6.6 LTS 源码,配合 bpftrace/perf 实测追踪。