LwIP入门实战 — 4 LwIP的网络接口管理

发布于:2025-09-10 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

4.1 netif结构体

4.2 netif使用

4.3 协议栈与网络接口初始化

4.4 发送流程

4.5 接受流程


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, &ethernetif_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 协议栈内核,是所有操作的起点。用于初始化内存池(pbuftcp_pcb 等)、协议栈核心模块(IP、ARP、UDP、TCP 等)、网络接口列表(netif_list)等。

netif_add():向协议栈注册一个网络接口(如以太网、WiFi),将其加入全局接口列表(netif_list)。需传入接口初始化回调(ethernetif_init)、数据包输入回调(tcpip_inputnetif_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、执行相关的业务逻辑处理等,完成数据从硬件到应用层的整个处理流程。


网站公告

今日签到

点亮在社区的每一天
去签到