前言
主要分析网络接口概念、网卡数据结构、网络接口、环回接口实现等等。
参考:
- 本文:https://www.cnblogs.com/lizhuming/p/16642648.html
- 李柱明笔记汇总:https://www.cnblogs.com/lizhuming/p/16557045.html
6.1 概念引入
网络接口(以太网接口)是硬件接口(网络接口又可以称之为网卡)。
LWIP 是软件那么而怎样让硬件和软件无缝连接起来呢?
而且网卡又多种多样,怎样才能让 LWIP 使用同样的软件兼容不同的硬件平台?
LWIP 中使用了一个netif
结构体来描述网卡但是网卡是直接和硬件平台打交道的:
-
用户提供最底层接口函数。
-
LWIP 提供统一的 API。
-
举例:
- 收:如网卡的初始化和网卡的收发数据,当 LWIP 底层得到数据之后,才会传入到内核中去处理。
- 发:LWIP 内核需要发送数据包的时候,也需要调用网卡的发送函数。
-
LWIP 中的 etherneif.c 文件的函数通常为硬件打交道的底层函数,当有数据需要通过网卡接收或者发送数据的时候就会被调用,通过 LWIP 的协议栈的内部进行处理后,从应用层就能得到数据或者可以发送数据。
小总结:
简单来说,netif 是 LWIP 抽象出来的网卡,LWIP 协议栈可以使用多种不同接口,而 etherneif.c 文件则提供了以太网网卡 netif 的抽象,每个网卡有不同的实现方式,每户只需要修改 ethernetif.c 文件即可。
小笔记:
-
网卡就是一个水管接口,把上层tcpip协议栈和底层数据链路对接起来,使其数据流通。
- 收:底层数据链路的数据,经过网卡,网卡把这些数据解析好,然后格式化为上层协议栈需要的数据格式,再把这些数据交给上层协议栈。
- 发:上层协议栈的数据,经过网卡,网卡把这些数据解析好,然后格式化为底层数据链路需要的数据格式,再把这些数据交给底层让其发出去。
-
网卡底层自由:
- 由于网络接口需要对接的上层包含了链路层的以太网帧处理
ethnet_input()
和ethnet_output()
。 - 所以网络接口底层对接的可以直接是自由数据,包括UART、SPI、以太网设备、GPRS、其它线程接过来的数据等等任何数据流。
- 由于网络接口需要对接的上层包含了链路层的以太网帧处理
6.2 网络接口层数据概念流图
图源自李柱明
6.3 网卡收包程序流图
图源自李柱明
6.4 网卡数据结构
6.4.1 struct netif源码
/** Generic data structure used for all lwIP network interfaces. * The following fields should be filled in by the initialization * function for the device driver: hwaddr_len, hwaddr[], mtu, flags */ struct netif {
6.4.2 字段分析
6.4.2.1 网卡链表
/** pointer to next in linked list */ struct netif *next;
LWIP 使用链表来统一管理同一设备的多个网卡。
netif.c 文件中定义两个全局指针 struct netif *netif_list
和 struct netif *netif_default
netif_list
就是网卡链表指针,指向网卡链表的首节点(第一个网卡)。netif_default
默认网卡。
6.4.2.2 网络 IP
ip_addr
:网络中的 IP 地址。
netmask
:子网掩码。
gw
:网关地址。
6.4.2.3 接收数据函数input()
/** This function is called by the network device driver * to pass a packet up the TCP/IP stack. */ netif_input_fn input;
该函数为网卡北向出口函数,是把底层数据包往协议栈送的,一般是ethernet_input()
,送入arp协议处理,再往上送。
6.4.2.4 网络IP层发送数据函数output()
函数由 IP 层调用,在接口上发送数据包。用户需要编写该函数并使 output 指向它。
通这个函数的处理步骤是首先解析硬件地址,然后发送数据包。
对于以太网物理层,该函数通常是 etharp_output()
,参数是 pbuf
,netif
,和 ip_addr
类型。
注意:其中 ipaddr
代表要将数据包发送到的地址,但不一定数据包最终到达的 ip 地址。比如要发送 ip 数据报到一个并不在本网络的主机上,该数据包要被发送到一个路由器上,这里的 ipaddr 就是路由器的 ip 地址。
6.4.2.5 链路层发送函数linkoutput()
/** This function is called by ethernet_output() when it wants * to send a packet on the interface. This function outputs * the pbuf as-is on the link medium. */ netif_linkoutput_fn linkoutput;
该函数和 output
类似,也需要用户自己实现一个函数,但是只有两个参数,一般是自定义函数 low_level_output()
。
当需要在网卡上发送一个数据包时,该函数会被 ethernet_output()
函数调用,在底层发送数据。
6.4.2.6 出口回调函数
当 netif 被删除时调用此函数。
6.4.2.7 用户私有数据
/** This field can be set by the device driver and could point * to state information for the device. */ void *state;
由设备驱动程序设置并指向设备的状态信息,主要将网卡的某些私有数据传递给上层。
6.4.2.8 最大传输单位
/** maximum transfer unit (in bytes) */ u16_t mtu;
6.4.2.9 链路硬件地址长度&地址
/** number of bytes used in hwaddr */ u8_t hwaddr_len; /** link level hardware address of this interface */ u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
6.4.2.10 网卡信息状态标志
/** flags (@see @ref netif_flags) */ u8_t flags;
网卡状态信息标志位,是很重要的控制字段,它包括网卡的功能使能,广播使能,ARP 使能等重要控制位。
6.4.2.11 网卡名字
/** descriptive abbreviation */ char name[2];
用于保存每一个网卡的名字,用两个字符的名字来标识。
网卡使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网卡表示的硬件的种类。
如果两个网卡具有相同的网络名字,我们就用 num 字段来区分相同类别的不同网卡。
6.4.2.12 网卡标识
/** number of this interface */ u8_t num;
标识使用同种驱动类型的不同网卡。
6.4.2.13 网卡提示
网卡提示,有些网卡相关的数据保存空间。
其数据结构:
struct netif_hint {
比如addr_hint
在ARP协议中的用途就是,当当前网卡发送了一包数据时用到了ARP缓存表某条arp entry时,就会保存这个arp entry的索引到addr_hint
中,下次这个网卡发送数据时,通过addr_hint
获取索引后,直接从这条索引开始遍历ARP缓存表,加速组包时间。
tci
是用于VLAN组包中的字段值。
6.5 netif 快速使用
6.5.1 简要步骤
-
定义一个
netif
作为网卡设备结构体。 -
挂载到
netif_list
链表中:netif_add();
。- 需要提供网卡初始化函数和网卡协议栈入口函数作为
netif_add()
的参数传入。
- 需要提供网卡初始化函数和网卡协议栈入口函数作为
-
使能网卡底层(链路层):
netif_set_link_up()
。 -
使能网卡上层(协议栈):
netif_set_up()
。
6.5.2 与 netif 相关的底层函数
low_level_output()
和low_level_input()
函数是网卡的南向直接操作函数,是对网卡设备的写、读处理。
相当于网卡设备的驱动范畴的函数。
主要 API:
/* 网卡初始化函数 */ static void low_level_init(struct netif *netif); /* 网卡的发送函数, * 将内核的数据包发送出去,数据包采用pbuf数据结构进行描述 */ static err_t low_level_output(struct netif *netif, struct pbuf *p); /* 网卡的数据接收函数, * 该函数必须将接收的数据封装成pbuf的形式 */ static struct pbuf * low_level_input(struct netif *netif);
相关 API:(如以太网)
/* 上层管理网卡netif的到时候会被调用的函数, * 最终还是调用 low_level_init() 函数 */ err_t ethernetif_init(struct netif *netif); /* 主要作用就是调用low_level_input()函数从网卡中读取一个数据包, * 然后解析该数据包的类型是属于ARP数据包还是IP数据包, * 再将包递交给上层。 * 无操作系统中使用:可以直接使用的函数,因为内核会周期性去处理该接收函数。 * 有操作系统中系统:一般会将其改写成一个线程的形式,可以周期性去调用low_level_input()网卡接收函数。*/ void ethernetif_input(void *pParams);
6.6 网卡信息状态标志
在网卡netif->flags;
成员中体现。
-
NETIF_FLAG_UP
:- 网络接口上层是否被使能。
- 属于一个软件(协议栈)就绪标志,表示lwip协议栈已经合法获取到该IP,lwip协议栈已经准备好接收处理这个网卡的数据了。
- 相当于一个lwip协议栈内部与外部南向通信的开关阀。
-
NETIF_FLAG_BROADCAST
:网络接口是否支持广播。 -
NETIF_FLAG_LINK_UP
:- 网络接口的底层链路是否被使能。
- 属于一个硬件(链路层)就绪标志,表示当前网卡南向接口使用的数据链路硬件就绪。
-
NETIF_FLAG_ETHARP
:网络接口是否支持ARP功能。 -
NETIF_FLAG_ETHERNET
:网络接口是否是以太网设备。 -
NETIF_FLAG_IGMP
:网络接口是否支持IGMP协议功能。 -
NETIF_FLAG_MLD6
:网络接口是否支持MLD6功能。
/** * @defgroup netif_flags Flags * @ingroup netif * @{ */ /** Whether the network interface is 'up'. This is * a software flag used to control whether this network * interface is enabled and processes traffic. * It must be set by the startup code before this netif can be used * (also for dhcp/autoip). */
6.7 添加网卡netif_add()
主要内容是:
- 网卡数据结构内容配置;
- 把这个网卡插入到网卡链表
netif_list
。
参数:
netif
:网卡数据结构资源。ipaddr
:网卡IP地址。netmask
:网卡子网掩码。gw
:网卡网关。state
:用户自定义的一些数据。init
:网卡初始化函数。各种网卡的初始化不完全一样,所以又用户自己实现。input
:网卡北方北向接口。是把数据传入TCPIP协议栈的接口。
/** * @ingroup netif * Add a network interface to the list of lwIP netifs. * * @param netif a pre-allocated netif structure * @param ipaddr IP address for the new netif * @param netmask network mask for the new netif * @param gw default gateway IP address for the new netif * @param state opaque data passed to the new netif * @param init callback function that initializes the interface * @param input callback function that is called to pass * ingress packets up in the protocol layer stack.<br> * It is recommended to use a function that passes the input directly * to the stack (netif_input(), NO_SYS=1 mode) or via sending a * message to TCPIP thread (tcpip_input(), NO_SYS=0 mode).<br> * These functions use netif flags NETIF_FLAG_ETHARP and NETIF_FLAG_ETHERNET * to decide whether to forward to ethernet_input() or ip_input(). * In other words, the functions only work when the netif * driver is implemented correctly!<br> * Most members of struct netif should be be initialized by the * netif init function = netif driver (init parameter of this function).<br> * IPv6: Don't forget to call netif_create_ip6_linklocal_address() after * setting the MAC address in struct netif.hwaddr * (IPv6 requires a link-local address). * * @return netif, or NULL if failed. */ 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) {
6.8 网卡上层协议栈使能netif_set_up()
netif_set_up()
函数用于使能网卡上层协议栈。
相当于打开网卡北向接口的开关阀,让网卡和协议栈的数据能够流通。
对应标志位:NETIF_FLAG_UP
。表示上层协议栈准备好了,也打通了网卡与协议栈的通道。
/** * @ingroup netif * Bring an interface up, available for processing * traffic. */ void netif_set_up(struct netif *netif) { LWIP_ASSERT_CORE_LOCKED(); LWIP_ERROR("netif_set_up: invalid netif", netif != NULL, return); if (!(netif->flags & NETIF_FLAG_UP)) { /* 协议栈还没使能 */ netif_set_flags(netif, NETIF_FLAG_UP); /* 标记使能协议栈 */ /* 记录网卡协议栈使能时间 */ MIB2_COPY_SYSUPTIME_TO(&netif->ts); /* 网卡状态回调 */ NETIF_STATUS_CALLBACK(netif);
6.9 网卡底层数据链路使能netif_set_link_up()
netif_set_link_up()
函数用于使能网卡底层数据链路。
相当于打开网卡南向接口的开关阀,让网卡和底层数据链路的数据能够流通。
对应标志位:NETIF_FLAG_LINK_UP
。表示底层数据链路准备好了,也打通了网卡与底层数据链路的通道。
因为在数据链路层新增了一个设备节点,所以需要在链路层做一些处理,通告下。
比如发起ARP请求,宣告当前IP被我使用,给为同僚ARP缓存表有空间就存一下吧。
链路层使能后,还需要通过回调通知其它业务,表明当前网卡状态更新了。
/** * @ingroup netif * Called by a driver when its link goes up */ void netif_set_link_up(struct netif *netif) { LWIP_ASSERT_CORE_LOCKED(); LWIP_ERROR("netif_set_link_up: invalid netif", netif != NULL, return); if (!(netif->flags & NETIF_FLAG_LINK_UP)) { /* 数据链路还没使能 */ netif_set_flags(netif, NETIF_FLAG_LINK_UP); /* 标记使能数据链路 */ /* 链路层新增了一个设备节点,需要在链路层通告处理下 */
6.10 以太网网卡伪代码分析
参考lwip官方提供的ethernetif.c
文件。
6.10.1 相关API说明
用户需要封装单个网卡底层数据链路相关的函数:
-
low_level_init()
:- 该函数用于初始化底层数据链路硬件设备,使其具备底层通信能力。
-
low_level_output()
:- 该函数用于底层数据链路发送数据,网卡的南向出口。
- 收到的上层数据包时pbuf类型的。
-
low_level_input()
:- 该函数用于底层数据链路接收数据,网卡的南向入口。
- 需要组装为pbuf类型再转交给上层。
ethernetif_init()
:网卡初始化函数。封装这个接口是给netif_add()
添加网卡时初始化当前网卡的。
ethernetif_input()
:数据链路接收数据并传入TCPIP协议栈。
发送的数据是由上层一层一层传下来的,相关接口也是层层调用即可。
但是接收数据不一样,我们代码不知道数据什么时候来,所以需要创建一条线程,专门用于检索接收到的数据包,然后传入网卡,让网卡一层一层上传处理。
lwip提供的参考,这个线程就是ethernetif_input()
。
6.10.2 ethernetif_init()
伪代码参考:
/** * Should be called at the beginning of the program to set up the * network interface. It calls the function low_level_init() to do the * actual setup of the hardware. * * This function should be passed as a parameter to netif_add(). * * @param netif the lwip network interface structure for this ethernetif * @return ERR_OK if the loopif is initialized * ERR_MEM if private data couldn't be allocated * any other err_t on error */ err_t ethernetif_init(struct netif *netif) { struct ethernetif *ethernetif; LWIP_ASSERT("netif != NULL", (netif != NULL)); ethernetif = mem_malloc(sizeof(struct ethernetif)); if (ethernetif == NULL) { LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_init: out of memoryn")); return ERR_MEM; }
6.10.3 ethernetif_input()
该函数为应该独立线程,用于检测和接收底层数据链路数据,打包好发往TCPIP协议栈。
/** * This function should be called when a packet is ready to be read * from the interface. It uses the function low_level_input() that * should handle the actual reception of bytes from the network * interface. Then the type of the received packet is determined and * the appropriate input function is called. * * @param netif the lwip network interface structure for this ethernetif */ static void ethernetif_input(struct netif *netif) { struct ethernetif *ethernetif; struct eth_hdr *ethhdr; struct pbuf *p; ethernetif = netif->state; /* 用户私人信息拿出来 */ /* 接收一个数据包,并封装成pbuf类型 */ p = low_level_input(netif); /* 如果无法读取数据包,则静默地忽略它 */ if (p != NULL) { /* 将所有数据包传递给ethernet_input,由ethernet_input决定支持哪些数据包 */ if (netif->input(p, netif) != ERR_OK) { LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input errorn")); pbuf_free(p); /* 上传失败,丢弃该数据包 */ p = NULL; } } }
6.10.4 low_level_init()
/** * In this function, the hardware should be initialized. * Called from ethernetif_init(). * * @param netif the already initialized lwip network interface structure * for this ethernetif */ static void low_level_init(struct netif *netif) { struct ethernetif *ethernetif = netif->state; /* 获取用户私人信息 */ /* 设置MAC硬件地址长度 */ netif->hwaddr_len = ETHARP_HWADDR_LEN; /* 设置MAC地址 */ netif->hwaddr[0] = ; /* ... */ netif->hwaddr[5] = ; /* 设置MTU */ netif->mtu = 1500; /* 设备性能标记 */ /* 根据实际设备性能而配置 */ netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
6.11环回接口
6.11.1 环回地址
ipv4的127 网段的所有地址都称为环回地址,主要用来测试网络协议是否工作正常的作用。
常用:
IPv4:127.0.0.1
IPv6:::1
默认记录IPv4。
环回地址表示“我自己”的意思。
6.11.2 环回数据包流图
图源自李柱明
6.11.3 相关宏及API
ENABLE_LOOPBACK
:开启环回功能。
LWIP_NETIF_LOOPBACK
:每个网卡都有环回功能。
LWIP_HAVE_LOOPIF
:可创建独立的环回网卡。
netif_init()
:lwip网卡模块初始化。如果开启了LWIP_NETIF_LOOPBACK
,则会在该函数里面添加一个环回网卡loop_netif
。
netif_loopif_init()
:环回网卡初始化。
- 相当于以太网网卡的
low_level_init()
。
netif_loop_output()
:环回输出接口。
-
如果数据包的目标IP和当前网卡的IP一致,则调用内部会调用该函数,而不是
netif->output()
。 -
如果是环回网卡,
netif->output()
也是该函数。- 所以如果目标IP和环回网卡一致,则直接调用
netif_loop_output()
。 - 如果不一致,则调用
netif->output()
,其实也是netif_loop_output()
。
- 所以如果目标IP和环回网卡一致,则直接调用
netif_poll()
:环回处理接口。
- 单线程环境下:在应用程序中循环调用。
- 多线程环境下:内部会通过消息,把
netif_poll()
转交给wlip内核线程去跑。 - 目的是把环回数据包环回发到对应网卡的IP层处理。
netif_loop_output()
输入的数据传到netif->input()
。
6.11.4 环回网卡初始化
lwip网卡模块初始化调用netif_init()
函数,只是在lwip协议栈初始化是调用,如果开启了环回LWIP_HAVE_LOOPIF
功能,才会有操作,就是创建一个环回IP的网卡,仅此而已。
对应到每一个实际网卡的初始化,在netif_add()
会讲解。
void netif_init(void) {
回环网卡初始化:netif_loopif_init()
/** * Initialize a lwip network interface structure for a loopback interface * * @param netif the lwip network interface structure for this loopif * @return ERR_OK if the loopif is initialized * ERR_MEM if private data couldn't be allocated */ static err_t netif_loopif_init(struct netif *netif) { LWIP_ASSERT("netif_loopif_init: invalid netif", netif != NULL); /* 初始化结构netif中的SNMP变量和计数器 * ifSpeed:不能做任何假设! */ MIB2_INIT_NETIF(netif, snmp_ifType_softwareLoopback, 0); /* 网卡名字 */ netif->name[0] = 'l'; netif->name[1] = 'o';
6.11.5 环回输出接口netif_loop_output()
传入的pbuf是被以复制的显示缓存到对应网卡的loop_first
链表中:
- 先申请新的pbuf r,用于存储拷贝传入的pbuf p的数据。
- 预测检查pbuf节点数是否超出限制
LWIP_LOOPBACK_MAX_PBUFS
。 - 拷贝pbuf数据。
- 拼接到对应网卡的环回pbuf链表
netif->loop_first
中。 - 如果网卡的环回pbuf链表还有数据,说明上次已经触发过
netif_poll()
,正常按理是不用再触发的。但是如果上次触发失败,这次还是要触发。 - 如果网卡大的环回链表没有数据,说明当前
netif_poll()
没有在跑。需要触发netif_poll()
。 - 如果
netif_poll()
触发失败,需要标记下,下次再发环回数据时补回触发。
/** * @ingroup netif * Send an IP packet to be received on the same netif (loopif-like). * The pbuf is copied and added to an internal queue which is fed to * netif->input by netif_poll(). * In multithreaded mode, the call to netif_poll() is queued to be done on the * TCP/IP thread. * In callback mode, the user has the responsibility to call netif_poll() in * the main loop of their application. * * @param netif the lwip network interface structure * @param p the (IP) packet to 'send' * @return ERR_OK if the packet has been sent * ERR_MEM if the pbuf used to copy the packet couldn't be allocated */ err_t netif_loop_output(struct netif *netif, struct pbuf *p) { struct pbuf *r; err_t err; struct pbuf *last;
6.11.6 环回处理接口netif_poll()
非可重入函数。
- pbuf数据从网卡环回pbuf链表
netif->loop_first
中出队,把数据转交给IP模块入口ip_input()
处理。
/** * Call netif_poll() in the main loop of your application. This is to prevent * reentering non-reentrant functions like tcp_input(). Packets passed to * netif_loop_output() are put on a list that is passed to netif->input() by * netif_poll(). */ void netif_poll(struct netif *netif) { /* 如果我们有一个loop, SNMP计数器会为此进行调整, * 如果不是,则调整为'netif'。 */