40、网络接口卡驱动程序详解

AI助手已提取文章相关产品:

网络接口卡驱动程序详解

1. 打开和关闭操作

当授权用户(如管理员)使用用户空间工具(如 ifconfig ip )配置网络接口时,内核会调用 ndo_open() 函数。该函数接收一个 struct net_device 对象作为参数,驱动程序应从中获取存储在 priv 字段中的设备特定对象。

网络控制器在接收或完成数据包传输时通常会引发中断,驱动程序需要注册一个中断处理程序。注册可以在 init()/probe() 例程或 open 函数中进行。有些设备需要通过设置硬件中的特殊寄存器来启用中断。

open 函数应执行以下操作:
1. 更新接口的MAC地址(如果用户更改了它,并且设备允许这样做)。
2. 必要时重置硬件,并将其从低功耗模式中唤醒。
3. 请求所需的资源(如I/O内存、DMA通道、IRQ)。
4. 映射IRQ并注册中断处理程序。
5. 检查接口的链接状态。
6. 调用 net_if_start_queue() 通知内核设备已准备好传输数据包。

以下是一个 open 函数的示例:

/*
 * This routine should set everything up new at each open, even
 * registers that should only need to be set once at boot, so that
 * there is non-reboot way to recover if something goes wrong.
 */
static int enc28j60_net_open(struct net_device *dev)
{
   struct priv_net_struct *priv = netdev_priv(dev);
   if (!is_valid_ether_addr(dev->dev_addr)) {
         [...] /* Maybe print a debug message ? */
         return -EADDRNOTAVAIL;
   }
   /*
 * Reset the hardware here and take it out of low
 * power mode
 */
   my_netdev_lowpower(priv, false);
   if (!my_netdev_hw_init(priv)) {
         [...] /* handle hardware reset failure */
         return -EINVAL;
   }
   /* Update the MAC address (in case user has changed it)
    * The new address is stored in netdev->dev_addr field
 */
set_hw_macaddr_registers(netdev, MAC_REGADDR_START,
netdev->addr_len, netdev->dev_addr);
   /* Enable interrupts */
   my_netdev_hw_enable(priv);
   /* We are now ready to accept transmit requests from
    * the queueing layer of the networking.
    */
   netif_start_queue(dev);
   return 0;
}

netif_start_queue() 允许上层调用设备的 ndo_start_xmit 例程,通知内核设备已准备好处理传输请求。

关闭操作则是打开操作的逆过程,示例代码如下:

/* The inverse routine to net_open(). */
static int enc28j60_net_close(struct net_device *dev)
{
   struct priv_net_struct *priv = netdev_priv(dev);
   my_netdev_hw_disable(priv);
   my_netdev_lowpower(priv, true);
    /**
     *   netif_stop_queue - stop transmitted packets
     *
     *   Stop upper layers calling the device ndo_start_xmit routine.
     *   Used for flow control when transmit resources are unavailable.
     */
   netif_stop_queue(dev);
   return 0;
}

netif_stop_queue() 通知内核停止调用设备的 ndo_start_xmit 例程。

2. 数据包处理

数据包处理包括数据包的传输和接收,这是网络接口驱动程序的主要任务。传输指发送出站帧,接收指接收入站帧。

有两种驱动网络数据交换的方式:轮询和中断。轮询是一种定时器驱动的中断,内核会定期检查设备是否有变化;中断模式下,内核等待设备通过IRQ通知变化。在高流量时,中断驱动的数据交换会增加系统开销,因此一些驱动程序会混合使用这两种方法,即New API(NAPI)。本文将重点介绍中断驱动方法。

2.1 数据包接收

当数据包到达网络接口卡时,驱动程序必须围绕它构建一个新的套接字缓冲区,并将数据包复制到 sk_buff->data 字段。复制方式可以使用DMA。驱动程序通常通过中断来感知新数据的到来。当NIC接收到数据包时,会引发一个中断,驱动程序需要检查设备的中断状态寄存器,以确定中断的真正原因。

以下是一个RX处理程序的示例:

/*
 * RX handler
 * This function is called in the work responsible of packet
 * reception (bottom half) handler. We use work because access to
 * our device (which sit on a SPI bus) may sleep
 */
