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

【Linux 网络子系统深度拆解】IP 层内核实现:路由查找、分片与转发

文章导航

分类入口
linuxnetworking
标签入口
#ip-layer#linux-kernel#fib#routing#lc-trie#ip-fragment#netfilter#pmtu#ecmp#bpftrace

目录

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

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

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

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)。


七、IP 分片与重组

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):


八、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 实测追踪。


By .