内核网络栈:sk_buff、NAPI 与 XDP

内核网络栈是 Linux 数据平面的核心,从网卡硬中断到 socket 读取,数据包要穿过 NAPI、协议栈、netfilter、qdisc 等多层结构。本文以 sk_buff 生命周期为主线,详解收发路径、GRO/RPS 优化、conntrack 状态跟踪、TCP 拥塞控制框架与 XDP 快速路径,并给出 netns/veth 容器网络与 perf 丢包定位的完整排查清单。

Linux 内核网络栈是整台机器数据平面的心脏。无论是容器之间的 veth 转发、云主机的虚拟网卡收包,还是裸金属上的十万级 QPS 服务,最终都要经过同一套收包、协议解析、连接跟踪与发送调度机制。理解这套机制的内部结构,是把网络性能从「能用」推到「极致」的前提。

本文以 struct sk_buff 的生命周期为主线,依次讲解网卡硬中断到 NAPI poll 的接收路径、dev_queue_xmit 到 qdisc 的发送路径,深入分析 GRO/RPS/RFS/XPS 这组多核扩展技术、netfilter 五钩子与 conntrack 状态表、struct tcp_sock 与拥塞控制框架,并覆盖 XDP/AF_XDP 快速路径、netns 与 veth/bridge 容器网络,最后给出一份基于 perf 与 tracepoint 的丢包定位清单。


一、sk_buff 结构与生命周期

struct sk_buff(简称 skb)是内核网络栈中承载一个数据包的通用容器。它不复制报文数据本身,而是通过一组指针描述数据在内存缓冲区中的位置,从而支持在协议栈各层之间零拷贝地推入与弹出头部。

1.1 关键字段

字段含义
head / end缓冲区首尾,二者之间是已分配的存储空间
data / tail当前有效数据的首尾,头部入栈时 data 前移
len / data_len线性区长度 / 分片(paged data)长度
dev / sk关联的网络设备 / 关联的 socket
cb[48]每层协议私有的控制块,TCP/IP 各自复用
_skb_refdst路由缓存引用(dst entry)
protocol三层协议类型(ETH_P_IP 等)
struct sk_buff {
    struct sk_buff *next, *prev;
    union { struct net_device *dev; unsigned long dev_scratch; };
    char cb[48] __aligned(8);        /* 各层私有控制块 */
    unsigned int len, data_len;      /* 线性区 + 分片长度 */
    __u16 mac_len, hdr_len, protocol;
    sk_buff_data_t tail, end, head;  /* 用偏移量定位,节省指针 */
    unsigned char *data;
    ...
};

注意 head/data/tail/end 中除 data 外都是偏移量(sk_buff_data_t)而非指针,这是为了缩小结构体、提升缓存命中率。

1.2 分配与释放

struct sk_buff *alloc_skb(unsigned int size, gfp_t priority);   /* 普通分配 */
struct sk_buff *__dev_alloc_skb(unsigned int len, gfp_t mask);  /* 软中断中分配 */
void kfree_skb(struct sk_buff *skb);      /* 引用计数归零时真正回收 */
void consume_skb(struct sk_buff *skb);    /* 消费一个引用 */

skb->users 是引用计数:skb_get() 加一,kfree_skb() 减一。计数降到 0 时,内核先调用 skb_release_all() 释放 frags、dst 引用、secpath 等附属资源,再把缓冲区归还给 skbuff_fclone_cache 或 skbuff_head_cache。

1.3 线性区与分片

skb 的数据分为两部分:线性区(data 到 tail,协议头解析必然访问的连续区域)与分片区(由 skb_shared_info 的 frags[] 描述,每个 frag 指向一个 page 的区间)。

struct skb_shared_info {
    __u8 nr_frags;               /* frag 数量 */
    __u8 tx_flags;
    unsigned short gso_size;     /* GSO 分段大小 */
    unsigned short gso_segs;
    struct sk_buff *frag_list;   /* 分片链表 */
    skb_frag_t frags[MAX_SKB_FRAGS];
};

