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

【Linux 网络子系统深度拆解】UDP 内核实现与 socket lookup 优化

文章导航

分类入口
linuxnetworking
标签入口
#udp#linux-kernel#socket-lookup#reuseport#udp-gro#recvmmsg#early-demux#encapsulation#bpftrace

目录

上一篇我们拆解了 TCP 数据传输与拥塞控制——TCP 的复杂性在于它为可靠传输付出了巨大的内核开销:连接状态机、滑动窗口、拥塞控制、丢包检测……每个连接需要一个 2KB+ 的 tcp_sock 对象和数十个定时器。

UDP 没有这些。没有连接、没有窗口、没有拥塞控制。udp_sockinclude/linux/udp.h:47)只在 inet_sock 基础上多了 200 字节的封装回调和接收队列。但”简单”不等于”不需要优化”——恰恰相反,UDP 的简单意味着每个包都要独立查找 socket、独立校验、独立入队,收包路径上的每一微秒都被放大。

当你用 UDP 承载 DNS 查询(每秒数十万 QPS)、QUIC 连接(一个端口上万个流)、或 VXLAN 隧道封装时,UDP 内核路径的效率直接决定了系统性能的天花板。

本文从内核源码出发,拆解 UDP 的五个核心机制:socket 查找优化、接收与发送路径、UDP GRO 聚合、批量收发、以及 UDP 封装支持。

下面这张图展示了 UDP 收包的核心路径——双哈希表查找、封装检查、入队与无锁读:

UDP 双哈希表 Socket 查找与收包路径

一、udp_sock:UDP 的内核控制块

UDP socket 的内核表示是 struct udp_sockinclude/linux/udp.h:47),它继承自 inet_sock

struct sock               // 通用 socket 基类
    └── struct inet_sock  // IPv4 socket(源/目的地址、端口、TTL)
        └── struct udp_sock  // UDP 特有字段

udp_sockinet_sock 基础上增加的关键字段:

struct udp_sock {
    struct inet_sock inet;

    /* 发送侧 */
    unsigned long  udp_flags;      /* UDP 标志位:Cork/GRO/Encap 等 */
    int            pending;         /* 待发送帧标记 */
    __u16          len;             /* 待发送帧总长 */
    __u16          gso_size;        /* GSO 段大小 */

    /* 封装回调(VXLAN/L2TP/ESP 等) */
    __u8           encap_type;      /* UDP_ENCAP_* 类型 */
    int  (*encap_rcv)(struct sock *sk, struct sk_buff *skb);
    void (*encap_destroy)(struct sock *sk);

    /* GRO 回调(per-socket 自定义聚合) */
    struct sk_buff *(*gro_receive)(struct sock *sk,
                                   struct list_head *head,
                                   struct sk_buff *skb);
    int  (*gro_complete)(struct sock *sk, struct sk_buff *skb,
                         int nhoff);

    /* 接收侧(缓存行对齐) */
    struct sk_buff_head reader_queue;  /* 快速读取队列——无锁读 */
    int  forward_deficit;              /* forward allocation 已归还量 */
    int  forward_threshold;            /* 触发归还的阈值(rcvbuf >> 2) */
};

tcp_sock 的 2KB+ 相比,udp_sock 约 400 字节——轻量正是 UDP 的优势。但代价是每个包都需要独立查找 socket。

二、双哈希表与 socket 查找

UDP 收包路径的第一步是”这个包该投递给哪个 socket”。当端口上只有一个 socket 时很简单,但 SO_REUSEPORT 允许多个 socket 绑定同一端口——此时需要高效的查找和分发。

哈希表结构

Linux 为 UDP 维护一个全局双哈希表 udp_tableinclude/net/udp.h:73):

struct udp_table {
    struct udp_hslot *hash;     /* 一级哈希:按端口号 */
    struct udp_hslot *hash2;    /* 二级哈希:按端口号 + 本地地址 */
    unsigned int      mask;     /* 哈希掩码(槽数 - 1) */
    unsigned int      log;      /* log2(槽数) */
};

