5.20 Net 设备
5.20 Net 设备
📚 本节导读
学习时长: 约 45 分钟
难度级别: ⭐⭐⭐⭐⭐
前置知识: 5.1 设备驱动框架、TCP/IP 协议栈基础、网络接口概念
🎯 学习目标
- 理解 OneOS Net 网络设备的抽象架构与分层设计
- 掌握网络设备的注册、MAC 地址获取与数据收发流程
- 理解 NAPI 轮询机制的原理与使用
- 了解网络协议栈操作接口(protocol_ops)的作用
- 能够编写基本的网络设备驱动注册代码
一、概述
Net(网络)设备是 OneOS 中网络接口的抽象层,为上层 TCP/IP 协议栈(如 LwIP)提供统一的硬件操作接口。Net 设备驱动位于 drivers/net/ 目录下,核心头文件为 net_dev.h。
1.1 Net 设备在协议栈中的位置
┌─────────────────────────────────────────┐
│ 应用层 (Socket / MQTT / HTTP) │
├─────────────────────────────────────────┤
│ TCP/IP 协议栈 (LwIP / OneOS-Net) │
├─────────────────────────────────────────┤
│ Net 设备框架 (net_dev.h) │ ← 统一抽象层
├──────────┬──────────┬───────────────────┤
│ Ethernet │ Wi-Fi │ Bluetooth ... │ ← 物理网络接口
│ (ETH) │ (WLAN) │ (BT) │
└──────────┴──────────┴───────────────────┘1.2 核心特点
- 统一抽象:将不同类型的网络接口(Ethernet、Wi-Fi、Bluetooth)统一为
os_net_device结构体 - 分层设计:设备操作(
ops)与协议栈操作(protocol_ops)分离,硬件驱动与协议栈解耦 - NAPI 支持:支持 NAPI(New API)轮询机制,在高吞吐场景下减少中断开销
- 链路管理:提供链路状态变化通知机制,支持上/下线事件处理
1.3 使用场景
一句话核心:Net 设备是"协议栈与网卡的桥梁"——它不处理 IP/TCP 协议,而是把硬件网卡抽象成协议栈能操作的 os_net_device,让上层(LwIP 等)用统一的 ops->send/protocol_ops->recv 收发,而不用关心底层是 ETH 还是 WLAN。
为什么需要 Net 抽象层(对比驱动直接对接 LwIP):
| 维度 | 用 Net 抽象层 | 驱动直接对接协议栈 |
|---|---|---|
| 协议栈解耦 | 换协议栈只换 protocol_ops | 驱动写死绑定某协议栈 |
| 多网卡 | 每个网卡一个 os_net_device,框架统一管理 | 各自实现,难复用 |
| 收发缓冲 | 框架按 MTU/MRU 分配 tx_buff/rx_buff | 驱动自己管理 |
| 链路管理 | os_net_linkchange 统一通知 | 各驱动自己实现 |
典型应用场景:
| 场景 | 谁在用 | 实际工作情况 |
|---|---|---|
| 以太网通信 | 以太网驱动 + LwIP | 注册 "eth0",协议栈经 os_net_tx_data 发、中断里 os_net_rx_data 上报收 |
| Wi-Fi 联网 | WLAN 驱动 + LwIP | STA 模式连 AP,链路变化调 os_net_linkchange 让协议栈 re-associate |
| 蓝牙(BT) | BT 协议栈 | net_dev_intf_bt 类型接口 |
| 高吞吐网卡 | 以太网 + NAPI | 启用 OS_USING_NET_NAPI,高负载切轮询降中断开销 |
选型边界表:
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 无网络需求 | 不用 Net | 无网卡无需抽象 |
| 极简单收发、单网卡 | 直接调协议栈 netif | 引入抽象层收益不大 |
| 多协议栈/多网卡 | Net 抽象层 | 解耦收益明显 |
结合源码的使用注意点:
- 注册前设 MTU/MRU:
os_net_device_register按info.MTU/MRU分配收发缓冲,未设则malloc(0); ops必须非空:注册时断言ops != OS_NULL,且send回调必须实现;- 驱动只上报、不碰协议栈:驱动收包调
os_net_rx_data,链路变化调os_net_linkchange,不要直接操作protocol_ops内部。
二、核心数据结构
2.1 网络接口类型
来源:drivers/net/net_dev.h
typedef enum
{
net_dev_intf_ether = 0,
net_dev_intf_bt = 1,
net_dev_intf_br = 2,
net_dev_intf_unknown
} os_net_intf_type_t;字段详解(源码无注释,下表补充说明,不属于源码):
| 枚举值 | 含义 |
|---|---|
net_dev_intf_ether | 以太网接口 |
net_dev_intf_bt | 蓝牙接口 |
net_dev_intf_br | 桥接接口 |
net_dev_intf_unknown | 未知接口类型(哨兵值,不映射真实接口) |
2.2 网络模式
来源:drivers/net/net_dev.h
typedef enum
{
net_dev_mode_ap = 0,
net_dev_mode_sta = 1,
net_dev_mode_unknown
} os_net_mode_t;字段详解(源码无注释,下表补充说明,不属于源码):
| 枚举值 | 含义 |
|---|---|
net_dev_mode_ap | AP 模式(接入点) |
net_dev_mode_sta | STA 模式(客户端/站点) |
net_dev_mode_unknown | 未知模式(哨兵值) |
2.3 设备操作接口 os_net_device_ops
来源:drivers/net/net_dev.h
struct os_net_device_ops
{
os_err_t (*init)(struct os_net_device *net_dev);
os_err_t (*deinit)(struct os_net_device *net_dev);
os_err_t (*send)(struct os_net_device *net_dev, uint8_t *data, int len);
os_err_t (*get_mac)(struct os_net_device *net_dev, uint8_t *addr);
os_err_t (*set_filter)(struct os_net_device *net_dev, uint8_t *buff, os_bool_t enable);
};函数指针详解(源码无注释,下表补充说明,不属于源码):
| 函数指针 | 说明 |
|---|---|
init | 设备初始化(由 os_net_device_register 内部调用,可空) |
deinit | 设备去初始化(由 os_net_device_unregister 内部调用,可空) |
send | 发送数据包:协议栈经 os_net_tx_data 调用,把 data 中 len 字节交给硬件 |
get_mac | 获取 MAC 地址(读出 6 字节到 addr) |
set_filter | 设置 MAC 过滤器(enable 控制开关,可空) |
2.4 协议栈操作接口 os_net_protocol_ops
来源:drivers/net/net_dev.h
struct os_net_protocol_ops
{
os_err_t (*init)(struct os_net_device *net_dev);
os_err_t (*deinit)(struct os_net_device *net_dev);
os_err_t (*recv)(struct os_net_device *net_dev, uint8_t *data, int len);
os_err_t (*linkchange)(struct os_net_device *net_dev, os_bool_t status);
};函数指针详解(源码无注释,下表补充说明,不属于源码):
| 函数指针 | 说明 |
|---|---|
init/deinit | 协议栈适配层初始化/去初始化(如 LwIP 的 netif 挂接) |
recv | 接收数据包:硬件经 os_net_rx_data 调用,把收到的包交给协议栈处理 |
linkchange | 链路状态变化通知:硬件经 os_net_linkchange 调用,协议栈据此更新 netif up/down |
注意:
protocol_ops由协议栈适配层(如os_net_protocol_lwip_init)填充,硬件驱动不要直接操作,只通过os_net_rx_data/os_net_linkchange等 API 间接调用。
2.5 网络设备信息 os_net_device_info
来源:drivers/net/net_dev.h
struct os_net_device_info
{
uint32_t MTU;
uint32_t MRU;
os_net_mode_t mode;
os_bool_t linkstatus;
os_net_intf_type_t intf_type;
char *ip_addr;
char *gw_addr;
char *ip_mask;
uint8_t dhcp_start_id;
uint8_t dhcp_end_id;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
MTU/MRU | 最大传输/接收单元(字节)。os_net_device_register 用它 malloc 收发缓冲(tx_buff = malloc(MTU)、rx_buff = malloc(MRU)) |
mode | 网络模式(AP/STA) |
linkstatus | 链路状态(OS_TRUE=UP,OS_FALSE=DOWN),由 os_net_linkchange 维护 |
intf_type | 接口类型(Ethernet/BT/BR) |
ip_addr/gw_addr/ip_mask | IP/网关/掩码字符串指针 |
dhcp_start_id/dhcp_end_id | DHCP 地址池起始/结束 ID |
关键理解 —— MTU 决定缓冲大小:
os_net_device_register内部用info.MTU/info.MRU分配发送/接收缓冲区(见net_dev.c),因此注册前必须先设好 MTU/MRU,否则malloc(0)或分配过小。
2.6 网络设备结构体 os_net_device
来源:drivers/net/net_dev.h
struct os_net_device
{
os_device_t dev;
struct os_net_device_info info;
const struct os_net_device_ops *ops;
const struct os_net_protocol_ops *protocol_ops;
void *tx_buff;
void *rx_buff;
void *userdata;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
dev | 首成员继承 os_device_t,复用设备框架的注册/查找/打开机制(os_net_device_register 里 os_device_register(&net_dev->dev, name)) |
info | 设备信息(MTU/MRU/mode/linkstatus/网络参数) |
ops | 硬件操作接口(init/deinit/send/get_mac/set_filter) |
protocol_ops | 协议栈操作接口(init/deinit/recv/linkchange) |
tx_buff/rx_buff | 发送/接收缓冲区(注册时按 MTU/MRU 分配,注销时释放) |
userdata | 用户自定义数据 |
关键理解 ——
dev首成员继承 + 零拷贝设计:os_net_device第一个成员是os_device_t dev,其地址与整个os_net_device相等,因此可作为普通设备注册/查找。tx_buff/rx_buff由框架在注册时分配,硬件驱动收发时可直接把数据放到这两个缓冲,避免频繁 malloc,实现"零拷贝"式收发。
两套 ops 分离的设计意义:
ops(硬件侧:怎么发、怎么读 MAC)与protocol_ops(协议栈侧:收的包给谁、链路变化通知谁)完全解耦——换协议栈(LwIP→OneOS-Net)只需换protocol_ops,换硬件只需换ops,互不影响。
三、API 参考
3.1 设备注册与注销
os_net_device_register —— 分配收发缓冲、初始化设备、注册到设备框架
函数签名:
os_err_t os_net_device_register(struct os_net_device *net_dev, const char *name);| 参数 | 说明 |
|---|---|
net_dev | 网络设备(注册前须填好 info.MTU/MRU 和 ops) |
name | 设备名,如 "eth0" |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 注册成功 |
OS_FAILURE | 缓冲分配失败 / 设备初始化失败 / 注册失败 |
完整源码(drivers/net/net_dev.c,与项目逐字一致):
os_err_t os_net_device_register(struct os_net_device *net_dev, const char *name)
{
OS_ASSERT(net_dev != OS_NULL);
OS_ASSERT(net_dev->ops != OS_NULL);
net_dev->tx_buff = malloc(net_dev->info.MTU);
OS_ASSERT(net_dev->tx_buff != OS_NULL);
net_dev->rx_buff = malloc(net_dev->info.MRU);
OS_ASSERT(net_dev->rx_buff != OS_NULL);
if (os_net_device_init(net_dev) != OS_SUCCESS)
{
LOG_E(EXT_TAG, "net device init failed %s", name);
return OS_FAILURE;
}
net_dev->dev.type = OS_DEVICE_TYPE_NETIF;
if (os_device_register(&net_dev->dev, name) != OS_SUCCESS)
{
return OS_FAILURE;
}
return OS_SUCCESS;
}分步详解:
第 1 步:参数断言(无条件执行)
OS_ASSERT(net_dev != OS_NULL);
OS_ASSERT(net_dev->ops != OS_NULL);详细解释:net_dev 和 net_dev->ops 都不能为空,否则直接断言失败(编译/调试期暴露错误)。ops 必须预先填好,后续 os_net_device_init 会调用 ops->init。
第 2 步:按 MTU/MRU 分配收发缓冲(无条件执行,但可能断言失败)
net_dev->tx_buff = malloc(net_dev->info.MTU);
OS_ASSERT(net_dev->tx_buff != OS_NULL);
net_dev->rx_buff = malloc(net_dev->info.MRU);
OS_ASSERT(net_dev->rx_buff != OS_NULL);详细解释:关键:发送缓冲大小 = info.MTU、接收缓冲大小 = info.MRU,必须在注册前把 MTU/MRU 设好,否则 malloc(0) 或分配过小。分配失败断言。这就是 5.1 示例里先设 info.MTU = 1500 再注册的原因。
第 3 步:初始化设备(条件 初始化失败则返回失败)
if (os_net_device_init(net_dev) != OS_SUCCESS)
{
LOG_E(EXT_TAG, "net device init failed %s", name);
return OS_FAILURE;
}详细解释:os_net_device_init(内部)先调 os_net_protocol_lwip_init(OS_NET_PROTOCOL_LWIP 下初始化协议栈,填充 protocol_ops),再调 ops->init(硬件初始化)。任一失败返回 OS_FAILURE。
第 4 步:注册到设备框架(条件 注册失败则返回失败)
net_dev->dev.type = OS_DEVICE_TYPE_NETIF;
if (os_device_register(&net_dev->dev, name) != OS_SUCCESS)
{
return OS_FAILURE;
}
return OS_SUCCESS;
}详细解释:设置 dev.type = OS_DEVICE_TYPE_NETIF(网络接口类型),os_device_register(&net_dev->dev, name) 注册到设备框架(因 dev 是首成员,&net_dev->dev 就是设备基址)。
os_net_device_register() 条件走向总结:
os_net_device_register(net_dev, name) 条件走向总结:
├── 断言 net_dev / net_dev->ops 非空
├── tx_buff = malloc(info.MTU);断言非空
├── rx_buff = malloc(info.MRU);断言非空
├── os_net_device_init(net_dev) 初始化
│ ├── 失败 → LOG_E + return OS_FAILURE
│ └── 成功 ↓
├── dev.type = OS_DEVICE_TYPE_NETIF
├── os_device_register(&dev, name)
│ ├── 失败 → return OS_FAILURE
│ └── 成功 → return OS_SUCCESSos_net_device_unregister —— 释放收发缓冲并去初始化:
os_err_t os_net_device_unregister(struct os_net_device *net_dev)
{
if (net_dev->tx_buff)
{
free(net_dev->tx_buff);
}
if (net_dev->rx_buff)
{
free(net_dev->rx_buff);
}
return os_net_device_deinit(net_dev);
}分步详解:
第 1 步:释放收发缓冲(条件 各自非空)
if (net_dev->tx_buff)
{
free(net_dev->tx_buff);
}
if (net_dev->rx_buff)
{
free(net_dev->rx_buff);
}详细解释:释放 os_net_device_register 分配的收发缓冲,free 前判空防重复释放。
第 2 步:去初始化(无条件执行)
return os_net_device_deinit(net_dev);
}详细解释:os_net_device_deinit(内部)先调 ops->deinit(硬件去初始化),再调 os_net_protocol_lwip_deinit(协议栈去初始化),返回其结果。
3.2 数据收发
os_net_tx_data —— 发送数据(协议栈 → 硬件)
完整源码(drivers/net/net_dev.c,与项目逐字一致):
os_err_t os_net_tx_data(struct os_net_device *net_dev, uint8_t *data, int len)
{
OS_ASSERT(net_dev->ops->send != OS_NULL);
return net_dev->ops->send(net_dev, data, len);
}| 参数/返回值 | 说明 |
|---|---|
net_dev/data/len | 网络设备 / 待发数据 / 长度(应 ≤ info.MTU) |
| 返回 | ops->send 的结果(OS_SUCCESS/OS_FAILURE) |
分步详解:先断言 ops->send 非空(发送回调必须实现),然后直接透传给 ops->send(net_dev, data, len) 让硬件发送。
os_net_rx_data —— 接收数据(硬件 → 协议栈)
完整源码(drivers/net/net_dev.c,与项目逐字一致):
os_err_t os_net_rx_data(struct os_net_device *net_dev, uint8_t *data, int len)
{
OS_ASSERT(net_dev->protocol_ops->recv != OS_NULL);
return net_dev->protocol_ops->recv(net_dev, data, len);
}| 参数/返回值 | 说明 |
|---|---|
net_dev/data/len | 网络设备 / 收到的数据 / 长度 |
| 返回 | protocol_ops->recv 的结果 |
分步详解:先断言 protocol_ops->recv 非空(协议栈必须实现接收),然后透传给 protocol_ops->recv(net_dev, data, len) 把收到的包交给协议栈。
os_net_rx_notify —— 接收通知(硬件通知协议栈有新数据待处理):
os_err_t os_net_rx_notify(struct os_net_device *net_dev);说明:
os_net_rx_notify在net_dev.h中声明,用于通知协议栈有新数据(非数据透传)。其具体实现在协议栈适配层(配合 NAPI 或收包线程)。数据透传走os_net_rx_data。
关键理解 —— 发送/接收的数据流向:
- 发送:协议栈 →
os_net_tx_data→ops->send→ 硬件(单向,驱动只负责发)- 接收:硬件中断 →
os_net_rx_data→protocol_ops->recv→ 协议栈(单向,驱动只负责上报)
驱动与协议栈通过这两个 API 解耦,互不直接调用对方内部。
3.3 MAC 地址与过滤
/* 获取 MAC 地址 */
os_err_t os_net_get_macaddr(struct os_net_device *net_dev,
uint8_t *addr);
/* 设置 MAC 地址过滤器 */
os_err_t os_net_set_filter(struct os_net_device *net_dev,
uint8_t *addr, os_bool_t enable);
/* MAC 地址字符串转换 */
os_err_t os_net_get_mac_string2addr(char *string, uint8_t *addr);3.4 链路状态管理
os_net_linkchange —— 通知协议栈链路状态变化
完整源码(drivers/net/net_dev.c,与项目逐字一致):
os_err_t os_net_linkchange(struct os_net_device *net_dev, os_bool_t status)
{
if (status)
LOG_I(EXT_TAG, "net link up %s", net_dev->dev.name);
else
LOG_I(EXT_TAG, "net link down %s", net_dev->dev.name);
if (net_dev->info.linkstatus != status)
{
net_dev->info.linkstatus = status;
if ((net_dev) && (net_dev->protocol_ops) && (net_dev->protocol_ops->linkchange))
{
return net_dev->protocol_ops->linkchange(net_dev, status);
}
}
return OS_NOSYS;
}| 参数/返回值 | 说明 |
|---|---|
net_dev | 网络设备 |
status | 目标链路状态(OS_TRUE=UP / OS_FALSE=DOWN) |
| 返回 | protocol_ops->linkchange 结果;链路状态没变或未实现回调返回 OS_NOSYS |
分步详解:
第 1 步:打印链路日志(无条件执行)
if (status)
LOG_I(EXT_TAG, "net link up %s", net_dev->dev.name);
else
LOG_I(EXT_TAG, "net link down %s", net_dev->dev.name);详细解释:按 status 打印 link up/down 日志,方便调试。
第 2 步:状态变化才通知协议栈(条件 info.linkstatus != status)
if (net_dev->info.linkstatus != status)
{
net_dev->info.linkstatus = status;
if ((net_dev) && (net_dev->protocol_ops) && (net_dev->protocol_ops->linkchange))
{
return net_dev->protocol_ops->linkchange(net_dev, status);
}
}
return OS_NOSYS;
}详细解释:关键设计 —— 状态去重:只有链路状态真的变了(info.linkstatus != status)才更新 info.linkstatus 并调用 protocol_ops->linkchange 通知协议栈;若状态没变,直接返回 OS_NOSYS,避免重复通知。protocol_ops 或 linkchange 为空时也返回 OS_NOSYS。
os_net_linkchange() 条件走向总结:
os_net_linkchange(net_dev, status) 条件走向总结:
├── 按 status 打印 link up/down 日志
├── info.linkstatus != status(链路状态发生变化)
│ ├── info.linkstatus = status(更新状态)
│ ├── net_dev / protocol_ops / linkchange 都非空
│ │ └── return protocol_ops->linkchange(net_dev, status)
│ └── 任一为空 → 跳过(落到 return OS_NOSYS)
└── info.linkstatus == status(状态没变)
└── return OS_NOSYS(不重复通知)四、NAPI 轮询机制
4.1 NAPI 简介
NAPI(New API)是 Linux 内核中引入的一种网络数据包处理机制,在 OneOS 中也被采纳。它的核心思想是:
- 低负载时:使用中断方式处理数据包,响应快
- 高负载时:切换到轮询模式,减少中断开销,提高吞吐量
4.2 NAPI 数据结构
来源:drivers/net/net_dev.h(OS_USING_NET_NAPI 条件编译下)
struct napi_struct
{
int weight;
unsigned long state;
os_list_node_t list;
struct os_net_device *dev;
int (*poll)(struct napi_struct *napi, int budget);
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
weight | 轮询权重(一次最多处理多少包,默认 NAPI_POLL_WEIGHT=64) |
state | NAPI 状态位(见 4.3,用 NAPIF_STATE_* 位掩码操作) |
list | 链表节点,挂到全局轮询链表 net_poll_list |
dev | 所属网络设备 |
poll | 轮询函数指针:轮询时调用 poll(napi, weight) 处理数据包,返回本次处理包数 |
4.3 NAPI 状态
状态枚举(drivers/net/net_dev.h,与项目逐字一致,含英文注释):
enum {
NAPI_STATE_SCHED, /* Poll is scheduled */
NAPI_STATE_MISSED, /* reschedule a napi */
NAPI_STATE_DISABLE, /* Disable pending */
NAPI_STATE_NPSVC, /* Netpoll - don't dequeue from poll_list */
NAPI_STATE_HASHED, /* In NAPI hash (busy polling possible) */
NAPI_STATE_NO_BUSY_POLL,/* Do not add in napi_hash, no busy polling */
NAPI_STATE_IN_BUSY_POLL,/* sk_busy_loop() owns this NAPI */
};状态位掩码枚举(drivers/net/net_dev.h,与项目逐字一致,供 n->state 按位操作):
enum {
NAPIF_STATE_SCHED = BIT(NAPI_STATE_SCHED),
NAPIF_STATE_MISSED = BIT(NAPI_STATE_MISSED),
NAPIF_STATE_DISABLE = BIT(NAPI_STATE_DISABLE),
NAPIF_STATE_NPSVC = BIT(NAPI_STATE_NPSVC),
NAPIF_STATE_HASHED = BIT(NAPI_STATE_HASHED),
NAPIF_STATE_NO_BUSY_POLL = BIT(NAPI_STATE_NO_BUSY_POLL),
NAPIF_STATE_IN_BUSY_POLL = BIT(NAPI_STATE_IN_BUSY_POLL),
};| 状态 | NAPIF_STATE_* 位掩码 | 含义 |
|---|---|---|
SCHED | BIT(0) | 轮询已调度 |
MISSED | BIT(1) | 需重新调度 |
DISABLE | BIT(2) | 禁用挂起 |
NPSVC | BIT(3) | Netpoll 模式,不从轮询链表出队 |
HASHED | BIT(4) | 已加入 NAPI 哈希表(可 busy polling) |
NO_BUSY_POLL | BIT(5) | 不加进 napi_hash,不 busy polling |
IN_BUSY_POLL | BIT(6) | 正在 busy polling |
关键理解 —— 为什么有 NAPI_STATE_ 和 NAPIF_STATE_ 两套:
NAPI_STATE_*是位索引(0~6,表示第几位),NAPIF_STATE_*是位掩码(BIT(index),即1<<index)。n->state里存的是位掩码**,操作时用NAPIF_STATE_SCHED(如napi_schedule_prep里n->state |= NAPIF_STATE_SCHED);判断"第几位"时用NAPI_STATE_SCHED。不要混用。
4.4 NAPI API
netif_napi_add —— 注册 NAPI 实例(drivers/net/net_dev.c,与项目逐字一致):
void netif_napi_add(struct os_net_device *dev, struct napi_struct *napi,
int (*poll)(struct napi_struct *napi, int budget), int weight)
{
os_ubase_t irq_save;
napi->poll = poll;
napi->weight = weight;
napi->dev = dev;
os_spin_lock_irqsave(&net_dev_lock, &irq_save);
os_list_add(&net_poll_list, &napi->list);
os_spin_unlock_irqrestore(&net_dev_lock, irq_save);
}分步详解:把 poll 函数、weight 权重、所属设备 dev 填进 napi,加锁后把 napi->list 挂到全局轮询链表 net_poll_list(后续 net_rx_action 遍历它轮询)。
napi_schedule_prep / napi_schedule —— 调度 NAPI:
os_bool_t napi_schedule_prep(struct napi_struct *n)
{
os_ubase_t irq_save;
os_spin_lock_irqsave(&net_dev_lock, &irq_save);
n->state |= NAPIF_STATE_SCHED;
os_spin_unlock_irqrestore(&net_dev_lock, irq_save);
return OS_TRUE;
}
void __napi_schedule(struct napi_struct *n)
{
os_semaphore_post(net_dev_rx_sem);
}
void napi_schedule(struct napi_struct *n)
{
if (napi_schedule_prep(n))
__napi_schedule(n);
}分步详解:napi_schedule_prep 置 NAPI_STATE_SCHED 状态位(加锁保护)并返回 OS_TRUE。napi_schedule 若 schedule_prep 成功则 __napi_schedule——它 os_semaphore_post(net_dev_rx_sem) 唤醒 net/rx 收包任务,任务里执行 net_rx_action 真正轮询。
napi_complete_done —— 完成 NAPI 轮询:
os_bool_t napi_complete_done(struct napi_struct *n, int work_done)
{
os_ubase_t irq_save;
os_spin_lock_irqsave(&net_dev_lock, &irq_save);
n->state &= ~NAPIF_STATE_SCHED;
os_spin_unlock_irqrestore(&net_dev_lock, irq_save);
return OS_TRUE;
}分步详解:清掉 NAPI_STATE_SCHED 位(加锁保护),表示本轮轮询结束,允许下次再调度。
4.5 NAPI 使用流程
中断到达
│
├─ 禁用网卡中断
│
├─ napi_schedule_prep() ← 检查是否可以调度
│ │
│ └─ __napi_schedule() ← 将 NAPI 加入调度队列
│
├─ 轮询线程执行 poll(state, budget)
│ │
│ ├─ 处理数据包 (最多 budget 个)
│ │
│ └─ napi_complete_done(state, work_done)
│ │
│ ├─ work_done < budget: 包已处理完
│ │ └─ 重新启用网卡中断
│ │
│ └─ work_done >= budget: 预算用完
│ └─ 继续轮询4.6 为什么 NAPI 能降低中断开销?
问题背景:高吞吐网络下,每个收包都触发一次中断(os_net_rx_data),CPU 频繁进出中断、保存/恢复上下文,中断本身的开销可能超过处理包的开销,导致吞吐下降。
NAPI 的核心思想:第一个包来了先关网卡中断、唤醒轮询任务;轮询任务在 net_rx_action 里一次批量处理多个包,处理完(budget 用完或超时)才重新开中断。这样把"N 次中断 + N 次单包处理"变成"1 次中断 + 批量轮询",大幅减少中断次数。
net_rx_action 的关键机制(drivers/net/net_dev.c,与项目逐字一致的核心片段):
while (1)
{
int work, work_total = 0;
struct napi_struct *n;
struct napi_struct *tmp;
os_list_for_each_entry_safe(n, tmp, &list, struct napi_struct, list)
{
work = napi_poll(n, &repoll);
work_total += work;
budget -= work;
if (budget <= 0 || time_after_eq(os_tick_get_value(), time_limit))
{
goto out;
}
}
if (work_total == 0)
break;
}net_rx_action() 数值演算(netdev_budget = OS_NET_NAPI_BUDGET、budget_usecs 时间上限):
| 条件 | 含义 | 动作 |
|---|---|---|
budget -= work 后 budget <= 0 | 本次已处理满预算(OS_NET_NAPI_BUDGET 个包) | goto out 结束本轮,下轮再调度 |
os_tick_get_value() >= time_limit | 已到时间上限(budget_usecs 换算的 tick) | goto out,防止长时间占用 CPU |
遍历完且 work_total == 0 | 本轮没处理到任何包(无包可收) | break 结束,等下次调度 |
一句话理解 NAPI:"关中断 → 批量轮询收包 → 预算/时间到了 → 开中断再等下一个突发"。低负载时包少,很快
break(中断式体验);高负载时靠批量轮询摊薄中断开销。这是吞吐与延迟的折中:NAPI 让 CPU 在突发流量下集中处理,避免频繁进出中断。
五、实现示例
5.1 以太网设备注册
#include <net_dev.h>
#include <driver.h>
/* 以太网硬件操作实现 */
static os_err_t eth_init(struct os_net_device *net_dev)
{
/* 初始化以太网 MAC 控制器 */
ETH_MAC_Init();
ETH_DMA_Init();
return OS_SUCCESS;
}
static os_err_t eth_send(struct os_net_device *net_dev,
uint8_t *data, int len)
{
/* 通过 DMA 发送数据包 */
ETH_DMA_Send(data, len);
return OS_SUCCESS;
}
static os_err_t eth_get_mac(struct os_net_device *net_dev,
uint8_t *addr)
{
/* 读取芯片内置 MAC 地址 */
ETH_GetMACAddr(addr);
return OS_SUCCESS;
}
static const struct os_net_device_ops eth_ops = {
.init = eth_init,
.deinit = OS_NULL,
.send = eth_send,
.get_mac = eth_get_mac,
.set_filter = OS_NULL,
};
/* 网络设备实例 */
static struct os_net_device eth_device;
os_err_t eth_device_register(void)
{
os_err_t ret;
uint8_t mac[6];
/* 初始化设备信息(MTU 决定收发缓冲大小,须先设好) */
eth_device.info.MTU = 1500;
eth_device.info.MRU = 1500;
eth_device.info.intf_type = net_dev_intf_ether;
eth_device.info.linkstatus = OS_FALSE;
eth_device.ops = ð_ops;
/* 注册到 Net 设备框架 */
ret = os_net_device_register(ð_device, "eth0");
if (ret != OS_SUCCESS)
{
os_kprintf("eth0 register failed!\n");
return ret;
}
/* 获取 MAC 地址并检查返回值 */
ret = os_net_get_macaddr(ð_device, mac);
if (ret != OS_SUCCESS)
{
os_kprintf("eth0 get mac failed, ret=%d\n", ret);
return ret;
}
os_kprintf("eth0 MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return OS_SUCCESS;
}5.2 接收数据上报
/* 以太网中断处理函数 */
void ETH_IRQHandler(void)
{
os_ubase_t level = os_irq_lock();
if (ETH_DMA_RxDone())
{
/* 从 DMA 描述符获取数据 */
uint8_t *data;
int len = ETH_DMA_GetRxPacket(&data);
if (len > 0)
{
/* 上报给协议栈(检查返回值) */
os_err_t ret = os_net_rx_data(ð_device, data, len);
if (ret != OS_SUCCESS)
{
os_kprintf("rx data to stack failed, ret=%d\n", ret);
}
}
}
os_irq_unlock(level);
}5.3 链路状态管理
/* 网线插入检测 */
void eth_link_up(void)
{
eth_device.info.linkstatus = OS_TRUE;
os_net_linkchange(ð_device, OS_TRUE);
os_kprintf("eth0: link up\n");
}
void eth_link_down(void)
{
eth_device.info.linkstatus = OS_FALSE;
os_net_linkchange(ð_device, OS_FALSE);
os_kprintf("eth0: link down\n");
}六、数据收发流程
6.1 发送流程
协议栈调用 os_net_tx_data()
│
├─ 调用 net_dev->ops->send(data, len)
│ │
│ └─ 硬件驱动将数据写入发送队列/DMA
│
└─ 硬件完成发送(中断或轮询确认)6.2 接收流程
硬件中断到达
│
├─ 驱动从硬件获取数据包
│
├─ 调用 os_net_rx_data(net_dev, data, len)
│ │
│ └─ 调用 net_dev->protocol_ops->recv(data, len)
│ │
│ └─ 协议栈处理数据包 (IP/TCP/UDP...)
│
└─ 或使用 NAPI 轮询模式批量处理七、Net 设备的适配(接入新网卡)
7.1 先认识适配层:Net 抽象层是怎么"长"到网卡上的
drivers/net/net_dev.c 是跨平台统一的框架层(设备注册、收发透传、链路管理、NAPI 调度),不碰具体网卡寄存器。真正操作网卡的是各厂商 HAL 驱动(如 ST drv_enet.c、WLAN 驱动)。换网卡就是实现一套 os_net_device_ops 并注册。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 协议栈(LwIP / OneOS-Net) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Net 抽象层(drivers/net/net_dev.c,通用不改) │
│ os_net_device_register / tx_data / rx_data / linkchange │
│ NAPI 调度(net_rx_action) │
└───────────────┬──────────────────────────┬────────────────┘
│ ①实现 os_net_device_ops │ ②中断里上报
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 厂商网卡驱动(drv_enet.c / wlan 驱动)★需实现★ │
│ init/deinit/send/get_mac/set_filter │
│ 收包中断 → os_net_rx_data;链路变化 → os_net_linkchange │
└───────────────┬──────────────────────────┬────────────────┘
│ HAL 库(HAL_ETH_*) │ 中断服务
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(以太网 MAC/PHY / Wi-Fi 芯片) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入新网卡只需做:① 实现 os_net_device_ops(init/deinit/send/get_mac/set_filter,send 必实现);② 在收包中断调 os_net_rx_data、链路变化调 os_net_linkchange;③ 用 os_net_device_register 注册。协议栈适配(protocol_ops)由框架/LwIP 适配层填充,驱动不用管。
7.2 以以太网为例,看一整套适配
① 实现 os_net_device_ops(5.1 示例已展示,核心是 send):
static const struct os_net_device_ops eth_ops = {
.init = eth_init, /* 初始化 MAC/DMA */
.deinit = OS_NULL, /* 可空 */
.send = eth_send, /* 经 DMA 发送(必实现) */
.get_mac = eth_get_mac, /* 读 MAC 地址 */
.set_filter = OS_NULL, /* 可空 */
};② 收包中断 → 上报协议栈(5.2 示例):DMA 收包完成后调 os_net_rx_data(ð_device, data, len),框架透传 protocol_ops->recv 给协议栈。
③ 链路变化 → 通知协议栈(5.3 示例):网线插入/拔出调 os_net_linkchange(ð_device, OS_TRUE/OS_FALSE),框架更新 info.linkstatus 并通知协议栈。
④ 注册设备(5.1 示例):填 info.MTU/MRU/intf_type/linkstatus/ops 后 os_net_device_register(ð_device, "eth0")。
7.3 接口契约:接入网卡需要实现什么
| 层 | 接口/回调 | 职责 | 示例 |
|---|---|---|---|
| 操作回调 | ops.init | 初始化 MAC/DMA(可空) | eth_init |
| 操作回调 | ops.send | 发送数据(必实现) | eth_send |
| 操作回调 | ops.get_mac | 读 MAC(可空) | eth_get_mac |
| 上报 | os_net_rx_data | 收包中断上报协议栈 | 5.2 |
| 上报 | os_net_linkchange | 链路变化通知 | 5.3 |
| 注册 | os_net_device_register | 注册设备 + 分配收发缓冲 | 5.1 |
| 协议栈适配 | protocol_ops | 由框架/LwIP 适配层填充 | 框架自动 |
7.4 适配新网卡的标准流程
第 1 步:确认网卡能力——接口类型(ETH/WLAN/BT)、MTU/MRU、收发方式(DMA/PIO)。
第 2 步:新建厂商驱动——实现 os_net_device_ops(init/send/get_mac,send 必实现)。
第 3 步:写收发中断——收包完成调 os_net_rx_data,发送完成可设标志;链路变化调 os_net_linkchange。
第 4 步:注册设备——填 info(MTU/MRU/intf_type/linkstatus)、ops,os_net_device_register 注册。
第 5 步:测试——用 ping 或协议栈 socket 收发验证;测链路插拔确认 linkchange。
7.5 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
ops.send 实现了? | 未实现则注册/发送时 OS_ASSERT 失败 |
| MTU/MRU 设了? | 未设则 malloc(0) 分配收发缓冲,可能断言失败 |
收包后调了 os_net_rx_data? | 否则协议栈收不到包 |
链路变化调了 os_net_linkchange? | 否则协议栈不知道网线插拔,IP 不会重新获取 |
驱动有没有直接操作 protocol_ops? | 应只调 os_net_rx_data/os_net_linkchange,不碰协议栈内部 |
八、注意事项
- 线程安全:数据收发函数可能在不同上下文(中断、任务)中调用,需保证
ops实现的线程安全 - 缓冲区管理:
tx_buff和rx_buff所有权在驱动和协议栈之间传递,需明确谁负责释放 - MTU 限制:发送数据不应超过
info.MTU,驱动应检查数据长度 - NAPI 权重:
weight需根据实际吞吐量调整,太小影响吞吐,太大会占用 CPU 时间 - 协议栈解耦:
protocol_ops由协议栈模块(如 LwIP 适配层)设置,驱动不应直接操作
📝 本节小结
核心要点回顾
本节围绕 Net 网络设备展开,核心要点如下:
- 统一抽象:将 Ethernet / Wi-Fi / Bluetooth 等不同网络接口统一为
os_net_device,继承os_device_t复用设备框架。 - 双操作接口分离:
ops(硬件操作:init/send/get_mac/set_filter)与protocol_ops(协议栈操作:recv/linkchange)解耦,驱动不直接操作协议栈。 - 数据收发:
os_net_tx_data走ops->send发送;os_net_rx_data/os_net_rx_notify把收到的包交给protocol_ops->recv。 - 注册分配缓冲:
os_net_device_register按info.MTU/MRU分配tx_buff/rx_buff,再初始化设备、注册到框架。 - 链路管理:
os_net_linkchange更新info.linkstatus并通知协议栈,状态没变则不重复通知。 - NAPI 轮询:低负载用中断、高负载切轮询;
netif_napi_add/napi_schedule/napi_complete_done管理调度;n->state存位掩码NAPIF_STATE_*。 - 硬件适配:接入新网卡 = 实现
os_net_device_ops(send 必实现)+ 收包中断调os_net_rx_data+ 链路变化调os_net_linkchange+ 注册,框架层不用改。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 发送超过 MTU | 驱动应检查 len <= info.MTU,超长包会被丢弃或出错 |
| 注册前未设 MTU/MRU | os_net_device_register 按 info.MTU/MRU malloc,未设则 malloc(0) 或断言失败 |
| 缓冲区所有权不清 | tx_buff/rx_buff 在驱动与协议栈间传递,需明确谁释放,避免内存泄漏 |
| 中断里做耗时处理 | 收包中断应尽快上报(os_net_rx_data)或启用 NAPI,避免阻塞中断 |
| NAPI 权重不当 | weight 太小降吞吐、太大占 CPU,需按实际流量调整 |
| 混淆 NAPI_STATE 与 NAPIF_STATE | NAPI_STATE_* 是位索引、NAPIF_STATE_* 是位掩码(BIT(index));n->state 存位掩码 |
| 驱动直接改协议栈 | protocol_ops 由协议栈适配层设置,驱动只调用 os_net_rx_data 上报即可 |
| 链路状态未通知 | 网线/信号变化必须调 os_net_linkchange,否则协议栈不知道链路状态 |
下一步
掌握了 Net 网络设备后,接下来学习 Audio 音频设备:
- 5.21 Audio设备:了解 OneOS 的 PCM 播放器框架、内存池队列与 I2S 硬件抽象。