发送大包时,应用数据直接放进 frags(零拷贝路径),协议头留在线性区,内核无需为每个报文复制整个 payload。skb_headlen(skb) 返回线性区长度,skb_pagelen(skb) 返回分片长度。

1.4 复制与克隆

struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t mask);  /* 共享数据,仅复制头部 */
struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t mask); /* 数据也复制 */
struct sk_buff *skb_share_check(struct sk_buff *skb, gfp_t mask);

skb_clone() 是零拷贝的,克隆体与原 skb 共享 skb_shared_info,其中 dataref 计数递增。因此克隆体不可修改数据,需要修改时必须先 pskb_expand_head() 或 skb_copy()。


二、收发路径与 NAPI 软中断

2.1 接收路径总览

一个报文从网卡到 socket 的完整路径:

网卡硬件
  └─ DMA 写入 RX ring buffer
  └─ 触发硬中断(MSI-X)
      └─ 中断处理程序调用 napi_schedule()
          └─ 触发 NET_RX_SOFTIRQ 软中断
              └─ napi_poll() 收包到 netdev_budget 上限
                  └─ napi_gro_receive() 做 GRO 合并
                      └─ netif_receive_skb()
                          └─ 协议栈:ip_rcv -> tcp_v4_rcv
                              └─ sk_data_ready() 唤醒 socket 等待队列

传统路径 netif_rx() 会把 skb 放入 per-CPU 的 backlog 队列,等待 NET_RX_SOFTIRQ 处理;现代 NAPI 驱动则直接在软中断上下文调用 napi_gro_receive()。

2.2 NAPI 与软中断

NAPI(New API)的核心是「中断 + 轮询」的混合模型:第一个包触发中断,随后关闭该队列的中断改为轮询收包,直到队列空或达到预算,再重新开启中断。

void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
                    int (*poll)(struct napi_struct *, int), int weight);
void napi_schedule(struct napi_struct *n);      /* 中断上下文中调度 */
int (*poll)(struct napi_struct *napi, int budget);  /* 轮询回调,返回处理报文数 */

NET_RX_SOFTIRQ 的处理函数是 net_rx_action(),它遍历 softnet_data.poll_list 上的 NAPI 实例逐个调用 poll()。当软中断运行超过 netdev_budget_usecs(默认 2000 微秒)或处理报文数超过 netdev_budget(默认 300)时,剩余工作交给 ksoftirqd 内核线程,避免长时间占用软中断上下文导致用户态饥饿。

2.3 发送路径

int dev_queue_xmit(struct sk_buff *skb);          /* 交给设备(经 qdisc) */
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
                        struct netdev_queue *txq); /* 直接调用驱动发送 */

dev_queue_xmit() 先做 qdisc 分类(__dev_xmit_skb()),再交给 qdisc 排队,qdisc 出队时调用 ndo_start_xmit()。若设备支持 GSO,dev_hard_start_xmit() 会把大包切分成多个 MTU 大小的段。发送完成后驱动通过 netif_tx_wake_queue() 唤醒被阻塞的发送队列。

2.4 netdev_budget 与 softnet_data

每个 CPU 都有一个 softnet_data 结构:

struct softnet_data {
    struct list_head poll_list;           /* 待轮询的 NAPI 实例 */
    struct sk_buff_head input_pkt_queue;  /* backlog 队列 */
    struct napi_struct backlog;
    unsigned int dropped;                 /* 因 backlog 满丢弃的计数 */
    unsigned int time_squeeze;            /* 因预算耗尽被迫退出的次数 */
    ...
};
sysctl net.core.netdev_budget          # 单次软中断处理的最大报文数(默认 300)
sysctl net.core.netdev_budget_usecs    # 单次软中断的最大耗时(默认 2000)
sysctl net.core.netdev_max_backlog     # per-CPU backlog 队列上限(默认 1000)

netdev_budget 过小会导致 time_squeeze 升高、包处理延迟增大;netdev_max_backlog 过小则在突发流量下直接丢包(dropped 计数增长)。