struct udp_hslot {
    struct hlist_head head;     /* socket 链表 */
    int               count;    /* 链表长度 */
    spinlock_t        lock;     /* 桶级自旋锁 */
};

一级哈希hash):只按端口号哈希。优点是查找快,缺点是同端口的所有 socket 都在一个桶里——当 SO_REUSEPORT 开启数十个 socket 时,桶内遍历成本高。

二级哈希hash2):按端口号 + 本地地址哈希。当 socket 绑定了特定地址(不是 INADDR_ANY),二级哈希能大幅缩小搜索范围。

查找路径

__udp4_lib_lookup()include/net/udp.h:302)是核心查找函数:

__udp4_lib_lookup(net, saddr, sport, daddr, dport, dif, sdif, tbl, skb)
    │
    ├── 第一步:尝试二级哈希(hash2)
    │   ├── slot2 = udp_hashslot2(tbl, hash2(dport, daddr))
    │   ├── 遍历 slot2 链表,按匹配度评分
    │   │   ├── 四元组完全匹配 → 最高分(connected UDP)
    │   │   ├── 目的端口 + 目的地址 → 次高分
    │   │   ├── 目的端口 + INADDR_ANY → 最低分
    │   │   └── SO_REUSEPORT → reuseport_select_sock()
    │   └── 找到最高分 → 返回
    │
    └── 第二步:回退一级哈希(hash)
        ├── slot = udp_hashslot(tbl, dport)
        ├── 遍历 slot 链表,同样评分
        └── 返回最高分 socket

评分机制的关键:connected UDP(调用过 connect() 绑定了四元组)得分最高,因为它匹配最精确。这是 DNS 客户端、QUIC 等场景的优化基础——connect() 一个 UDP socket 不建立连接,但让内核把后续包直接路由到这个 socket,跳过链表遍历。

SO_REUSEPORT 多核分发

当多个 socket 通过 SO_REUSEPORT 绑定同一端口时,reuseport_select_sock()include/net/sock_reuseport.h:37)从中选一个:

reuseport_select_sock(sk, hash, skb, hdr_len)
    ├── 如果挂载了 eBPF 程序
    │   └── 调用 BPF 程序选择 socket(自定义负载均衡)
    │
    └── 默认:hash % num_socks
        └── 用包的四元组哈希选择 socket

eBPF 选择器是高性能 UDP 服务的关键优化。你可以用 reuseport_attach_prog() 挂载 BPF 程序,根据包内容(如 QUIC Connection ID)把包精确路由到处理该连接的 socket,避免跨 CPU 分发。

# 查看 SO_REUSEPORT 组信息
ss -ulnp | grep -c ':53'  # 同端口的 socket 数量

三、接收路径:udp_rcv 到 socket 接收队列

主接收路径

IP 层的 ip_local_deliver_finish() 根据协议号(17 = UDP)调用 udp_rcv()include/net/udp.h:285)。实际处理在 __udp4_lib_rcv() 中:

__udp4_lib_rcv(skb, udp_table, proto)
    │
    ├── udp 头校验(长度、偏移)
    │
    ├── __udp4_lib_lookup_skb(skb, sport, dport)
    │   └── 查找目标 socket(走双哈希表)
    │
    ├── 找到 socket?
    │   ├── 是 → udp_unicast_rcv_skb(sk, skb)
    │   │   ├── 封装检查:encap_type 非零?
    │   │   │   └── 是 → encap_rcv(sk, skb)
    │   │   │       → ESP/L2TP/VXLAN 解封装
    │   │   ├── udp_queue_rcv_skb(sk, skb)
    │   │   │   └── __udp_enqueue_schedule_skb(sk, skb)
    │   │   │       ├── 检查 sk_rcvbuf 限制
    │   │   │       │   └── 超限 → 丢弃(UDP receive buffer errors)
    │   │   │       ├── __skb_queue_tail(&sk->sk_receive_queue, skb)
    │   │   │       └── sk_data_ready(sk)  唤醒等待的 recvmsg
    │   │   └── 返回
    │   │
    │   └── 否 → icmp_send(ICMP_DEST_UNREACH, ICMP_PORT_UNREACH)
    │       └── 发 ICMP 端口不可达
    │
    └── 广播/组播路径(另行处理)

