3. 使用NAPI机制优化收包

使用NAPI机制优化收包 学习卡片

任务说明

本案例将Linux网卡驱动收包路径从传统中断方式改造为NAPI机制。传统方式下每个数据包到达都会触发一次硬件中断,高负载时CPU频繁被打断,上下文切换开销巨大。改造后,驱动注册poll函数,首次中断后关闭硬件中断并转入轮询收包,直到收完当前批次数据再重新开启中断,从而显著降低中断次数和CPU占用。

NAPI核心数据结构

// 驱动私有结构体(简化)
struct my_priv {
    struct net_device *dev;
    struct napi_struct napi;
    void __iomem *reg_base;
    struct rx_ring *rx_ring;   // 接收环形队列
    unsigned int rx_count;     // 当前待处理包数
    spinlock_t lock;
};

// 初始化NAPI(在驱动probe或open阶段调用)
netif_napi_add(priv->dev, &priv->napi, my_poll, 64);  // budget=64
napi_enable(&priv->napi);

中断处理函数改造

// 传统方式:中断中直接收包
// 改造后:中断中仅调度NAPI
static irqreturn_t my_interrupt(int irq, void *data)
{
    struct my_priv *priv = data;

    // 读取中断状态寄存器,确认是RX中断
    u32 status = ioread32(priv->reg_base + INT_STATUS);
    if (!(status & INT_RX)) {
        return IRQ_NONE;
    }

    // 关闭RX硬件中断(防止中断风暴)
    iowrite32(0, priv->reg_base + INT_ENABLE);

    // 调度NAPI(若未在运行)
    if (napi_schedule_prep(&priv->napi)) {
        __napi_schedule(&priv->napi);
    }

    return IRQ_HANDLED;
}

poll函数实现

// NAPI轮询收包函数
static int my_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;
    struct sk_buff *skb;
    u32 rx_status;

    while (work_done < budget) {
        // 检查硬件RX描述符是否有新包
        rx_status = ioread32(priv->reg_base + RX_STATUS);
        if (!(rx_status & RX_READY)) {
            break;  // 无更多数据包
        }

        // 从硬件FIFO读取数据,构造skb
        skb = my_rx_construct_skb(priv);
        if (!skb) {
            break;  // 内存不足等错误
        }

        // 交给协议栈
        napi_gro_receive(napi, skb);
        work_done++;
        priv->rx_count--;
    }

    // 判断是否收完本轮数据
    if (work_done < budget) {
        // 收完,重新开启中断并结束NAPI
        napi_complete_done(napi, work_done);
        iowrite32(INT_RX_ENABLE, priv->reg_base + INT_ENABLE);
    }

    return work_done;
}

// 辅助函数:从硬件读取数据构造skb
static struct sk_buff *my_rx_construct_skb(struct my_priv *priv)
{
    struct sk_buff *skb;
    unsigned int pkt_len;
    void *hw_addr;

    pkt_len = ioread32(priv->reg_base + RX_LEN);
    if (pkt_len == 0 || pkt_len > 2048) {
        return NULL;
    }

    skb = napi_alloc_skb(&priv->napi, pkt_len);
    if (!skb) {
        return NULL;
    }

    hw_addr = priv->reg_base + RX_DATA_FIFO;
    memcpy_fromio(skb_put(skb, pkt_len), hw_addr, pkt_len);

    // 更新硬件RX队列指针
    iowrite32(RX_NEXT, priv->reg_base + RX_CTRL);

    return skb;
}

驱动卸载时清理

// 在驱动remove或close阶段
static void my_cleanup(struct my_priv *priv)
{
    napi_disable(&priv->napi);
    netif_napi_del(&priv->napi);
}

验证方法

编译并加载驱动后,使用以下步骤验证NAPI效果:

  1. 功能验证:ifconfig eth0 up后,从对端主机持续ping或发送UDP数据包,确认网络连通且无丢包。
  2. 中断计数对比:分别加载传统中断版本和NAPI版本驱动,在相同流量下读取/proc/interrupts中对应网卡中断号计数。NAPI版本的中断次数应显著减少(高负载下通常降低80%以上)。
  3. CPU占用观测:使用top或mpstat -P ALL 1观察软中断(si)和硬中断(hi)占比。NAPI版本在高吞吐时CPU占用应明显低于传统中断版本。
  4. NAPI状态检查:cat /proc/net/softnet_stat观察第三列(time_squeeze)是否增长过快,若增长过快说明budget设置偏小。

判定标准:在相同发包速率(如100k pps)下,NAPI版本相比传统中断版本,/proc/interrupts中该网卡中断次数降低至少一个数量级,且CPU软中断占用率下降30%以上,同时无新增丢包。

NAPI收包流程时序 网卡硬件 中断处理函数 NAPI poll 内核协议栈 包到达 读取状态寄存器 关闭硬件中断 napi_schedule 循环读取RX FIFO napi_gro_receive 上层处理 napi_complete 重新使能中断 中断路径(红色)仅在首包触发一次,后续数据通过轮询(绿色)批量收包

关键配置说明

  • budget参数:poll函数单次最多处理的包数(示例为64)。该值影响单次轮询耗时和延迟,需根据硬件速率调整。
  • GRO协同:使用napi_gro_receive替代netif_receive_skb可启用硬件GRO聚合,进一步减少协议栈处理开销。
  • 中断阈值:部分网卡支持中断合并(如Intel的ITR),可与NAPI配合进一步优化,但本案例仅实现NAPI核心逻辑。

完成上述改造后,驱动在持续高负载下中断次数大幅减少,CPU占用显著下降。若需验证极端情况,可使用pktgen或iperf打满网卡带宽,观察NAPI版本与传统版本在top中软中断占比的差异。