网络接口卡驱动程序详解
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. 未来发展趋势
随着网络技术的不断发展,网络接口卡驱动程序也面临着新的挑战和机遇。未来可能会有以下发展趋势:
-
更高的性能要求
:随着网络速度的不断提高,驱动程序需要支持更高的带宽和更低的延迟,以满足用户对高速网络的需求。
-
智能化管理
:硬件接口将变得更加智能,能够自动调整参数和优化性能,驱动程序需要与之配合,实现智能化的管理。
-
多协议支持
:网络中使用的协议越来越多,驱动程序需要支持多种协议,以适应不同的网络环境。
-
与新硬件的兼容性
:随着新的网络硬件的不断推出,驱动程序需要及时更新,以确保与新硬件的兼容性。
总之,网络接口卡驱动程序的开发是一个复杂而又重要的领域,需要开发者不断学习和掌握新的技术,以适应不断变化的网络环境。通过合理设计和实现驱动程序,可以提高网络设备的性能和稳定性,为用户提供更好的网络体验。

7941


被折叠的 条评论
为什么被折叠?