Early Demux 优化

在 IP 层路由查找之前udp_v4_early_demux()include/net/udp.h:275)尝试提前确定目标 socket:

ip_rcv_finish()
    ├── ip_rcv_finish_core()
    │   ├── 如果 net.ipv4.ip_early_demux 开启
    │   │   └── udp_v4_early_demux(skb)
    │   │       ├── 用包的四元组查找 socket
    │   │       ├── 找到 → 缓存 skb->dst(路由缓存)
    │   │       └── 后续 ip_route_input_noref() 直接用缓存
    │   └── ip_route_input_noref()  路由查找
    └── ...

Early Demux 的核心收益:对已有 socket 的包,跳过完整的路由查找——dst_entry 直接从上次的缓存取。在 DNS 服务器等高 QPS 场景下,这个优化减少了每包数百纳秒的路由开销。

# 查看/控制 Early Demux
sysctl net.ipv4.ip_early_demux  # 默认 1(开启)

接收队列:reader_queue

Linux 4.10 之后,UDP 引入了 reader_queue——一个独立于 sk_receive_queue 的快速读取队列,用于减少收发路径之间的锁竞争:

收包路径(softirq 上下文):
    __udp_enqueue_schedule_skb()
    → spin_lock(&sk->sk_receive_queue.lock)
    → __skb_queue_tail(&sk->sk_receive_queue, skb)
    → spin_unlock()

读取路径(用户进程上下文):
    __skb_recv_udp()
    → 先检查 reader_queue(无锁)
    │   └── 非空?直接取 → 返回
    └── reader_queue 空?
        → spin_lock(&sk->sk_receive_queue.lock)
        → 把 sk_receive_queue 整体"移入" reader_queue
        → spin_unlock()
        → 从 reader_queue 无锁取

这个设计把锁的粒度从”每个包一次锁”降低到”批量移入一次锁”——在高 PPS 场景下,读取路径大部分时间不需要竞争锁。

四、发送路径:udp_sendmsg

用户调用 sendto() / sendmsg() 时,最终到达 udp_sendmsg()include/net/udp.h:279):

udp_sendmsg(sk, msg, len)
    │
    ├── 连接检查
    │   ├── socket 已 connect()?→ 使用缓存的目的地址
    │   └── 未 connect?→ 从 msg->msg_name 取目的地址
    │
    ├── Cork 模式(UDP_CORK 或 MSG_MORE)?
    │   ├── 是 → 数据追加到 pending 帧
    │   │   └── 等 cork 解除后一次性发出
    │   └── 否 → 立即发送
    │
    ├── ip_make_skb(sk, ...)
    │   ├── 分配 sk_buff
    │   ├── 拷贝用户数据(或零拷贝引用页面)
    │   ├── 如果数据超过 MTU → IP 层分片
    │   └── 构造 UDP 头(source/dest/len/checksum)
    │
    └── udp_send_skb(skb, ...)
        └── ip_send_skb(skb)
            → ip_local_out() → Netfilter → 路由 → 驱动

UDP Cork:合并小包

UDP_CORK socket 选项(类似 TCP 的 TCP_CORK)让多次 sendmsg() 的数据合并为一个大 UDP 包:

int cork = 1;
setsockopt(fd, SOL_UDP, UDP_CORK, &cork, sizeof(cork));
sendto(fd, data1, len1, 0, ...);  /* 暂不发送 */
sendto(fd, data2, len2, 0, ...);  /* 继续追加 */
cork = 0;
setsockopt(fd, SOL_UDP, UDP_CORK, &cork, sizeof(cork));
/* 解除 cork → udp_push_pending_frames() → 发出合并后的大包 */

MSG_MORE 标志提供同样的效果但更灵活——每次 sendto() 单独决定是否继续追加。

UDP GSO:发送侧分段卸载

UDP GSO(Generic Segmentation Offload)让应用一次 sendmsg() 发送一个大缓冲区,内核按 gso_size 延迟到驱动层再分段:

/* 设置 GSO 段大小 */
uint16_t gso_size = 1472;  /* MTU - IP头 - UDP头 */
setsockopt(fd, SOL_UDP, UDP_SEGMENT, &gso_size, sizeof(gso_size));

/* 一次发送 64KB */
sendto(fd, buf, 65536, 0, ...);
/* 内核在 GSO 层拆成 45 个 1472 字节的 UDP 包 */

这个优化把”每包一次 sendmsg 系统调用”降低到”一次 sendmsg 搞定”——对高吞吐 UDP 应用(如 QUIC)效果显著。

五、UDP GRO:接收侧聚合

UDP GRO(Generic Receive Offload)是 UDP GSO 的接收端对应——把网卡收到的多个小 UDP 包合并为一个大 skb,让上层一次 recvmsg() 读取多个包的数据。

启用方式

int val = 1;
setsockopt(fd, SOL_UDP, UDP_GRO, &val, sizeof(val));

设置后,udp_sock->udp_flagsUDP_FLAGS_GRO_ENABLED 置位。

聚合路径

GRO 在 NAPI 收包路径的 napi_gro_receive() 中执行(参见 收包路径全解)。UDP 的 GRO 回调链:

napi_gro_receive(napi, skb)
    ↓
dev_gro_receive(napi, skb)
    ↓
udp4_gro_receive(head, skb)        /* include/net/gro.h:405 */
    ├── 查找匹配的 GRO 流(四元组 + UDP 端口)
    ├── 校验:所有包的 payload 长度相同?
    │   └── 否 → GRO_NORMAL(不聚合)
    ├── 聚合到已有 skb 的 frag_list
    │   └── 更新 skb->len,增加 gro_count
    └── 返回 GRO_MERGED
        ↓
    flush 时机到达:
    udp4_gro_complete(skb, nhoff)  /* include/net/gro.h:407 */
    ├── 设置 skb->csum_start 和 csum_offset
    └── 交给 udp_rcv 处理聚合后的大包

Socket 级 GRO 回调

udp_sock 中的 gro_receivegro_complete 函数指针(include/linux/udp.h:79-85)允许封装协议注册自定义 GRO 逻辑。例如 VXLAN 驱动注册自己的 GRO 回调,在外层 UDP GRO 之后继续对内层包做 GRO。

GRO 的限制

UDP GRO 只聚合连续到达的、payload 长度相同的UDP 包。如果包大小不一致或到达间隔过长(flush timer 触发),就退化为逐包处理。在 QUIC 等场景下,配合 UDP GSO 使用效果最佳——发送端按固定段大小发包,接收端精确聚合。

六、批量收发:recvmmsg 与 sendmmsg

系统调用是 UDP 高 PPS 场景的主要瓶颈之一——每次 recvmsg() 都要经过用户态→内核态切换、锁获取、数据拷贝、锁释放、内核态→用户态切换。recvmmsg()sendmmsg() 把多个消息合并为一次系统调用。

recvmmsg

/* include/linux/socket.h:412 */
int recvmmsg(int sockfd, struct mmsghdr *msgvec, unsigned int vlen,
             unsigned int flags, struct timespec *timeout);
struct mmsghdr {
    struct msghdr msg_hdr;   /* 标准 msghdr */
    unsigned int  msg_len;   /* 实际接收长度(内核回填) */
};