static int my_rx_interrupt(struct net_device *ndev)
{
   struct priv_net_struct *priv = netdev_priv(ndev);
   int pk_counter, ret;
   /* Let's get the number of packet our device received */
   pk_counter = my_device_reg_read(priv, REG_PKT_CNT);
   if (pk_counter > priv->max_pk_counter) {
         /* update statistics */
         priv->max_pk_counter = pk_counter;
   }
   ret = pk_counter;
   /* set receive buffer start */
   priv->next_pk_ptr = KNOWN_START_REGISTER;
   while (pk_counter-- > 0)
         /*
* By calling this internal helper function in a "while"
* loop, packets get extracted one by one from the device
* and forwarder to the network layer.
*/
         my_hw_rx(ndev);
   return ret;
}

以下是辅助函数 my_hw_rx 的代码:

/*
 * Hardware receive function.
 * Read the buffer memory, update the FIFO pointer to
 * free the buffer.
 * This function decrements the packet counter.
 */
static void my_hw_rx(struct net_device *ndev)
{
   struct priv_net_struct *priv = netdev_priv(ndev);
   struct sk_buff *skb = NULL;
   u16 erxrdpt, next_packet, rxstat;
   u8 rsv[RSV_SIZE];
   int packet_len;
   packet_len = my_device_read_current_packet_size();
   /* Can't cross boundaries */
   if ((priv->next_pk_ptr > RXEND_INIT)) {
         /* packet address corrupted: reset RX logic */
         [...]
         /* Update RX errors stats */
         ndev->stats.rx_errors++;
         return;
   }
   /* Read next packet pointer and rx status vector
    * This is device-specific
    */
   my_device_reg_read(priv, priv->next_pk_ptr, sizeof(rsv), rsv);
   /* Check for errors in the device RX status reg,
    * and update error stats accordingly
    */
   if(an_error_is_detected_in_device_status_registers())
         /* Depending on the error,
          * stats.rx_errors++;
          * ndev->stats.rx_crc_errors++;
          * ndev->stats.rx_frame_errors++;
          * ndev->stats.rx_over_errors++;
          */
   } else {
         skb = netdev_alloc_skb(ndev, len + NET_IP_ALIGN);
         if (!skb) {
               ndev->stats.rx_dropped++;
         } else {
               skb_reserve(skb, NET_IP_ALIGN);
               /*
                 * copy the packet from the device' receive buffer
                 * to the socket buffer data memory.
                 * Remember skb_put() return a pointer to the
                 * beginning of data region.
                 */
               my_netdev_mem_read(priv,
                     rx_packet_start(priv->next_pk_ptr),
                     len, skb_put(skb, len));
               /* Set the packet's protocol ID */
               skb->protocol = eth_type_trans(skb, ndev);
               /* update RX statistics */
               ndev->stats.rx_packets++;
               ndev->stats.rx_bytes += len;
               /* Submit socket buffer to the network layer */
               netif_rx_ni(skb);
         }
   }
   /* Move the RX read pointer to the start of the next
    * received packet.
    */
   priv->next_pk_ptr = my_netdev_update_reg_next_pkt();
}
2.2 数据包传输

当内核需要通过接口发送数据包时,会调用驱动程序的 ndo_start_xmit 方法。该方法成功时应返回 NETDEV_TX_OK ,失败时返回 NETDEV_TX_BUSY 。此函数受自旋锁保护,防止并发调用。

数据包传输通常是异步进行的。上层会填充要传输的 sk_buff ,其 data 字段包含要发送的数据包。驱动程序应将数据包从 sk_buff->data 提取并写入设备硬件FIFO,或先放入临时TX缓冲区。数据只有在FIFO达到阈值或驱动程序触发传输时才会真正发送。

ndo_start_xmit 函数大致包含以下步骤:
1. 调用 netif_stop_queue() 通知内核设备将忙于数据传输。
2. 将 sk_buff->data 的内容写入设备FIFO。
3. 触发传输(指示设备开始传输)。

以下是 ndo_start_xmit 函数的示例:

/* Somewhere in the code */
INIT_WORK(&priv->tx_work, my_netdev_hw_tx);
static netdev_tx_t my_netdev_start_xmit(struct sk_buff *skb,
                           struct net_device *dev)
{
   struct priv_net_struct *priv = netdev_priv(dev);
   /* Notify the kernel our device will be busy */
   netif_stop_queue(dev);
   /* Remember the skb for deferred processing */
   priv->tx_skb = skb;
   /* This work will copy data from sk_buffer->data to
    * the hardware's FIFO and start transmission
    */
   schedule_work(&priv->tx_work);
   /* Everything is OK */
   return NETDEV_TX_OK;
}

以下是硬件传输函数的代码:

/*
 * Hardware transmit function.
 * Fill the buffer memory and send the contents of the
 * transmit buffer onto the network
 */
static void my_netdev_hw_tx(struct priv_net_struct *priv)
{
   /* Write packet to hardware device TX buffer memory */
   my_netdev_packet_write(priv, priv->tx_skb->len,
priv->tx_skb->data);
/*
 * does this network device support write-verify?
 * Perform it
 */
[...];
   /* set TX request flag,
 * so that the hardware can perform transmission.
 * This is device-specific
 */
   my_netdev_reg_bitset(priv, ECON1, ECON1_TXRTS);
}

数据包发送后,网络接口卡会引发一个中断。中断处理程序应检查中断原因,更新传输统计信息,并通过 netif_wake_queue() 通知内核设备可以再次发送新数据包。

3. 驱动程序示例

以下是一个简单的虚拟以太网驱动程序示例:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/of.h>                   /* For DT*/
#include <linux/platform_device.h>      /* For platform devices */

struct eth_struct {
    int bar;
    int foo;
    struct net_device *dummy_ndev;
};

static int fake_eth_open(struct net_device *dev) {
    printk("fake_eth_open called\n");
    /* We are now ready to accept transmit requests from
    * the queueing layer of the networking.
    */
   netif_start_queue(dev);
    return 0;
}

static int fake_eth_release(struct net_device *dev) {
    pr_info("fake_eth_release called\n");
    netif_stop_queue(dev);
    return 0;
}

static int fake_eth_xmit(struct sk_buff *skb, struct net_device *ndev) {
    pr_info("dummy xmit called...\n");
    ndev->stats.tx_bytes += skb->len;
    ndev->stats.tx_packets++;
    skb_tx_timestamp(skb);
   dev_kfree_skb(skb);
   return NETDEV_TX_OK;
}

static int fake_eth_init(struct net_device *dev)
{
    pr_info("fake eth device initialized\n");
    return 0;
};

static const struct net_device_ops my_netdev_ops = {
     .ndo_init = fake_eth_init,
     .ndo_open = fake_eth_open,
     .ndo_stop = fake_eth_release,
     .ndo_start_xmit = fake_eth_xmit,
     .ndo_validate_addr    = eth_validate_addr,
     .ndo_validate_addr    = eth_validate_addr,
};

static const struct of_device_id fake_eth_dt_ids[] = {
    { .compatible = "packt,fake-eth", },
    { /* sentinel */ }
};

static int fake_eth_probe(struct platform_device *pdev)
{
    int ret;
    struct eth_struct *priv;
    struct net_device *dummy_ndev;
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    dummy_ndev = alloc_etherdev(sizeof(struct eth_struct));
    dummy_ndev->if_port = IF_PORT_10BASET;
    dummy_ndev->netdev_ops = &my_netdev_ops;
    /* If needed, dev->ethtool_ops = &fake_ethtool_ops; */
    ret = register_netdev(dummy_ndev);
    if(ret) {
        pr_info("dummy net dev: Error %d initalizing card ...", ret);
        return ret;
    }
    priv->dummy_ndev = dummy_ndev;
    platform_set_drvdata(pdev, priv);
    return 0;
}

static int fake_eth_remove(struct platform_device *pdev)
{
    struct eth_struct *priv;
   priv = platform_get_drvdata(pdev);
   pr_info("Cleaning Up the Module\n");
    unregister_netdev(priv->dummy_ndev);
    free_netdev(priv->dummy_ndev);
   return 0;
}

static struct platform_driver mypdrv = {
    .probe      = fake_eth_probe,
    .remove     = fake_eth_remove,
    .driver     = {
        .name     = "fake-eth",
        .of_match_table = of_match_ptr(fake_eth_dt_ids),
        .owner    = THIS_MODULE,
    },
};

