4、网络设备驱动移植:网卡驱动框架(NETDEV)、DMA环缓冲区配置、中断与轮询模式切换
各位同学,今天我们来聊聊网络设备驱动移植中最核心的几个环节。说实话,网卡驱动这块,我早年刚入行时也踩过不少坑。那时候移植一个百兆网卡,光DMA环缓冲区配置就折腾了两天,最后发现是描述符对齐的问题。嗯,咱们今天就把这些经验掰开揉碎了讲。
4.1 网卡驱动框架(NETDEV)
Linux内核的网卡驱动,说白了就是围绕 struct net_device 这个核心结构体展开的。你想想看,无论是千兆、万兆还是Wi-Fi,最终都要注册到这个框架里来。我个人习惯把NETDEV框架理解成「三件套」:
- 分配与初始化:用
alloc_etherdev()或alloc_netdev()分配结构体 - 操作函数集:实现
struct net_device_ops中的ndo_open、ndo_stop、ndo_start_xmit等回调 - 注册与注销:调用
register_netdev()和unregister_netdev()
我在项目中遇到过一种情况:某款FPGA加速卡,它的网卡驱动需要同时支持虚拟化和非虚拟化场景。这时候就不能简单用 alloc_etherdev,得用 alloc_netdev_mqs 来指定多队列。你想想看,如果一开始没规划好队列数,后面改起来就是牵一发动全身。
核心要点:NETDEV框架的移植,关键在于 net_device_ops 的实现。尤其是 ndo_start_xmit 函数,它直接决定了数据包的发送性能。我建议你在移植时,先把这个函数写成空函数,只做统计计数,等DMA和中断调通了再回来优化。
4.2 DMA环缓冲区配置
DMA环缓冲区,这是网卡驱动的心脏。说白了,就是CPU和网卡之间共享的一块环形内存区域。配置不对,要么丢包,要么死锁。
咱们先看一个典型的DMA描述符结构:
struct my_dma_desc {
dma_addr_t buf_addr; // 数据缓冲区物理地址
u32 buf_len; // 数据长度
u32 flags; // 状态标志位
struct my_dma_desc *next; // 指向下一个描述符(硬件链式)
} __attribute__((aligned(64)));
这里有个细节:__attribute__((aligned(64)))。为什么是64字节对齐?因为很多网卡硬件要求描述符必须对齐到Cache Line大小。我记得有一次,某款国产网卡芯片,文档里写的是32字节对齐,结果实际测试发现必须64字节对齐才能稳定工作。嗯,这就是典型的「文档和硬件不一致」的坑。
配置DMA环缓冲区时,我一般遵循以下步骤:
- 分配连续物理内存:用
dma_alloc_coherent()分配描述符数组和数据缓冲区 - 初始化描述符链:将描述符串成环形链表,最后一个指向第一个
- 设置硬件寄存器:将描述符基地址和环大小写入网卡寄存器
- 启动DMA引擎:使能发送和接收通道
避坑指南:我曾经在移植一款PCIe网卡时,发现DMA传输总是超时。排查了两天,最后发现是 dma_alloc_coherent 返回的地址是64位,但网卡只支持32位DMA地址。解决方案很简单:在 struct net_device_ops 中设置 netdev_features_t 的 NETIF_F_HIGHDMA 标志位,或者用 dma_set_mask() 限制DMA地址范围。
4.3 中断与轮询模式切换
中断模式和轮询模式,这是网卡驱动的两种「呼吸方式」。中断模式省CPU,但中断频率高了会触发活锁;轮询模式吞吐高,但CPU占用率也高。实际项目中,我们往往需要动态切换。
Linux内核提供了NAPI(New API)机制,完美解决了这个问题。NAPI的核心思想是:
- 数据包少时,用中断通知CPU
- 数据包多时,关闭中断,切换到轮询模式
- 数据包处理完后,重新开启中断
实现NAPI的关键代码片段:
// 初始化NAPI
netif_napi_add(netdev, &priv->napi, my_poll, 64);
// 中断处理函数
irqreturn_t my_interrupt(int irq, void *dev_id) {
struct my_priv *priv = dev_id;
if (likely(napi_schedule_prep(&priv->napi))) {
// 关闭硬件中断
writel(0, priv->regs + INT_ENABLE);
__napi_schedule(&priv->napi);
}
return IRQ_HANDLED;
}
// 轮询函数
int my_poll(struct napi_struct *napi, int budget) {
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
// 从DMA环中取出数据包
while (work_done < budget) {
struct sk_buff *skb = get_packet_from_ring(priv);
if (!skb) break;
napi_gro_receive(napi, skb);
work_done++;
}
if (work_done < budget) {
// 数据包处理完了,重新开启中断
napi_complete_done(napi, work_done);
writel(1, priv->regs + INT_ENABLE);
}
return work_done;
}
这里有个关键参数:budget。它限制了每次轮询最多处理多少个数据包。我建议你根据CPU频率和网络带宽来调整这个值。比如在1Gbps链路上,每个数据包平均1500字节,那么每秒约83333个包。如果CPU主频2GHz,每个包处理耗时约24微秒,那么budget设为64是比较合理的。
个人经验:在低延迟场景下,我倾向于把budget设小一点(比如32),然后配合 busy_poll 机制。这样既能保证低延迟,又不会让CPU被网络处理完全占满。你可以在 /proc/sys/net/core/busy_poll 中调整全局的忙轮询时间。
4.4 实战中的性能调优
讲完了理论,咱们聊聊实战。我在移植一款40Gbps网卡时,遇到了一个典型问题:小包(64字节)吞吐上不去。排查后发现是中断合并(Interrupt Coalescing)参数没调好。
中断合并,说白了就是让网卡攒一批中断再通知CPU。参数包括:
| 参数 | 说明 | 推荐值(低延迟) | 推荐值(高吞吐) |
|---|---|---|---|
| rx-usecs | 接收中断延迟(微秒) | 0-10 | 50-100 |
| rx-frames | 接收中断触发帧数 | 1-4 | 16-64 |
| tx-usecs | 发送中断延迟(微秒) | 0-10 | 50-100 |
| tx-frames | 发送中断触发帧数 | 1-4 | 16-64 |
对于低延迟场景,我建议把 rx-usecs 设为0,rx-frames 设为1。这样每个数据包都会立即触发中断,延迟最低。但代价是CPU占用率会飙升。如果CPU资源紧张,可以适当调高这些值。
核心总结:网卡驱动移植,本质上就是三件事——把NETDEV框架搭好,把DMA环配通,把中断和轮询的切换逻辑写对。这三件事做好了,剩下的就是调参和优化。我个人习惯在移植完成后,先用 perf 工具分析中断处理函数和轮询函数的CPU占用,再针对性地调整参数。
这张图把整个网卡驱动移植的流程串起来了。从NETDEV框架注册开始,到DMA环配置,再到中断与轮询切换,最后是性能调优。每个环节都有对应的坑点和优化技巧。你想想看,如果一开始就把这些环节理清楚,移植工作至少能省一半时间。
调试小技巧:在移植初期,我习惯在 ndo_start_xmit 和NAPI轮询函数中加入 printk_ratelimited 打印。这样既能观察数据包流向,又不会因为打印太多导致系统卡死。等基本功能调通后,再把这些打印去掉。
好了,关于网卡驱动移植的核心内容就讲到这里。记住,NETDEV框架是骨架,DMA环是血脉,中断与轮询切换是呼吸。这三者配合好了,你的网卡驱动就能跑得又快又稳。