三、GRO、RPS、RFS 与 XPS

单核处理能力有限,Linux 用一组技术把收包工作分摊到多核:GRO 减少报文数量,RPS/RFS 把处理分散到多个 CPU,XPS 则优化发送侧的队列选择。

3.1 GRO 合并

GRO(Generic Receive Offload)在软中断中把同一 TCP 流的连续小包合并成一个大包再上送协议栈,显著减少 ip_rcv()/tcp_v4_rcv() 的调用次数。

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb);

合并条件是:同一五元组、序列号连续、时间间隔小于阈值。合并后的大包由 napi_gro_complete() 送入协议栈。

nstat -az | grep -E 'GRO|IpInDelivers'
ethtool -k eth0 | grep -E 'gro|gso'
ethtool -K eth0 gro off    # 需要抓包精确分析时关闭

3.2 RPS

RPS(Receive Packet Steering)在软件层模拟多队列:根据报文哈希计算目标 CPU,把 skb 放入该 CPU 的 input_pkt_queue,由目标 CPU 的软中断处理。

echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus   # 让 8 核都参与收包
cat /sys/class/net/eth0/queues/rx-0/rps_cpus

RPS 适用于单队列网卡。若网卡本身支持多队列(RSS),优先使用硬件分流,RPS 收益有限。

3.3 RFS

RFS(Receive Flow Steering)在 RPS 基础上进一步优化:它记录每条流的应用层处理 CPU,把报文尽量投递到运行该应用线程的 CPU 上,提升 CPU 缓存命中率。

sysctl net.core.rps_sock_flow_entries=32768      # 全局流表大小
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

RFS 需要应用侧配合:内核在 sendmsg/recvmsg 时通过 rps_record_sock_flow() 记录 socket 与 CPU 的关联。

3.4 XPS

XPS(Transmit Packet Steering)作用于发送侧:为每个 CPU 选择固定的发送队列,减少跨 CPU 的队列锁竞争。

echo f > /sys/class/net/eth0/queues/tx-0/xps_cpus   # CPU 0-3 使用 tx 队列 0
技术方向核心作用
GRO接收合并小包,减少协议栈调用
RPS接收软件层多核分摊收包
RFS接收按应用亲和性投递到目标 CPU
XPS发送固定 CPU 到发送队列映射

四、netfilter 与 conntrack

4.1 五个 hook 点

netfilter 在 IP 层的五个位置注册钩子,iptables/nftables 规则即挂载于此:

Hook触发位置典型用途
NF_INET_PRE_ROUTING报文进入本机、路由决策之前DNAT、raw 表
NF_INET_LOCAL_IN路由判定为发给本机之后防火墙 INPUT
NF_INET_FORWARD路由判定为转发转发过滤、K8s 转发
NF_INET_LOCAL_OUT本机产生的报文OUTPUT 过滤
NF_INET_POST_ROUTING报文即将离开本机SNAT、MASQUERADE
static unsigned int my_hook(void *priv, struct sk_buff *skb,
                            const struct nf_hook_state *state)
{
    /* 返回 NF_ACCEPT 继续,NF_DROP 丢弃,NF_STOLEN 接管 */
    return NF_ACCEPT;
}

static const struct nf_hook_ops my_ops = {
    .hook = my_hook,
    .pf = NFPROTO_IPV4,
    .hooknum = NF_INET_PRE_ROUTING,
    .priority = NF_IP_PRI_FIRST,
};
nf_register_net_hook(net, &my_ops);

4.2 iptables 与 nftables

iptables 的规则按表(table)组织,每张表挂在特定 hook 上:

iptables -t filter -L -n -v --line-numbers    # 查看规则与计数器
nft list ruleset                              # nftables 等价操作
nft add rule ip filter input tcp dport 22 accept
iptables-save > rules.v4                      # 迁移到 nftables
iptables-restore-translate -f rules.v4 > rules.nft

nftables 用统一字节码虚拟机取代了 iptables 的多表多链遍历,规则匹配复杂度更低,在大规模规则集下性能优势明显。