/* 一次调用接收最多 vlen 个包 */
struct mmsghdr msgs[256];
/* ... 初始化每个 msg_hdr 的 iov ... */
int n = recvmmsg(fd, msgs, 256, MSG_WAITFORONE, NULL);
/* n 个包已接收,msgs[i].msg_len 是每个包的长度 */

MSG_WAITFORONE 标志(include/linux/socket.h:320)的语义:阻塞等待第一个包到达,之后非阻塞地收取剩余包。这避免了”要么全阻塞要么全非阻塞”的两难。

sendmmsg

/* include/linux/socket.h:416 */
int sendmmsg(int sockfd, struct mmsghdr *msgvec, unsigned int vlen,
             unsigned int flags);

MSG_BATCH 标志(include/linux/socket.h:320)告诉内核”后面还有更多消息”,让内核延迟 flush 以提高批量发送效率。

性能收益

在典型的 DNS 服务器场景(每包 ~512 字节):

方式 系统调用次数/万包 相对吞吐
recvmsg 逐包 10000
recvmmsg(64) ~157 1.5-2×
recvmmsg(256) ~40 2-3×

七、UDP 封装:隧道协议的基础

UDP 的另一个重要角色是作为隧道封装层。VXLAN、WireGuard、IPsec(NAT-T)、L2TP 都在 UDP 端口上建立隧道。

封装类型

udp_sock->encap_typeinclude/linux/udp.h:57)标记封装类型,对应 include/uapi/linux/udp.h:32-44 的常量:

类型 协议
UDP_ENCAP_ESPINUDP 2 IPsec ESP-in-UDP(RFC 3948)
UDP_ENCAP_L2TPINUDP 3 L2TP-in-UDP(RFC 2661)
UDP_ENCAP_GTP0 4 GTP v0(GSM)
UDP_ENCAP_GTP1U 5 GTP v1-U(3GPP)
UDP_ENCAP_RXRPC 6 RxRPC

封装接收路径

encap_type 非零时,udp_queue_rcv_skb() 在入 socket 队列之前调用 encap_rcv() 回调:

udp_unicast_rcv_skb(sk, skb)
    ├── encap_type != 0?
    │   └── 是 → sk->encap_rcv(sk, skb)
    │       ├── VXLAN: vxlan_rcv() → 解封装 → netif_rx(inner_skb)
    │       ├── WireGuard: wg_receive() → 解密 → netif_rx()
    │       └── ESP: xfrm4_udp_encap_rcv() → IPsec 解密
    └── 否 → udp_queue_rcv_skb()(正常 UDP 入队)

VXLAN 是最典型的 UDP 封装用户。它在目标端口(默认 4789)注册 encap_rcv 回调,收到包时剥离 VXLAN 头、查找 FDB 表、然后把内层以太网帧重新注入协议栈。关于隧道的详细实现,将在后续文章 隧道协议内核实现 中深入拆解。

八、内存管理:forward allocation

UDP 的接收缓冲区管理比 TCP 更直接——没有窗口协商,就靠 sk_rcvbuf 硬限制。但”超限就丢”太粗暴——Linux 引入了 forward allocation 机制来优化内存效率。

机制

/* include/linux/udp.h:87-94 */
struct sk_buff_head reader_queue;     /* 快速读取队列 */
int  forward_deficit;                  /* 已归还的 forward alloc 量 */
int  forward_threshold;                /* 触发归还阈值 = rcvbuf >> 2 */

传统路径中,每个 skb 入队时从 sk->sk_forward_alloc 扣减 truesize,出队时归还。问题是归还操作需要原子操作,在高 PPS 下成为瓶颈。

forward allocation 的优化:

  1. 延迟归还——udp_recvmsg() 读取 skb 后不立刻归还内存,而是累积到 forward_deficit
  2. 批量归还——当 forward_deficit 超过 forward_thresholdrcvbuf 的 25%),一次性归还
  3. 减少原子操作——把 N 次原子减操作合并为 1 次
