3 onps栈移植说明——添加网卡( 二 )

ethernet_add()函数提供的参数看起来较为复杂,但其实就完成了一件事情:告诉协议栈这个新增加的网卡的相关身份信息及功能接口,包括名称、地址、数据读写接口等 。这个函数有两个地方需要特别说明:一个是样例代码中该函数的返回值保存在了一个静态存储时期的变量l_pstNetifEth中;另一个是入口参数pfunStartTHEmacRecv 。前一个用于接收注册成功后返回的PST_NETIF指针;后一个则是需要提供一个线程启动函数,启动协议栈内部的以太网接收线程thread_ethernet_ii_recv(),该线程在协议栈源码ethernet.c文件中实现 。PST_NETIF指针非常重要,它是网卡能够正常工作的关键 。报文收发均用到这个指针 。它的生命周期应该与协议栈的生命周期相同,因此这个指针变量在上面的样例代码中被定义成一个静态存储时期的变量,并确保网卡的接收、发送函数均能访问 。pfunStartTHEmacRecv参数指向的函数要实现的功能与前面我们编写的os适配层函数os_thread_onpstack_start()相同,其就是调用os提供的线程启动函数启动thread_ethernet_ii_recv()线程 。比如rt-thread下:
#define THETHIIRECV_PRIO21//* ethernet网卡接收线程(任务)优先级#define THETHIIRECV_STK_SIZE384 * 4 //* 接收线程栈大小,这个栈要相对大一些,太小会报错#define THETHIIRECV_TIMESLICE 10//* 单次任务调度线程能够工作的最大时间片static void start_thread_ethernet_ii_recv(void *pvParam){rt_thread_t tid = rt_thread_create("EthRcv", thread_ethernet_ii_recv, pvParam, THETHIIRECV_STK_SIZE, THETHIIRECV_PRIO, THETHIIRECV_TIMESLICE);if(tid != RT_NULL)rt_thread_startup(tid);}其余os与之类似 。我们启动的这个以太网接收线程完成实际的以太网层的报文接收及处理工作 。其轮询等待网卡接收中断函数发送的报文到达信号,收到信号则立即读取并处理到达的报文 。我们在后面讲述网卡接收函数的移植细节时还会提到这个接收线程 。
对于网卡发送函数,有一点需要注意的是——其原型必须符合协议栈的要求,因为我们在进行网卡注册时还要向协议栈注册发送函数的入口地址 。前面在介绍ethernet_add()注册函数时我们已经给出了发送函数的原型定义,也就是pfunEmacSend参数指向的函数原型 。协议栈的目标系统是资源受限的单片机系统,为了最大限度节省内存,协议栈采用了写时零复制(zero copy)技术,网卡发送函数需要结合协议栈的buf list机制编写实现代码,其伪代码实现如下:
int ethernet_send(SHORT sBufListHead, UCHAR *pubErr){SHORT sNextNode = sBufListHead;UCHAR *pubData;USHORT usDataLen;//* 调用buf_list_get_len()函数计算当前要发送的ethernet报文长度,其由协议栈提供UINT unEthPacketLen = buf_list_get_len(sBufListHead);//* 逐个取出buf list节点发送出去__lblGetNextNode:pubData = https://www.huyubaike.com/biancheng/(UCHAR *)buf_list_get_next_node(&sNextNode, &usDataLen); //* 获取下一个节点,buf_list_get_next_node()函数由协议栈提供if (NULL == pubData) //* 返回空意味着已经到达链表尾部,没有要发送的数据了,直接返回就可以了return (int)unEthPacketLen;//* 启动发送,将取出的数据发送出去,其中pubData指向要发送的数据,usDataLen为要发送的数据长度,这两个值已经通过buf_list_get_next_node()函数得到//* 在这里添加与具体目标网卡相关的数据发送代码……//* 取下一个数据节点goto __lblGetNextNode;}关于buf list,其实现机制其实很简单 。以udp通讯为例,用户要发送数据到对端,会直接调用udp发送函数,将数据传递给udp层 。udp层收到用户数据后,为了节省内存,避免复制,协议栈直接将用户数据挂接到buf list链表上成为链表的数据节点 。接着,udp层会再申请一个节点把udp报文头挂接到数据节点的前面,组成一个拥有两个节点的完整udp报文链表——链表第一个节点挂载udp报文头,第二个节点挂载用户要发送的数据 。至此,udp层的报文封装工作完成,数据继续向ip层传递 。ip层会继续申请一个节点把ip报文头挂接到udp报文头节点的前面,组成一个拥有三个节点的完整ip 报文链表 。ip报文在ip层经过路由选择后被送达数据链路层,也就是ethernet层 。在这一层,协议栈再将ethernet ii报文头挂接到ip报文头节点的前面 。至此,整个报文的封装完成 。协议栈此时会根据网卡注册信息调用对应网卡的ethernet_send()函数将报文发送出去 。ethernet_send()函数的核心处理逻辑就是按照上述机制再依序取出链表节点携带的各层报文数据,然后顺序发送出去 。

经验总结扩展阅读