4.3 conntrack 状态表

连接跟踪(conntrack)为每条流维护一个 struct nf_conn,记录双向的元组、状态与超时。这是 NAT 与有状态防火墙的基础。

conntrack -L                                   # 查看当前连接跟踪表
conntrack -L -p tcp --state ESTABLISHED
conntrack -C                                   # 当前条目数
conntrack -S                                   # 统计信息(插入失败、无效包等)
sysctl net.netfilter.nf_conntrack_max          # 表容量上限
sysctl net.netfilter.nf_conntrack_tcp_timeout_established

TCP 连接状态机在 conntrack 中维护:NEW -> ESTABLISHED -> FIN_WAIT -> TIME_WAIT -> CLOSE。若 nf_conntrack_max 被占满,新连接会被直接丢弃并打印 nf_conntrack: table full, dropping packet——这是高并发服务器最常见的隐蔽丢包源之一。

4.4 调优建议

sysctl -w net.netfilter.nf_conntrack_max=1048576            # 提升表容量(每条约 300 字节)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600  # 加快条目回收
# options nf_conntrack hashsize=262144                       # 提升哈希桶,重启生效

对于纯转发或负载均衡场景,可考虑用 eBPF 的 bpf_ct_* 接口或 XDP 层的无状态处理绕开 conntrack,把每包开销降到最低。


五、socket 层与 TCP 拥塞控制

5.1 struct sock 与 tcp_sock

struct sock 是所有协议族 socket 的公共基类,struct tcp_sock 在其基础上扩展 TCP 专属状态:

struct tcp_sock {
    struct inet_connection_sock inet_conn;
    u32 rcv_nxt, snd_nxt;        /* 期望接收 / 下一个发送序号 */
    u32 snd_una, snd_wnd;        /* 未确认序号 / 发送窗口 */
    u32 snd_cwnd, snd_ssthresh;  /* 拥塞窗口 / 慢启动阈值 */
    u32 srtt_us, mdev_us;        /* RTT 估计值 */
    u32 rcv_wnd, rcv_ssthresh;
    struct tcp_congestion_ops *ca_ops;   /* 拥塞控制算法 */
    ...
};

收发队列通过 sk_receive_queue(已到达、待应用读取)与 sk_write_queue(已发送、待 ACK)管理,配合 sk_rmem_alloc/sk_wmem_alloc 做内存记账。

5.2 tcp_sendmsg 与 tcp_recvmsg

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size);
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,
                int nonblock, int flags, int *addr_len);

tcp_sendmsg() 把用户数据按 MSS 切分、构造成 skb 挂入 sk_write_queue,然后调用 tcp_push() 触发发送。tcp_recvmsg() 从 sk_receive_queue 取数据;若队列为空且为阻塞模式,进程会睡在 sk_sleep() 等待队列上,由 sk_data_ready() 唤醒。

ss -tnm      # 查看 socket 收发队列积压
ss -s        # 全局汇总
ss -tni      # 显示 cwnd、rtt、retrans 等 TCP 内部信息

5.3 拥塞控制框架

TCP 拥塞控制通过 struct tcp_congestion_ops 抽象,算法以可插拔模块形式注册:

struct tcp_congestion_ops {
    struct list_head list;
    u32 (*ssthresh)(struct sock *sk);
    void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
    void (*set_state)(struct sock *sk, u8 new_state);
    void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
    void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
    u32 (*undo_cwnd)(struct sock *sk);
    char name[TCP_CA_NAME_MAX];
};

慢启动阶段 snd_cwnd 指数增长,达到 snd_ssthresh 后进入拥塞避免改为线性增长;检测到丢包时执行 ssthresh() 缩减窗口。

5.4 cubic、bbr 与 sysctl