# 监控 UDP 接收缓冲区使用
cat /proc/net/udp | awk '{print $5}'  # rx_queue 列

# 查看 UDP 内存使用
cat /proc/net/sockstat | grep UDP
# UDP: inuse 42 mem 128

丢包检测

sk_rcvbuf 耗尽时,__udp_enqueue_schedule_skb() 丢弃新包并递增计数器:

# 查看 UDP 接收缓冲区溢出
cat /proc/net/snmp | grep Udp
# Udp: ... RcvbufErrors ...

# 实时监控
watch -n1 'nstat -az | grep UdpRcvbuf'

九、UDP-Lite:部分校验和

UDP-Lite(RFC 3828)是 UDP 的变体,允许只校验包的前 N 个字节——适用于语音/视频等能容忍部分数据损坏的场景。

内核实现复用了 UDP 的大部分代码(include/net/udplite.h),区别在于:

#define UDPLITE_SEND_CSCOV   10  /* 发送端校验覆盖长度 */
#define UDPLITE_RECV_CSCOV   11  /* 接收端最小校验覆盖长度 */

UDP-Lite 使用独立的协议号(136)和哈希表(udplite_table),收包时 udplite_checksum_init() 根据覆盖长度做部分校验。

十、可观测性实战

socket 查找延迟追踪

