2. 实现网卡驱动的中断处理
任务说明
本案例为虚拟网卡驱动添加中断处理能力。目标是在驱动中注册中断处理函数,模拟硬件产生接收中断,并在中断上下文中完成数据包接收与上送协议栈的完整路径。通过本案例,你将理解内核中断注册机制、下半部(tasklet)的使用,以及NAPI在接收路径中的角色。
驱动框架与中断设计
我们基于一个虚拟网卡驱动(类似veth但更简化)实现中断处理。驱动维护一个环形缓冲区(ring buffer)模拟硬件接收队列。当有“数据包到达”时,通过调用自定义函数vnet_rx_irq_trigger()模拟硬件中断信号,该函数内部调用generic_handle_irq()或直接调用注册的中断处理函数(为简化,我们直接调用handler)。实际硬件中,中断控制器会调用驱动注册的handler,这里我们模拟该行为。
中断处理函数运行在中断上下文,不能睡眠,因此我们将实际的数据包处理工作放入tasklet(下半部)中执行。tasklet在软中断上下文运行,可以调用内核内存分配函数(如netdev_alloc_skb)。接收到的数据包通过netif_receive_skb()上送协议栈。
代码实现
// vnet_interrupt.c
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/interrupt.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/if_ether.h>
#define VNET_QUEUE_SIZE 16
#define VNET_IRQ_NUM 27 // 虚拟中断号
struct vnet_priv {
struct net_device *dev;
struct tasklet_struct rx_tasklet;
spinlock_t lock;
struct sk_buff *rx_queue[VNET_QUEUE_SIZE];
int rx_head, rx_tail;
int irq;
};
static int vnet_ring_push(struct vnet_priv *priv, struct sk_buff *skb)
{
unsigned long flags;
int next = (priv->rx_head + 1) % VNET_QUEUE_SIZE;
spin_lock_irqsave(&priv->lock, flags);
if (next == priv->rx_tail) {
spin_unlock_irqrestore(&priv->lock, flags);
return -ENOSPC; // 队列满
}
priv->rx_queue[priv->rx_head] = skb;
priv->rx_head = next;
spin_unlock_irqrestore(&priv->lock, flags);
return 0;
}
static struct sk_buff *vnet_ring_pop(struct vnet_priv *priv)
{
struct sk_buff *skb;
unsigned long flags;
spin_lock_irqsave(&priv->lock, flags);
if (priv->rx_head == priv->rx_tail) {
spin_unlock_irqrestore(&priv->lock, flags);
return NULL;
}
skb = priv->rx_queue[priv->rx_tail];
priv->rx_tail = (priv->rx_tail + 1) % VNET_QUEUE_SIZE;
spin_unlock_irqrestore(&priv->lock, flags);
return skb;
}
// 下半部:在软中断上下文处理数据包
static void vnet_rx_tasklet(unsigned long data)
{
struct vnet_priv *priv = (struct vnet_priv *)data;
struct sk_buff *skb;
while ((skb = vnet_ring_pop(priv)) != NULL) {
skb->dev = priv->dev;
skb->protocol = eth_type_trans(skb, priv->dev);
netif_receive_skb(skb);
priv->dev->stats.rx_packets++;
priv->dev->stats.rx_bytes += skb->len;
}
}
// 中断处理函数(上半部)
static irqreturn_t vnet_irq_handler(int irq, void *dev_id)
{
struct net_device *dev = dev_id;
struct vnet_priv *priv = netdev_priv(dev);
// 模拟硬件:读取中断状态寄存器,确认是RX中断
// 实际硬件会禁用该中断源直到处理完成,这里简化
tasklet_schedule(&priv->rx_tasklet);
return IRQ_HANDLED;
}
// 模拟硬件产生接收中断(外部可调用)
void vnet_rx_irq_trigger(struct net_device *dev, struct sk_buff *skb)
{
struct vnet_priv *priv = netdev_priv(dev);
if (vnet_ring_push(priv, skb) == 0) {
// 触发中断:实际硬件会置位中断寄存器并通知中断控制器
// 这里直接调用handler模拟中断上下文
vnet_irq_handler(priv->irq, dev);
} else {
dev_kfree_skb_any(skb);
dev->stats.rx_dropped++;
}
}
EXPORT_SYMBOL(vnet_rx_irq_trigger);
static int vnet_open(struct net_device *dev)
{
netif_start_queue(dev);
return 0;
}
static int vnet_stop(struct net_device *dev)
{
netif_stop_queue(dev);
return 0;
}
static int vnet_init(struct net_device *dev)
{
struct vnet_priv *priv = netdev_priv(dev);
priv->dev = dev;
priv->rx_head = priv->rx_tail = 0;
spin_lock_init(&priv->lock);
tasklet_init(&priv->rx_tasklet, vnet_rx_tasklet, (unsigned long)priv);
priv->irq = VNET_IRQ_NUM;
return 0;
}
static const struct net_device_ops vnet_ops = {
.ndo_open = vnet_open,
.ndo_stop = vnet_stop,
.ndo_init = vnet_init,
};
static void vnet_setup(struct net_device *dev)
{
ether_setup(dev);
dev->netdev_ops = &vnet_ops;
}
static int __init vnet_init_module(void)
{
struct net_device *dev;
struct vnet_priv *priv;
int ret;
dev = alloc_netdev(sizeof(struct vnet_priv), "vnet0", NET_NAME_UNKNOWN, vnet_setup);
if (!dev)
return -ENOMEM;
ret = register_netdev(dev);
if (ret) {
free_netdev(dev);
return ret;
}
priv = netdev_priv(dev);
ret = request_irq(priv->irq, vnet_irq_handler, IRQF_SHARED, "vnet", dev);
if (ret) {
unregister_netdev(dev);
free_netdev(dev);
return ret;
}
pr_info("vnet: registered with IRQ %d\n", priv->irq);
return 0;
}
static void __exit vnet_cleanup_module(void)
{
struct net_device *dev = dev_get_by_name(&init_net, "vnet0");
if (dev) {
struct vnet_priv *priv = netdev_priv(dev);
free_irq(priv->irq, dev);
tasklet_kill(&priv->rx_tasklet);
unregister_netdev(dev);
free_netdev(dev);
dev_put(dev);
}
}
module_init(vnet_init_module);
module_exit(vnet_cleanup_module);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Virtual NIC with interrupt handling");
中断处理流程
编译与加载
# 编译
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 加载
sudo insmod vnet_interrupt.ko
dmesg | tail -5 # 应看到 "vnet: registered with IRQ 27"
# 查看接口
ip link show vnet0
验证方法
编写一个测试模块或使用vnet_rx_irq_trigger()函数注入数据包。由于该函数已导出,可在另一个模块中调用:
// test_inject.c (示例片段)
extern void vnet_rx_irq_trigger(struct net_device *dev, struct sk_buff *skb);
static int inject_packet(void)
{
struct net_device *dev = dev_get_by_name(&init_net, "vnet0");
struct sk_buff *skb;
if (!dev) return -ENODEV;
skb = alloc_skb(64, GFP_KERNEL);
skb_put_data(skb, "Hello VNET IRQ", 14);
skb_reset_mac_header(skb);
vnet_rx_irq_trigger(dev, skb);
dev_put(dev);
return 0;
}
判定标准:
- 加载模块后
dmesg无错误,ip link show vnet0能看到接口且状态为DOWN(未open)。 - 执行
ip link set vnet0 up后,注入测试数据包,使用tcpdump -i vnet0应能捕获到注入的报文。 - 检查
/sys/class/net/vnet0/statistics/rx_packets,数值应随注入次数递增。 - 卸载模块时无中断泄漏警告(
free_irq成功)。
若在真实硬件环境中,中断号、寄存器操作需按硬件手册调整,本案例使用虚拟中断号27和软件触发方式,重点演示中断上下文与下半部的协作机制。