目录
4.1 netif结构体
netif
结构体是 lwIP抽象和管理网络接口的核心数据结构,其设计初衷是为了解决不同网络硬件(如以太网、Wi-Fi、LoRa 等)与协议栈的适配问题,通过统一的接口封装实现硬件与协议栈的解耦。
netif
结构体整合了网络接口的关键信息:包括硬件标识(MAC 地址、接口名称)、网络配置(IP 地址、子网掩码、网关)、数据处理函数(接收数据的input
、发送数据的output
等回调)、运行状态(是否启用、链路是否连接等标志)及扩展字段(硬件私有数据指针、状态变化回调)。使用netif
结构体,协议栈无需关心底层硬件细节,可通过统一的方法管理各类网络接口,简化了多接口场景下的开发复杂度,同时让硬件驱动专注于具体的收发实现,大幅提升了网络模块的可移植性和扩展性。
struct netif {
// 链表节点:用于将多个网络接口连接成链表
struct netif *next;
// 网络接口名称(通常为2个字符,如"en"表示以太网)
const char *name;
// 接口索引(用于区分同一类型的多个接口,如en0、en1)
uint8_t num;
// 网络层地址(IP地址、子网掩码、网关)
ip_addr_t ip_addr; // 接口IP地址
ip_addr_t netmask; // 子网掩码
ip_addr_t gw; // 网关地址
// 硬件(链路层)相关操作函数
netif_input_fn input; // 从链路层接收数据后,提交到网络层的回调函数
netif_output_fn output; // 从网络层发送数据到链路层的函数(直接发送)
netif_linkoutput_fn linkoutput; // 用于ARP协议的链路层发送函数
// 接口状态标志(通过位运算组合)
uint8_t flags; // 如NETIF_FLAG_UP(接口已启用)、NETIF_FLAG_LINK_UP(链路已连接)等
// 链路层地址信息(通常为MAC地址)
struct eth_addr hwaddr; // 硬件地址(6字节MAC地址)
uint8_t hwaddr_len; // 硬件地址长度(以太网为6字节)
// 接口最大传输单元(MTU),以太网通常为1500字节
uint16_t mtu;
// 接口类型(如NETIF_TYPE_ETHERNET、NETIF_TYPE_WIFI等)
uint8_t type;
// 统计信息(可选,用于流量监控)
struct netif_stats stats; // 包含发送/接收的字节数、数据包数、错误数等
// 用户自定义数据指针(可扩展接口功能,如绑定硬件私有数据)
void *state;
// 链路状态变化回调函数(如链路连接/断开时触发)
netif_status_callback_fn status_callback;
// 地址变化回调函数(如IP地址修改时触发)
netif_status_callback_fn link_callback;
// 多播相关配置(用于组播功能)
struct ip_mreq multicast_mac_filter[NETIF_MAX_MULTICAST_FILTERS];
uint16_t multicast_mac_filter_cnt;
};
为什么是在IP 层进行分片处理?
因为链路层不提供任何的差错处理机制,如果在网卡中接收的数据包不满足网卡自身的属性,那么网卡可能就会直接丢弃该数据包,也可能在底层进行分包发送,但是这种分包在 IP 层看来是不可接受的,因为它打乱了数据的结构,所以只能由 IP层进行分片处理。
4.2 netif使用
那么 netif 具体该如何使用呢?其实使用还是非常简单的。首先我们需要根据我们的网卡定义一个netif 结构体变量 struct netif gnetif,我们首先要把网卡挂载到netif_list 链表上才能使用,因为LwIP 是通过链表来管理所有的网卡,所有第一步是通过netif_add()函数将我们的网卡挂载到netif_list 链表上,netif_add()函数具体见代码清单。
/**
* @ingroup netif
* 向lwIP的网络接口列表中添加一个网络接口
*
* @param netif 预分配的网络接口结构体
* @param ipaddr 新接口的IP地址
* @param netmask 新接口的子网掩码
* @param gw 新接口的默认网关IP地址
* @param state 传递给新接口的不透明数据(驱动私有数据)
* @param init 用于初始化接口的回调函数
* @param input 用于将入站数据包上传到协议栈的回调函数
*
* @return 成功返回netif,失败返回NULL
*/
struct netif *
netif_add(struct netif *netif,
#if LWIP_IPV4
const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw,
#endif /* LWIP_IPV4 */
void *state, netif_init_fn init, netif_input_fn input)
{
#if LWIP_IPV6
s8_t i; // 用于IPv6地址数组的循环索引
#endif
// 断言检查核心锁是否已获取(确保线程安全)
LWIP_ASSERT_CORE_LOCKED();
#if LWIP_SINGLE_NETIF
// 如果配置为单网络接口模式,检查是否已存在默认接口
if (netif_default != NULL) {
LWIP_ASSERT("single netif already set", 0); // 断言失败,单接口已被设置
return NULL;
}
#endif
// 错误检查:网络接口结构体不能为空
LWIP_ERROR("netif_add: invalid netif", netif != NULL, return NULL);
// 错误检查:初始化回调函数不能为空
LWIP_ERROR("netif_add: No init function given", init != NULL, return NULL);
#if LWIP_IPV4
// 如果未提供IP地址相关参数,使用默认的"任意地址"
if (ipaddr == NULL) {
ipaddr = ip_2_ip4(IP4_ADDR_ANY);
}
if (netmask == NULL) {
netmask = ip_2_ip4(IP4_ADDR_ANY);
}
if (gw == NULL) {
gw = ip_2_ip4(IP4_ADDR_ANY);
}
/* 重置新接口的配置状态 */
// 初始化IPv4地址相关字段为0
ip_addr_set_zero_ip4(&netif->ip_addr);
ip_addr_set_zero_ip4(&netif->netmask);
ip_addr_set_zero_ip4(&netif->gw);
// 设置默认的IPv4输出函数(空实现)
netif->output = netif_null_output_ip4;
#endif /* LWIP_IPV4 */
#if LWIP_IPV6
// 初始化IPv6地址数组
for (i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) {
ip_addr_set_zero_ip6(&netif->ip6_addr[i]); // 地址清零
netif->ip6_addr_state[i] = IP6_ADDR_INVALID; // 标记地址为无效
#if LWIP_IPV6_ADDRESS_LIFETIMES
// 设置地址生存时间为静态(默认)
netif->ip6_addr_valid_life[i] = IP6_ADDR_LIFE_STATIC;
netif->ip6_addr_pref_life[i] = IP6_ADDR_LIFE_STATIC;
#endif /* LWIP_IPV6_ADDRESS_LIFETIMES */
}
// 设置默认的IPv6输出函数(空实现)
netif->output_ip6 = netif_null_output_ip6;
#endif /* LWIP_IPV6 */
// 启用所有类型的校验和检查
NETIF_SET_CHECKSUM_CTRL(netif, NETIF_CHECKSUM_ENABLE_ALL);
netif->mtu = 0; // 初始化MTU(最大传输单元)为0,由驱动后续设置
netif->flags = 0; // 初始化接口标志为0
#ifdef netif_get_client_data
// 初始化客户端数据区(清零)
memset(netif->client_data, 0, sizeof(netif->client_data));
#endif /* LWIP_NUM_NETIF_CLIENT_DATA */
#if LWIP_IPV6
#if LWIP_IPV6_AUTOCONFIG
// 默认禁用IPv6地址自动配置
netif->ip6_autoconfig_enabled = 0;
#endif /* LWIP_IPV6_AUTOCONFIG */
// 重置该接口的IPv6邻居发现协议状态
nd6_restart_netif(netif);
#endif /* LWIP_IPV6 */
#if LWIP_NETIF_STATUS_CALLBACK
// 初始化状态回调函数为NULL
netif->status_callback = NULL;
#endif /* LWIP_NETIF_STATUS_CALLBACK */
#if LWIP_NETIF_LINK_CALLBACK
// 初始化链路回调函数为NULL
netif->link_callback = NULL;
#endif /* LWIP_NETIF_LINK_CALLBACK */
#if LWIP_IGMP
// 初始化IGMP(互联网组管理协议)的MAC过滤函数为NULL
netif->igmp_mac_filter = NULL;
#endif /* LWIP_IGMP */
#if LWIP_IPV6 && LWIP_IPV6_MLD
// 初始化MLD(多播监听发现)的MAC过滤函数为NULL
netif->mld_mac_filter = NULL;
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */
#if ENABLE_LOOPBACK
// 初始化环回数据包队列的首尾指针
netif->loop_first = NULL;
netif->loop_last = NULL;
#endif /* ENABLE_LOOPBACK */
/* 保存网络接口的特定状态信息 */
netif->state = state; // 保存驱动私有数据
netif->num = netif_num; // 分配临时的接口编号
netif->input = input; // 设置输入数据包处理函数
// 重置网络接口的提示信息(用于路径MTU发现等)
NETIF_RESET_HINTS(netif);
#if ENABLE_LOOPBACK && LWIP_LOOPBACK_MAX_PBUFS
// 初始化环回数据包计数
netif->loop_cnt_current = 0;
#endif /* ENABLE_LOOPBACK && LWIP_LOOPBACK_MAX_PBUFS */
#if LWIP_IPV4
// 设置IPv4地址、子网掩码和网关
netif_set_addr(netif, ipaddr, netmask, gw);
#endif /* LWIP_IPV4 */
/* 调用用户指定的网络接口初始化函数 */
if (init(netif) != ERR_OK) {
return NULL; // 初始化失败,返回NULL
}
#if LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES
// 初始化IPv6的MTU为驱动设置的值(可通过路由通告更新)
netif->mtu6 = netif->mtu;
#endif /* LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES */
#if !LWIP_SINGLE_NETIF
/* 分配一个唯一的接口编号(范围0..254),使(num+1)可以作为u8_t类型的接口索引
假设新接口尚未添加到列表中
此算法为O(n^2),但对于lwIP来说足够高效
*/
{
struct netif *netif2;
int num_netifs;
do {
// 编号循环(0-254)
if (netif->num == 255) {
netif->num = 0;
}
num_netifs = 0;
// 检查当前编号是否已被其他接口使用
for (netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) {
LWIP_ASSERT("netif already added", netif2 != netif); // 确保未重复添加
num_netifs++;
// 断言检查接口数量不超过255(编号限制)
LWIP_ASSERT("too many netifs, max. supported number is 255", num_netifs <= 255);
// 如果编号已存在,递增编号并重新检查
if (netif2->num == netif->num) {
netif->num++;
break;
}
}
} while (netif2 != NULL); // 直到找到未使用的编号
}
// 更新下一个可用的接口编号
if (netif->num == 254) {
netif_num = 0;
} else {
netif_num = (u8_t)(netif->num + 1);
}
/* 将此接口添加到接口列表 */
netif->next = netif_list; // 新接口的next指向当前列表头部
netif_list = netif; // 列表头部更新为新接口
#endif /* !LWIP_SINGLE_NETIF */
// 通知MIB2(管理信息库)添加了新接口
mib2_netif_added(netif);
#if LWIP_IGMP
/* 启动IGMP处理(如果接口启用了IGMP标志) */
if (netif->flags & NETIF_FLAG_IGMP) {
igmp_start(netif);
}
#endif /* LWIP_IGMP */
// 调试信息:打印添加的接口信息
LWIP_DEBUGF(NETIF_DEBUG, ("netif: added interface %c%c IP",
netif->name[0], netif->name[1]));
#if LWIP_IPV4
LWIP_DEBUGF(NETIF_DEBUG, (" addr "));
ip4_addr_debug_print(NETIF_DEBUG, ipaddr);
LWIP_DEBUGF(NETIF_DEBUG, (" netmask "));
ip4_addr_debug_print(NETIF_DEBUG, netmask);
LWIP_DEBUGF(NETIF_DEBUG, (" gw "));
ip4_addr_debug_print(NETIF_DEBUG, gw);
#endif /* LWIP_IPV4 */
LWIP_DEBUGF(NETIF_DEBUG, ("\n"));
// 调用扩展回调函数,通知接口已添加
netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL);
return netif; // 返回添加的网络接口
}
挂载网卡代码如下:
// 定义IP地址的各个字节
#define IP_ADDR0 192
#define IP_ADDR1 168
#define IP_ADDR2 1
#define IP_ADDR3 122
// 定义子网掩码的各个字节
#define NETMASK_ADDR0 255
#define NETMASK_ADDR1 255
#define NETMASK_ADDR2 255
#define NETMASK_ADDR3 0
// 定义网关地址的各个字节
#define GW_ADDR0 192
#define GW_ADDR1 168
#define GW_ADDR2 1
#define GW_ADDR3 1
// 声明一个netif结构体变量,用于表示网络接口
struct netif gnetif;
// 声明三个ip4_addr_t类型的变量,分别用于存储IP地址、子网掩码和网关地址
ip4_addr_t ipaddr;
ip4_addr_t netmask;
ip4_addr_t gw;
// 声明三个uint8_t类型的数组,但在提供的代码段中未使用
uint8_t IP_ADDRESS[4];
uint8_t NETMASK_ADDRESS[4];
uint8_t GATEWAY_ADDRESS[4];
// TCPIP_Init函数,用于初始化TCP/IP网络
void TCPIP_Init(void)
{
// 初始化LwIP栈
tcpip_init(NULL, NULL);
// 根据是否定义了USE_DHCP宏来设置IP地址、子网掩码和网关
#ifdef USE_DHCP
// 如果定义了USE_DHCP,则将IP地址、子网掩码和网关设置为零地址,以使用DHCP自动获取
ip_addr_set_zero_ip4(&ipaddr);
ip_addr_set_zero_ip4(&netmask);
ip_addr_set_zero_ip4(&gw);
#else
// 如果没有定义USE_DHCP,则使用宏定义的地址手动设置IP地址、子网掩码和网关
IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1, NETMASK_ADDR2, NETMASK_ADDR3);
IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
#endif /* USE_DHCP */
// 将网络接口添加到LwIP栈中,并配置其IP地址、子网掩码、网关等
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input);
// 将当前网络接口设置为默认网络接口
netif_set_default(&gnetif);
// 检查网络接口链路状态
if (netif_is_link_up(&gnetif))
{
// 如果链路已建立,则将网络接口设置为“活动”状态
netif_set_up(&gnetif);
}
else
{
// 如果链路未建立,则将网络接口设置为“非活动”状态
netif_set_down(&gnetif);
}
}
挂载网卡的过程是非常简单的,如果一个设备当前是还没有网卡的,当调用 netif_add()函数挂载网卡后,其过程如图所示,当设备需要挂载多个网卡的时候,就多次调用netif_add()函数即可,新挂载的网卡会在链表的最前面。
图 挂载网卡
lwip为什么使用链表而不使用数组管理netif?
链表支持运行时动态增删接口、无需预分配固定内存、不依赖连续地址空间,且接口数量通常极少(1~5个),O(n)遍历开销可忽略;而数组需要预先确定大小、浪费内存或限制扩展、增删效率低
4.3 协议栈与网络接口初始化
初始化是协议栈工作的前提,主要完成 lwIP 内核初始化、网络接口(如以太网)注册与硬件初始化。核心函数调用流程:
系统启动 → lwip_init() → netif_add() → ethernetif_init() → low_level_init()
lwip_init():初始化 lwIP 协议栈内核,是所有操作的起点。用于初始化内存池(pbuf
、tcp_pcb
等)、协议栈核心模块(IP、ARP、UDP、TCP 等)、网络接口列表(netif_list
)等。
netif_add():向协议栈注册一个网络接口(如以太网、WiFi),将其加入全局接口列表(netif_list
)。需传入接口初始化回调(ethernetif_init
)、数据包输入回调(tcpip_input
或 netif_input
)等。
ethernetif_init():以太网接口的初始化入口(驱动与协议栈的对接层),由 netif_add()
调用。用于配置 netif
结构体(如接口名称、MTU、支持的标志 NETIF_FLAG_ETHARP
(支持 ARP)、NETIF_FLAG_BROADCAST
(支持广播)等);绑定链路层发送函数(netif->linkoutput = low_level_output
)。并调用硬件底层初始化函数 low_level_init()
。
low_level_init():直接操作硬件的底层初始化(用户需根据硬件实现)。初始化以太网 MAC 控制器(如时钟、寄存器配置);设置本地 MAC 地址(写入 netif->hwaddr
);配置硬件中断(接收 / 发送中断);使能硬件的接收 / 发送功能。
netif_set_default()(可选):将某个网络接口设为默认接口(用于无特定路由的数据包发送)。
4.4 发送流程
发送流程是数据从应用层(如 UDP/TCP 应用)经过协议栈逐层处理,最终通过硬件发送到物理链路的过程。以 UDP 发送 IPv4 数据包 为例,核心流程如下:
应用层 sendto() → udp_send() → ip_output() → etharp_output() → low_level_output() → 硬件发送
应用层sendto():应用层发送数据的入口(用户调用),传入目标 IP、端口和数据。通常是对 lwIP 提供的 udp_sendto()
等函数的封装。
传输层udp_send() / udp_sendto():UDP 协议处理,封装 UDP 头部(源端口、目标端口、长度、校验和)。进一步将封装后的数据(含 UDP 头部)传递给网络层的 ip_output()
。
网络层ip_output():IP 协议处理,封装 IP 头部(版本、长度、TTL、协议类型、源 / 目标 IP 等)。用于选择输出接口(通过路由表或默认接口);若数据包超过 MTU,进行分片(IP 层分片);进一步调用链路层发送函数(netif->output
,通常绑定为 etharp_output
)。
链路层etharp_output():处理以太网链路层逻辑,通过 ARP 协议获取目标 IP 对应的 MAC 地址。若 ARP 缓存中有目标 MAC,直接封装以太网帧头;若 ARP 缓存中无目标 MAC,发送 ARP 请求,待收到回复后重发数据。进一步调用硬件发送函数(netif->linkoutput
,即 low_level_output
)。
硬件驱动层low_level_output():将协议栈传递的 pbuf
数据通过硬件发送。从 pbuf
链式结构中提取数据拼接成连续的以太网帧,并将数据写入硬件发送缓冲区进行发送。
4.5 接受流程
接收流程是硬件收到数据后,经协议栈逐层解析,最终传递到应用层的过程。以 以太网接收 IPv4 UDP 数据包 为例,核心流程如下:
硬件接收中断 → ethernetif_input() → low_level_input() → netif_input() → ip_input() → udp_input() → 应用层回调
硬件层:硬件(如以太网控制器)检测到新数据帧到达接收缓冲区时,会自动触发中断,这是数据接收的起点。中断服务程序(ISR)随之响应,通过置位接收标志在软件层面标记有新数据待处理,并清除硬件中断标志以避免中断重复触发,之后通常会调用ethernetif_input()
函数,将数据处理交接给驱动与协议栈对接层。
驱动与协议栈对接层ethernetif_input():ethernetif_input()
作为以太网接收处理的入口函数,是硬件驱动与网络协议栈之间的桥梁,主要负责协调硬件数据读取与协议栈上传。它会先过滤无效帧,比如剔除 CRC 错误、长度错误以及目标 MAC 地址既不匹配本机也非广播地址的帧,然后调用硬件驱动层的low_level_input()
函数,从硬件接收缓冲区读取有效数据并将其封装为 lwIP 协议栈可处理的pbuf
结构体。
硬件驱动层low_level_input():low_level_input()
的作用是直接操作硬件,完成数据从硬件缓冲区到软件数据结构的转换。它会从硬件接收缓冲区读取包括以太网帧头、payload 等在内的原始数据,然后按照 lwIP 协议栈的要求,将这些原始数据封装为包含数据指针、长度等信息的pbuf
结构体,以便协议栈后续处理,生成的pbuf
结构体会传递给网络接口层的netif_input()
函数。
网络接口层netif_input():netif_input()
的主要功能是根据以太网帧类型,将接收的pbuf
分发到对应的网络层协议。它会解析以太网帧头部的类型字段(EtherType),以此识别帧承载的上层协议,比如 0x0800 表示封装的是 IP 协议数据,0x0806 表示封装的是 ARP 协议数据,之后调用对应协议的处理函数,如 IP 协议调用ip_input()
,ARP 协议调用etharp_input()
。
网络层ip_input():ip_input()
负责解析 IP 头部,处理 IP 层逻辑并将数据传递到传输层。它会解析 IP 头部的版本、首部长度、协议类型、源 / 目的 IP 地址等信息,若数据包是 IP 分片,会将分片重组为完整的数据包,然后根据 IP 头部的协议类型字段识别传输层协议,如 0x06 对应 TCP 协议,调用tcp_input()
处理,0x11 对应 UDP 协议,调用udp_input()
处理,最终将重组后的完整数据传递到对应的传输层处理函数。
传输层udp_input():udp_input()
的作用是解析 UDP 头部,定位目标应用程序并传递数据。它会解析 UDP 头部的源端口、目标端口、长度、校验和等信息,然后根据目标端口在系统维护的 UDP 控制块(udp_pcb
,记录端口与应用程序的绑定关系)中查找对应的条目,找到匹配的udp_pcb
后,将 UDP 数据段(payload)传递到应用层注册的回调函数。
应用层:应用层回调函数是数据处理的终点,由用户实现,主要负责处理接收的数据,包括解析 payload、执行相关的业务逻辑处理等,完成数据从硬件到应用层的整个处理流程。