第四章 网络驱动程序接口:驱动模型概述

网络驱动,说白了就是协议栈和硬件之间的翻译官。你想想看,上层协议栈要发数据,底层网卡要收数据,这两者怎么沟通?靠的就是驱动接口。

我在QNX上做网络驱动开发也有些年头了。刚开始接触时,总觉得这东西不就是个中间层嘛,有啥复杂的?后来踩了几个坑才明白——驱动接口设计得好不好,直接决定了整个网络子系统的稳定性和性能。

4.1 驱动模型概述

QNX的网络驱动模型,核心思想就四个字:分层解耦。它把网络栈分成了三层:

  • 协议层:处理TCP/IP、UDP等协议逻辑
  • 接口层:也就是NDIS层,负责协议和驱动的桥接
  • 驱动层:直接操作硬件寄存器、DMA、中断

为什么要这么分?我举个例子。你在项目中换了一块新网卡,如果驱动和协议栈是紧耦合的,那得改协议栈代码。但有了NDIS这层抽象,你只需要写一个新的驱动,协议栈完全不用动。

核心要点:驱动模型的目标是让协议栈开发者不用关心硬件细节,让驱动开发者不用关心协议细节。

4.2 NDIS在QNX中的实现

NDIS,全称是Network Driver Interface Specification。这玩意儿最早是微软搞出来的,但QNX把它移植过来并做了大量优化。

QNX的NDIS实现,本质上是一组标准化的函数接口。驱动必须实现这些接口,协议栈才能正确调用。我整理了一下核心接口:

接口函数 功能描述 调用时机
ndis_attach() 驱动初始化,绑定硬件 驱动加载时
ndis_detach() 驱动卸载,释放资源 驱动卸载时
ndis_start() 启动网络接口 ifconfig up时
ndis_stop() 停止网络接口 ifconfig down时
ndis_transmit() 发送数据包 协议栈有数据要发时
ndis_receive() 接收数据包(回调) 硬件收到数据时
ndis_ioctl() 控制命令处理 用户调用ioctl时

嗯,这里要注意一点。QNX的NDIS和Windows的NDIS虽然名字一样,但实现细节差别很大。QNX更强调实时性和确定性,所以它的NDIS接口里加了很多针对中断延迟和DMA缓冲区的控制参数。

个人经验:我在一个工业控制项目里,需要保证网络延迟在100微秒以内。当时就是靠调整NDIS层的中断亲和性和DMA描述符数量,才把抖动压下来的。

4.3 驱动加载与初始化流程

驱动加载,听起来简单,其实里面门道不少。QNX的驱动加载流程大致分这么几步:

  1. 系统启动,io-net进程拉起
  2. io-net解析配置文件,确定要加载哪些驱动
  3. 调用驱动的入口函数,执行初始化
  4. 驱动注册到NDIS层,暴露接口
  5. 协议栈绑定驱动,网络接口就绪

我习惯把驱动初始化分成两个阶段:硬件初始化软件注册

硬件初始化阶段

这个阶段,驱动要做的事情很具体:

  • 映射PCI设备的BAR空间
  • 分配DMA缓冲区
  • 注册中断处理函数
  • 初始化MAC控制器
  • 设置MAC地址

代码看起来大概是这样的:

// 驱动入口函数
int ndis_attach(void *dev, void *bus_info) {
    // 1. 获取PCI设备信息
    pci_info_t *pci = (pci_info_t *)bus_info;
    
    // 2. 映射BAR空间
    uintptr_t reg_base = mmap_device_io(0x1000, 
                                        pci->base_addr[0]);
    
    // 3. 分配DMA缓冲区
    dma_addr_t dma_buf;
    dma_alloc(&dma_buf, 64 * 1024, 0);
    
    // 4. 注册中断
    InterruptAttach(irq, isr_handler, NULL, 0, 0);
    
    // 5. 初始化硬件寄存器
    mac_init(reg_base);
    
    // 6. 注册到NDIS层
    ndis_register(dev, &ndis_ops);
    
    return EOK;
}

避坑指南:我曾经在初始化时忘记检查PCI设备的vendor ID和device ID,结果驱动加载到了一个不兼容的网卡上,系统直接panic。后来我养成了习惯——初始化第一步先做硬件ID校验。

软件注册阶段

硬件初始化完成后,驱动需要把自己注册到NDIS层。这一步很关键,注册的内容包括:

  • 驱动能力标志(支持哪些特性)
  • 回调函数表(收发、控制、统计等)
  • 缓冲区管理策略
  • 中断处理策略

注册完成后,协议栈就可以通过NDIS接口调用驱动了。这时候你执行ifconfig,就能看到新网卡出现了。

4.4 驱动加载的常见问题

说实话,驱动加载阶段最容易出问题。我总结了几类典型故障:

问题现象 可能原因 排查方法
驱动加载失败 PCI设备未识别 检查pci -v输出
中断不触发 中断号冲突或MSI配置错误 查看/proc/irq
DMA传输异常 缓冲区对齐或大小问题 检查dma_alloc参数
MAC地址全零 EEPROM读取失败 手动设置MAC地址

我记得有一次,客户反馈说网卡在系统启动后偶尔不工作。查了两天才发现,是驱动初始化时DMA描述符环的地址没有做cache一致性处理。QNX的缓存模型和Linux不太一样,这个坑很多人会踩。

调试技巧:在驱动初始化函数里加一些printfslogf输出,记录每一步的执行状态。虽然看起来土,但排查问题时特别管用。

4.5 驱动卸载与资源回收

驱动卸载,很多人不重视。但在我眼里,能安全卸载的驱动才是好驱动

卸载流程和加载相反:

  1. 停止网络接口(停止收发)
  2. 注销中断处理函数
  3. 释放DMA缓冲区
  4. 解除BAR空间映射
  5. 从NDIS层注销

这里有个容易忽略的点——卸载时要确保没有正在进行的收发操作。我一般会在卸载函数里加一个引用计数,等所有待处理的数据包都完成后,才真正释放资源。

int ndis_detach(void *dev) {
    // 等待所有待处理操作完成
    while (ref_count > 0) {
        delay(10);  // 等待10ms
    }
    
    // 停止硬件
    ndis_stop(dev);
    
    // 释放中断
    InterruptDetach(irq);
    
    // 释放DMA缓冲区
    dma_free(dma_buf);
    
    // 解除内存映射
    munmap_device_io(reg_base, 0x1000);
    
    return EOK;
}

重要提醒:驱动卸载时如果资源没释放干净,下次加载时就会出现内存泄漏或设备状态异常。我见过一个项目,因为卸载时没释放中断,导致系统运行几天后中断资源耗尽,所有网卡都挂了。

好了,关于QNX网络驱动接口的基本模型和初始化流程,就讲到这里。下一章我们会深入驱动开发的具体实践,包括如何编写一个完整的NDIS驱动。到时候我会拿一个真实的网卡驱动做例子,一步步带你走通整个流程。