sysctl net.ipv4.tcp_available_congestion_control    # cubic reno bbr
sysctl -w net.ipv4.tcp_congestion_control=bbr       # 切换全局默认算法
tc qdisc replace dev eth0 root fq                   # BBR 需配合 fq 队列
# setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", 4);  # 单连接指定
算法核心思想适用场景
cubic三次函数增长窗口,丢包驱动通用默认,长肥管道
bbr基于带宽与 RTT 建模,不依赖丢包高丢包链路、跨洲传输
reno经典 AIMD兼容性测试

5.5 TCP_NODELAY 与 TCP_CORK

Nagle 算法会把小包攒起来合并发送,降低网络利用率但对延迟敏感的应用有害:

int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));  /* 禁用 Nagle */

int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));     /* 攒包 */
/* ... 多次 write ... */
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));     /* flush */

延迟敏感(如 RPC、游戏)用 TCP_NODELAY;批量拼包(如 HTTP 响应头 + 文件)用 TCP_CORK。二者互斥,同时设置时 TCP_NODELAY 优先。


六、XDP 与 AF_XDP 快速路径

6.1 XDP 动作

XDP(eXpress Data Path)让 eBPF 程序在驱动收到报文的最早时刻执行,早于 sk_buff 分配,是内核中性能最高的可编程数据路径:

返回值含义
XDP_PASS交给正常协议栈处理
XDP_DROP立即丢弃(可用于 DDoS 清洗)
XDP_TX从同一网卡原路发回
XDP_REDIRECT重定向到其他网卡或 AF_XDP socket
XDP_ABORTED程序出错,丢弃
SEC("xdp")
int xdp_drop_udp(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end) return XDP_PASS;
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end) return XDP_PASS;
    if (ip->protocol == IPPROTO_UDP) return XDP_DROP;
    return XDP_PASS;
}

6.2 三种运行模式

ip link set dev eth0 xdp obj prog.o sec xdp           # native:驱动直接调用,性能最高
ip link set dev eth0 xdpgeneric obj prog.o sec xdp    # generic:skb 之后执行,全兼容
ip link set dev eth0 xdpoffload obj prog.o sec xdp    # offload:卸载到智能网卡
ip link show eth0 ; ip link set dev eth0 xdp off      # 查看与卸载
模式执行时机性能兼容性
native驱动 RX 路径,skb 之前最高需驱动支持
genericnetif_receive_skb 之后中等全兼容
offload网卡硬件最高需专用网卡

6.3 AF_XDP zero-copy

AF_XDP 是一种 socket 类型,允许用户态直接读写网卡的 UMEM 区域,绕过内核协议栈实现零拷贝收包:

int fd = socket(AF_XDP, SOCK_RAW, 0);
struct sockaddr_xdp sxdp = {
    .sxdp_family = AF_XDP,
    .sxdp_ifindex = ifindex,
    .sxdp_queue_id = 0,
    .sxdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST,
};
bind(fd, (struct sockaddr *)&sxdp, sizeof(sxdp));

收发通过四个环形队列(RX/TX/FILL/COMPLETION)在用户态与内核之间传递描述符,数据本身不复制。

6.4 与 DPDK 对比

维度AF_XDPDPDK
网卡占用与内核共享完全接管(需绑定 vfio)
零拷贝支持(需驱动配合)支持
内核协议栈可共存,按需旁路完全旁路
部署复杂度低,随内核版本交付高,需大页、独占核
生态eBPF 生态成熟 DPDK 生态

AF_XDP 适合「大部分流量走内核、少量热点流量旁路」的场景;DPDK 适合整机专职转发的场景。


七、netns 与 veth/bridge 容器网络

7.1 ip netns 与 veth pair

ip netns add ns1 ; ip netns add ns2                # 创建两个网络命名空间
ip netns exec ns1 ip link show                     # 在命名空间内查看设备
ip netns exec ns1 ip link set lo up

ip link add veth1 type veth peer name veth1-peer   # 创建 veth 对
ip link set veth1-peer netns ns1                   # 一端放入 ns1
ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1-peer
ip netns exec ns1 ip link set veth1-peer up ; ip link set veth1 up

