1、LwIP结构体netif
网络接口(如以太网接口)是硬件接口,网络接口又可以称之为网卡 , LwIP 是软件,那么怎么让硬件与软件无缝连接起来呢?而且, 网卡又有多种多样,怎么能让 LwIP 使用同样的软件能兼容不同的硬件呢?LwIP 使用一个数据结构——netif 来描述一个网卡, 但是由于网卡是直接与硬件打交道的,硬件不同则处理基本是不同的, 所以必须由用户提供最底层接口函数, LwIP 提供统一的接口,但是底层的实现需要用户自己去完成,比如网卡的初始化, 网卡的收发数据,当 LwIP 底层得到了网络的数据之后,才会传入内核中去处理;同理, LwIP 内核需要发送一个数据包的时候,也需要调用网卡的发送函数,这样子才能把数据从硬件接口到软件内核无缝连接起来。
LwIP 中的 ethernetif.c 文件即为底层接口的驱动的模版,用户为自己的网络设备实现驱动时应参照此模块做修改。 ethernetif.c 文件中的函数通常为与硬件打交道的底层函数,当有数据需要通过网卡接收或者发送数据的时候就会被调用,经过 LwIP 协议栈内部进行处理后,从应用层就能得到数据或者可以发送数据。
简单来说, netif 是 LwIP 抽象出来的网卡, LwIP 协议栈可以使用多个不同的接口,而ethernetif.c 文件则提供了 netif 访问各种不同的网卡,每个网卡有不同的实现方式, 用户只需要修改 ethernetif.c 文件即可。
在单网卡中,这个 netif 结构体只有一个,可能还有人会问,那么一个设备中有多个网卡怎么办,很简单, LwIP 会将每个用 netif 描述的网卡连接成一个链表(单向链表),该链表就记录每个网卡的 netif。 屏蔽硬件接口的差异,完成了对不同网卡的抽象,因此了解netif 结构体是移植 LwIP 的关键。
//netif 数据结构 struct netif { #if !LWIP_SINGLE_NETIF struct netif *next; /* 指向 netif 链表中的下一个 */ (1) #endif /* 网络字节中的 IP 地址、子网掩码、默认网关配置 */ #if LWIP_IPV4 ip_addr_t ip_addr; ip_addr_t netmask; ip_addr_t gw; (2) #endif /* LWIP_IPV4 */ netif_input_fn input; /* 此函数由网络设备驱动程序调用,将数据包传递到 TCP/IP 协议栈。对于以太网物理层,这通常是 ethernet_input()*/ (3) #if LWIP_IPV4 netif_output_fn output; /* 此函数由 IP 层调用,在接口上发送数据包。通常这个功能,首先解析硬件地址,然后发送数据包。对于以太网物理层,这通常是 etharp_output() */ (4) #endif /* LWIP_IPV4 */ netif_linkoutput_fn linkoutput; /* 此函数由 ethernet_output()调用,当需要在网卡上发送一个数据包时。底层硬件输出数据函数,一般是调用自定义函数 low_level_output*/ (5) #if LWIP_NETIF_STATUS_CALLBACK netif_status_callback_fn status_callback; /*当 netif 状态设置为 up 或 down 时调用此函数*/ (6) #endif /* LWIP_NETIF_STATUS_CALLBACK */ #if LWIP_NETIF_LINK_CALLBACK netif_status_callback_fn link_callback; /* 当 netif 链接设置为 up 或 down 时,将调用此函数 */ (7) #endif /* LWIP_NETIF_LINK_CALLBACK */ #if LWIP_NETIF_REMOVE_CALLBACK netif_status_callback_fn remove_callback; /* 当 netif 被删除时调用此函数 */ (8) #endif /* LWIP_NETIF_REMOVE_CALLBACK */ void *state; /* 此字段可由设备驱动程序设置并指向设备的状态信息。主要是将网卡的某些私有数据传递给上层,用户可以自由发挥,也可以不用。 */ (9) #ifdef netif_get_client_data void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA]; #endif #if LWIP_NETIF_HOSTNAME const char* hostname; /* 这个 netif 的主机名, NULL 也是一个有效值 */ #endif /* LWIP_NETIF_HOSTNAME */ #if LWIP_CHECKSUM_CTRL_PER_NETIF u16_t chksum_flags; #endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/ u16_t mtu; /** 最大传输单位(以字节为单位),对于以太网一般设为 1500 */ (10) u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; /** 此网卡的链路层硬件地址 */ (11) u8_t hwaddr_len; /** 硬件地址长度,对于以太网就是 MAC 地址长度,为 6 字节 */ (12) u8_t flags; /* 网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播使能、 ARP 使能等等重要控制位。 */ (13) /* 字段用于保存每一个网卡的名字。用两个字符的名字来标识网络接口使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网卡表示的硬件的种类。比如蓝牙设备( bluetooth)的网卡名字可以是 bt, 而 IEEE 802.11b WLAN 设备的名字就可以是 wl,当然设置什么名字用户是可以自由发挥的,这并不影响用户对网卡的使用。当然,如果两个网卡具有相同的网络名字,我们就用 num 字段来区分相同类别的不同网卡*/ char name[2]; (14) u8_t num; /* 用来标示使用同种驱动类型的不同网卡 */ (15) #if MIB2_STATS u8_t link_type; /* 连接类型 */ u32_t link_speed; /* 连接速度 */ u32_t ts; /* 最后一次更改的时间戳 */ struct stats_mib2_netif_ctrs mib2_counters; #endif /* MIB2_STATS */ #if LWIP_IPV4 && LWIP_IGMP netif_igmp_mac_filter_fn igmp_mac_filter; /** 可以调用此函数来添加或删除多播中的条目以太网 MAC 的过滤表。 */ #endif /* LWIP_IPV4 && LWIP_IGMP */ #if LWIP_NETIF_USE_HINTS struct netif_hint *hints; #endif /* LWIP_NETIF_USE_HINTS */ #if ENABLE_LOOPBACK /* List of packets to be queued for ourselves. */ struct pbuf *loop_first; struct pbuf *loop_last; #if LWIP_LOOPBACK_MAX_PBUFS u16_t loop_cnt_current; #endif /* LWIP_LOOPBACK_MAX_PBUFS */ #endif /* ENABLE_LOOPBACK */ };
(1):LwIP 使用链表来管理同一设备的多个网卡。在 netif.c 文件中定义两个全局指针: struct netif *netif_list 和 struct netif *netif_default,其中 netif_list 就是网卡链表指针,指向网卡链表的首节点(第一个网卡) ,后者表示默认情况下(有多网口时)使用哪个网卡。 next 字段指向下一个 netif 结构体指针,在一个设备中有多个网卡时, 才使用该字段。 (2):ip_addr 字段记录的是网络中的 IP 地址, netmask 字段记录的是子网掩码, gw 记录的是网关地址,这些字段是用于描述网卡的网络地址属性。IP 地址必须与网卡对应,即设备拥有多少个网卡那就必须有多少个 IP 地址;子网掩码可以用来判断某个 IP 地址与当前网卡是否处于同一个子网中, IP 在发送数据包的时候会选择与目标 IP 地址处于同一子网的网卡来发送;网关地址在数据包的发送、转发过程非常重要,如果要向不属于同一子网的主机(主机目标 IP 地址与网卡不属于同一子网)发送一个数据包,那么 LwIP 就会将数据包发送到网关中,网关设备会对该数据包进行正确的转发,除此之外,网关还提供很多高级功能,如 DNS, DHCP 等。 (3):input 是一个函数指针, 指向一个函数,该函数由网络设备驱动程序调用,将数据包传递到 TCP/IP 协议栈(IP 层) 。 对于以太网物理层,这通常是ethernet_input(), 参数为 pbuf 和 netif 类型,其中 pbuf 为接收到的数据包。 (4):output 也是一个函数指针, 指向一个函数, 此函数由 IP 层调用,在接口上发送数据包。 用户需要编写该函数并使 output 指向它, 通这个函数的处理步骤是首先解析硬件地址,然后发送数据包。对于以太网物理层, 该函数通常是 etharp_output(),参数为 pbuf、 netif 和 ip_addr 类型,其中, ipaddr 代表要将该数据包发送到的地址,但不一定是数据包最终到到达的 IP 地址,比如,要发送 IP 数据报到一个并不在本网络的主机上,该数据包要被发送到一个路由器上,这里的 ipaddr 就是路由器 IP 地址。 (5):linkoutput 字段和 output 类似,也需要用户自己实现一个函数, 但只有两个参数,它是由 ARP 模块调用的, 一般是自定义函数 low_level_output()。 当需要在网卡上发送一个数据包时,该函数会被 ethernet_output()函数调用。 (6):当 netif 状态设置为 up 或 down 时, 将调用此函数。 (7):当 netif 连接设置为 up 或 down 时,将调用此函数。 (8):当 netif 被删除时调用此函数。 (9):此字段可由设备驱动程序设置并指向设备的状态信息。主要是将网卡的某些私有数据传递给上层,用户可以自由发挥,也可以不用。 (10):最大传输单位(以字节为单位),对于以太网一般为 1500,在 IP层发送数据的时候, LwIP 会使用该字段决定是否需要对数据包进行分片处理,为什么是在IP 层进行分片处理?因为链路层不提供任何的差错处理机制,如果在网卡中接收的数据包不满足网卡自身的属性,那么网卡可能就会直接丢弃该数据包,也可能在底层进行分包发送,但是这种分包在 IP 层看来是不可接受的,因为它打乱了数据的结构,所以只能由 IP层进行分片处理。 (11):此网卡的链路层硬件地址。 (12):硬件地址长度,对于以太网就是 MAC 地址长度,为 6 字节 (13):网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播使能、 ARP 使能等等重要控制位。 (14):name 字段用于保存每一个网卡的名字。用两个字符的名字来标识网卡使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网卡表示的硬件的种类。比如蓝牙设备(bluetooth)的网卡名字可以是 bt,而 IEEE 802.11b WLAN 设备的名字就可以是 wl,当然设置什么名字用户是可以自由发挥的,这并不影响用户对网卡的使用。当然,如果两个网卡具有相同的网络名字,我们就用 num 字段来区分相同类别的不同网卡。 (15):用来标识使用同种驱动类型的不同网卡。
2、 netif结构体的使用
首先我们需要根据我们的网卡定义一个 netif 结构体变量 struct netif gnetif, 我们首先要把网卡挂载到 netif_list 链表上才能使用,因为 LwIP 是通过链表来管理所有的网卡,所有第一步是通过 netif_add()函数将我们的网卡挂载到 netif_list 链表上, netif_add()函数具体见代码清单 4-2。
struct netif *netif_add(struct netif *netif,const ip4_addr_t *ipaddr,const ip4_addr_t *netmask, const ip4_addr_t *gw,void *state, netif_init_fn init, netif_input_fn input) { LWIP_ASSERT_CORE_LOCKED(); 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); } /*清空主机 IP 地址、子网掩码、网关等字段信息*/ /* reset new interface configuration state */ ip_addr_set_zero_ip4(&netif->ip_addr); ip_addr_set_zero_ip4(&netif->netmask); ip_addr_set_zero_ip4(&netif->gw); netif->output = netif_null_output_ip4; NETIF_SET_CHECKSUM_CTRL(netif, NETIF_CHECKSUM_ENABLE_ALL); netif->mtu = 0; netif->flags = 0; memset(netif->client_data, 0, sizeof(netif->client_data)); /*根据传递进来的参数填写网卡 state、 input 等字段的相关信息*/ /* remember netif specific state information data */ netif->state = state; netif->num = netif_num; netif->input = input; NETIF_RESET_HINTS(netif); /*调用网卡设置函数 netif_set_addr()设置网卡 IP 地址、子网掩码、网关等信息*/ netif_set_addr(netif, ipaddr, netmask, gw); (3) /*通过传递进来的回调函数 init()进行网卡真正的初始化操作, 所以该函数是由用户实现的, 对于不同网卡就使用不一样的初始化, 而此处是以太网, 则该回调函数一般为 ethernetif_init()*/ /* call user specified initialization function for netif */ if (init(netif) != ERR_OK) { return NULL; } { struct netif *netif2; int num_netifs; do { if (netif->num == 255) { netif->num = 0; } num_netifs = 0; for(netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) { num_netifs++; 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); /8*初始化网卡成功,则遍历当前设备拥有多少个网卡,并为当前网卡分配唯一标识 num*/ } /将当前网卡插入 netif_list 链表中*/ /* add this netif to the list */ netif->next = netif_list; netif_list = netif; mib2_netif_added(netif); ip4_addr_debug_print(NETIF_DEBUG, ipaddr); ip4_addr_debug_print(NETIF_DEBUG, netmask); ip4_addr_debug_print(NETIF_DEBUG, gw); netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL); return netif; }
在使用之前需要进行初始化主机 IP 地址、子网掩码、网关等,并且在调用 netif_add()函数之后会触发 netif_init_fn 的回调函数。
总之一句话,在开始使用 LwIP 协议栈的时候,我们就需要将网卡底层移植完成,才能开始使用,而移植的第一步,就是将网络进行初始化,并且设置该网卡为默认网卡,让LwIP 能通过网卡进行收发数据。
3、与 netif 相关的底层函数
每个 netif 接口都需要一个底层接口文件提供访问硬件的支持, 而 LwIP 作者将这种支持做成一个框架供我们参考,如 ethernetif.c 文件就是实现为一个框架的形式,我们在移植的时候只需要根据实际的网卡特性完善这里面的函数即可。框架中的函数名、参数等都已经实现,我们只需往里面填充完善即可,当然,网卡的驱动与这些函数名字我们也可以进行修改,只要 LwIP 内核能正确识别网卡中的功能即可,为了方便,我们还是使用 LwIP 作者提供的框架进行移植操作,当一个设备使用了多个网卡的时候,那就需要编写多个不同的网卡驱动。
与网卡驱动密切相关的函数有三个,分别是:
static void low_level_init(struct netif *netif); static err_t low_level_output(struct netif *netif, struct pbuf *p); static struct pbuf * low_level_input(struct netif *netif);
low_level_init()为网卡初始化函数,它主要完成网卡的复位及参数初始化,根据实际的网卡属性进行配置 netif 中与网卡相关的字段,例如网卡的 MAC 地址、长度,最大发送单元等。
low_level_output()函数为网卡的发送函数, 它主要将内核的数据包发送出去,数据包采用 pbuf 数据结构进行描述,该数据结构是一个比较复杂的数据结构。
low_level_input()函数为网卡的数据接收函数,该函数会接收一个数据包,为了内核易于对数据包的管理,该函数必须将接收的数据封装成 pbuf 的形式。
除此之外,还有两个函数也与网卡与关系, 分别是:
err_t ethernetif_init(struct netif *netif); void ethernetif_input(void *pParams);
ethernetif_init()函数是在上层管理网卡 netif 的到时候会被调用的函数,如使用netif_add()添加网卡的时候,就会调用 ethernetif_init()函数对网卡进行初始化,其实该函数的最终调用的初始化函数就是 low_level_init()函数,我们目前只有一个网卡,就暂时不用对该函数进行改写,直接使用即可,它内部会将网卡的 name、 output、 linkoutput 等字段进行初始化, 这样子就能将内核与网卡无缝连接起来。
ethernetif_input()函数的主要作用就是调用 low_level_input()函数从网卡中读取一个数据包,然后解析该数据包的类型是属于 ARP 数据包还是 IP 数据包,再将包递交给上层,在无操作系统的时候 ethernetif_input()就是一个可以直接使用的函数,已经无需我们自己去修改,内核会周期性处理该接收函数。而在多线程操作系统的时候,我们一般会将其改写成一个线程的形式,可以周期性去调用 low_level_input()网卡接收函数;也可以使用中断的形式去处理,当这个线程将在尚未接收到数据包的时候,处于阻塞状态,当收到数据包的时候,中断利用操作系统的 IPC 通信机制来唤醒线程去处理接收到的数据包,并将数据包递交上层,这样子的效率会更加高效,事实上我们也是这样子处理的。