上一篇我们拆解了 TCP
数据传输与拥塞控制——TCP
的复杂性在于它为可靠传输付出了巨大的内核开销:连接状态机、滑动窗口、拥塞控制、丢包检测……每个连接需要一个
2KB+ 的 tcp_sock 对象和数十个定时器。
UDP
没有这些。没有连接、没有窗口、没有拥塞控制。udp_sock(include/linux/udp.h:47)只在
inet_sock 基础上多了 200
字节的封装回调和接收队列。但”简单”不等于”不需要优化”——恰恰相反,UDP
的简单意味着每个包都要独立查找
socket、独立校验、独立入队,收包路径上的每一微秒都被放大。
当你用 UDP 承载 DNS 查询(每秒数十万 QPS)、QUIC 连接(一个端口上万个流)、或 VXLAN 隧道封装时,UDP 内核路径的效率直接决定了系统性能的天花板。
本文从内核源码出发,拆解 UDP 的五个核心机制:socket 查找优化、接收与发送路径、UDP GRO 聚合、批量收发、以及 UDP 封装支持。
下面这张图展示了 UDP 收包的核心路径——双哈希表查找、封装检查、入队与无锁读:
一、udp_sock:UDP 的内核控制块
UDP socket 的内核表示是
struct udp_sock(include/linux/udp.h:47),它继承自
inet_sock:
struct sock // 通用 socket 基类
└── struct inet_sock // IPv4 socket(源/目的地址、端口、TTL)
└── struct udp_sock // UDP 特有字段
udp_sock 在 inet_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_table(include/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_flags 中
UDP_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_receive 和
gro_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 | 1× |
recvmmsg(64) |
~157 | 1.5-2× |
recvmmsg(256) |
~40 | 2-3× |
七、UDP 封装:隧道协议的基础
UDP 的另一个重要角色是作为隧道封装层。VXLAN、WireGuard、IPsec(NAT-T)、L2TP 都在 UDP 端口上建立隧道。
封装类型
udp_sock->encap_type(include/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 的优化:
- 延迟归还——
udp_recvmsg()读取 skb 后不立刻归还内存,而是累积到forward_deficit - 批量归还——当
forward_deficit超过forward_threshold(rcvbuf的 25%),一次性归还 - 减少原子操作——把 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 场景需要开启 |
参考文献
- Linux
内核源码,
include/linux/udp.h,6.8(struct udp_sock 定义于第 47 行) - Linux
内核源码,
include/net/udp.h,6.8(struct udp_table 定义于第 73 行,__udp4_lib_lookup 于第 302 行) - Linux
内核源码,
include/net/gro.h,6.8(udp4_gro_receive 于第 405 行) - Linux
内核源码,
include/net/sock_reuseport.h,6.8(reuseport_select_sock 于第 37 行) - RFC 768, “User Datagram Protocol”, J. Postel, 1980
- RFC 3828, “The Lightweight User Datagram Protocol (UDP-Lite)”, L-A. Larzon et al., 2004
- Linux
内核文档,
Documentation/networking/ip-sysctl.rst
下一篇:Socket 层内核实现:从 VFS 到协议栈的桥梁
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【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 集成的完整实现。
【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 协议与加密路由模型,以及各协议的封装开销与硬件卸载能力对比。
【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 头缓存优化。
【Linux 网络子系统深度拆解】TCP 内核实现(下):数据传输与拥塞控制
tcp_sendmsg 把用户数据拷到 sk_buff 就完事了?远没有。后面还有 Nagle 合并、TSQ 限流、cwnd/rwnd 双窗口门控、RACK-TLP 丢包检测、拥塞状态机五态跳转、sk_pacing_rate 软件限速。本文从 Linux 6.6 内核源码拆解 TCP 数据传输的完整路径——从 send() 到 ACK 处理——以及拥塞控制框架 tcp_congestion_ops 的可插拔架构。