每个 netns 拥有独立的路由表、iptables、conntrack 与端口空间,这正是容器网络隔离的基础。veth 是一对虚拟网卡,从一端发出的报文会从另一端收到,用于连接两个 netns。

7.2 bridge 与 tc qdisc

Linux bridge 是软件交换机,把多个 veth 端口接入同一广播域;tc 决定报文在发送队列中的排队与调度策略:

ip link add br0 type bridge ; ip link set br0 up   # 创建网桥并接入端口
ip link set veth1 master br0
brctl show                                         # 旧工具等价查看

tc qdisc show dev eth0                             # 查看当前 qdisc
tc qdisc replace dev eth0 root fq_codel            # 缓解 bufferbloat
tc qdisc add dev eth0 root tbf rate 100mbit burst 32kbit latency 400ms
tc qdisc add dev eth0 root netem delay 100ms loss 1%

常用 qdisc:pfifo_fast(默认)、fq(BBR 伴侣)、fq_codel(抗 bufferbloat)、tbf(令牌桶限速)、netem(网络模拟)。Docker 默认创建的 docker0 就是一个 bridge,容器 veth 的一端接入其中,与宿主机的 NAT 规则配合实现外网访问。

7.3 容器网络数据路径

容器进程 write()
  └─ 容器内 eth0(veth 端)-> veth pair
      └─ 宿主机 docker0/br0 bridge 转发
          └─ netfilter POST_ROUTING 做 SNAT
              └─ 宿主机物理网卡 dev_queue_xmit -> qdisc -> 驱动

排查容器网络问题时,需要分别进入容器 netns 与宿主机视角检查:nsenter -t <pid> -n ip route、conntrack -L | grep <容器IP>、iptables -t nat -L -n -v。


八、丢包定位与排查清单

丢包可能发生在 ring buffer、backlog、qdisc、conntrack、socket 缓冲区中的任意一环。以下是一套从底层到上层的定位方法。

8.1 /proc/net/softnet_stat

每行对应一个 CPU,字段为十六进制:

cat /proc/net/softnet_stat
# 第 1 列:处理的报文总数
# 第 2 列:因 backlog 满而丢弃的报文数(dropped)
# 第 3 列:time_squeeze,预算耗尽被迫退出次数
# 第 9 列:CPU 冲突次数(collision)
# 第 14 列:收到的 RPS 报文数

第 2 列或第 3 列持续增长,说明软中断处理能力不足,应提升 netdev_budget、netdev_max_backlog 或启用 RPS。

8.2 netstat -s 与 nstat

netstat -s | grep -i -E 'drop|overflow|error|retrans'
nstat -az | grep -E 'Drop|Prune|Backlog|Retrans'
ss -s                                    # socket 汇总
# 关键指标:TcpExtListenDrops / ListenOverflows(accept 队列满)
#           TcpRetransSegs(重传次数)
#           UdpInErrors / RcvbufErrors(UDP 接收缓冲区溢出)

8.3 dropwatch 与 perf trace

dropwatch -l kas                          # 交互式输入 start 查看 kfree_skb 调用位置
perf trace -e net:* sleep 5               # 追踪网络 tracepoint
perf trace -e skb:kfree_skb -a sleep 5
perf record -e skb:kfree_skb -a -g -- sleep 10 ; perf report   # 按调用栈统计

8.4 tracepoint skb:kfree_skb 与 GRO

skb:kfree_skb 是最有价值的丢包 tracepoint,它的调用点即为丢包发生处。配合 napi_gro_* tracepoint 可观察 GRO 合并行为:

echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe
bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack] = count(); }'   # 丢包原因分布

8.5 常见丢包点排查清单

