第6章 数据包接收路径
学习目标
- 理解Linux网络栈的完整数据包接收路径
- 掌握硬中断、软中断、NAPI机制
- 了解sk_buff数据结构和内存管理
- 能够使用工具观察和调优网络性能
🔬 原理
完整数据包接收路径
网卡 → DMA → Ring Buffer → 硬中断 → 软中断(NAPI)
→ GRO → 协议栈 → Socket缓冲区 → 应用
详细流程:
- 网卡接收:物理信号转换为数字数据
- DMA传输:网卡直接将数据写入内存
- 硬中断:通知CPU有数据到达
- 软中断:NAPI轮询处理数据包
- GRO合并:将小包合并成大包
- 协议栈处理:IP、TCP/UDP解析
- Socket接收:数据进入应用缓冲区
硬中断机制
中断类型:
- MSI-X:消息信号中断,支持多队列
- MSI:消息信号中断,单队列
- 传统中断:共享中断线
中断处理:
// 中断处理函数
static irqreturn_t e1000_intr(int irq, void *data) {
struct net_device *netdev = data;
struct e1000_adapter *adapter = netdev_priv(netdev);
// 读取中断状态
u32 icr = er32(ICR);
if (!icr) {
return IRQ_NONE;
}
// 禁用中断,启用NAPI
napi_schedule(&adapter->napi);
return IRQ_HANDLED;
}
NAPI机制
NAPI(New API):
- 高流量时关闭中断,使用轮询
- 低流量时恢复中断触发
- 减少中断开销,提升性能
NAPI结构:
struct napi_struct {
struct list_head poll_list; // 轮询列表
unsigned long state; // NAPI状态
int weight; // 权重
int (*poll)(struct napi_struct *, int); // 轮询函数
struct net_device *dev; // 网络设备
// ... 更多字段
};
NAPI状态:
- NAPIF_STATE_SCHED:已调度
- NAPIF_STATE_DISABLE:已禁用
- NAPIF_STATE_NPSVC:正在轮询
sk_buff数据结构
sk_buff结构:
struct sk_buff {
struct sk_buff *next; // 下一个skb
struct sk_buff *prev; // 上一个skb
struct sock *sk; // 关联的socket
struct net_device *dev; // 网络设备
unsigned char *head; // 数据缓冲区起始
unsigned char *data; // 当前数据指针
unsigned char *tail; // 数据结束位置
unsigned char *end; // 缓冲区结束位置
unsigned int len; // 数据长度
unsigned int data_len; // 数据长度
__u16 protocol; // 协议类型
__u16 transport_header; // 传输层头偏移
__u16 network_header; // 网络层头偏移
__u16 mac_header; // MAC层头偏移
// ... 更多字段
};
内存布局:
head data tail end
| | | |
|-------|-------|-------|
|headroom|payload|tailroom|
GRO机制
GRO(Generic Receive Offload):
- 接收侧合并小包成大包
- 减少协议栈处理次数
- 提升吞吐量
GRO类型:
- GRO_NETVSC:Hyper-V网络
- GRO_GRE:GRE隧道
- GRO_GSO:通用分段卸载
️ 实现
网卡驱动实现
网卡初始化:
static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent) {
struct net_device *netdev;
struct e1000_adapter *adapter;
// 分配网络设备
netdev = alloc_etherdev(sizeof(struct e1000_adapter));
if (!netdev) {
return -ENOMEM;
}
// 初始化适配器
adapter = netdev_priv(netdev);
adapter->netdev = netdev;
// 设置网络设备操作
netdev->netdev_ops = &e1000_netdev_ops;
netdev->ethtool_ops = &e1000_ethtool_ops;
// 注册网络设备
if (register_netdev(netdev)) {
free_netdev(netdev);
return -EIO;
}
return 0;
}
数据包接收:
static int e1000_clean_rx_irq(struct e1000_adapter *adapter, int budget) {
struct net_device *netdev = adapter->netdev;
struct pci_dev *pdev = adapter->pdev;
struct e1000_ring *rx_ring = adapter->rx_ring;
struct sk_buff *skb;
int cleaned = 0;
while (cleaned < budget) {
// 从Ring Buffer获取描述符
struct e1000_rx_desc *rx_desc = E1000_RX_DESC(rx_ring, rx_ring->next_to_clean);
if (!(rx_desc->status & E1000_RXD_STAT_DD)) {
break; // 没有数据
}
// 分配sk_buff
skb = netdev_alloc_skb_ip_align(netdev, adapter->rx_buffer_len);
if (!skb) {
break; // 内存不足
}
// 从DMA区域复制数据
memcpy(skb->data, rx_desc->buffer_addr, rx_desc->length);
skb->len = rx_desc->length;
// 设置网络设备
skb->dev = netdev;
// 发送到协议栈
netif_receive_skb(skb);
cleaned++;
}
return cleaned;
}
协议栈处理
网络层处理:
int ip_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev) {
struct iphdr *iph;
u32 len;
// 验证IP头
if (!pskb_may_pull(skb, sizeof(struct iphdr))) {
goto drop;
}
iph = ip_hdr(skb);
// 验证IP头校验和
if (ip_fast_csum((u8 *)iph, iph->ihl)) {
goto drop;
}
// 验证长度
len = ntohs(iph->tot_len);
if (skb->len < len) {
goto drop;
}
// 路由查找
if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev)) {
goto drop;
}
// 发送到传输层
return dst_input(skb);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
传输层处理:
int tcp_v4_rcv(struct sk_buff *skb) {
struct iphdr *iph;
struct tcphdr *th;
struct sock *sk;
// 解析TCP头
if (!pskb_may_pull(skb, sizeof(struct tcphdr))) {
goto drop;
}
th = tcp_hdr(skb);
iph = ip_hdr(skb);
// 查找socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk) {
goto drop;
}
// 处理TCP段
return tcp_v4_do_rcv(sk, skb);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
🛠️ 命令
网络统计信息
# 查看网络统计
cat /proc/net/dev
# 查看协议统计
cat /proc/net/snmp
# 查看连接统计
ss -s
# 查看软中断统计
cat /proc/softirqs
网卡状态检查
# 查看网卡信息
ethtool eth0
# 查看网卡统计
ethtool -S eth0
# 查看网卡特性
ethtool -k eth0
# 查看Ring Buffer大小
ethtool -g eth0
中断信息
# 查看中断分布
cat /proc/interrupts | grep eth0
# 查看中断亲和性
cat /proc/irq/*/smp_affinity_list
# 查看软中断统计
cat /proc/softirqs | head -n2
代码
网络统计监控程序
// net_stats.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct net_stats {
char interface[16];
unsigned long rx_bytes;
unsigned long rx_packets;
unsigned long rx_errors;
unsigned long rx_dropped;
unsigned long tx_bytes;
unsigned long tx_packets;
unsigned long tx_errors;
unsigned long tx_dropped;
};
int parse_net_dev(struct net_stats *stats, int max_interfaces) {
FILE *fp = fopen("/proc/net/dev", "r");
if (!fp) {
perror("fopen");
return -1;
}
char line[256];
int count = 0;
// 跳过前两行
fgets(line, sizeof(line), fp);
fgets(line, sizeof(line), fp);
while (fgets(line, sizeof(line), fp) && count < max_interfaces) {
if (sscanf(line, "%15s %lu %lu %lu %lu %lu %lu %lu %lu %lu",
stats[count].interface,
&stats[count].rx_bytes,
&stats[count].rx_packets,
&stats[count].rx_errors,
&stats[count].rx_dropped,
&stats[count].tx_bytes,
&stats[count].tx_packets,
&stats[count].tx_errors,
&stats[count].tx_dropped) == 9) {
count++;
}
}
fclose(fp);
return count;
}
int main() {
struct net_stats stats[32];
int count = parse_net_dev(stats, 32);
if (count < 0) {
return 1;
}
printf("%-15s %12s %12s %12s %12s %12s %12s %12s %12s\n",
"Interface", "RX Bytes", "RX Packets", "RX Errors", "RX Dropped",
"TX Bytes", "TX Packets", "TX Errors", "TX Dropped");
printf("%-15s %12s %12s %12s %12s %12s %12s %12s %12s\n",
"--------", "--------", "----------", "---------", "----------",
"--------", "----------", "---------", "----------");
for (int i = 0; i < count; i++) {
printf("%-15s %12lu %12lu %12lu %12lu %12lu %12lu %12lu %12lu\n",
stats[i].interface,
stats[i].rx_bytes,
stats[i].rx_packets,
stats[i].rx_errors,
stats[i].rx_dropped,
stats[i].tx_bytes,
stats[i].tx_packets,
stats[i].tx_errors,
stats[i].tx_dropped);
}
return 0;
}
编译运行:
gcc net_stats.c -o net_stats
./net_stats
软中断监控程序
// softirq_monitor.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct softirq_stats {
unsigned long hi;
unsigned long timer;
unsigned long net_tx;
unsigned long net_rx;
unsigned long block;
unsigned long irq_poll;
unsigned long tasklet;
unsigned long sched;
unsigned long hrtimer;
unsigned long rcu;
};
int parse_softirqs(struct softirq_stats *stats) {
FILE *fp = fopen("/proc/softirqs", "r");
if (!fp) {
perror("fopen");
return -1;
}
char line[256];
if (fgets(line, sizeof(line), fp)) {
if (sscanf(line, " %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu",
&stats->hi, &stats->timer, &stats->net_tx, &stats->net_rx,
&stats->block, &stats->irq_poll, &stats->tasklet,
&stats->sched, &stats->hrtimer, &stats->rcu) == 10) {
fclose(fp);
return 0;
}
}
fclose(fp);
return -1;
}
int main() {
struct softirq_stats stats1, stats2;
if (parse_softirqs(&stats1) < 0) {
return 1;
}
sleep(1);
if (parse_softirqs(&stats2) < 0) {
return 1;
}
printf("SoftIRQ Statistics (per second):\n");
printf("HI: %lu\n", stats2.hi - stats1.hi);
printf("Timer: %lu\n", stats2.timer - stats1.timer);
printf("Net TX: %lu\n", stats2.net_tx - stats1.net_tx);
printf("Net RX: %lu\n", stats2.net_rx - stats1.net_rx);
printf("Block: %lu\n", stats2.block - stats1.block);
printf("IRQ Poll: %lu\n", stats2.irq_poll - stats1.irq_poll);
printf("Tasklet: %lu\n", stats2.tasklet - stats1.tasklet);
printf("Sched: %lu\n", stats2.sched - stats1.sched);
printf("HRTimer: %lu\n", stats2.hrtimer - stats1.hrtimer);
printf("RCU: %lu\n", stats2.rcu - stats1.rcu);
return 0;
}
编译运行:
gcc softirq_monitor.c -o softirq_monitor
./softirq_monitor
🧪 实验
实验1:观察数据包接收路径
目标:理解数据包从网卡到应用的完整路径
步骤:
# 1. 查看网卡统计
ethtool -S eth0 | grep -E 'rx_packets|rx_bytes|rx_errors|rx_dropped'
# 2. 查看软中断统计
cat /proc/softirqs | head -n2
# 3. 发送数据包
ping -c 100 8.8.8.8
# 4. 再次查看统计
ethtool -S eth0 | grep -E 'rx_packets|rx_bytes|rx_errors|rx_dropped'
cat /proc/softirqs | head -n2
# 5. 观察变化
echo "RX packets increased: $(ethtool -S eth0 | grep rx_packets | awk '{print $2}')"
echo "NET_RX softirq increased: $(cat /proc/softirqs | grep NET_RX | awk '{print $2}')"
预期结果:
- 理解数据包计数
- 观察软中断处理
- 掌握统计信息解读
实验2:GRO影响测试
目标:测试GRO对网络性能的影响
步骤:
# 1. 查看当前GRO状态
ethtool -k eth0 | grep generic-receive-offload
# 2. 测试基线性能
iperf3 -s &
iperf3 -c localhost -t 30
# 3. 关闭GRO
sudo ethtool -K eth0 gro off
# 4. 测试性能
iperf3 -c localhost -t 30
# 5. 重新开启GRO
sudo ethtool -K eth0 gro on
# 6. 测试性能
iperf3 -c localhost -t 30
预期结果:
- 理解GRO的作用
- 观察性能差异
- 掌握GRO调优
实验3:中断亲和性优化
目标:优化中断处理性能
步骤:
# 1. 查看当前中断分布
cat /proc/interrupts | grep eth0
# 2. 查看CPU核心数
nproc
# 3. 设置中断亲和性
# 假设有4个CPU核心,将eth0中断绑定到CPU 0-3
for i in {0..3}; do
IRQ=$(grep eth0-TxRx-$i /proc/interrupts | awk -F: '{print $1}')
if [ ! -z "$IRQ" ]; then
echo $i | sudo tee /proc/irq/$IRQ/smp_affinity_list
fi
done
# 4. 验证设置
cat /proc/interrupts | grep eth0
# 5. 测试性能
iperf3 -c localhost -t 30
预期结果:
- 理解中断亲和性
- 观察性能提升
- 掌握中断优化
实验4:内存使用监控
目标:监控网络内存使用情况
步骤:
# 1. 查看网络内存统计
cat /proc/net/sockstat
# 2. 查看sk_buff使用情况
cat /proc/net/sockstat | grep sk
# 3. 发送大量数据
dd if=/dev/zero bs=1M count=100 | nc localhost 8080
# 4. 再次查看统计
cat /proc/net/sockstat
# 5. 查看内存使用
free -h
预期结果:
- 理解网络内存管理
- 观察内存使用变化
- 掌握内存监控
排错
常见问题排查
问题1:网卡丢包严重
# 检查网卡统计
ethtool -S eth0 | grep -i drop
# 检查Ring Buffer大小
ethtool -g eth0
# 增大Ring Buffer
sudo ethtool -G eth0 rx 4096 tx 4096
# 检查中断处理
cat /proc/interrupts | grep eth0
问题2:软中断CPU使用率高
# 查看软中断统计
cat /proc/softirqs | head -n2
# 检查中断亲和性
cat /proc/irq/*/smp_affinity_list
# 启用RPS
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
问题3:网络延迟高
# 检查网络统计
cat /proc/net/snmp | grep -i tcp
# 检查重传情况
ss -ti | grep -i retrans
# 调整TCP参数
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
排错清单
- [ ] 检查网卡状态(ethtool、dmesg)
- [ ] 验证中断处理(/proc/interrupts)
- [ ] 确认软中断分布(/proc/softirqs)
- [ ] 检查内存使用(/proc/net/sockstat)
- [ ] 验证GRO设置(ethtool -k)
- [ ] 测试网络连通性(ping、iperf3)
- [ ] 查看系统日志(dmesg、/var/log/syslog)