onps栈移植说明(1)——onps栈的配置及裁剪

时间:2022-11-04 15:10:54

       onps栈的移植涉及几个部分:1)系统配置及裁剪;2)基础数据类型定义;3)RTOS适配层实现;4)编写网卡驱动并注册网卡。本文作为onps栈移植的指导性文件将给出一般性的移植说明及建议,具体的移植样例工程及说明请移步码云下载:

关于onps栈的前世今生请移步上一篇博文《开源网络协议栈onps诞生记》。

1. onps栈的配置及裁剪

       协议栈源码(码云/github)port/include/port/sys_config.h文件是协议栈的配置文件。它提供了一系列的配置宏用于裁剪、配置协议栈。我们可以根据目标系统的具体情况对协议栈进行裁剪,调整配置,以减少或增加对系统资源的占用率。配置文件主要涉及几方面的内容:

1)打开或关闭某个功能模块;

2)指定mmu(内存管理单元)管理的内存大小;

3)协议层相关配置项,如缺省ttl值、路由表大小、arp缓存表大小等;

       onps栈在数据链路层支持两种类型的网络接口:ethernet有线以太网络接口;ppp点对点拨号网络接口。用户必须选择其中至少一个接口:

#define SUPPORT_PPP      1 //* 是否支持ppp:1,支持;0,不支持
#define SUPPORT_ETHERNET 1 //* 是否支持ethernet:1,支持;0,不支持

注意,你的目标系统要么支持ppp,要么支持ethernet,要么二者都支持,不能两个都选择不支持,否则协议栈将无法正常工作。另外协议栈还提供了几个常用的网络工具供用户选择使用,用户可以根据具体应用情形选择打开或关闭相关工具:

//* 网络工具配置项,0:不支持;1:支持,协议栈将编译连接工具代码到目标系统
//* ===============================================================================================
#define NETTOOLS_PING       1 //* ping工具,确定目标网络地址是否能到达
#define NETTOOLS_DNS_CLIENT 1 //* dns查询客户端,通过指定的dns服务器查询请求域名对应的ip地址
#define NETTOOLS_SNTP       1 //* sntp客户端,通过指定的ntp服务器进行网络校时
//* ===============================================================================================

考虑协议栈的目标系统可能无法提供pc下常见的文件存储系统,所以协议栈的调试日志等信息是通过标准输出提供的:

#define SUPPORT_PRINTF 1 //* 是否支持调用printf()输出相关调试或系统信息
#if SUPPORT_PRINTF
  #define PRINTF_THREAD_MUTEX 1 //* 是否支持使用printf线程互斥锁,确保不同线程的调试输出信息不被互相干扰,值为1则支持互斥锁
  #define DEBUG_LEVEL         1	//* 共5个调试级别:
                                //* 0 输出协议栈底层严重错误
                                //* 1 输出所有系统错误(包括0级错误)
                                //* 2 输出协议栈重要的配置、运行信息,同时包括0、1级信息
                                //* 3 输出网卡的原始通讯通讯报文(ppp为收发,ethnernet为发送),以及0、1、2级信息
                                //* 4 输出ethernet网卡接收的原始通讯报文,被协议栈丢弃的非法(校验和错误、通讯链路不存在等原因)通讯报文,以及0、1、2、3级信息(除ethernet发送的原始报文)                                    
#endif

基本上所有单片机系统均会提供几个串行口,我们只需选择其中一个将其作为printf函数的标准输出口,我们就可以使能协议栈支持日志输出功能,通过printf()函数输出的日志信息对目标系统进行调试。如果你的目标系统支持某个串口作为printf()函数的标准输出口,建议将SUPPORT_PRINTF宏置1,打开协议栈的日志输出功能。PRINTF_THREAD_MUTEX宏用于解决多线程环境下日志输出的冲突问题。如果你的目标系统互斥资源够用,建议打开该功能,否则你在标准输出口看到的日志会出现乱序问题。

       协议栈在很多情形下需要动态申请不同大小的内存以供接下来的逻辑处理过程使用。所以,为了最大限度地提高协议栈运行过程中的内存利用率并尽可能地减少内存碎片,我们还单独设计了一个独立的内存管理单元(mmu)。考虑协议栈的目标系统为资源受限的单片机系统,这种系统的内存资源往往都是极度紧张的,因此我们提供了配置宏让用户决定分配多少字节的内存空间给协议栈的mmu:

//* 内存管理单元(mmu)相关配置项,其直接影响协议栈能分配多少个socket给用户使用
//* ===============================================================================================
#define BUDDY_PAGE_SIZE  32   //* 系统能够分配的最小页面大小,其值必须是2的整数次幂
#define BUDDY_ARER_COUNT 9    //* 指定buddy算法管理的内存块数组单元数量

