今天我们来讨论LWIP是怎样来处理与底层硬件,即网卡芯片间的关系的。
为什么要首先讨论这个问题呢?与许多其他的TCP/IP实现一样,LWIP也是以分层的协议为参照来设计实现TCP/IP的。LWIP从逻辑上看分为四层:链路层、网络层、传输层和应用层。注意,虽然LWIP也采用了分层机制,但它没有在各层之间进行严格的划分,各层协议之间可以进行或多或少的交叉存取,即上层可以意识到下层协议所使用的缓存处理机制。因此各层可以更有效地重用缓冲区。而且,应用进程和协议栈代码可以使用相同的内存,应用可以直接读写内部缓存,因此节省了执行拷贝的开销。我们将从LWIP的最底层链路层起步,开始整个LWIP内部协议之旅。
在LWIP中,是通过一个叫做netif
的网络结构体来描述一个硬件网络接口的。这个接口结构比较简单,下面我们从源代码结构来分析分析这个结构:
struct netif {
struct netif *next; // 指向下一个netif结构的指针
struct ip_addr ip_addr; // IP地址相关配置
struct ip_addr netmask;
struct ip_addr gw;
err_t (* input)(struct pbuf *p, struct netif *inp); //调用这个函数可以从网卡上取得一个数据包
err_t (* output)(struct netif *netif, struct pbuf *p, // IP层调用这个函数可以向网卡发送一个数据包
struct ip_addr *ipaddr);
err_t (* linkoutput)(struct netif *netif, struct pbuf *p); // ARP模块调用这个函数向网卡发送一个数据包
void *state; // 用户可以独立发挥该指针,用于指向用户关心的网卡信息
u8_t hwaddr_len; // 硬件地址长度,对于以太网就是MAC地址长度,为6各字节
u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //MAC地址
u16_t mtu; // 一次可以传送的最大字节数,对于以太网一般设为1500
u8_t flags; // 网卡状态信息标志位
char name[2]; // 网络接口使用的设备驱动类型的种类
u8_t num; // 用来标示使用同种驱动类型的不同网络接口
};
next字段是指向下一个netif
结构的指针。我们的一个产品可能会有多个网卡芯片,LWIP会把所有网卡芯片的结构体链成一个链表进行管理,有一个netif_list
的全局变量指向该链表的头部。next
字段就是用于链表用。
ip_addr、netmask、gw三个字段用于发送和处理数据包用,分别表示IP地址、子网掩码和网关地址。前两个字段在数据包发送时有重要作用,第三个字段似乎没什么用。IP地址和网卡设备必须一一对应。如果你连什么叫IP地址、子网掩码和它们的作用都不晓得,那你有必要去看看TCP/IP协议详解卷1第三章。
input字段指向一个函数,这个函数将网卡设备接收到的数据包提交给IP层,使用时将input
指针指向该函数即可,后面将详细讨论这个问题。该函数的两个参数是pbuf类型和netif
类型的,返回参数是err_t
类型。其中pbuf代表接收到的数据包。
output字段向一个函数,这个函数和具体网络接口设备驱动密切相关,它用于IP层将一个数据包发送到网络接口上。用户需要根据实际网卡编写该函数,并将output
字段指向该函数。该函数的三个参数是pbuf
类型、netif
类型和ip_addr
类型,返回参数是err_t
类型。其中pbuf
代表要发送的数据包。ipaddr
代表网卡需要将该数据包发送到的地址,该地址应该是接收实际的链路层帧的主机的 IP 地址,而不一定为数据包最终需要到达的IP地址。例如,当要发送 IP信息包到一个并不在本地网络里的主机上时,链路层帧会被发送到网络里的一个路由器上。在这种情况下,给 output
函数的 IP
地址将是这个路由器的地址。
linkoutput字段和上面的output
基本上是起相同的作用,但是这个函数是在ARP
模块中被调用的,这里不赘述了。注意这个函数只有两个参数。实际上output字段函数的实现最终还是调用linkoutput字段函数将数据包发送出去的。
state字段可以指向用户关心的关于设备的一些信息,用户可以自由发挥,也可以不用。hwaddr_len
和hwaddr[]
表示MAC地址长度和MAC地址,一般MAC地址
长度为6
。
mtu字段表示该网络一次可以传送的最大字节数,对于以太网一般设为1500
,不多说。
flags字段是网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播使能、ARP使能等等重要控制位。
name[]字段用于保存每一个网络网络接口的名字。用两个字符的名字来标识网络接口使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网络接口表示的硬件的种类。比如蓝牙设备(bluetooth
)的网络接口名字可以是 bt
,而 IEEE 802.11b WLAN
设备的名字就可以是 wl
,当然设置什么名字用户是可以自由发挥的,这并不影响用户对网络接口的使用。当然,如果两个网络接口具有相同的网络名字,我们就用num
字段来区分相同类别的不同网络接口。
到这里,你可能一头雾水,太抽象的东西太容易让人纠结。我们举个例子来看看一个以太网网卡接口结构是这样被初始化,还有数据包是如何接收和发送的。先来看初始化过程,源码:
static struct netif enc28j60; (1)
struct ip_addr ipaddr, netmask, gw; (2)
IP4_ADDR(&gw, 192,168,0,1); (3)
IP4_ADDR(&ipaddr, 192,168,0,60); (4)
IP4_ADDR(&netmask, 255,255,255,0); (5)
netif_init(); (6)
netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, ethernetif_init, tcpip_input); (7)
netif_set_default(&enc28j60); (8)
netif_set_up(&enc28j60); (9)
上面的(1)声明了一个netif
结构的变量enc28j60
,由于在我的板子上使用的是网卡芯片enc28j60
,所以我选择使用了这个名字。(2)声明了三个分别用于暂存IP地址、子网掩码和网关地址的变量,它们是32位长度的。(3)~ (5)分别是对上述三个地址值的初始化,该过程简单。
(6)很简单,它只需初始化上面所述的全局变量netif_list
即可:netif_list = NULL
。
(7)调用netif_add
函数初始化变量enc28j60
,其中比较重要的两个参数是ethernetif_init
和tcpip_input
,前者是用户自己定义的底层接口初始化函数,tcpip_input
函数是向IP
层递交数据包的函数,从前面的讲述中可以很明显的看出,该值会被传递给enc28j60
的input
字段。再来看看源码:
struct netif *
netif_add(struct netif *netif, struct ip_addr *ipaddr, struct ip_addr *netmask,
struct ip_addr *gw,
void *state,
err_t (* init)(struct netif *netif),
err_t (* input)(struct pbuf *p, struct netif *netif))
{
static u8_t netifnum = 0;
netif->ip_addr.addr = 0; //复位变量enc28j60中各字段的值
netif->netmask.addr = 0;
netif->gw.addr = 0;
netif->flags = 0; //该网卡不允许任何功能使能
netif->state = state; //指向用户关心的信息,这里为NULL
netif->num = netifnum++; //设置num字段,
netif->input = input; //如前所诉,input函数被赋值
netif_set_addr(netif, ipaddr, netmask, gw); //设置变量enc28j60的三个地址
if (init(netif) != ERR_OK) { //用户自己的底层接口初始化函数
return NULL;
}
netif->next = netif_list; //将初始化后的节点插入链表netif_list
netif_list = netif; // netif_list指向链表头
return netif;
}
上面的初始化函数调用了用户自己定义的底层接口初始化函数,这里为ethernetif_init
,再来看看它的源代码:
err_t ethernetif_init(struct netif *netif)
{
netif->name[0] = IFNAME0; //初始化变量enc28j60的name字段
netif->name[1] = IFNAME1; // IFNAME在文件外定义的,这里不必关心它的具体值
netif->output = etharp_output; //IP层发送数据包函数
netif->linkoutput = low_level_output; // //ARP模块发送数据包函数
low_level_init(netif); //底层硬件初始化函数
return ERR_OK;
}
还有函数调用!low_level_init
函数就是与我们使用的硬件密切相关的函数了。