Android内核网络栈调试与驱动数据通路优化 - 第2节
2.1 struct net_device 结构体详解
struct net_device 是 Linux 内核网络子系统中最重要的数据结构之一,它抽象了一个网络设备的所有属性和操作。无论是物理网卡、虚拟网桥还是 VLAN 接口,在内核中都以 struct net_device 实例表示。理解其内部布局是驱动开发和网络栈调试的基础。
2.1.1 核心字段分类
struct net_device 包含数百个字段,按功能可划分为以下几类:
| 分类 | 关键字段 | 说明 |
|---|---|---|
| 标识 | name, dev_id, ifindex | 设备名称(如 eth0)、设备 ID、全局唯一接口索引 |
| 配置 | mtu, flags, features | 最大传输单元、IFF_UP 等标志位、设备特性位图 |
| 地址 | dev_addr, broadcast, perm_addr | MAC 地址、广播地址、永久硬件地址 |
| 统计 | stats, tstats, lstats | 收发统计计数(包数、字节数、错误数) |
| 操作函数 | netdev_ops, ethtool_ops | 设备打开/关闭/发送等操作函数表 |
| NAPI | napi_list, poll_controller | NAPI 轮询列表、轮询控制函数 |
| 队列 | rx_queue, tx_queue, ingress_queue | 接收/发送/入口队列管理结构 |
| 链路层 | type, hard_header_len, addr_len | 链路类型(ARPHRD_ETHER)、头部长度、地址长度 |
调试要点: 在 Android 内核调试中,通过 /proc/net/dev 可查看所有 net_device 的统计信息;使用 cat /sys/class/net/<ifname>/ 可访问设备属性。驱动开发时最常操作的是 netdev_ops 和 features 字段。
2.1.2 netdev_ops 操作函数表
每个网络设备驱动必须实现 struct net_device_ops 中的核心回调函数:
struct net_device_ops {
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);
void (*ndo_get_stats64)(struct net_device *dev, struct rtnl_link_stats64 *stats);
int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
int (*ndo_do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
void (*ndo_poll_controller)(struct net_device *dev);
int (*ndo_set_features)(struct net_device *dev, netdev_features_t features);
};
驱动开发提示: ndo_start_xmit 是数据发送的核心入口,必须在原子上下文中快速返回。Android 设备中,Wi-Fi 和 Cellular 驱动的 ndo_start_xmit 常需要处理 skb 的 DMA 映射和硬件队列管理。
2.1.3 设备特性位图 (features)
net_device->features 使用位图表示设备硬件卸载能力,直接影响网络栈的数据处理路径:
| 特性标志 | 含义 | 影响 |
|---|---|---|
NETIF_F_SG | 支持分散/聚集 I/O | 允许 skb 使用多个片段 |
NETIF_F_IP_CSUM | IPv4 TCP/UDP 校验和卸载 | 减少 CPU 计算开销 |
NETIF_F_HW_VLAN_CTAG_RX | 硬件 VLAN 剥离 | 接收时自动去除 VLAN 标签 |
NETIF_F_TSO | TCP 分段卸载 | 大包直接交给网卡分段 |
NETIF_F_LRO | 大接收卸载 | 合并接收包减少中断 |
NETIF_F_RXCSUM | 接收校验和卸载 | 硬件验证校验和 |
调试陷阱: Android 内核中,某些虚拟设备(如 tun、veth)不支持硬件卸载特性。若驱动错误声明 features,会导致 skb 校验和错误或数据损坏。使用 ethtool -k <ifname> 可查看当前启用的特性。
2.1.4 struct net_device 内部结构示意
下图展示了 struct net_device 的核心字段组织关系:
2.2 网络设备注册与注销流程
网络设备从创建到销毁经历完整的生命周期管理。驱动必须遵循内核提供的 API 进行注册和注销,否则会导致设备不可用或内核崩溃。
2.2.1 设备注册流程
标准注册流程包含以下步骤:
- 分配 net_device:调用
alloc_netdev()或alloc_etherdev()分配设备结构体 - 初始化私有数据:驱动填充
netdev_ops、ethtool_ops、features等字段 - 设置硬件参数:配置 MAC 地址、MTU、队列数等
- 注册设备:调用
register_netdev()将设备加入内核网络子系统 - 启动设备:通过
netif_carrier_on()和netif_start_queue()激活链路和发送队列
// 典型驱动注册代码片段
struct net_device *ndev;
struct my_priv *priv;
// 1. 分配设备(以太网设备,额外私有数据大小 sizeof(*priv))
ndev = alloc_etherdev(sizeof(struct my_priv));
if (!ndev)
return -ENOMEM;
// 2. 设置操作函数表
ndev->netdev_ops = &my_netdev_ops;
ndev->ethtool_ops = &my_ethtool_ops;
// 3. 设置特性
ndev->features |= NETIF_F_IP_CSUM | NETIF_F_SG;
// 4. 设置 MAC 地址(从硬件读取)
dev_addr_set(ndev, hw_mac);
// 5. 注册设备
ret = register_netdev(ndev);
if (ret) {
free_netdev(ndev);
return ret;
}
// 6. 激活链路
netif_carrier_on(ndev);
netif_start_queue(ndev);
Android 特有场景: 在 Android 内核中,register_netdev() 会触发 NETDEV_REGISTER 通知链,netd 守护进程通过 netlink 监听此事件并配置 IP 地址。驱动开发者需确保注册时机正确,避免在设备未就绪时暴露给用户空间。
2.2.2 设备注销流程
注销流程是注册的逆操作,需确保所有引用被释放:
- 停止队列:调用
netif_stop_queue()停止发送 - 关闭链路:调用
netif_carrier_off()通知链路断开 - 注销设备:调用
unregister_netdev()从内核移除 - 释放设备:调用
free_netdev()释放内存
// 典型驱动注销代码片段
void my_remove(struct platform_device *pdev)
{
struct net_device *ndev = platform_get_drvdata(pdev);
// 1. 停止发送队列
netif_stop_queue(ndev);
// 2. 关闭链路
netif_carrier_off(ndev);
// 3. 注销设备(触发 NETDEV_UNREGISTER 通知链)
unregister_netdev(ndev);
// 4. 释放私有数据和设备结构体
free_netdev(ndev);
}
调试陷阱: 注销时若存在未释放的 skb 或未完成的 DMA 操作,会导致内存泄漏或硬件异常。建议在 ndo_stop 回调中清空所有待处理数据包,并等待 DMA 完成后再调用 unregister_netdev()。
2.2.3 注册与注销流程示意
2.3 NAPI 轮询机制原理
NAPI(New API)是 Linux 内核为高性能网络设备设计的混合中断-轮询机制。它解决了传统中断驱动模型在高吞吐量场景下的“中断风暴”问题,是 Android 设备中 Wi-Fi、以太网和 Cellular 驱动的标准数据接收方式。
2.3.1 传统中断 vs NAPI 对比
| 特性 | 传统中断模式 | NAPI 模式 |
|---|---|---|
| 数据接收方式 | 每个包触发一次中断 | 中断后切换为轮询 |
| 高吞吐性能 | 中断风暴导致 CPU 100% | 轮询减少中断开销 |
| 低延迟场景 | 延迟低(立即响应) | 需等待轮询周期 |
| CPU 占用 | 中断上下文频繁切换 | 绑定到特定 CPU 核心 |
| 适用场景 | 低速设备(10Mbps 以下) | 高速设备(100Mbps 以上) |
2.3.2 NAPI 工作流程
NAPI 的核心思想是:在数据流量低时使用中断,流量高时自动切换为轮询。具体流程如下:
- 中断触发:网卡收到数据包,触发硬件中断
- 中断处理:ISR 调用
napi_schedule()将设备的 NAPI 结构加入轮询列表 - 关闭中断:NAPI 框架自动屏蔽该设备的接收中断
- 轮询回调:内核在软中断上下文中调用驱动的
poll函数 - 批量处理:
poll函数从硬件读取多个包,通过netif_receive_skb()送入协议栈 - 退出条件:当处理完所有包或达到预算(budget)时,重新开启中断
// NAPI poll 函数典型实现
static int my_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
struct sk_buff *skb;
while (work_done < budget) {
skb = my_hw_read_packet(priv); // 从硬件读取数据包
if (!skb)
break;
// 送入网络协议栈
netif_receive_skb(skb);
work_done++;
}
// 如果处理完所有包,重新开启中断
if (work_done < budget) {
napi_complete_done(napi, work_done);
my_hw_enable_interrupt(priv); // 重新使能硬件中断
}
return work_done;
}
// 中断处理函数
irqreturn_t my_interrupt(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct my_priv *priv = netdev_priv(ndev);
// 关闭硬件中断(NAPI 接管)
my_hw_disable_interrupt(priv);
// 调度 NAPI 轮询
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
性能优化要点: budget 参数控制单次轮询处理的最大包数,默认值为 64。在 Android 设备中,Wi-Fi 驱动的 budget 可调整至 128-256 以提升吞吐量,但需注意软中断执行时间不能过长(建议 < 1ms)。