#define BUDDY_MEM_SIZE   8192 //* buddy算法管理的内存总大小,其值由BUDDY_PAGE_SIZE、BUDDY_ARER_COUNT两个宏计算得到:
                              //* 32 * (2 ^ (9 - 1)),即BUDDY_MEM_SIZE = BUDDY_PAGE_SIZE * (2 ^ (BUDDY_ARER_COUNT - 1))
                              //* 之所以在此定义好要管理的内存大小,原因是buddy管理的内存其实就是一块提前分配好的静态存储时期的字节型
                              //* 一维数组,以此来确保协议栈不占用宝贵的堆空间
//* ===============================================================================================

协议栈的内存管理单元采用了buddy伙伴算法。上述三个宏的关系参见BUDDY_MEM_SIZE宏的注释。前面说过,mmu管理的内存用于协议栈的不同业务情形,其中最核心的一种业务情形就是socket,用户分配的内存大小直接决定了用户编写网络应用时能够申请的socket数量。如果你在申请分配一个新的socket时报ERRREQMEMTOOLARGE(The requested memory is too large, please refer to the macro definition BUDDY_MEM_SIZE)或ERRNOFREEMEM(The mmu has no memory available)错误,则意味着内存已经不够用了,需要你增加内存或者检视你的代码看是否存在未及时释放的socket句柄。另外,决定内存利用效率的关键配置项是BUDDY_PAGE_SIZE宏,因为mmu分配内存的最小单位就是“页”。这个宏设置单个内存页的大小,单位为字节,其值必须是2的整数次幂。如果你的通讯报文不大,建议把页面大小调整的小一些,比如16字节、32字节等,以尽量减少单个页面的空余字节数。

       sys_config.h文件的其余宏均为协议层相关的配置项。这其中有几个与底层网络接口相关的配置项需要特别关注:

#define SUPPORT_PPP 1 //* 是否支持ppp模块:1,支持;0,不支持,如果选择支持,则系统会将ppp模块代码加入到协议栈中
#if SUPPORT_PPP
  #define APN_DEFAULT           "4gnet"    //* 根据实际情况在这里设置缺省APN
  #define AUTH_USER_DEFAULT     "card"     //* ppp认证缺省用户名
  #define AUTH_PASSWORD_DEFAULT "any_char" //* ppp认证缺省口令

  #define PPP_NETLINK_NUM      1 //* 协议栈加载几路ppp链路(系统存在几个modem这里就指定几就行)
  #define SUPPORT_ECHO         1 //* 对端是否支持echo链路探测
  #define WAIT_ACK_TIMEOUT_NUM 5 //* 在这里指定连续几次接收不到对端的应答报文就进入协议栈故障处理流程(STACKFAULT),这意味着当前链路已经因严重故障终止了
#else
  #define PPP_NETLINK_NUM 0
#endif

#define SUPPORT_ETHERNET 1 //* 是否支持ethernet:1,支持;0,不支持
#if SUPPORT_ETHERNET
  #define ETHERNET_NUM 1  //* 要添加几个ethernet网卡(实际存在几个就添加几个)    
  #define ARPENTRY_NUM 32 //* arp条目缓存表的大小,只要不小于局域网内目标通讯节点的个数即可确保arp寻址次数为1,否则就会出现频繁寻址的可能,当然这也不会妨碍正常通讯逻辑,只不过这会降低通讯效率    
#else
  #define ETHERNET_NUM 0
#endif

如果目标系统需要用到ppp拨号,我们在打开协议栈对ppp模块的支持后还需要设置缺省的拨号参数值,比如apn、拨号账号及密码等。当然你也可以不用设置,后面我们在编写os适配层接口的时候也会设置这几项。系统会使用os适配层的设置值代替缺省值。另外协议栈在设计之初即考虑支持多路ppp同时拨号的情形,目标系统支持几路ppp,宏PPP_NETLINK_NUM值置几即可。SUPPORT_ECHO宏指定ppp链路是否启用echo回显探测功能。某些ppp接入服务商可能会关闭此项功能,如果你不确定,建议缺省情况下关闭此功能。因为echo链路探测功能一旦被启用,协议栈会每隔一小段时间发送探测报文到对端。对端如果不支持此功能会丢弃该探测报文不做任何响应,这将导致协议栈判定ppp链路故障,从而主动结束链路、重新拨号。

       协议栈同样支持多路ethernet网卡,ETHERNET_NUM宏用于指定目标系统存在几路ethernet网卡。这里需要特别注意的是ARPENTRY_NUM宏,这个宏用于指定ethernet网络环境下进行通讯时mac地址缓存表的大小。如果缓存表过小,进行通讯的目标地址并不在缓存表中时,协议栈会先发送arp查询报文,得到对端的mac地址后才会发送实际的通讯报文。虽然这一切都是协议栈自动进行的,但通讯效率会受到影响。如果目标系统的内存够用,建议放大缓存表的容量,最合理的大小是等于计划通讯的目标地址的数量。       

       其余协议层相关的配置项均属于ip及其支持的上层协议:

//* ip支持的上层协议相关配置项
//* ===============================================================================================
#define SUPPORT_IPV6 0	//* 是否支持IPv6:1,支持;0,不支持
#define SUPPORT_SACK 0  //* 系统是否支持sack项,sack项需要协议栈建立发送队列,这个非常消耗内存,通用版本不支持该项

#define ICMPRCVBUF_SIZE_DEFAULT 128   //* icmp发送echo请求报文时指定的接收缓冲区的缺省大小,注意,如果要发送较大的ping包就必须指定较大的接收缓冲区

#define TCPRCVBUF_SIZE_DEFAULT  2048  //* tcp层缺省的接收缓冲区大小,大小应是2^n次幂才能最大限度不浪费budyy模块分配的内存
#define TCPUDP_PORT_START       20000 //* TCP/UDP协议动态分配的起始端口号
#define TCP_WINDOW_SCALE        0     //* 窗口扩大因子缺省值
#define TCP_CONN_TIMEOUT        30    //* 缺省TCP连接超时时间
#define TCP_ACK_TIMEOUT         3     //* 缺省TCP应答超时时间
#define TCP_MSL                 15    //* 指定TCP链路TIMEWAIT态的最大关闭时长:2 * TCP_MSL,单位:秒
#define TCP_LINK_NUM_MAX        16    //* 系统支持最多建立多少路TCP链路(涵盖所有TCP客户端 + TCP服务器的并发连接数),超过这个数量将无法建立新的tcp链路

#if SUPPORT_ETHERNET
  #define TCPSRV_BACKLOG_NUM_MAX 10 //* tcp服务器支持的最大请求队列数量,任意时刻所有已开启的tcp服务器的请求连接队列数量之和应小于该值,否则将会出现拒绝连接的情况
  #define TCPSRV_NUM_MAX         2  //* 系统能够同时建立的tcp服务器数量
  #define TCPSRV_RECV_QUEUE_NUM  64 //* tcp服务器接收队列大小,所有已开启的tcp服务器共享该队列资源,如果单位时间内到达所有已开启tcp服务器的报文数量较大,应将该值调大
#endif

#define UDP_LINK_NUM_MAX 4  //* 调用connect()函数连接对端udp服务器的最大数量(一旦调用connect()函数,收到的非服务器报文将被直接丢弃)
#define SOCKET_NUM_MAX   16 //* 系统支持的最大SOCKET数量,如实际应用中超过这个数量则会导致用户层业务逻辑无法全部正常运行(icmp/tcp/udp业务均受此影响),其值应大于等于TCP_LINK_NUM_MAX值
#define IP_TTL_DEFAULT   64 //* 缺省TTL值
#define ROUTE_ITEM_NUM   8  //* 系统路由表数量
//* ===============================================================================================

目前协议栈暂不支持ipv6也不支持tcp sack选项(后续版本会支持),所以SUPPORT_IPV6和SUPPORT_SACK两个宏不要做任何改动,始终为0即可。ICMPRCVBUF_SIZE_DEFAULT宏与ping工具有关,如果你不想使用ping工具可以将这个值设小一些以节省内存。TCP_WINDOW_SCALE宏建议不要做任何调整,对于内存空间有限的单片机系统tcp窗口直接使用指定值即可。TCP_ACK_TIMEOUT宏用于指定tcp报文发送到对端后等待对端回馈tcp ack报文的超时时间,单位:秒。UDP_LINK_NUM_MAX宏决定了目标系统在使用udp通讯时,能够建立的udp客户端的最大数量。比如目标系统需要建立5个udp客户端,由于UDP_LINK_NUM_MAX值为4,那么只有4个客户端能正常调用connect()函数,第5个客户端在调用connect()函数时会报ERRNOUDPLINKNODE(the udp link list is empty)错误。ROUTE_ITEM_NUM宏用于指定系统缓存的路由条目数量,你可以根据实际网络情形调整这个值,但不能低于目标系统注册的网卡数量。协议层相关的其它配置项请根据注释自行依据实际情况进行调整即可。