网络接口管理
1.1 网络接口层简介
TCP/IP的最底层为网络接口层,而网络接口层可分为逻辑链路层和物理层,逻辑链路层又可以细分为介质访问控制子层(MAC层)与逻辑链路控制子层(LLC层),大致关系如下:
1.物理层
物理层通过把上层的比特流(0/1二进制流)转换为电压的高低、灯光的亮灭等物理信号将数据传输出去,接收端收到这些物理信号后再将这些电压的高低、灯光的亮灭信号恢复为比特流。因此,物理层的规范中包括比特流的转换规则和传输介质两部分。
物理层传输介质大体可分为有线传输介质与无线传输介质两大类:有线传输介质有同轴电缆、双绞线、光纤电缆等;无线传输介质有蓝牙、Wi-Fi、红外线IrDA、移动通信等。
物理层比特流的转换规则就是数据编解码方法,根据信号类型也可分为模拟数据编码和数字数据编码两大类:模拟数据编码(常用于无线传输介质)主要分为振幅键控(ASK)、移频键控(FSK)、移相键控(PSK)等;数字数据编码(常用于有线传输介质)主要分为非归零码NRZ、曼切斯特编码、差分曼切斯特编码等。
2.逻辑链路控制层
MAC子层的主要作用也可分为两部分:一部分面向上层LLC负责把物理层的0/1比特流组建成数据帧或把数据帧分解为0/1比特流(数据帧的封装与卸装),包括数据帧的寻址、识别、发送、接收、差错控制等;另一部分面向下层PHY提供对共享介质的访问方法,包括以太网的带冲突检测的载波侦听多路访问(CSMA/CD)、令牌环(TokenRing)、光纤分布式数据接口(FDDI)等。
MAC子层对介质的访问控制主要包括介质分配(避免碰撞)和竞争裁决(碰撞处理),对传输介质的访问可大体分为共享介质和非共享介质两类:共享介质指多个设备共享一个通信介质,包括蓝牙/WIFI使用的无线信道,为了避免多个设备同时发送数据帧引起冲突,常在发送数据帧前检测通信介质上的数据流动情况,一般为半双工通信;非共享介质主要是每个设备直连交换机,由交换机负责转发数据帧,每个设备与交换机之间的通信介质是专用的,这种情况不需要进行冲突检测,可实现全双工通信。
MAC子层管理的数据帧为了便于寻址,包含物理地址字段(也叫MAC地址),数据帧也正是靠源MAC地址与目的MAC地址实现数据帧的寻址(仅限于同一数据链路段,可以跨交换机、网桥、中继器等链路层中转设备,但不能跨路由器等网络层中转设备,跨越路由器的寻址靠IP地址完成)、发送与接收的。MAC子层将目标计算机的物理地址添加到数据帧上,当此数据帧传递到对端的MAC子层后,它检查该地址是否与自己的地址相匹配,如果帧中的地址与自己的地址不匹配,就将这一帧抛弃;如果相匹配,就将它发送到上一层中。在有线介质比如以太网或FDDI中根据IEEE802.3的规范使用MAC地址,在无线介质比如Wi-Fi(IEEE802.11)、蓝牙(IEEE802.15)等设备中也使用相同规范的MAC地址,所以MAC子层的存在屏蔽了不同物理链路种类的差异性。
MAC地址长48比特,在使用网卡(NIC)的情况下,MAC地址一般会被烧入到ROM中。因此,任何一个网卡的MAC地址都是唯一的,在全世界都不会重复(实际上有例外情况,比如虚拟MAC地址可能有重复,重复的MAC地址只要不是同属于一个数据链路就不会出现问题,一个主机是靠IP地址与MAC地址共同唯一确定的)。MAC地址中3~24位表示厂商识别码OUI(Organizationally unique identifier),每个NIC厂商都有特定唯一的识别数字,后24位是厂商内部为识别每个网卡而用的,前两位则跟MAC地址类型有关,MAC地址的结构如下图示:
MAC地址的源地址就是真实的MAC地址(属于单播地址),目的地址则可分为三类:单播地址、多播地址和广播地址。单播地址通常与一个网卡的具体地址相对应,它要求第一个字节的bit0(即最先发出去的位)必须为0;多播地址则要求第一个字节的bit0为1,在网络中多播地址不会与任何网卡的MAC地址相同,而多播数据可以被多个网卡同时接收;广播地址的所有48位全为1(即FF-FF-FF-FF-FF-FF),同一链路层局域网中的所有网卡都可以接收到广播数据包。
LLC子层向高层提供一个或多个逻辑接口,这些接口被称为服务访问点(SAP),多个逻辑接口可能共用一个物理接口,每个逻辑接口可能通过SAP服务一个上层协议。LLC子层将物理链路抽象为逻辑链路,基本与物理介质完全无关了,屏蔽了不同MAC子层之间的差异,对上层网络层来的不同协议进行翻译和控制,并向下传递同样的数据帧,以使其可以在物理层传送。相对于有线传输的线缆直连,无线传输的链路管理更为复杂,需要将电磁波资源划分为不同的信道,无线链路的建立、加密、认证、释放等也是由LLC规定的,读者感兴趣可以详细了解下IEEE802.11(Wi-Fi)与IEEE802.15(BlueTooth)的规范,本系列主要介绍更上层的协议栈,就以IEEE802.3以太网规范为例了。
3.以太网数据帧
现在使用比较多的以太网帧是前者,跟IEEE802.3定义的以太网帧略有差别,两者前面都包含目标MAC地址与源MAC地址字段,用于数据帧的寻址、发送、接收等,最后都有FCS(Frame Check Sequence)用于差错校验。不同的是源MAC地址后与数据前的部分,以太网帧的类型字段用于标识上层协议类型,而IEEE802.3帧的帧长度字段不足以标识上层协议类型,需要靠LLC/SNAP(SubNetwork Access Protocol)字段标识出上层协议类型信息。常见的协议类型编号如下:
以太网帧前端还有一个叫做前导码的部分,它由0/1交替组合而成,表示一个以太网帧的开始,也是为了实现物理层数据的正确传输,物理层使用7个字节的前同步码实现物理层帧输入/输出的同步,使用1个字节的SFD(Start Frame Delimiter帧首定界符,末尾是11)标识帧的开始,共8字节的前导码结构如下:
1.2 网络接口的描述
网络接口层旨在对具体网络硬件、软件进行统一的封装,并为协议栈上层(IP层)提供统一的接口服务。在LWIP运行的目标系统上可能存在多个网络接口,比如可能有多个网卡,也可能有串行网络接口(串口),还可能有环回接口(提供了一种对硬件接口的纯软件模拟,允许用户在没有硬件网络接口的环境下运行并调试协议栈,也可用于进程间通信)。
为了实现对这些接口结构的有效管理,LWIP会为每个接口分配一个netif结构,用这个结构来描述每种接口的特性,如接口IP、接口状态等,同时在该结构中为每个接口注册对应的操作函数,如数据包输入函数、输出函数等。内核将所有网络接口的netif结构组织在一个叫做netif_list的链表上,当有IP数据包需要发送时,IP层会根据数据包的目的IP地址,在netif_list链表中选择一个最合适的网络接口,并调用其注册的数据包发送函数将数据包发送出去;当网卡接收到数据包时,其注册的数据包输入函数会被调用,完成将数据包递交给IP层的任务。从整个过程来看,网络接口管理有效地为上层屏蔽掉底层各个硬件接口间的差异,并为底层网络接口驱动程序的编写提供了规范化的接口定义。
数据结构netif完成了对各种类型网络接口的抽象,它的定义如下:
struct netif { struct netif *next; //执行下一个netif结构的指针 //该网络接口的网络地址属性 ip_addr_t ip_addr; //IP地址 ip_addr_t netmask; //子网掩码 ip_addr_t gw; //网关 //这三个函数指针既然是用于输入、输出数据包的,其中一个重要参数就是上一章介绍的pbuf类型,另一个参数就是现在介绍的netif类型。在网卡初始化时,需要向这三个函数指针注册相应的输入、输出函数,这也是协议栈移植的重点之一。 netif_input_fn input; //函数指针,用于将网络设备接收到的数据包提交给IP层 netif_output_fn output; //函数指针,用于将IP层的数据包发送到目的地址处 netif_linkoutput_fn linkoutput; //函数指针,用于将IP层的数据包发送到目的地址处 /** This field can be set by the device driver and could point * to state information for the device. */ void *state; u16_t mtu; //最大可传送单元 u8_t hwaddr_len; //表示该网卡的物理地址长度 u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //表示该网卡的具体地址信息 u8_t flags; //网络接口的状态、属性信息字段,包括网络接口的软件使能、广播属性、ARP属性等重要控制位 char name[2];//用于保存每个网络接口的名字,可用于标识网络接口的种类 u8_t num; //用于为每个网络接口设置一个编号,用于唯一的标识各个网络接口 #if ENABLE_LOOPBACK //当LWIP环回接口功能LOOPBACK启用的情况下 /* List of packets to be queued for ourselves. */ struct pbuf *loop_first; //指向上层发往环回接口地址(通常为127.0.0.1)处的数据包pbuf链表的第一个pbuf struct pbuf *loop_last;//指向--------最后一个pbuf #endif /* ENABLE_LOOPBACK */ }; /** must be the maximum of all used hardware address lengths across all types of interfaces in use */ #define NETIF_MAX_HWADDR_LEN 6U /** Whether the network interface is 'up'. This is * a software flag used to control whether this network * interface is enabled and processes traffic. * It is set by the startup code (for static IP configuration) or * by dhcp/autoip when an address has been assigned.*/ #define NETIF_FLAG_UP 0x01U /** If set, the netif has broadcast capability. * Set by the netif driver in its init function. */ #define NETIF_FLAG_BROADCAST 0x02U /** If set, the netif is one end of a point-to-point connection. * Set by the netif driver in its init function. */ #define NETIF_FLAG_POINTTOPOINT 0x04U /** If set, the interface is configured using DHCP. * Set by the DHCP code when starting or stopping DHCP. */ #define NETIF_FLAG_DHCP 0x08U /** If set, the interface has an active link * (set by the network interface driver). * Either set by the netif driver in its init function (if the link * is up at that time) or at a later point once the link comes up * (if link detection is supported by the hardware). */ #define NETIF_FLAG_LINK_UP 0x10U /** If set, the netif is an ethernet device using ARP. * Set by the netif driver in its init function. * Used to check input packet types and use of DHCP. */ #define NETIF_FLAG_ETHARP 0x20U /** If set, the netif is an ethernet device. It might not use * ARP or TCP/IP if it is used for PPPoE only.*/ #define NETIF_FLAG_ETHERNET 0x40U /** If set, the netif has IGMP capability. * Set by the netif driver in its init function. */ #define NETIF_FLAG_IGMP 0x80U /** Function prototype for netif init functions. Set up flags and output/linkoutput * callback functions in this function. * @param netif The netif to initialize */ typedef err_t (*netif_init_fn)(struct netif *netif); /** Function prototype for netif->input functions. This function is saved as 'input' * callback function in the netif struct. Call it when a packet has been received. * @param p The received packet, copied into a pbuf * @param inp The netif which received the packet */ typedef err_t (*netif_input_fn)(struct pbuf *p, struct netif *inp); /** Function prototype for netif->output functions. Called by LWIP when a packet * shall be sent. For ethernet netif, set this to 'etharp_output' and set * 'linkoutput'. * @param netif The netif which shall send a packet * @param p The packet to send (p->payload points to IP header) * @param ipaddr The IP address to which the packet shall be sent */ typedef err_t (*netif_output_fn)(struct netif *netif, struct pbuf *p, ip_addr_t *ipaddr); /** Function prototype for netif->linkoutput functions. Only used for ethernet * netifs. This function is called by ARP when a packet shall be sent. * @param netif The netif which shall send a packet * @param p The packet to send (raw ethernet packet) */ typedef err_t (*netif_linkoutput_fn)(struct netif *netif, struct pbuf *p); |
上面的netif结构去掉了部分条件编译选项,只列出了部分重要字段。
1.3 网络接口的操作
对网络接口的操作主要有初始化、添加、移除、设置、启用、禁用等几种,其中网络接口添加函数包含了对netif结构的初始化与函数指针的注册等操作。netif_add函数完成了netif结构体的初始化,注册了数据包接收函数并回调了网络接口初始化函数,最后将该网络结构插入到netif_list链表中。网络接口的其他操作函数如下表示:
操作函数 | 功能描述 |
void netif_set_addr(struct netif *netif, ip_addr_t *ipaddr, ip_addr_t *netmask, ip_addr_t *gw) | 重新设置该网络接口的IP地址、子网掩码、网关地址 |
void netif_set_ipaddr(struct netif *netif, ip_addr_t *ipaddr) | 重新设置该网络接口的IP地址 |
void netif_set_netmask(struct netif *netif, ip_addr_t *netmask) | 重新设置该网络接口的子网掩码 |
void netif_set_gw(struct netif *netif, ip_addr_t *gw) | 重新设置该网络接口的网关地址 |
void netif_remove(struct netif * netif) | 从netif_list中移除该网络接口,若该接口正使用需先禁用 |
struct netif *netif_find(char *name) | 根据网络接口名查找并返回对应的接口结构netif |
void netif_set_default(struct netif *netif) | 将该网络接口netif设为系统默认网络接口 |
void netif_set_up(struct netif *netif) | 启用该网络接口 |
void netif_set_down(struct netif *netif) | 禁用该网络接口 |
void netif_set_link_up(struct netif *netif) | 网卡底层打开,表示其可进行链路数据的收发 |
void netif_set_link_down(struct netif *netif) | 网卡底层关闭,表示其不能再接收链路层数据 |
需要提醒的一点是,如果协议栈运行过程中想动态更改某网络接口的IP地址,需要在重新设置网卡地址前先禁用该网卡,当地址重新设置后再启用该网卡,否则可能会导致内存访问错误等问题。
1.4 特殊的环回接口
环回接口可以实现对网络接口功能的完全软件模拟,使得我们可以在没有网络硬件的情况下也可以使用并调试网络协议栈;另外,环回接口为同一台设备上的两个进程间的数据通信提供了一种可行方式。环回接口采用环回IP地址,根据通常惯例,环回接口的IP地址被设为127.0.0.1,也就是本地ping本地。
当IP层要发送数据包的目的地址是环回接口时,环回接口注册的数据包发送函数(netif_loop_output)被调用,该函数不会像以太网接口那样使得数据包发送到物理网络中,而只是简单地将该数据包连接到自己netif结构中的loop_first链表上,该链表上的数据包只能通过调用函数netif_poll才会被递交给IP层,当有操作系统模拟层时netif_poll会被内核自动调用,无操作系统模拟层时则需用户自己在程序中调用netif_poll。两个应用程序通过环回接口进行数据交互的流程如下图示:
要使用环回接口功能,需要在配置文件中定义ENABLE_LOOPBACK(也即定义LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF),跟环回接口操作相关的函数有netif_loop_output、netif_poll、netif_poll_all。函数netif_poll将结构netif中loop_first链表上的所有数据包依次递交给IP层,从上面的代码可以看出,一个pbuf链表中可以保存多个数据包,只有通过判断pbuf中的tot_len字段与len字段的值相等才能确定一个数据包已经结束。
更多内容详见下一节:LWIP协议栈解析(五)——网际寻址与路由