# 测量 UDP socket lookup 耗时
bpftrace -e '
kprobe:__udp4_lib_lookup { @start[tid] = nsecs; }
kretprobe:__udp4_lib_lookup /@start[tid]/ {
    @lookup_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
interval:s:10 { print(@lookup_ns); clear(@lookup_ns); }'

接收队列深度监控

# 追踪 UDP 入队——检查是否接近 rcvbuf 上限
bpftrace -e '
kprobe:__udp_enqueue_schedule_skb {
    $sk = (struct sock *)arg0;
    $rmem = $sk->sk_rmem_alloc.refs.counter;
    $rcvbuf = $sk->sk_rcvbuf;
    @queue_pct = hist($rmem * 100 / $rcvbuf);
}
interval:s:5 { print(@queue_pct); clear(@queue_pct); }'

UDP 丢包追踪

# 追踪 UDP 收包丢弃(rcvbuf 溢出)
bpftrace -e '
kprobe:__udp_enqueue_schedule_skb {
    @sk[tid] = arg0;
}
kretprobe:__udp_enqueue_schedule_skb /@sk[tid]/ {
    if (retval != 0) {
        $sk = (struct sock *)@sk[tid];
        $port = $sk->__sk_common.skc_num;
        printf("UDP drop: port=%d err=%d\n", $port, retval);
        @drops_by_port[$port] = count();
    }
    delete(@sk[tid]);
}'

GRO 聚合效果监控

# 查看 GRO 聚合统计
ethtool -S eth0 | grep gro

# 追踪 UDP GRO 聚合比——每次 flush 时的 gro_count
bpftrace -e '
kprobe:udp4_gro_complete {
    $skb = (struct sk_buff *)arg0;
    @gro_count = hist($skb->gso_segs);
}'

sendmmsg/recvmmsg 批量效率

# 追踪 recvmmsg 每次调用收取的消息数
bpftrace -e '
tracepoint:syscalls:sys_exit_recvmmsg {
    if (args->ret > 0) {
        @msgs_per_call = hist(args->ret);
    }
}
interval:s:10 { print(@msgs_per_call); clear(@msgs_per_call); }'

十一、关键参数速查

参数 默认值 内核对应 调优建议
net.core.rmem_max 212992 socket 接收缓冲区上限 UDP 高 PPS 建议增大到 16MB+
net.core.rmem_default 212992 socket 接收缓冲区默认值 配合 rmem_max 增大
net.ipv4.udp_mem 低/压力/高 UDP 全局内存限制 高负载服务器按内存比例调整
net.ipv4.udp_rmem_min 4096 UDP socket 最小接收缓冲区 通常不需要调整
net.ipv4.udp_wmem_min 4096 UDP socket 最小发送缓冲区 通常不需要调整
net.ipv4.ip_early_demux 1 Early Demux 开关 保持开启,对 UDP 性能有益
net.ipv4.udp_l3mdev_accept 0 是否接受 L3 设备上的 UDP VRF 场景需要开启

参考文献

  1. Linux 内核源码,include/linux/udp.h,6.8(struct udp_sock 定义于第 47 行)
  2. Linux 内核源码,include/net/udp.h,6.8(struct udp_table 定义于第 73 行,__udp4_lib_lookup 于第 302 行)
  3. Linux 内核源码,include/net/gro.h,6.8(udp4_gro_receive 于第 405 行)
  4. Linux 内核源码,include/net/sock_reuseport.h,6.8(reuseport_select_sock 于第 37 行)
  5. RFC 768, “User Datagram Protocol”, J. Postel, 1980
  6. RFC 3828, “The Lightweight User Datagram Protocol (UDP-Lite)”, L-A. Larzon et al., 2004
  7. Linux 内核文档,Documentation/networking/ip-sysctl.rst

上一篇TCP 内核实现(下):数据传输与拥塞控制

下一篇Socket 层内核实现:从 VFS 到协议栈的桥梁

同主题继续阅读

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

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】Socket 层内核实现:从 VFS 到协议栈的桥梁

你调用 socket(AF_INET, SOCK_STREAM, 0) 创建一个 TCP 连接,底层发生了什么?内核分配了两个核心对象——VFS 层的 struct socket 和协议层的 struct sock,通过 proto_ops 和 proto 两张分发表,把文件系统语义的 read/write 翻译成协议语义的 tcp_sendmsg/tcp_recvmsg。本文从 Linux 6.6 内核源码拆解 socket 创建、双层分发、SO_REUSEPORT 多核分发、epoll 集成的完整实现。

2026-04-26 · linux / networking

【Linux 网络子系统深度拆解】隧道协议内核实现:VXLAN、IPIP、GRE 与 WireGuard

隧道是 overlay 网络的基础设施。本文从 Linux 6.6 内核源码拆解四类隧道协议的实现:ip_tunnel 通用框架与 struct ip_tunnel_key 元数据、IPIP 最小开销封装、GRE 可选头部与 ERSPAN 集成、VXLAN 的 UDP 封装路径与 FDB 转发表、metadata mode 流式隧道与 OVS/Cilium 集成、WireGuard 的 Noise 协议与加密路由模型,以及各协议的封装开销与硬件卸载能力对比。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】邻居子系统与 ARP:L2 地址解析的内核实现

IP 层知道下一跳是 10.0.0.1,但网卡发帧需要 MAC 地址。ARP 解析只是表面——底层是邻居子系统(neighbour subsystem)的完整状态机:NUD_INCOMPLETE → NUD_REACHABLE → NUD_STALE → NUD_DELAY → NUD_PROBE → NUD_FAILED。本文从 Linux 6.6 内核源码拆解 struct neighbour、neigh_table 双哈希表、ARP 请求/响应处理、NDP(IPv6)、Proxy ARP、GC 回收机制,以及 neigh_connected_output 快路径的 L2 头缓存优化。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】TCP 内核实现(下):数据传输与拥塞控制

tcp_sendmsg 把用户数据拷到 sk_buff 就完事了?远没有。后面还有 Nagle 合并、TSQ 限流、cwnd/rwnd 双窗口门控、RACK-TLP 丢包检测、拥塞状态机五态跳转、sk_pacing_rate 软件限速。本文从 Linux 6.6 内核源码拆解 TCP 数据传输的完整路径——从 send() 到 ACK 处理——以及拥塞控制框架 tcp_congestion_ops 的可插拔架构。


By .