丢包点观测指标排查手段
网卡 ring bufferethtool -S 中 rx_dropped/rx_no_bufferethtool -G eth0 rx 4096 增大环形缓冲
backlog 队列/proc/net/softnet_stat 第 2 列提升 netdev_max_backlog
软中断预算/proc/net/softnet_stat 第 3 列提升 netdev_budget,启用 RPS
qdisc 队列tc -s qdisc show 中 dropped调整限速或改用 fq_codel
conntrack 表满dmesg 中 table full提升 nf_conntrack_max
socket 接收缓冲ss -tnm 中 Recv-Q 与 sk_rmem_alloc提升 net.core.rmem_max/rmem_default
UDP 缓冲溢出nstat 中 UdpRcvbufErrors提升 net.core.rmem_max,加快应用读取
accept 队列TcpExtListenOverflows提升 somaxconn 与 tcp_max_syn_backlog

相关阅读

  • https://plumephp.com/os-linux-network/ —— Linux 网络子系统整体架构与协议栈分层解析
  • https://plumephp.com/os-interrupts/ —— 硬中断、软中断与 NAPI 收包的中断处理机制
  • https://plumephp.com/os-ebpf-observability/ —— 用 eBPF 观测内核网络路径与丢包点

延伸阅读

  1. Linux Kernel Documentation: Documentation/networking/(driver.rst、scaling.rst 等)
  2. Linux Kernel Documentation: Documentation/networking/filter.rst(BPF/XDP 过滤器)
  3. Christian Benvenuti, 《Understanding Linux Network Internals》(O’Reilly)
  4. LWN.net: “The return of the AF_XDP” 与 “Network receive path” 系列文章
  5. Linux 源码:net/core/dev.c、net/core/skbuff.c、net/ipv4/tcp_input.c

# ============================================================
# 完整可运行示例:内核网络栈健康检查与丢包定位(需 root)
# ============================================================
set -u
IFACE="${1:-eth0}"
echo "=== 目标网卡: $IFACE ==="

echo "=== 1. 网卡驱动丢包统计 ==="
ethtool -S "$IFACE" 2>/dev/null | grep -iE 'drop|error|no_buffer|missed' || echo "  (无匹配)"

echo "=== 2. ring buffer 大小 ==="
ethtool -g "$IFACE" 2>/dev/null | sed -n '1,10p'

echo "=== 3. 每 CPU 软中断统计(processed / dropped / squeeze)==="
awk '{ printf "CPU%-3d %-12d %-12d %-12d\n", NR-1, strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3) }' \
    /proc/net/softnet_stat

echo "=== 4. 关键 sysctl 参数 ==="
for k in net.core.netdev_max_backlog net.core.netdev_budget net.core.netdev_budget_usecs \
         net.core.rmem_max net.core.rmem_default net.core.wmem_max net.core.somaxconn \
         net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_congestion_control; do
    printf "  %-38s = %s\n" "$k" "$(sysctl -n "$k" 2>/dev/null || echo n/a)"
done

echo "=== 5. conntrack 表使用情况 ==="
if [ -r /proc/sys/net/netfilter/nf_conntrack_count ]; then
    cnt=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
    max=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
    echo "  条目数: $cnt / $max ($(( cnt * 100 / max ))%)"
else
    echo "  (conntrack 未启用)"
fi

echo "=== 6. qdisc 队列与丢包 ==="
tc -s qdisc show dev "$IFACE" 2>/dev/null | grep -E 'qdisc|dropped|backlog' || echo "  (无)"

echo "=== 7. 协议层错误统计 ==="
nstat -az 2>/dev/null | grep -E 'Drop|Prune|Overflow|Retrans|RcvbufErrors' || \
    netstat -s 2>/dev/null | grep -iE 'drop|overflow'

echo "=== 8. socket 收发队列积压(Recv-Q > 0 的 TCP 连接)==="
ss -s 2>/dev/null
ss -tnm 2>/dev/null | awk 'NR>1 && $2+0 > 0 { print "  " $0 }' | head -10

echo "=== 9. 仍无法定位时追踪丢包调用栈 ==="
echo "  perf record -e skb:kfree_skb -a -g -- sleep 10 && perf report"
echo "  bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack] = count(); }'"

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. ARM64 体系结构与内核实现
  2. eBPF 开发实战:CO-RE 与 libbpf
  3. 内存回收机制:LRU、水位与 OOM