14.1 Linux 网络设备驱动的结构
网络设备与字符/块设备最大的不同:没有设备文件,只能通过 socket 接口访问。四层结构:
应用层 (socket)
↓
网络协议接口层 (net/core/skbuff.c, net/ipv4/) ------ sk_buff、协议栈
↓
网络设备接口层 (net/core/dev.c) ------ net_device、netdev_ops
↓
设备驱动功能层 (你的驱动) ------ 收/发、NAPI、中断
↓
物理网卡 / QEMU 虚拟网卡
14.1.1 网络协议接口层
核心是 struct sk_buff (include/linux/skbuff.h),它承载一个网络包及其元数据。
cpp
struct sk_buff {
union {
struct { struct sk_buff *next, *prev; }; /* 链表 */
struct rb_node rbnode;
struct list_head list;
};
struct sock *sk;
struct net_device *dev;
char cb[48] __aligned(8); /* 驱动私有区 */
unsigned long _skb_refdst;
struct sec_path *sp;
unsigned int len, data_len;
__u16 mac_len, hdr_len;
__u16 queue_mapping;
__u8 ip_summed;
__u8 csum_level;
__u8 csum_not_inet;
__u32 priority;
...
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head, *data;
...
};
关键操作:
| API | 作用 |
|---|---|
dev_alloc_skb(len) |
分配 skb(用于接收) |
alloc_skb(len, gfp) |
分配 skb |
kfree_skb(skb) / dev_kfree_skb(skb) |
释放 |
skb_put(skb, len) |
尾部扩展(接收时) |
skb_push(skb, len) |
头部扩展(发送时) |
skb_pull(skb, len) |
头部收缩 |
skb_reserve(skb, len) |
预留头部空间 |
skb_headroom() / skb_tailroom() |
剩余空间 |
skb_clone() / skb_copy() |
复制 |
skb_checksum_help() |
计算校验和 |
14.1.2 网络设备接口层
核心是 struct net_device (include/linux/netdevice.h,数千行)和 struct net_device_ops。
cpp
struct net_device_ops {
int (*ndo_init)(struct net_device *dev);
void (*ndo_uninit)(struct net_device *dev);
int (*ndo_open)(struct net_device *dev);
int (*ndo_stop)(struct net_device *dev);
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
struct net_device *dev);
netdev_features_t (*ndo_features_check)(struct sk_buff *skb,
struct net_device *dev,
netdev_features_t features);
u16 (*ndo_select_queue)(struct net_device *dev, struct sk_buff *skb,
struct net_device *sb_dev);
void (*ndo_set_rx_mode)(struct net_device *dev);
int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
int (*ndo_validate_addr)(struct net_device *dev);
int (*ndo_eth_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
void (*ndo_tx_timeout)(struct net_device *dev, unsigned int txqueue);
void (*ndo_get_stats64)(struct net_device *dev,
struct rtnl_link_stats64 *storage);
...
int (*ndo_bpf)(struct net_device *dev, struct netdev_bpf *bpf);
int (*ndo_xdp_xmit)(struct net_device *dev, int n,
struct xdp_frame **frames, u32 flags);
...
};
7.2.5 的变化(对比 4.0):
| 项 | 4.0 | 7.2.5 |
|---|---|---|
ndo_start_xmit |
存在 | 存在,但不再要求返回 NETDEV_TX_BUSY 后自己 stop_queue(见 14.5) |
ndo_tx_timeout |
单参数 | 新增 txqueue 参数 |
ndo_get_stats64 |
存在 | 首选(ndo_get_stats 已废弃) |
ndo_do_ioctl |
通用 | 拆分为 ndo_eth_ioctl/ndo_siocdevprivate 等 |
ndo_xdp_xmit/ndo_bpf |
早期版本 | 成熟,XDP 成为重要特性 |
ndo_set_rx_mode_async |
无 | 新增 |
init_etherdev |
存在 | 已删除 |
netif_napi_add(dev, napi, poll, weight) |
4 参数 | weight 参数默认值用 netif_napi_add();显式指定用 netif_napi_add_weight() |
14.1.3 设备驱动功能层
驱动实现的核心职责:
- 初始化 :分配
net_device、填netdev_ops、注册。 - 打开/关闭 :
ndo_open/ndo_stop中申请中断、启动队列、使能 NAPI。 - 发送 :
ndo_start_xmit把 skb 交给硬件。 - 接收 :中断触发 → 关闭中断 → 调度 NAPI →
napi_poll收包 →napi_gro_receive()/netif_receive_skb()。 - 统计 :
ndo_get_stats64返回rtnl_link_stats64。 - 参数设置 :
ethtool_ops(include/linux/ethtool.h)。
14.2 网络设备驱动的注册与注销
cpp
/* include/linux/netdevice.h */
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *),
unsigned int txqs, unsigned int rxqs);
struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs,
unsigned int rxqs);
struct net_device *devm_alloc_etherdev_mqs(struct device *dev, int sizeof_priv,
unsigned int txqs, unsigned int rxqs);
void free_netdev(struct net_device *dev);
int register_netdev(struct net_device *dev);
void unregister_netdev(struct net_device *dev);
int register_netdevice(struct net_device *dev);
void unregister_netdevice(struct net_device *dev);
register_netdev() vs register_netdevice():
| API | 说明 |
|---|---|
register_netdev() |
上层封装,注册前做命名与合法性检查(驱动优先用这个) |
register_netdevice() |
底层接口,要求 dev->name 已设置,dev->reg_state 正确 |
7.2.5 推荐流程(devm 版本):
cpp
static int my_net_probe(struct platform_device *pdev)
{
struct net_device *ndev;
struct my_priv *priv;
ndev = devm_alloc_etherdev_mqs(&pdev->dev, sizeof(struct my_priv),
1, 1);
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
priv->ndev = ndev;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
ndev->netdev_ops = &my_netdev_ops;
ndev->ethtool_ops = &my_ethtool_ops;
ndev->watchdog_timeo = 5 * HZ;
SET_NETDEV_DEV(ndev, &pdev->dev);
return register_netdev(ndev); /* devm 会在卸载时自动 free */
}
注销:
cpp
static void my_net_remove(struct platform_device *pdev)
{
struct net_device *ndev = dev_get_drvdata(&pdev->dev);
unregister_netdev(ndev);
/* devm_alloc_etherdev 会自动 free_netdev */
}
注意 :unregister_netdev() 会等待所有 in-flight 的发送/接收完成,因此必须在释放硬件资源之前调用。
14.3 网络设备的初始化
cpp
static void my_setup(struct net_device *ndev)
{
/* 二层类型 */
ndev->type = ARPHRD_ETHER;
/* 头部长度:以太网头 14 字节 */
ndev->hard_header_len = ETH_HLEN;
/* MTU 与最大帧 */
ndev->mtu = ETH_DATA_LEN; /* 1500 */
ndev->min_mtu = ETH_MIN_MTU;
ndev->max_mtu = ETH_DATA_LEN;
/* 头部空间(用于 skb_reserve) */
ndev->needed_headroom = 0;
ndev->needed_tailroom = 0;
/* 设备特性 */
ndev->features = NETIF_F_HW_CSUM | NETIF_F_SG | NETIF_F_FRAGLIST;
ndev->hw_features = ndev->features;
/* 地址长度 */
ndev->addr_len = ETH_ALEN;
/* 广播地址 */
eth_broadcast_addr(ndev->broadcast);
/* 关闭 ARP 等,由协议栈决定 */
ndev->flags |= IFF_NOARP; /* 仅当设备不支持 ARP 时 */
}
常用字段表:
| 字段 | 说明 |
|---|---|
type |
ARPHRD_ETHER、ARPHRD_LOOPBACK、ARPHRD_IEEE80211 |
hard_header_len |
链路层头长度 |
mtu |
最大传输单元 |
min_mtu / max_mtu |
MTU 范围(4.0 之后引入,必须设置) |
features / hw_features |
硬件卸载能力 |
vlan_features |
VLAN 相关能力 |
addr_len |
MAC 地址长度 |
dev_addr |
MAC 地址 |
broadcast |
广播地址 |
watchdog_timeo |
发送超时时间 |
netdev_ops / ethtool_ops |
操作集 |
分配私有数据:
cpp
struct my_priv {
struct net_device *ndev;
void __iomem *base;
struct napi_struct napi;
struct net_device_stats stats;
spinlock_t lock;
...
};
priv = netdev_priv(ndev); /* 从 net_device 反查私有数据 */
14.4 网络设备的打开与释放
cpp
static int my_net_open(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
int ret;
/* 1. 申请中断 */
ret = request_irq(ndev->irq, my_interrupt, IRQF_SHARED,
ndev->name, ndev);
if (ret)
return ret;
/* 2. 使能 NAPI */
napi_enable(&priv->napi);
/* 3. 初始化硬件(使能收发) */
my_hw_init(priv);
/* 4. 启动发送队列 */
netif_start_queue(ndev);
/* 5. 启动 PHY/链路(真实网卡) */
phy_start(priv->phydev);
netif_info(priv, ifup, ndev, "device up\n");
return 0;
}
static int my_net_stop(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
netif_stop_queue(ndev); /* 先停队列 */
my_hw_disable(priv); /* 再停硬件 */
napi_disable(&priv->napi); /* 再停 NAPI */
free_irq(ndev->irq, ndev); /* 最后释放中断 */
return 0;
}
顺序至关重要:
- 打开:申请中断 → 使能 NAPI → 启动硬件 → 启动队列。
- 关闭:停队列 → 停硬件 → 停 NAPI → 释放中断。
顺序错误会导致"中断处理函数访问已释放资源"或"队列启动后硬件未就绪"。
14.5 数据发送流程
cpp
static netdev_tx_t my_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
unsigned long flags;
spin_lock_irqsave(&priv->lock, flags);
if (!my_tx_ready(priv)) {
/* 7.2.5:直接返回 BUSY 并让核心层 stop 队列,
* 或者自己调用 netif_stop_queue() 后返回 NETDEV_TX_BUSY */
netif_stop_queue(ndev);
spin_unlock_irqrestore(&priv->lock, flags);
return NETDEV_TX_BUSY;
}
/* 映射 DMA */
priv->tx_dma = dma_map_single(&ndev->dev, skb->data, skb->len,
DMA_TO_DEVICE);
if (dma_mapping_error(&ndev->dev, priv->tx_dma)) {
spin_unlock_irqrestore(&priv->lock, flags);
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
/* 启动硬件传输 */
my_hw_start_tx(priv, priv->tx_dma, skb->len);
priv->tx_skb = skb; /* 保存,完成时释放 */
spin_unlock_irqrestore(&priv->lock, flags);
return NETDEV_TX_OK;
}
netdev_tx_t 返回值:
| 值 | 含义 |
|---|---|
NETDEV_TX_OK |
已接受(成功或已丢弃) |
NETDEV_TX_BUSY |
硬件忙,稍后重试(调用者必须 stop 队列) |
NETDEV_TX_LOCKED |
已被锁占用(已废弃,不要用) |
7.2.5 的发送流程变化:
- 不再需要驱动自己处理
NETDEV_TX_BUSY的队列恢复 :核心层会保证在返回NETDEV_TX_BUSY前队列已被停止。 - 多队列:
ndev->real_num_tx_queues、netdev_get_tx_queue()、netif_tx_stop_queue()支持每队列独立控制。 netif_wake_queue()在发送完成中断中调用,恢复发送。ndo_tx_timeout新增txqueue参数,用于多队列设备定位超时队列。
发送完成中断:
cpp
static irqreturn_t my_tx_irq(int irq, void *data)
{
struct net_device *ndev = data;
struct my_priv *priv = netdev_priv(ndev);
if (my_tx_complete(priv)) {
if (priv->tx_skb) {
dma_unmap_single(&ndev->dev, priv->tx_dma,
priv->tx_skb->len, DMA_TO_DEVICE);
dev_kfree_skb_irq(priv->tx_skb); /* 中断上下文用 _irq 版本 */
priv->tx_skb = NULL;
}
netif_wake_queue(ndev);
}
return IRQ_HANDLED;
}
注意 :中断上下文中释放 skb 必须用 dev_kfree_skb_irq() 或 dev_kfree_skb_any() ,直接用 kfree_skb() 可能睡眠。
14.6 数据接收流程
7.2.5 的标准 NAPI 流程:
cpp
/* 1. 中断处理函数:只调度 NAPI,不真正收包 */
static irqreturn_t my_rx_irq(int irq, void *data)
{
struct net_device *ndev = data;
struct my_priv *priv = netdev_priv(ndev);
if (!my_is_rx_irq(priv))
return IRQ_NONE;
/* 关闭接收中断,避免中断风暴 */
my_disable_rx_irq(priv);
if (napi_schedule_prep(&priv->napi)) {
my_ack_rx_irq(priv);
__napi_schedule(&priv->napi);
}
return IRQ_HANDLED;
}
/* 2. 轮询函数:在软中断上下文批量收包 */
static int my_napi_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
struct net_device *ndev = priv->ndev;
int work_done = 0;
while (work_done < budget) {
struct sk_buff *skb;
unsigned int len;
skb = my_rx_one(priv, &len);
if (!skb)
break;
/* 填充协议信息 */
skb->protocol = eth_type_trans(skb, ndev);
skb->ip_summed = CHECKSUM_NONE; /* 或 CHECKSUM_UNNECESSARY */
/* 交给协议栈(支持 GRO 合并) */
napi_gro_receive(napi, skb);
ndev->stats.rx_packets++;
ndev->stats.rx_bytes += len;
work_done++;
}
if (work_done < budget) {
napi_complete_done(napi, work_done);
my_enable_rx_irq(priv); /* 重新使能中断 */
}
return work_done;
}
关键 API 对比:
| API | 上下文 | 用途 |
|---|---|---|
netif_rx(skb) |
任何(含中断) | 旧式,把 skb 放入 backlog 队列,稍后处理 |
netif_receive_skb(skb) |
进程/软中断 | 直接上送协议栈 |
napi_gro_receive(napi, skb) |
NAPI 轮询中 | 支持 GRO 合并(现代首选) |
netif_receive_skb_list() |
软中断 | 批量上送 |
NAPI 注册(7.2.5):
cpp
/* 默认 weight(64) */
netif_napi_add(ndev, &priv->napi, my_napi_poll);
/* 显式 weight */
netif_napi_add_weight(ndev, &priv->napi, my_napi_poll, 64);
/* 需要时在 ndo_open 中 enable,ndo_stop 中 disable */
napi_enable(&priv->napi);
napi_disable(&priv->napi);
为什么用 NAPI:
- 高流量下中断风暴会耗尽 CPU;NAPI 在中断后关闭中断,用轮询批量收包。
budget限制单次轮询的包数,保证公平性。- GRO 把同一流的多个小包合并成一个大包,减少协议栈开销。
接收路径的必须操作:
skb->protocol = eth_type_trans(skb, ndev)------以太网驱动必须调用。- 设置
skb->ip_summed(CHECKSUM_UNNECESSARY表示硬件已校验)。 - 更新
ndev->stats(或实现ndo_get_stats64)。 - 用
napi_gro_receive()上送。
14.7 网络连接状态
cpp
/* 链路状态 */
netif_carrier_on(ndev); /* 链路建立 */
netif_carrier_off(ndev); /* 链路断开 */
netif_carrier_ok(ndev); /* 查询 */
/* 队列状态 */
netif_start_queue(ndev);
netif_stop_queue(ndev);
netif_wake_queue(ndev);
netif_queue_stopped(ndev);
/* 运行状态 */
netif_running(ndev); /* ndo_open 是否成功 */
netif_device_present(ndev); /* 设备是否存在(热插拔) */
netif_device_attach(ndev) / netif_device_detach(ndev);
carrier 语义 :netif_carrier_on() 会触发 netdev_carrier_changed 通知,用户空间通过 netlink 收到 RTM_NEWLINK 事件。
PHY 层(真实网卡):
cpp
/* include/linux/phy.h */
struct phy_device *phydev;
phydev = phy_connect(ndev, phy_id, my_adjust_link, PHY_INTERFACE_MODE_RGMII);
phy_start(phydev);
phy_stop(phydev);
phy_disconnect(phydev);
/* 在 adjust_link 中更新链路状态 */
static void my_adjust_link(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
if (priv->phydev->link)
netif_carrier_on(ndev);
else
netif_carrier_off(ndev);
}
14.8 参数设置和统计数据
14.8.1 ethtool
cpp
/* include/linux/ethtool.h */
struct ethtool_ops {
u32 (*get_link)(struct net_device *);
int (*get_link_ksettings)(struct net_device *,
struct ethtool_link_ksettings *);
int (*set_link_ksettings)(struct net_device *,
const struct ethtool_link_ksettings *);
void (*get_drvinfo)(struct net_device *, struct ethtool_drvinfo *);
int (*get_sset_count)(struct net_device *, int);
void (*get_strings)(struct net_device *, u32, u8 *);
void (*get_ethtool_stats)(struct net_device *, struct ethtool_stats *, u64 *);
void (*get_regs)(struct net_device *, struct ethtool_regs *, void *);
int (*get_coalesce)(struct net_device *, struct ethtool_coalesce *);
int (*set_coalesce)(struct net_device *, struct ethtool_coalesce *);
...
};
7.2.5 变化:
get_settings/set_settings已删除 ,必须用get_link_ksettings/set_link_ksettings。- 新增
get_module_info/get_module_eeprom_by_page、get_link_ext_state等。 - 统计用
get_ethtool_stats+get_sset_count+get_strings三件套。
14.8.2 统计计数
cpp
/* 方式一:直接维护 net_device 的 stats(简单) */
ndev->stats.rx_packets++;
ndev->stats.rx_bytes += len;
ndev->stats.tx_packets++;
ndev->stats.tx_bytes += len;
ndev->stats.rx_errors++;
ndev->stats.rx_dropped++;
/* 方式二:实现 ndo_get_stats64(推荐,支持 64 位原子计数) */
static void my_get_stats64(struct net_device *ndev,
struct rtnl_link_stats64 *stats)
{
struct my_priv *priv = netdev_priv(ndev);
stats->rx_packets = atomic64_read(&priv->rx_packets);
stats->tx_packets = atomic64_read(&priv->tx_packets);
stats->rx_bytes = atomic64_read(&priv->rx_bytes);
stats->tx_bytes = atomic64_read(&priv->tx_bytes);
stats->rx_errors = priv->rx_errors;
stats->tx_errors = priv->tx_errors;
}
用户空间查看:
bash
ip -s link show eth0
ethtool eth0
ethtool -S eth0
cat /proc/net/dev
14.9 DM9000 网卡设备驱动实例
14.9.1 DM9000 网卡硬件描述
DM9000 是原书使用的经典以太网控制器:
| 特性 | 说明 |
|---|---|
| 接口 | 8/16 位本地总线(也支持 SRAM 接口) |
| 地址线 | CMD 引脚区分索引寄存器与数据寄存器 |
| 中断 | 单一 IRQ,状态寄存器区分收发/错误 |
| DMA | 无,CPU 逐字节/字搬运 |
| 收发缓冲 | 内部 16KB SRAM,分区为 TX/RX FIFO |
寄存器访问模式(原书重点):
cpp
/* CMD = 0:访问索引寄存器(INDEX) */
/* CMD = 1:访问数据寄存器(DATA) */
#define DM9000_INDEX (priv->base) /* 低地址 */
#define DM9000_DATA (priv->base + priv->io_addr)
static u8 dm9000_readb(struct dm9000 *db, u16 reg)
{
writeb(reg, db->addr_lock);
return readb(db->io_data);
}
static void dm9000_writeb(struct dm9000 *db, u16 reg, u8 value)
{
writeb(reg, db->addr_lock);
writeb(value, db->io_data);
}
关键寄存器:
| 寄存器 | 地址 | 作用 |
|---|---|---|
NCR |
0x00 | 网络控制(复位、回环) |
NSR |
0x01 | 网络状态(链路、速度) |
TCR |
0x02 | 发送控制 |
TSR1/TSR2 |
0x03/0x04 | 发送状态 |
RCR |
0x05 | 接收控制 |
RSR |
0x06 | 接收状态 |
ISR |
0xFE | 中断状态 |
IMR |
0xFF | 中断屏蔽 |
MWCMD |
0xF8 | 内存写命令 |
MRCMD |
0xF2 | 内存读命令 |
14.9.2 DM9000 网卡驱动设计分析
QEMU 替代方案:QEMU 没有 DM9000 模型,但有三种等效路径:
- 读源码 :
drivers/net/ethernet/davicom/dm9000.c在 7.2.5 中依然存在,直接阅读。 - 用 virtio-net 学真实驱动 :
drivers/net/virtio_net.c是 7.2.5 中最现代、最值得读的网络驱动。 - 写一个"内存模拟网卡" :用
ndo_start_xmit直接回环(loopback),完整走通netdev_ops流程。
DM9000 驱动的核心结构(7.2.5 源码要点):
cpp
struct board_info {
void __iomem *io_addr; /* 索引寄存器基址 */
void __iomem *io_data; /* 数据寄存器基址 */
struct net_device *ndev;
struct resource *addr_res;
struct resource *data_res;
struct resource *irq_res;
struct timer_list timer;
struct mii_if_info mii;
unsigned int flags;
...
};
驱动注册(drivers/net/ethernet/davicom/dm9000.c):
cpp
static struct platform_driver dm9000_driver = {
.driver = {
.name = "dm9000",
.pm = &dm9000_drv_pm_ops,
.of_match_table = of_match_ptr(dm9000_of_matches),
},
.probe = dm9000_probe,
.remove = dm9000_drv_remove, /* 7.2.5:返回 void */
};
dm9000_probe() 关键步骤:
platform_get_resource()取地址与中断资源。devm_ioremap()映射索引/数据寄存器。alloc_etherdev()分配net_device。- 读取芯片 ID(
DM9000_VIDL/DM9000_PIDL)验证硬件。 - 读取 EEPROM/设备树中的 MAC 地址。
- 设置
netdev_ops = &dm9000_netdev_ops。 - 初始化 NAPI、定时器(PHY 轮询)。
register_netdev()。
dm9000_netdev_ops:
cpp
static const struct net_device_ops dm9000_netdev_ops = {
.ndo_open = dm9000_open,
.ndo_stop = dm9000_stop,
.ndo_start_xmit = dm9000_start_xmit,
.ndo_tx_timeout = dm9000_timeout,
.ndo_set_multicast_list = dm9000_hash_table,
.ndo_do_ioctl = dm9000_ioctl,
.ndo_change_mtu = eth_change_mtu, /* 7.2.5 中已被移除,需自己实现或忽略 */
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = eth_mac_addr,
.ndo_get_stats64 = dm9000_get_stats64,
};
7.2.5 移植提醒:
ndo_set_multicast_list已删除 ,改用ndo_set_rx_mode。ndo_change_mtu的默认实现eth_change_mtu已删除,应使用ndo_change_mtu自定义或依赖核心层检查。ndo_do_ioctl被拆分为ndo_eth_ioctl等。.remove返回void。
发送流程(DM9000):
cpp
static netdev_tx_t dm9000_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct board_info *db = netdev_priv(dev);
if (db->tx_pkt_cnt > 1)
return NETDEV_TX_BUSY;
netif_stop_queue(dev);
/* 写入长度 */
dm9000_writew(db, DM9000_TXPLL, skb->len);
dm9000_writew(db, DM9000_TXPLH, skb->len >> 8);
/* 写命令,逐字节搬运数据 */
dm9000_writeb(db, DM9000_MWCMD, 0);
writesb(db->io_data, skb->data, skb->len);
/* 启动发送 */
dm9000_writeb(db, DM9000_TCR, TCR_TXREQ);
dev->stats.tx_bytes += skb->len;
dev_kfree_skb(skb);
db->tx_pkt_cnt++;
return NETDEV_TX_OK;
}
接收流程(DM9000):
cpp
static int dm9000_rx(struct net_device *dev)
{
struct board_info *db = netdev_priv(dev);
u8 rxbyte, *rdptr;
u16 RxLen, RxStatus;
struct sk_buff *skb;
int good_packet;
/* 读接收状态 */
dm9000_readb(db, DM9000_MRCMD, &rxbyte, 1);
RxStatus = ...;
RxLen = ...;
if (RxLen > DM9000_PKT_MAX) { /* 异常 */ }
/* 分配 skb 并搬运数据 */
skb = dev_alloc_skb(RxLen + 4);
rdptr = skb_put(skb, RxLen - 4);
dm9000_readb(db, DM9000_MRCMD, rdptr, RxLen - 4);
skb->protocol = eth_type_trans(skb, dev);
netif_rx(skb); /* 或 napi_gro_receive() */
dev->stats.rx_packets++;
dev->stats.rx_bytes += RxLen - 4;
return 0;
}
QEMU 学习建议 :用 drivers/net/virtio_net.c 对照阅读,它展示了 7.2.5 网络驱动的所有现代特性(多队列、NAPI、XDP、ethtool、DMA、TSO)。
14.10 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
忘记 eth_type_trans() |
协议栈收不到包 | 接收时必须调用 |
中断上下文用 kfree_skb() |
睡眠警告 | 用 dev_kfree_skb_irq() |
ndo_start_xmit 返回 BUSY 但未停队列 |
队列状态不一致 | 核心层会处理,但显式 stop 更清晰 |
ndo_open 中先启动队列后申请中断 |
中断到达时未就绪 | 顺序:中断→NAPI→硬件→队列 |
ndo_stop 中先释放中断后停硬件 |
中断丢失/竞态 | 顺序:队列→硬件→NAPI→中断 |
未实现 ndo_get_stats64 |
ip -s link 无数据 |
实现或直接维护 stats |
ndo_tx_timeout 少 txqueue 参数 |
编译错误 | 加参数 |
ndo_set_multicast_list |
已删除 | 用 ndo_set_rx_mode |
ethtool get_settings |
已删除 | 用 get_link_ksettings |
NAPI 中 budget 用尽仍 napi_complete |
包积压 | 只在 work_done < budget 时完成 |
忘记 netif_wake_queue() |
发送永久停止 | 在发送完成中唤醒 |
alloc_etherdev 后手动 kfree |
双重释放 | 用 free_netdev() |
14.11 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| virtio-net 收发 | 学习现代网络驱动 | ip link、ping |
| 内存回环网卡 | 学习 netdev_ops 全流程 |
ip addr add + ping 自己 |
| NAPI 观察 | 学习收包机制 | /proc/interrupts 中断数不再线性增长 |
| ethtool 实现 | 学习参数设置 | ethtool -i/-S/--show-ring |
| 统计计数 | 学习 rtnl_link_stats64 |
ip -s link 数据正确 |
| XDP 程序加载 | 学习 7.2.5 新特性 | bpftool prog load 成功 |
多队列(-smp 4) |
学习多队列发送 | /sys/class/net/eth0/queues/ |
14.12 总结
- 网络设备没有设备文件,走 socket 路径;核心结构是
net_device+netdev_ops+sk_buff。 - 7.2.5 用
netif_napi_add()/netif_napi_add_weight()注册 NAPI,中断只调度、轮询才收包。 - 接收路径必须调用
eth_type_trans(),并用napi_gro_receive()支持 GRO。 - 发送路径中
NETDEV_TX_BUSY的队列管理已由核心层保证;ndo_tx_timeout新增txqueue参数。 init_etherdev、ndo_set_multicast_list、eth_change_mtu、ethtool get_settings等旧接口已删除。- DM9000 在 7.2.5 中仍可读,但更推荐对照
virtio_net.c学习现代写法。 - QEMU 的
virtio-net加上自写的回环网卡,即可完整复现原书的全部网络驱动知识点。