module_platform_driver(mypdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake Ethernet driver");

加载该模块并匹配设备后,系统将创建一个以太网接口。可以使用 dmesg ifconfig -a 等命令查看相关信息,并通过 ifconfig 配置接口的IP地址。

4. 状态和控制

设备控制指内核主动或响应用户操作更改接口属性的情况。可以使用 struct net_device_ops 结构中的操作,也可以使用 ethtool 工具。状态则是指报告接口的状态。

5. 中断处理程序

目前我们只处理了两种中断:新数据包到达和出站数据包传输完成。现在的硬件接口越来越智能,能够报告错误、链接状态变化等信息,这些中断都应在中断处理程序中处理。

以下是中断处理程序的示例:

static irqreturn_t my_netdev_irq(int irq, void *dev_id)
{
   struct priv_net_struct *priv = dev_id;
   /*
    * Can't do anything in interrupt context because we need to
    * block (spi_sync() is blocking) so fire of the interrupt
    * handling workqueue.
    * Remember, we access our netdev registers through SPI bus
    * via spi_sync() call.
    */
   schedule_work(&priv->irq_work);
   return IRQ_HANDLED;
}

static void my_netdev_irq_work_handler(struct work_struct *work)
{
   struct priv_net_struct *priv =
         container_of(work, struct priv_net_struct, irq_work);
   struct net_device *ndev = priv->netdev;
   int intflags, loop;
   /* disable further interrupts */
   my_netdev_reg_bitclear(priv, EIE, EIE_INTIE);
   do {
         loop = 0;
         intflags = my_netdev_regb_read(priv, EIR);
         /* DMA interrupt handler (not currently used) */
         if ((intflags & EIR_DMAIF) != 0) {
               loop++;
               handle_dma_complete();
               clear_dma_interrupt_flag();
         }
         /* LINK changed handler */
         if ((intflags & EIR_LINKIF) != 0) {
               loop++;
               my_netdev_check_link_status(ndev);
               clear_link_interrupt_flag();
         }
         /* TX complete handler */
         if ((intflags & EIR_TXIF) != 0) {
             // 此处原文档未完整给出代码
         }
   } while (loop > 0);
}

综上所述,网络接口卡驱动程序的开发涉及打开和关闭操作、数据包的接收和传输、状态和控制以及中断处理等多个方面。通过合理实现这些功能,可以确保网络设备的正常运行。

网络接口卡驱动程序详解(续)

6. 中断处理程序的详细分析

中断处理程序在网络接口卡驱动中起着关键作用,它负责处理各种硬件产生的中断信号。在之前给出的中断处理程序示例中,由于设备连接在SPI总线上,为避免在中断上下文进行阻塞操作,将中断处理工作放入了工作队列中。下面我们详细分析这个过程。

6.1 主中断处理函数
static irqreturn_t my_netdev_irq(int irq, void *dev_id)
{
   struct priv_net_struct *priv = dev_id;
   /*
    * Can't do anything in interrupt context because we need to
    * block (spi_sync() is blocking) so fire of the interrupt
    * handling workqueue.
    * Remember, we access our netdev registers through SPI bus
    * via spi_sync() call.
    */
   schedule_work(&priv->irq_work);
   return IRQ_HANDLED;
}

在这个函数中,首先获取设备的私有数据结构 priv 。由于SPI总线操作是阻塞的,不能在中断上下文中进行,所以通过 schedule_work 函数将中断处理工作放入工作队列 priv->irq_work 中。最后返回 IRQ_HANDLED 表示中断已被处理。

6.2 工作队列处理函数
static void my_netdev_irq_work_handler(struct work_struct *work)
{
   struct priv_net_struct *priv =
         container_of(work, struct priv_net_struct, irq_work);
   struct net_device *ndev = priv->netdev;
   int intflags, loop;
   /* disable further interrupts */
   my_netdev_reg_bitclear(priv, EIE, EIE_INTIE);
   do {
         loop = 0;
         intflags = my_netdev_regb_read(priv, EIR);
         /* DMA interrupt handler (not currently used) */
         if ((intflags & EIR_DMAIF) != 0) {
               loop++;
               handle_dma_complete();
               clear_dma_interrupt_flag();
         }
         /* LINK changed handler */
         if ((intflags & EIR_LINKIF) != 0) {
               loop++;
               my_netdev_check_link_status(ndev);
               clear_link_interrupt_flag();
         }
         /* TX complete handler */
         if ((intflags & EIR_TXIF) != 0) {
             // 此处原文档未完整给出代码
         }
   } while (loop > 0);
}

在工作队列处理函数中,首先通过 container_of 宏获取设备的私有数据结构 priv 和网络设备结构 ndev 。然后通过 my_netdev_reg_bitclear 函数禁用进一步的中断,防止在处理当前中断时被新的中断打断。

接着进入一个循环,不断读取中断状态寄存器 EIR 的值 intflags 。根据不同的中断标志位,调用相应的处理函数:
- DMA中断 :当 EIR_DMAIF 标志位被设置时,调用 handle_dma_complete 函数处理DMA完成事件,并调用 clear_dma_interrupt_flag 函数清除DMA中断标志。
- 链接状态改变中断 :当 EIR_LINKIF 标志位被设置时,调用 my_netdev_check_link_status 函数检查链接状态,并调用 clear_link_interrupt_flag 函数清除链接中断标志。
- 传输完成中断 :当 EIR_TXIF 标志位被设置时,需要处理传输完成事件,但原文档此处代码未完整给出。

循环会一直进行,直到没有中断标志位被设置。

7. 数据包处理流程总结

为了更清晰地理解数据包的接收和传输过程,我们可以用一个流程图来表示:

graph TD;
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B{数据包类型}:::decision;
    B -->|接收| C(接收中断触发):::process;
    B -->|传输| D(内核调用ndo_start_xmit):::process;
    C --> E(读取数据包数量):::process;
    E --> F{是否有数据包}:::decision;
    F -->|是| G(提取数据包):::process;
    F -->|否| H(结束接收):::process;
    G --> I(构建套接字缓冲区):::process;
    I --> J(复制数据包到缓冲区):::process;
    J --> K(更新接收统计信息):::process;
    K --> L(提交到网络层):::process;
    L --> E;
    D --> M(调用netif_stop_queue):::process;
    M --> N(复制数据到硬件FIFO):::process;
    N --> O(触发传输):::process;
    O --> P(等待传输完成中断):::process;
    P --> Q(更新传输统计信息):::process;
    Q --> R(调用netif_wake_queue):::process;
    R --> S(结束传输):::process;

从这个流程图可以看出,数据包的接收和传输过程是相互独立但又紧密关联的。接收过程主要包括中断触发、数据包提取、缓冲区构建和数据提交;传输过程主要包括内核调用、队列停止、数据写入、传输触发和状态更新。

8. 驱动程序开发的注意事项

在开发网络接口卡驱动程序时,有一些重要的注意事项需要牢记:
- 资源管理 :在打开设备时,要正确请求所需的资源(如I/O内存、DMA通道、IRQ),并在关闭设备时释放这些资源,避免资源泄漏。
- 并发控制 :数据包传输函数 ndo_start_xmit 是受自旋锁保护的,要确保在函数内部不会出现并发访问的问题。
- 错误处理 :在各个操作中,如硬件初始化、数据包接收和传输等,都要进行错误检查,并及时处理错误,避免程序崩溃。
- 中断处理 :对于不同类型的中断,要在中断处理程序中进行正确的处理,确保设备的正常运行。

9. 未来发展趋势

随着网络技术的不断发展,网络接口卡驱动程序也面临着新的挑战和机遇。未来可能会有以下发展趋势:
- 更高的性能要求 :随着网络速度的不断提高,驱动程序需要支持更高的带宽和更低的延迟,以满足用户对高速网络的需求。
- 智能化管理 :硬件接口将变得更加智能,能够自动调整参数和优化性能,驱动程序需要与之配合,实现智能化的管理。
- 多协议支持 :网络中使用的协议越来越多,驱动程序需要支持多种协议,以适应不同的网络环境。
- 与新硬件的兼容性 :随着新的网络硬件的不断推出,驱动程序需要及时更新,以确保与新硬件的兼容性。

总之,网络接口卡驱动程序的开发是一个复杂而又重要的领域,需要开发者不断学习和掌握新的技术,以适应不断变化的网络环境。通过合理设计和实现驱动程序,可以提高网络设备的性能和稳定性,为用户提供更好的网络体验。

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值