基于RT-Thread的lwip网卡优化笔记
- 一、RT-Thread的lwip框架
- 二、网卡驱动
- 三、网卡吞吐速率测试
- 四、网卡吞吐速率优化
- 4.1 TCP参数优化
- 4.2 lwip参数优化
- 4.3 内存拷贝优化
- 4.3.1 rt_memcpy优化
- 4.3.2 使用uboot下的memcpy.S
- 4.4 网卡收发优化
- 4.3.1 lwip发送优化
- 4.4.2 网卡发送优化
一、RT-Thread的lwip框架
RT-Thread的网络框架主要是实现eth_device,并通过eth_device_init注册,下面是eth_device的定义。
struct eth_device
{
/* inherit from rt_device */
struct rt_device parent;
/* network interface for lwip */
struct netif *netif;
struct rt_semaphore tx_ack;
rt_uint16_t flags;
rt_uint8_t link_changed;
rt_uint8_t link_status;
/* eth device interface */
struct pbuf* (*eth_rx)(rt_device_t dev);
rt_err_t (*eth_tx)(rt_device_t dev, struct pbuf* p);
};
网卡移植主要是要在驱动层实现eth_rx和eth_tx。eth_rx即网卡接收,eth_tx即网卡发送。一般来说,网卡驱动需要结合dma来使用,例如当需要发送数据时,lwip最终会调用到eth_tx函数指针进行发送,lwip将要发送的数据放到了pbuf中,我们需要从pbuf从读取数据放入网卡发送DMA中,启动发送;当网卡收到数据时,emac将产生中断,在中断里会唤醒接收线程,这个接收线程会调用eth_rx函数指针接收数据,我们需要申请pbuf,并将网卡DMA中的数据拷贝到pbuf中,最后给lwip返回这个pbuf。
lwip的ethernetif.c是一个供移植的文件,在RT-Thread上,RT-Thread已经全都定义好了,从下面的代码可以看出,eth_system_device_init_private函数创建了两个线程:erx和etx。一般来说,LWIP_NO_RX_THREAD宏都是没定义的,也就是erxmb信号量和erx线程会被创建,当emac接收数据并产生中断时,会调用eth_device_ready函数发送erxmb信号量唤醒erx接收线程,这个线程再去调用eth_rx接收函数。LWIP_NO_TX_THREAD宏可以定义也可以不定义,当定义这个宏时,发送数据时将会直接在lwip的tcpip线程调用eth_tx发送函数,当未定义这个宏时,发送的数据将会由tcpip线程通过邮箱的形式传给etx线程,由etx线程发出。
int eth_system_device_init_private(void)
{
rt_err_t result = RT_EOK;
/* initialize Rx thread. */
#ifndef LWIP_NO_RX_THREAD
/* initialize mailbox and create Ethernet Rx thread */
result = rt_mb_init(ð_rx_thread_mb, "erxmb",
ð_rx_thread_mb_pool[0], sizeof(eth_rx_thread_mb_pool)/4,
RT_IPC_FLAG_FIFO);
RT_ASSERT(result == RT_EOK);
result = rt_thread_init(ð_rx_thread, "erx", eth_rx_thread_entry, RT_NULL,
ð_rx_thread_stack[0], sizeof(eth_rx_thread_stack),
RT_ETHERNETIF_THREAD_PREORITY, 16);
RT_ASSERT(result == RT_EOK);
result = rt_thread_startup(ð_rx_thread);
RT_ASSERT(result == RT_EOK);
#endif
/* initialize Tx thread */
#ifndef LWIP_NO_TX_THREAD
/* initialize mailbox and create Ethernet Tx thread */
result = rt_mb_init(ð_tx_thread_mb, "etxmb",
ð_tx_thread_mb_pool[0], sizeof(eth_tx_thread_mb_pool)/4,
RT_IPC_FLAG_FIFO);
RT_ASSERT(result == RT_EOK);
result = rt_thread_init(ð_tx_thread, "etx", eth_tx_thread_entry, RT_NULL,
ð_tx_thread_stack[0], sizeof(eth_tx_thread_stack),
RT_ETHERNETIF_THREAD_PREORITY, 16);
RT_ASSERT(result == RT_EOK);
result = rt_thread_startup(ð_tx_thread);
RT_ASSERT(result == RT_EOK);
#endif
return (int)result;
}
下面的代码是注册lwip网卡驱动的过程。
struct emac_device *edev = &g_emac_device[EMAC0_100M];
lwip_sys_init();
edev->module = EMAC0_100M;
edev->irq = VC0768_IRQ_EMAC0_DMA;
edev->mac_base = VC0768_EMAC0_BASE;
edev->dma_base = VC0768_EMAC0_BASE + REG_EMAC_DMA_OFFSET;
edev->phy_type = PHY_TYPE_INT;
edev->phy_addr = DEFAULT_INT_PHY_ADDR;
edev->speed = SPEED100M;
edev->duplex = FULLDUPLEX;
edev->auto_nego = true;
sprintf(edev->name, "%s", EMAC0_NAME);
rt_memcpy(edev->dev_addr, mac_addr, sizeof(mac_addr));
edev->tx_buf = (u32 *)rt_malloc_align(EMAC_TX_BUF_SIZE * TRANSMIT_DESC_SIZE, RT_CPU_CACHE_LINE_SZ);
edev->rx_buf = (u32 *)rt_malloc_align(EMAC_RX_BUF_SIZE * RECEIVE_DESC_SIZE, RT_CPU_CACHE_LINE_SZ);
memset(edev->tx_buf, 0, EMAC_TX_BUF_SIZE);
memset(edev->rx_buf, 0, EMAC_RX_BUF_SIZE);
emac_clk_init(edev);
emac_phy_init(edev);
emac_dma_init(edev);
emac_mac_init(edev);
emac_tx_sem = rt_sem_create("emac rx on sem", 0, RT_IPC_FLAG_FIFO);
edev->parent.eth_rx = emac_rx;
edev->parent.eth_tx = emac_tx;
edev->parent.parent.init = emac_ops.init;
edev->parent.parent.control = emac_ops.control;
rt_kprintf("%s %d\n", __func__, __LINE__);
eth_device_init(&(edev->parent), edev->name);
request_irq(edev->irq, vmc_interrupt, 0, edev->name, edev);
enable_irq(edev->irq);
netif_set_link_up(edev->parent.netif);
二、网卡驱动
VC0768有两个EMAC,分别是EMAC0和EMAC1,其中EMAC0最高支持到100Mbps,EMAC1最高支持到1000Mbps,EMAC0内部自带PHY,外部只需加上网络变压器即可通信,EMAC1则需要外接千兆PHY,例如开发板上接的就是Realtek的RTL8211F。在软件配置上,EMAC0和EMAC1的配置是差不多的,但EMAC1需要额外配置相应的引脚。
网卡驱动移植最重要的就是配置网卡DMA,VC0768的网卡DMA分为发送DMA和接收DMA,以接收DMA举例。DMA的缓存的个数以及每个块的大小都是可以配置的,例如uboot中配置的DMA单个接收块有256个,每个块大小为2048字节(实际大于1518字节即可),DMA有链式(chain)模式和环形(ring)模式,以环形模式来说,在逻辑上,这就像是个环形的队列,队列的每个块大小是固定的。当EMAC接收到数据时,会自动放入到DMA的某块缓存中,下一次接收时,将自动放入下一块缓存中,即其本身具备地址自增功能。这就是一个最经典的生产者和消费者模型,需要确保DMA的数据及时读取,否则所有的DMA数据块被写满后,将会丢包。代码中可以判断对应块的status来判断这个块当前时属于CPU还是属于DMA,属于CPU即代码可以访问,属于DMA即这个块DMA正在使用。
三、网卡吞吐速率测试
网卡吞吐速率测试一般使用iperf工具,iperf有很多版本,最新的是iperf3,iperf3的功能比较齐全。但iperf和iperf3并不通用,有些设备只支持iperf,那么对端也只能使用iperf。最简单的就是服务器端执行iperf -s,客户端执行iperf -c xx:xx:xx:xx,xx为服务端的ip地址。iperf还可以设置发包数量、tcp窗口大小等参数。iperf是个命令行工具,Jperf则是基于iperf的图形化工具,可以简单轻松的对iperf进行配置,如下所示。
四、网卡吞吐速率优化
网卡吞吐速率优化是一个比较繁琐的性能优化过程,里面涉及到很多,包括TCP参数、lwip参数、内存拷贝效率、网卡收发逻辑等,下面是几个比较重要的优化点。
4.1 TCP参数优化
- TCP_MSS:即Maxitum Segment Size,TCP单次传输最大的有效字节数,这个值最大可以为1460字节,因此需要改成1460字节。
- TCP_WND:即TCP的滑动窗口大小,不过实测这个值对吞吐速率影响不大,窗口大小是会双方自动调节的。
4.2 lwip参数优化
lwip的opt.h文件有很多lwip的默认配置,如果想修改配置,修改lwipopt.h文件即可。lwip的参数优化主要就是修改lwipopt.h这个文件。
-
内存申请方式优化:lwip共有三种内存分配方式,第一种是使用C库的malloc,第二种是lwip自己实现的动态内存堆分配,第三种是使用lwip自己实现的动态内存池分配。C库的malloc是最不推荐使用的,内存大的情况下,推荐使用内存池分配。内存池相比于内存堆,优点是分配和释放速度快且不会产生内存碎片,缺点是容易浪费空间,因为内存池里的块大小是不变的。具体的改动如下。各种类型的pbuf的个数、大小也需要合理设置,不至于在吞吐速率高时,pbuf不够用。
#define MEM_LIBC_MALLOC 0 #define MEM_USE_POOLS 1 #define MEMP_USE_CUSTOM_POOLS 1
-
LWIP_NETIF_TX_SINGLE_PBUF宏需要改为0,虽然官方说这个宏是为了让多个pbuf尽可能合成一个,有利于DMA发送,但实际上这个宏定义了之后会有拷贝的操作,影响效率,需要斟酌是否需要改为0。
-
LWIP_STATS宏设置为0,这个是lwip内部统计功能,关闭可以提高代码执行效率。
4.3 内存拷贝优化
4.3.1 rt_memcpy优化
系统主频为1.2GHz,但实测拷贝12.5MB(100Mbit)数据,使用原生的rt_memcpy需要600ms,这会验证影响网卡的收发效率,因为lwip里面有不少pbuf拷贝的操作,例如网卡发送时,需要将pbuf中的payload拷贝到网卡DMA中发送。看rt_memcpy的源码可以发现,4字节对齐时,以long为字长进行拷贝,当内存非4字节对齐时,直接使用单字节方式进行拷贝,没有考虑2字节对齐的情况。且4字节拷贝时,可以使用neon指令进行拷贝,速度会更快。因此可以对rt_memcpy做如下优化:
void *rt_memcpy(void *dst, const void *src, rt_ubase_t count)
{
#define UNALIGNED(X, Y) \
(((long)X & (sizeof (long) - 1)) | ((long)Y & (sizeof (long) - 1)))
#define BIGBLOCKSIZE (sizeof (long) << 2)
#define LITTLEBLOCKSIZE (sizeof (long))
#define TOO_SMALL(LEN) ((LEN) < BIGBLOCKSIZE)
char *dst_ptr = (char *)dst;
char *src_ptr = (char *)src;
long *aligned_dst;
long *aligned_src;
int len = count;
unsigned short *test_dst;
unsigned short *test_src;
/* 增加neon拷贝 */
if((len > 63) && (!((long)dst_ptr & 3)) && !((long)src_ptr & 3))
{
/* 此处只做演示,可能会溢出 */
if (len & 63)
len = (len & -64) + 64;
asm volatile (
"NEONCopyPLD: \n"
" VLDM %[src]!,{d0-d7} \n"
" VSTM %[dst]!,{d0-d7} \n"
" SUBS %[len],%[len],#0x40 \n"
" BGT NEONCopyPLD \n"
: [dst]"+r"(dst), [src]"+r"(src), [len]"+r"(len) : : "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "cc", "memory");
return dst_ptr+len;
}
/* 原生的4字节拷贝 */
if (!TOO_SMALL(len) && !UNALIGNED(src_ptr, dst_ptr))
{
aligned_dst = (long *)dst_ptr;
aligned_src = (long *)src_ptr;
while (len >= BIGBLOCKSIZE)
{
*aligned_dst++ = *aligned_src++;
*aligned_dst++ = *aligned_src++;
*aligned_dst++ = *aligned_src++;
*aligned_dst++ = *aligned_src++;
len -= BIGBLOCKSIZE;
}
while (len >= LITTLEBLOCKSIZE)
{
*aligned_dst++ = *aligned_src++;
len -= LITTLEBLOCKSIZE;
}
dst_ptr = (char *)aligned_dst;
src_ptr = (char *)aligned_src;
}
/* 增加2字节对齐时的拷贝 */
if( !((long)src_ptr & 0x01) && !((long)dst_ptr & 0x01))
{
test_dst = (unsigned short*)dst_ptr;
test_src = (unsigned short*)src_ptr;
while (len > 1)
{
*test_dst++ = *test_src++;
len -= 2;
}
dst_ptr = (char *)test_dst;
src_ptr = (char *)test_src;
}
/* 原生的单字节拷贝 */
while (len--)
{
*dst_ptr++ = *src_ptr++;
}
return dst;
#undef UNALIGNED
#undef BIGBLOCKSIZE
#undef LITTLEBLOCKSIZE
#undef TOO_SMALL
#endif
}
优化的效果如下:
- rt_memcpy优化2字节拷贝后udp发送速率从30Mbps提升到48Mbps,TCP发送速率从22Mbps提升到30Mbps,tcp接收仍然是6Mbps。
- rt_memcpy4字节对齐时加入neon指令进行拷贝,udp发送仍然是48Mbps,TCP发送速率从30Mbps提升到36Mbps,TCP接收仍然是6Mbps。
优化2字节对齐的拷贝,性能有较明显的提升,原因是Ethernet II头部是6字节目的地址+6字节源地址+2字节长度,共14字节,后面紧跟着payload,如下图所示。当payload是4字节对齐时,目的地址所在的内存是2字节对齐的。lwip发送数据时,传下来的pbuf->payload就是这样的情况,pbuf->payload是一个2字节对齐的地址,而不是4字节对齐的,因此优化2字节对齐时,对lwip整体性能也有提升。
网上有一个开源的rt_memcpy加强版,使用汇编指令,实测效果比neon指令略差一些,但比原生的rt_memcpy要强得多。地址如下:https://github.com/mysterywolf/rt_memcpy_cm
4.3.2 使用uboot下的memcpy.S
实测同一段内存拷贝测试程序,在uboot下执行只需要8ms,而在melis下执行需要102ms,差了整整10倍多。因此将uboot下arch/arm/lib/memcpy.S直接拿来用,实测仍然是102ms,没有提升。可能是跳转到melis系统后,哪里没有设置对,待排查,TCP接收速率也明显不行,也需要排查。
4.4 网卡收发优化
4.3.1 lwip发送优化
前文提到了lwip在发送报文时,如果没有定义LWIP_NO_TX_THREAD宏,那么发送的数据会通过邮箱传递给etx线程,由etx线程代发,缺点是这里引入了一次操作系统的调度,这是需要时间上的开销的,优点是tcpip线程只需要专心处理数据,隔离性比较好。如果追求性能,可以考虑定义LWIP_NO_TX_THREAD这个宏。
4.4.2 网卡发送优化
前文提到,DMA有多个块,在中断里会判断发送是否完成,并释放发送完成的信号量。实际上网卡发送时,不必每次都等待这个信号量,因为当前发送可能正在进行,但是可以把数据装载到下一个DMA块里。所以发送时应直接取下一块DMA块,判断当前是否属于CPU,如果是属于CPU就直接使用,无需等待信号量;如果是属于DMA,说明所有的DMA发送块都写满了,这时候才需要等待发送完成。所以将DMA发送块的个数设置的稍大一些,在一定程度上也是可以提升性能的。