基于 Linux 内核主线源码的系统性梳理,从网卡接收到协议栈、从本地发送到队列调度,逐层剖析数据包可能被丢弃的位置、触发条件、对应源码与计数器,并给出排查思路。
0. 全景视角:数据包在内核中的生命周期
理解丢包,首先要建立整体路径模型。一个数据包在内核网络栈中大致经历如下阶段(接收方向):
NIC RX Ring → 驱动 napi_poll → netif_receive_skb → (XDP / TC ingress) → enqueue_to_backlog (per-CPU) → net_rx_action (软中断) → ip_rcv → (Netfilter PRE_ROUTING) → ip_rcv_finish → 路由决策 → ip_local_deliver → (Netfilter LOCAL_IN) → tcp_v4_rcv / udp_rcv → sock_queue_rcv_skb → 用户态
发送方向:
用户态 sendmsg → tcp_sendmsg / udp_sendmsg → ip_queue_xmit → (Netfilter LOCAL_OUT) → ip_output → neigh_resolve_output → (Netfilter POST_ROUTING) → dev_queue_xmit → qdisc enqueue → sch_direct_xmit → 驱动 hard_start_xmit → NIC TX Ring
几乎每一个箭头位置都存在丢包可能。下文按层次逐一展开。
1. 丢包原因的统一抽象:skb_dropreason
现代内核(5.17+)引入了 enum skb_drop_reason,将所有丢包原因统一编码,定义在 include/net/dropreason-core.h:
// include/net/dropreason-core.h (节选)#define DEFINE_DROP_REASON(FN, FNe) \ FN(NOT_SPECIFIED) \ FN(NO_SOCKET) \ FN(SOCKET_CLOSE) \ FN(SOCKET_FILTER) \ FN(SOCKET_RCVBUFF) \ FN(PKT_TOO_SMALL) \ FN(TCP_CSUM) \ FN(UDP_CSUM) \ FN(NETFILTER_DROP) \ FN(IP_CSUM) \ FN(IP_INHDR) \ FN(IP_RPFILTER) \ FN(XFRM_POLICY) \ FN(SOCKET_BACKLOG) \ FN(TCP_ZEROWINDOW) \ FN(TCP_LISTEN_OVERFLOW) \ FN(QDISC_DROP) \ FN(QDISC_OVERLIMIT) \ FN(CPU_BACKLOG) \ FN(XDP) \ FN(TC_INGRESS) \ FN(TC_EGRESS) \ FN(FULL_RING) \ FN(NOMEM) \/* ... 共计 120+ 种原因 ... */ \ FNe(MAX)
丢包函数 kfree_skb_reason(skb, reason) 在释放 skb 的同时记录原因,配合 tracepoint:skb:kfree_skb 与 dropreason ftrace 事件,可精准定位丢包点。这套机制是排查内核丢包的「第一性工具」。
2. NIC 驱动层丢包
2.1 硬件层与环形缓冲区
丢包点:网卡 RX Ring / FIFO / 内部缓冲区
NIC 自身有 FIFO 缓冲,若 RX 速率超过主机 DMA 搬运速率,硬件会直接丢弃数据包。同时,RX 描述符环形缓冲区(Ring Buffer)若被驱动填满,新到的帧也会被丢弃。
对应计数器(定义于 include/linux/netdevice.h):
structnet_device_stats { NET_DEV_STAT(rx_dropped); // 软件层丢包 NET_DEV_STAT(rx_over_errors); // 接收环形缓冲区满 NET_DEV_STAT(rx_fifo_errors); // 硬件 FIFO 溢出 NET_DEV_STAT(rx_missed_errors); // 数据包错过(如 carrier 错误) NET_DEV_STAT(rx_length_errors); // 长度异常 NET_DEV_STAT(rx_crc_errors); // CRC 校验失败 NET_DEV_STAT(rx_frame_errors); // 帧对齐错误/* ... */};
查看方式:
# 通用统计ip -s link show eth0# 驱动详细计数(取决于驱动)ethtool -S eth0 | grep -E 'rx_dropped|rx_missed|rx_no_dma|rx_fifo'# 查看环形缓冲区大小与是否需要调大ethtool -g eth0
常见原因:
- CPU 处理 NAPI 轮询不及时,描述符来不及回收
2.2 驱动软件层
驱动调用 napi_alloc_skb() / netdev_alloc_skb() 分配 skb 失败时,对应 SKB_DROP_REASON_NOMEM。典型代码模式:
skb = napi_alloc_skb(napi, len);if (!skb) {// 统计 rx_dropped,数据包直接被驱动丢弃 dev->stats.rx_dropped++;return; // 不进入协议栈}
3. NAPI 与软中断层丢包
3.1 NAPI 轮询预算超限
net_rx_action() 是软中断 NET_RX_SOFTIRQ 的处理函数(net/core/dev.c:7657)。它为每个 NAPI 实例设置预算(budget,默认 64),当处理包数达到预算时会主动让出 CPU,剩余包留待下一轮。
关键问题:若 NAPI 预算耗尽但网卡持续灌包,环形缓冲区会再次填满,最终导致硬件层丢包(见 §2.1)。
3.2 Per-CPU Backlog 队列溢出(enqueue_to_backlog)
对于非 NAPI 驱动或开启 RPS 的场景,数据包会被加入 per-CPU 的 softnet_data.input_pkt_queue。当队列长度超过 netdev_max_backlog(默认 1000)时直接丢包:
// net/core/dev.c:5158staticintenqueue_to_backlog(struct sk_buff *skb, int cpu, unsignedint *qtail){enum skb_drop_reason reason;structsoftnet_data *sd;/* ... */ reason = SKB_DROP_REASON_CPU_BACKLOG; // ← 丢包原因 sd = &per_cpu(softnet_data, cpu); qlen = skb_queue_len_lockless(&sd->input_pkt_queue); max_backlog = READ_ONCE(net_hotdata.max_backlog);if (unlikely(qlen > max_backlog))goto cpu_backlog_drop; // ← 触发丢包/* ... 入队成功 ... */cpu_backlog_drop: atomic_inc(&sd->dropped);bad_dev: dev_core_stats_rx_dropped_inc(skb->dev); kfree_skb_reason(skb, reason); // ← 最终释放return NET_RX_DROP;}
触发条件:
- RPS flow limit 命中(
SKB_DROP_REASON_CPU_BACKLOG,注释明确说明包含 flow limit 情形)
查看方式:
# /proc/net/softnet_stat 第二列非 0 表示 backlog 丢包cat /proc/net/softnet_stat# 输出格式:processed dropped time_squeeze ... # 例如:123456 78 9 ...# ^^ 这个就是 sd->dropped 计数
3.3 XDP 与 TC Ingress 丢包
在 __netif_receive_skb_core() 中,若网卡或驱动加载了 XDP 程序,且 XDP 返回 XDP_DROP,数据包立即丢弃:
// net/core/dev.c:5471kfree_skb_reason(*pskb, SKB_DROP_REASON_XDP);
TC ingress hook(sch_handle_ingress)返回 TC_ACT_SHOT 时同样丢包,原因 SKB_DROP_REASON_TC_INGRESS(net/core/dev.c:4394)。
4. IP 协议层丢包
ip_rcv() 是 IP 层入口,丢包点密集,集中在 net/ipv4/ip_input.c。
4.1 ip_rcv_core() 基础校验
// net/ipv4/ip_input.c:461static struct sk_buff *ip_rcv_core(struct sk_buff *skb, struct net *net){/* OTHERHOST: 混杂模式下收到的不属于本机的包 */if (skb->pkt_type == PACKET_OTHERHOST) { drop_reason = SKB_DROP_REASON_OTHERHOST;goto drop; }/* 共享 skb,复制失败(OOM)*/ skb = skb_share_check(skb, GFP_ATOMIC);if (!skb) { __IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);goto out; }/* 头部空间不足 */if (!pskb_may_pull(skb, sizeof(struct iphdr)))goto inhdr_error;/* 版本/IHL 非法 */if (iph->ihl < 5 || iph->version != 4)goto inhdr_error;/* IP 头校验和错误 */if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))goto csum_error; // → SKB_DROP_REASON_IP_CSUM/* 总长度字段小于实际长度 */ len = iph_totlen(skb, iph);if (skb->len < len) { drop_reason = SKB_DROP_REASON_PKT_TOO_SMALL; __IP_INC_STATS(net, IPSTATS_MIB_INTRUNCATEDPKTS);goto drop; }/* ... */}
4.2 ip_rcv_finish_core() 路由与策略
// net/ipv4/ip_input.c:371(节选自 ip_rcv_finish_core)/* 路由查找失败 */if (!skb_valid_dst(skb)) { drop_reason = ip_route_input_noref(skb, iph->daddr, iph->saddr, ip4h_dscp(iph), dev);if (unlikely(drop_reason))goto drop_error; // → SKB_DROP_REASON_IP_INADDRERRORS / IP_INNOROUTES 等}/* RP_FILTER 反向路径校验失败(防 IP 欺骗)*/// 在 fib_validate_source() 内部,失败后:// drop_reason = SKB_DROP_REASON_IP_RPFILTER;// __NET_INC_STATS(net, LINUX_MIB_IPRPFILTER);/* L2 多播中携带 L3 单播包(防 hole-196 攻击)*/if (in_dev && IN_DEV_ORCONF(in_dev, DROP_UNICAST_IN_L2_MULTICAST)) { drop_reason = SKB_DROP_REASON_UNICAST_IN_L2_MULTICAST;goto drop;}
对应 SNMP 计数器(/proc/net/snmp 中的 Ip: 节):
| |
|---|
InHdrErrors | |
InAddrErrors | |
InUnknownProtos | |
InDiscards | |
InTruncatedPkts | |
ReasmReqds | |
InNoRoutes | |
RP_FILTER 调试:
# 查看当前设置sysctl net.ipv4.conf.all.rp_filtersysctl net.ipv4.conf.eth0.rp_filter# 0=不校验, 1=严格模式(默认), 2=宽松模式# 若多路径/非对称路由场景,应改为 2 或 0
4.3 IP 分片重组失败
ip_defrag() 在以下情况丢包:
SKB_DROP_REASON_DUP_FRAG:重复分片SKB_DROP_REASON_FRAG_REASM_TIMEOUT:重组超时(默认 30s,由 ipfrag_time 控制)SKB_DROP_REASON_FRAG_TOO_FAR:分片距离过远(ipfrag_max_dist)SKB_DROP_REASON_NOMEM:分片队列内存耗尽
查看:/proc/sys/net/ipv4/ipfrag_time、ipfrag_max_dist、ipfrag_high_thresh。
4.4 ip_local_deliver_finish() 协议分发
// net/ipv4/ip_input.c:187voidip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol){ ipprot = rcu_dereference(inet_protos[protocol]);if (ipprot) {/* XFRM (IPsec) 策略检查 */if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb_reason(skb, SKB_DROP_REASON_XFRM_POLICY);return; } ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb);/* ... */ } else {/* 协议未注册(如协议号 99 等未启用)*/if (!raw) { icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); kfree_skb_reason(skb, SKB_DROP_REASON_IP_NOPROTO); } }}
5. Netfilter / iptables 层丢包
5.1 nf_hook_slow 核心逻辑
所有 Netfilter 钩子最终汇聚到 nf_hook_slow()(net/netfilter/core.c:616):
intnf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,const struct nf_hook_entries *e, unsignedint s){unsignedint verdict;int ret;for (; s < e->num_hook_entries; s++) { verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);switch (verdict & NF_VERDICT_MASK) {case NF_ACCEPT:break;case NF_DROP: kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP); // ← 标记原因 ret = NF_DROP_GETERR(verdict);if (ret == 0) ret = -EPERM;return ret;case NF_QUEUE: ret = nf_queue(skb, state, s, verdict);/* ... */case NF_STOLEN:return NF_DROP_GETERR(verdict); } }return1;}
5.2 五大 Hook 点
| | |
|---|
NF_INET_PRE_ROUTING | | raw 表、mangle 表(如 TTL 改写失败) |
NF_INET_LOCAL_IN | | filter 表 INPUT 链(最常见的 firewall DROP) |
NF_INET_FORWARD | | |
NF_INET_LOCAL_OUT | | |
NF_INET_POST_ROUTING | | |
5.3 排查手段
# 1. 查看规则计数iptables -nvL# 2. 跟踪具体包的 netfilter 决策modprobe nf_log_ipv4sysctl -w net.netfilter.nf_log.2=NFLOGiptables -t raw -A PREROUTING -p tcp --dport 80 -j LOG --log-prefix "TRACE:"# 3. 使用 nft 监控(更现代)nft monitor trace# 4. 使用 dropreason ftraceecho 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enablecat /sys/kernel/debug/tracing/trace_pipe
5.4 Conntrack 表满
连接追踪表(nf_conntrack)满后,新连接的初始包会被丢弃,表现为 SKB_DROP_REASON_NETFILTER_DROP:
# 查看当前连接数与上限cat /proc/sys/net/netfilter/nf_conntrack_countcat /proc/sys/net/netfilter/nf_conntrack_max# 日志中会出现:# "nf_conntrack: table full, dropping packet"
调优:增大 nf_conntrack_max、缩短 nf_conntrack_tcp_timeout_established。
6. TCP 协议层丢包
TCP 是丢包原因最丰富的协议(dropreason-core.h 中 TCP 相关原因近 30 种)。入口为 tcp_v4_rcv()(net/ipv4/tcp_ipv4.c:2195)。
6.1 tcp_v4_rcv() 前置校验
// net/ipv4/tcp_ipv4.c:2195inttcp_v4_rcv(struct sk_buff *skb){/* 非 PACKET_HOST 直接丢弃 */if (skb->pkt_type != PACKET_HOST)goto discard_it;/* 头部不足以容纳 TCP 头 */if (!pskb_may_pull(skb, sizeof(struct tcphdr)))goto discard_it;/* doff 非法(小于最小 TCP 头长度)*/if (unlikely(th->doff < sizeof(struct tcphdr) / 4)) { drop_reason = SKB_DROP_REASON_PKT_TOO_SMALL;goto bad_packet; }/* 校验和初始化失败(含伪首部)*/if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))goto csum_error; // → SKB_DROP_REASON_TCP_CSUM/* ... */}
6.2 Socket 查找失败
// net/ipv4/tcp_ipv4.c:2239sk = __inet_lookup_skb(...);if (!sk)goto no_tcp_socket; // → SKB_DROP_REASON_NO_SOCKET
典型场景:
- 半连接队列中找不到匹配的
request_sock
6.3 监听队列与全连接队列溢出
这是 TCP 最常见的丢包场景,发生在三次握手阶段:
// net/ipv4/tcp_minisocks.c:880listen_overflow: SKB_DR_SET(*drop_reason, TCP_LISTEN_OVERFLOW);if (!READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_abort_on_overflow)) { inet_rsk(req)->acked = 1;returnNULL; // ← 默认静默丢弃,等待 SYN 重传 }/* 若开启 tcp_abort_on_overflow,则发 RST */
// net/ipv4/tcp_ipv4.c:1772if (sk_acceptq_is_full(sk))goto exit_overflow; // ← 全连接队列满
两个队列:
- 半连接队列(SYN queue):
tcp_max_syn_backlog 控制,存储收到 SYN 但未完成握手 - 全连接队列(Accept queue):
somaxconn + listen() backlog 取最小值,存储已完成三次握手待 accept()
查看方式:
# ss 显示当前队列占用ss -lnt# State Recv-Q Send-Q Local Address:Port# LISTEN 0 128 0.0.0.0:80# ^ ^# | └─ 全连接队列上限# └─ 当前全连接队列长度# 计数器nstat -az | grep -i -E 'ListenOverflows|ListenDrops|EmbryonicRsts'# TcpExtListenOverflows: 全连接队列满# TcpExtListenDrops: 监听 socket 丢包总数# TcpExtEmbryonicRsts: 半连接 RST
调优:
sysctl -w net.core.somaxconn=65535sysctl -w net.ipv4.tcp_max_syn_backlog=65535# 应用层 listen(fd, 65535)
6.4 TCP 状态机相关丢包
tcp_rcv_state_process() 和 tcp_rcv_established() 中根据 socket 状态与 TCP 标志位进行大量校验:
| |
|---|
TCP_RESET | |
TCP_INVALID_SYN | |
TCP_INVALID_ACK_SEQUENCE | ACK 序号不在 [snd_una, snd_nxt] 窗口内 |
TCP_INVALID_SEQUENCE | |
TCP_INVALID_END_SEQUENCE | |
TCP_RFC7323_PAWS | |
TCP_RFC7323_PAWS_ACK | |
TCP_RFC7323_TW_PAWS | |
TCP_OLD_ACK | |
TCP_TOO_OLD_ACK | |
TCP_ACK_UNSENT_DATA | |
TCP_ZEROWINDOW | |
TCP_OVERWINDOW | |
TCP_OLD_DATA | |
TCP_OFOMERGE | |
TCP_OFO_QUEUE_PRUNE | |
TCP_OFO_DROP | |
TCP_FLAGS | |
TCP_CLOSE | |
TCP_MINTTL | |
TCP_MD5NOTFOUND | |
TCP_MD5UNEXPECTED | |
TCP_MD5FAILURE | |
TCP_AO* | TCP-AO 相关(AONOTFOUND/AOUNEXPECTED/AOKEYNOTFOUND/AOFAILURE) |
TCP_ABORT_ON_DATA | linger2 < 0 |
TCP_FASTOPEN | |
查看方式:
nstat -az | grep -i tcp# TcpInSegs / TcpOutSegs / TcpRetransSegs / TcpInErrs / TcpOutRsts ...# TcpExtPAWSEstabRejected / TcpExtPAWSOldAck ...# TcpExtTCPOFOQueue / TcpExtTCPOFODrop / TcpExtTCPOFOMerge ...
6.5 Socket Backlog 与接收缓冲区满
TCP 通过 tcp_v4_do_rcv() → sk_backlog_rcv() 路径将数据交给 socket 时,会检查接收队列容量(net/core/sock.c:488):
// net/core/sock.c:488int __sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){/* 接收缓冲区已满 */if (atomic_read(&sk->sk_rmem_alloc) >= READ_ONCE(sk->sk_rcvbuf)) { atomic_inc(&sk->sk_drops); // ← sk_drops 计数 trace_sock_rcvqueue_full(sk, skb);return -ENOMEM; // → SKB_DROP_REASON_SOCKET_RCVBUFF }/* 协议级内存配额超限 */if (!sk_rmem_schedule(sk, skb, skb->truesize)) { atomic_inc(&sk->sk_drops);return -ENOBUFS; // → SKB_DROP_REASON_PROTO_MEM }/* ... 入队 ... */}
sk_receive_skb() / __sk_receive_skb() 中还有一层 backlog 检查:
// net/core/sock.c:564if (sk_rcvqueues_full(sk, READ_ONCE(sk->sk_rcvbuf))) { atomic_inc(&sk->sk_drops); reason = SKB_DROP_REASON_SOCKET_RCVBUFF;goto discard_and_relse;}/* socket 被用户态占用时,加入 backlog */if ((err = sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf)))) {if (err == -ENOMEM) reason = SKB_DROP_REASON_PFMEMALLOC; // PFMEMALLOC 包进入非 reserve 路径if (err == -ENOBUFS) reason = SKB_DROP_REASON_SOCKET_BACKLOG; // backlog 队列满 atomic_inc(&sk->sk_drops);goto discard_and_relse;}
关键变量:
sk->sk_rcvbuf:socket 接收缓冲区上限(SO_RCVBUF × 2,默认由 net.core.rmem_default/rmem_max 控制)sk->sk_rmem_alloc:当前已用接收内存(含 receive_queue + backlog + ofo_queue)sk->sk_backlog.len:backlog 队列长度(用户态持锁时积压)
查看方式:
# /proc/net/udp 中 drops 列cat /proc/net/udp# sl local_address rem_address st tx_queue rx_queue ... drops# 0: 00000000:0050 00000000:0000 0A 0 0 ... 0 ← drops 列# TCP 没有 drops 列,但可从 ss -m 看每个 socket 的内存占用ss -tm | head# 计数器nstat -az | grep -i -E 'RcvbufErrors|InErrs|BacklogDrop'
调优:
sysctl -w net.core.rmem_max=16777216sysctl -w net.core.rmem_default=262144sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"# 应用层:setsockopt(SO_RCVBUF, ...)
6.6 全局 TCP 内存压力
tcp_memory_allocated 超过 tcp_mem[2] 时,TCP 进入内存压力状态,新包进入 OFO 队列会被裁剪(TCP_OFO_QUEUE_PRUNE),甚至直接拒绝。
# 查看当前 TCP 内存(页为单位,4KB/页)cat /proc/net/sockstat# TCP: inuse 8 orphan 0 tw 4 alloc 16 mem 3# ^^^ TCP 已用内存(页)# 阈值sysctl net.ipv4.tcp_mem# net.ipv4.tcp_mem = 18968 25291 37936 (low pressure max)
7. UDP 协议层丢包
UDP 无连接、无重传,丢包更具「静默性」。入口在 udp_rcv() → __udp4_lib_rcv()(net/ipv4/udp.c)。
7.1 主要丢包点
// net/ipv4/udp.c:2655(__udp4_lib_rcv 节选)/* 长度不足 */if (pskb_may_pull(skb, sizeof(struct udphdr)))goto drop;/* 校验和错误 */if (udp_lib_checksum_complete(skb))goto csum_error; // → SKB_DROP_REASON_UDP_CSUM/* 找不到对应 socket(端口未监听)*/if (!sk) { __UDP_INC_STATS(net, UDP_MIB_NOPORTS, ...); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);goto drop; // → SKB_DROP_REASON_NO_SOCKET}
7.2 udp_queue_rcv_skb() 中的丢包
// net/ipv4/udp.c:2009if (udp_lib_checksum_complete(skb)) { __UDP_INC_STATS(net, UDP_MIB_CSUMERRORS, is_udplite); __UDP_INC_STATS(net, UDP_MIB_INERRORS, is_udplite); kfree_skb_reason(skb, SKB_DROP_REASON_UDP_CSUM);return0;}/* UDP-Lite 覆盖范围校验 */if (UDP_SKB_CB(skb)->cscov < pcrlen)goto drop;/* socket filter 拒绝 */if (sk_filter_trim_cap(sk, skb, sizeof(struct udphdr), &reason))goto drop; // → SKB_DROP_REASON_SOCKET_FILTER/* 接收队列满 */if (__udp_queue_rcv_skb(sk, skb) < 0) { __UDP_INC_STATS(net, UDP_MIB_RCVBUFERRORS, ...); __UDP_INC_STATS(net, UDP_MIB_INERRORS, ...);goto drop; // → SKB_DROP_REASON_SOCKET_RCVBUFF}
7.3 UDP 全局内存限制
udp_memory_allocated 超过 udp_mem[2] 时,UDP 包直接丢弃,对应 SKB_DROP_REASON_PROTO_MEM:
sysctl net.ipv4.udp_memsysctl net.ipv4.udp_rmem_min
7.4 查看 UDP 丢包
# /proc/net/snmp 中的 Udp: 节nstat -az | grep -i udp# UdpInDatagrams UdpOutDatagrams# UdpInErrors UdpNoPorts UdpRcvbufErrors UdpSndbufErrors# UdpInCsumErrors UdpIgnoredMulti# /proc/net/udp 中 drops 列累计cat /proc/net/udp | awk '{s+=$6} END {print "UDP drops:", s}'
7.5 multicast 与 early demux
UdpIgnoredMulti:被 mc_forwarding 等机制忽略的多播包udp_v4_early_demux() 失败会提前命中 socket,但若 socket 已死或缓冲满,仍会丢包
8. 邻居子系统(ARP/Neighbor)丢包
发送路径上 neigh_resolve_output() → neigh_event_send() 中,若邻居处于 NUD_INCOMPLETE 状态,数据包会被加入 neigh->arp_queue 等待 ARP 解析完成(net/core/neighbour.c:1250):
// net/core/neighbour.c:1250if (neigh->nud_state == NUD_INCOMPLETE) {if (skb) {/* arp_queue 已满,丢弃最老的包 */while (neigh->arp_queue_len_bytes + skb->truesize > NEIGH_VAR(neigh->parms, QUEUE_LEN_BYTES)) {structsk_buff *buff; buff = __skb_dequeue(&neigh->arp_queue);if (!buff)break; neigh->arp_queue_len_bytes -= buff->truesize; kfree_skb_reason(buff, SKB_DROP_REASON_NEIGH_QUEUEFULL); NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards); } __skb_queue_tail(&neigh->arp_queue, skb); }}/* 邻居状态失败(NUD_FAILED)*/kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_FAILED);/* 邻居死亡 */kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_DEAD);/* 硬件头填充失败 */kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_HH_FILLFAIL);/* 创建邻居失败(内存不足)*/kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_CREATEFAIL);
触发条件:
- ARP 请求未及时响应(ARP 队列默认上限
GC_QUEUE_LEN_BYTES 约 64KB) - 邻居表满(
neigh_table 大小超 gc_thresh3)
查看方式:
# 邻居表ip neigh show# 192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE# 192.168.1.2 dev eth0 FAILED ← 失败状态# 计数器cat /proc/net/stat/arp_cache# entries allocs destroys hash_grows lookups hits ... unres_discards# 阈值sysctl net.ipv4.neigh.default.gc_thresh1 # 1024sysctl net.ipv4.neigh.default.gc_thresh2 # 2048sysctl net.ipv4.neigh.default.gc_thresh3 # 4096sysctl net.ipv4.neigh.default.unres_qlen_bytes
9. 流量控制(TC / Qdisc)层丢包
发送方向上,dev_queue_xmit() → __dev_xmit_skb() 会调用 q->enqueue() 将数据包加入 qdisc 队列(net/core/dev.c:4080):
// net/core/dev.c:4080rc = q->enqueue(skb, q, to_free) & NET_XMIT_MASK;
9.1 NOOP qdisc(默认无 qdisc 时)
// net/sched/sch_generic.c:635staticintnoop_enqueue(struct sk_buff *skb, struct Qdisc *qdisc, struct sk_buff **to_free){ dev_core_stats_tx_dropped_inc(skb->dev); __qdisc_drop(skb, to_free); // ← 直接丢弃return NET_XMIT_CN;}
9.2 pfifo / fq / codel 等
SKB_DROP_REASON_QDISC_DROP:qdisc 队列满(最常见,如 pfifo_fast 队列长度超限)SKB_DROP_REASON_QDISC_OVERLIMIT:超过总缓冲限制SKB_DROP_REASON_QDISC_CONGESTED:AQM 算法主动丢包(如 codel/fq_codel 拥塞控制)SKB_DROP_REASON_CAKE_FLOOD:CAKE qdisc 防洪(BLUE)丢包SKB_DROP_REASON_FQ_BAND_LIMIT / FQ_HORIZON_LIMIT / FQ_FLOW_LIMIT:FQ 系列限制SKB_DROP_REASON_TC_EGRESS:TC egress hook 返回 TC_ACT_SHOTSKB_DROP_REASON_TC_RECLASSIFY_LOOP:TC 重分类超过最大迭代次数(net/core/dev.c:4133)
9.3 查看方式
# qdisc 统计tc -s qdisc show dev eth0# qdisc mq 8: root# Sent 1234567890 bytes 9876543 pkt (dropped 1234, overlimits 567 requeues 89)# ^^^^^^^^ ^^^^^^^^^^^# QDISC_DROP QDISC_OVERLIMIT# class 统计tc -s class show dev eth0# 查看队列长度ip -s link show eth0# ... TX: bytes packets errors dropped carrier collsns# ^^^^^^ ^^^^^^^ ^^^^^^^# 发送失败 qdisc 丢包
9.4 调优
# 增大 txqueuelenip link set eth0 txqueuelen 10000# 改用 fq_codel(推荐)tc qdisc replace dev eth0 root fq_codel# 或 BBR + fqtc qdisc replace dev eth0 root fqsysctl -w net.ipv4.tcp_congestion_control=bbr
10. 发送路径(IP Output)丢包
10.1 路由查找失败
// net/ipv4/ip_output.c:539(__ip_queue_xmit)no_route: rcu_read_unlock(); IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); kfree_skb_reason(skb, SKB_DROP_REASON_IP_OUTNOROUTES);return -EHOSTUNREACH;
10.2 BPF cgroup egress 拒绝
// net/ipv4/ip_output.c:327kfree_skb_reason(skb, SKB_DROP_REASON_BPF_CGROUP_EGRESS);
10.3 IP 分片失败
发送超大包且无法分片(DF 位置 1)或分片内存不足时,丢包原因 SKB_DROP_REASON_PKT_TOO_BIG,并发送 ICMP Fragmentation Needed。
# PMTUD 相关nstat -az | grep -i -E 'FragCreates|FragFails|ReasmFails'sysctl net.ipv4.ip_no_pmtu_disc
10.4 驱动发送失败
sch_direct_xmit() 调用驱动 ndo_start_xmit(),若返回 NETDEV_TX_BUSY,skb 会被重新入队;若返回错误或网卡 TX Ring 满,最终会通过 kfree_skb_reason() 丢弃。
ethtool -S eth0 | grep -E 'tx_dropped|tx_busy|tx_timeout'
11. 内存压力相关丢包
11.1 skb 分配失败
__alloc_skb()(net/core/skbuff.c:640)在内存紧张时返回 NULL,调用方通常直接丢包,原因 SKB_DROP_REASON_NOMEM。
# 系统内存压力cat /proc/meminfo | grep -E 'MemFree|MemAvailable|Slab'# 触发 OOM 时观察 dmesgdmesg -T | grep -i -E 'oom|out of memory'
11.2 协议内存配额
每个协议有独立内存配额:
# TCP 内存cat /proc/net/sockstat | grep TCP# UDP 内存sysctl net.ipv4.udp_mem# 全局 socket 缓冲sysctl net.core.optmem_max
11.3 PFMEMALLOC 限制
SKB_DROP_REASON_PFMEMALLOC:skb 来自内存预留(PFMEMALLOC reserve),但当前路径/socket 不允许使用预留内存(如非 SOCK_MEMALLOC 类型的 socket)。常发生于系统内存紧张时。
12. 其他常见丢包原因
| | |
|---|
SKB_DROP_REASON_SECURITY_HOOK | | |
SKB_DROP_REASON_HDR_TRUNC | pskb_may_pull() | |
SKB_DROP_REASON_SKB_CSUM | | |
SKB_DROP_REASON_SKB_GSO_SEG | | |
SKB_DROP_REASON_SKB_UCOPY_FAULT | | MSG_ZEROCOPY |
SKB_DROP_REASON_DEV_HDR | | |
SKB_DROP_REASON_DEV_READY | | |
SKB_DROP_REASON_FULL_RING | | |
SKB_DROP_REASON_TAP_FILTER | | |
SKB_DROP_REASON_ICMP_CSUM | | |
SKB_DROP_REASON_IP_TUNNEL_ECN | | |
SKB_DROP_REASON_VXLAN_* | | |
SKB_DROP_REASON_BRIDGE_INGRESS_STP_STATE | | |
SKB_DROP_REASON_MAC_INVALID_SOURCE | | |
13. 系统性排查方法论
13.1 第一性工具:dropreason ftrace
# 开启 skb 丢包跟踪echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enablecat /sys/kernel/debug/tracing/trace_pipe# 输出示例:# tcp_v4_rcv+0x123/0x450 skbaddr=0xffff... location=tcp_v4_rcv+0x123/0x450 \# protocol=2048 reason=TCP_LISTEN_OVERFLOW
reason 字段直接给出 SKB_DROP_REASON_*,配合 location 反汇编即可精确定位代码行。
13.2 分层定位法
1. ethtool -S / ip -s link → NIC/驱动层2. /proc/net/softnet_stat → NAPI/backlog 层3. nstat -az / /proc/net/snmp → IP/TCP/UDP 协议层4. iptables -nvL / conntrack -L → Netfilter 层5. tc -s qdisc show → Qdisc 层6. ip neigh / /proc/net/stat/arp* → 邻居子系统7. ss -tm / /proc/net/sockstat → Socket 缓冲区8. ftrace dropreason → 精确代码定位
13.3 perf 与 BPF
# perf 采样 kfree_skb 调用栈perf record -g -a -e skb:kfree_skbperf script# bpftrace 一行命令bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack(5)] = count(); }'# BCC 工具# https://github.com/iovisor/bcc# tools/droptrace.py、tcplife、tcpretrans
13.4 常见症状与根因对照
| | |
|---|
| | nf_conntrack_count |
| | ss -lnt Recv-Q = Send-Q;TcpExtListenOverflows |
| udp_rmem | UdpRcvbufErrors |
| | LINUX_MIB_IPRPFILTER |
| | arp_cache |
| | ethtool -S |
| | TcpExtTCPOFOQueuePrune |
14. 总结
Linux 内核数据包丢包点遍布整个网络栈,没有任何一个工具能一次性定位所有丢包。理解每个层次的丢包原因码(SKB_DROP_REASON_*)与对应计数器是系统化排查的基础:
- 驱动与硬件层关注
ethtool -S、rx_dropped、rx_missed_errors - 软中断层关注
/proc/net/softnet_stat、netdev_max_backlog - IP 层关注
/proc/net/snmp 的 Ip: 节、RP_FILTER - Netfilter 层关注
iptables -nvL、conntrack 表 - TCP 层关注
nstat、ss -lnt 的队列长度、tcp_mem - UDP 层关注
UdpRcvbufErrors、udp_mem - Qdisc 层关注
tc -s qdisc show - 邻居子系统关注
ip neigh、gc_thresh
终极定位工具是 ftrace skb:kfree_skb 事件 + dropreason 子系统,它能在生产环境低开销地给出每一次丢包的精确原因与代码位置。结合 bpftrace / BCC 等现代可观测性工具,可以做到秒级定位丢包根因。
掌握这套分层方法论,再复杂的丢包问题也能抽丝剥茧、定位到根。
附录:关键源码索引
| | |
|---|
| include/net/dropreason-core.h | enum skb_drop_reason |
| net/core/skbuff.c | kfree_skb_reason() |
| include/linux/netdevice.h | struct net_device_stats |
| net/core/dev.c:5158 | enqueue_to_backlog() |
| net/core/dev.c:7657 | net_rx_action() |
| net/core/dev.c:5761 | __netif_receive_skb_core() |
| net/core/dev.c:4370/4414 | sch_handle_ingress() |
| net/netfilter/core.c:616 | nf_hook_slow() |
| net/ipv4/ip_input.c:461 | ip_rcv_core() |
| net/ipv4/ip_input.c:187 | ip_protocol_deliver_rcu() |
| net/ipv4/ip_output.c | __ip_queue_xmit() |
| net/ipv4/tcp_ipv4.c:2195 | tcp_v4_rcv() |
| net/ipv4/tcp_input.c | tcp_rcv_state_process() |
| net/ipv4/tcp_minisocks.c:880 | tcp_check_req() |
| net/ipv4/udp.c | __udp4_lib_rcv() |
| net/core/sock.c:488 | __sock_queue_rcv_skb() |
| net/core/sock.c:555 | __sk_receive_skb() |
| net/core/neighbour.c:1230 | neigh_event_send() |
| net/sched/sch_generic.c:635 | noop_enqueue() |
| net/core/dev.c:4086 | __dev_xmit_skb() |