1 Cavium OCTEON
本文主要参考Cavium的programmer guide和CPU的硬件文档,涉及的内容主要包括Cavium的收发包流程以及针对收发包过程中的特性来进行性能上的调整。
首先从一般的包收发过程来说,一般情况下,网卡收到数据包后通过DMA映射到指定的内存位置,然后中断通知CPU来取数据包,经过几次内存拷贝后到达协议栈。为了加速包的处理效率,一些CPU采用各种协处理器来帮助完成包的处理,经过多年的发展逐渐形成了以FreeScal Cavium Netlogic几家以RICS的MIPS架构为主流的NP处理器。相比于通用处理器所面对的各种各样的需求,网络处理器面对的应用需求是有限的,因此网络处理器逐渐形成了除了收发包流程加速的协处理器外的针对类似于加密解密一类VPN SSL应用的加速协处理器。在Cavium中,前者主要是指:SSO(POW)和PKO,后者则有相应的处理引擎。
2 OCTEON COPROCESSER
Cavium的OCTEON为网络做了大量优化,主要包括数量众多的协处理器,不同的协处理器完成特定的任务,大大简化了软件的复杂度提高了性能,并且能够从硬件上保证一些特性比如包保序。如图是OCTEON的Arch:
主要有:FPA PIP/IPD SSO PKO RAID_Engine FAU
2.1 FPA
FPA-Free Pool Alloctor主要负责分配收发包过程中的packet work entry以及packet的data buffer和PKO command buffer。
对FPA的操作主要有三种:
- buffer_allocte(synchronous): core会等待可用的buffer返回或者是返回的NULL
- buffer_allocte(asynchronous): core不会等待buffer地址返回,而是在之后的时间里会从特定位置接受到buffer地址
- buffer_free(synchronous): 这个操作会把buffer地址返还到特定的FPA pool
FPA的alloc free操作
static inline void *cvmx_fpa_alloc(uint64_t pool) {
adress = cvmx_read_csr(CVMX_ADDR_DID(CVMX_FULL_DID(CMVX_OCT_DID_FPA, pool)));
}
static inline void cmvx_fpa_free(......) {
newptr.u64 = cvmx_ptr_to_phys(ptr);
newptr.sfilldidispace.didspace = CVMX_ADDR_DIDSPACE(CVMX_FULL_DID(CVMX_OCT_DID_FPA,
pool));
cvmx_write_io(newptr.u64, unm_cache_lines);
}
FPA的initialize, cvmx-helper-fpa.c
int cvmx_helper_initialize_fpa(.....) {
return __cvmx_helper_initialize_fpa(
CVMX_FPA_PACKET_POOL, cvmx_fpa_packet_pool_size_get(), packet_buffers,
CVMX_FPA_WQE_POOL, CVMX_FPA_WQE_POOL_SIZE, work_queue_entries,
CVMX_FPA_OUTPUT_BUFFER_POOL, CVMX_FPA_OUTPUT_BUFFER_POOL_SIZE, pko_buffers,
CVMX_FPA_TIMER_POOL, CVMX_FPA_TIMER_POOL_SIZE, tim_buffers,
CVMX_FPA_DFA_POOL, CVMX_FPA_DFA_POOL_SIZE, dfa_buffers,
}
在__cvmx_helper_initialize_fpa_pool()
有我们需要的重要细节:
在原来的SDK中时这样的:
memory = cvmx_bootmem_alloc(buffer_size * buffers, align);
但是代码却是这样:
memory = KMALLOC(buffer_size*buffers + CVMX_CACHE_LINE_SIZE);
区别在于cvmx_bootmeme_alloc()
和KMALLOC()
分别是在哪里分配的内存。
void *cmvx_bootmem_alloc_range(uint64_t size, uint64_t alignment,
uint64_t min_addr, unit64_t max_addr) {
int64_t address;
......
address = cvmx_bootmem_phy_alloc(size, min_addr, max_addr, alignment, 0)
......
return cvmx_phy_to_ptr(address);
......
}
可见address
是physical address。cvmx_bootmem_phy_alloc()
是executive中提供的底层的memory alloc的操作。
FPA的配置,cvmx-config.h
#define CVMX_CACHE_LINE_SIZE (128) //in bytes
#define CVMX_FPA_POOL_0_SZIE (17 * CVMX_CACHE_LINE_SIZE)
#define CVMX_FPA_POOL_1_SIZE (1 * CVMX_CACHE_LINE_SIZE)
#define CVMX_FPA_POOL_2_SIZE (8 * CVMX_CACHE_LINE_SIZE)
#define CVMX_FPA_POOL_3_SIZE (8 * CVMX_CACHE_LINE_SIZE)
#define CVMX_FPA_POOL_4_SZIE (17 * CVMX_CACHE_LINE_SIZE)
#define CVMX_FPA_PACKET_POOL (0) /* packet buffers */
#define CVMX_FPA_PACKET_POOL_SIZE CVMX_FPA_POOL_0_SIZE
int cvmx_fpa_setup_pool(......) {
cvmx_fpa_pool_info[pool].name = name;
cvmx_fpa_pool_info[pool].size = block_size;
cvmx_fpa_pool_info[pool].starting_element_count = num_blocks;
cvmx_fpa_pool_info[pool].base = buffer;
}
主要使用了FPA 0 1 2,0负责在收到packet后存储packet buffer data部分,1负责对packet header部分进行简单hash后的信息存储,2是pko command buffers。在banfflite的一个BUG中人为的在FPA0中分配packet buffer的时候会出现crash的现象,Cavium的人就此给出一个workaround就是建议在FPA4中分配packet buffer去发送这个包,但是OCTEON在硬件上规定收包必须用FPA0.因此packet buffer的大小就是2176。
在cvmx_fpa_setup_pool()
中除了配置名字 block_size主要是配置FPA的base address。
2.2 PIP/IPD
这个协处理器的主要作用就是从interface上比如:SGMII XAUI口上收到数据后由PIP对数据包的头部进行5-tuple的计算后得到这个包的WQE和group tag Qos类提交给SSO所需要的数据,而IPD则是将收到的数据包的数据部分copy到data FPA中申请的fpa buffer中去。这个协处理器提供收包以及包的最简单的CRC 错误检查以及丢弃操作,OCTEON也提供另外一种收包方法,在不用PIP/IPD的时候可以使用内核的NAPI的方式使用cnMIPS core的poll方式来进行收包。
- 检查packet,包括L2/L3头部的错误
- 提供拥塞控制,可在PIP部分丢掉部分包
- 创建WQE(Work Queue Entry)
- 决定提供给SSO的WQE的包属性(Group Qos Tag-Type Tage-Value)
- 把收到的包存储在PIP/IPD内部的buffer和RAM中
- 发送WQE到SSO完成调度
一般配置流程:
- 一个core调用:
cvmx_helper_initialize_packet_io_global()
- 每个core都调用:
cvmx_helper_initialize_packet_io_local()
- 使用
cvmx_pip*
和cvmx_ipd*
来配置PIP/IPD - 使用:
cvmx_helper_ipd_and_packet_input_enable()
打开收包功能
大致的介绍PIP IPD的配置代码:
cvmx_helper_initialize_packet_io_global() {
......
result |= __cvmx_helper_global_setup_ipd();
cvmx_ipd_config(cvmx_fpa_packet_pool_size_get()/8,
CVMX_HELPER_FIRST_MBUFF_SKIP/8,
CVMX_HELPER_NOT_FIRST_MBUFF_SKIP/8,
(CVMX_HELPER_FIRST_MBUFF_SKIP+8) / 128,
/* The +8 is to account for the next ptr */
(CVMX_HELPER_NOT_FIRST_MBUFF_SKIP+8) / 128,
/* The +8 is to account for the next ptr */
CVMX_FPA_WQE_POOL,
CVMX_HELPER_IPD_DRAM_MODE,
1);
......
}
cvmx_ipd_config(.....) {
......
first_skip.u64 = 0;
first_skip.s.skip_sz = first_mbuff_skip;
cvmx_write_csr(CVMX_IPD_1ST_MBUFF_SKIP, first_skip.u64);
not_first_skip.u64 = 0;
not_first_skip.s.skip_sz = not_first_mbuff_skip;
cvmx_write_csr(CVMX_IPD_NOT_1ST_MBUFF_SKIP, not_first_skip.u64);
size.u64 = 0;
size.s.mb_size = mbuff_size;
cvmx_write_csr(CVMX_IPD_PACKET_MBUFF_SIZE, size.u64);
ipd_ctl_reg.u64 = cvmx_read_csr(CVMX_IPD_CTL_STATUS);
ipd_ctl_reg.s.opc_mode = cache_mode;
ipd_ctl_reg.s.pbp_en = back_pres_enable_flag;
cvmx_write_csr(CVMX_IPD_CTL_STATUS, ipd_ctl_reg.u64);
/* Note: the example RED code that used to be here has been moved to
cvmx_helper_setup_red */
}
来着重说说cvmx_ipd_config()
中用到的一些reg:
- IPD_1ST_MBUFF_SKIP: The number of eight-byte words from the top of the first MBUF that the IPD stores the next pointer. Legal values for this field are 0 to 32
- IPD_NOT_1ST_MBUFF_SKIP: The number of eight-byte words from the top of any MBUF that is not the first MBUF that the IPD writes the next-pointer
- IPD_1ST_NEXT_PTR_BACK: Used to find head of buffer from the next pointer header
- IPD_WQE_FPA_QUEUE: Specifies the FPA queue from which to fetch page-pointers for work-queue entries
- IPD_CTL_STATUS:OPC_MODE
IPD_CTL_STATUS:OPC_MODE: Select the style of write to the L2C.
- 0 = all packet data and next-buffer pointers are written through to memory.
- 1 = all packet data and next-buffer pointers are written into the cache.
- 2 = the first aligned cache block holding the packet data and initial next-buffer pointer is written to the L2 cache. All remaining cache blocks are not written to the L2 cache.
- 3 = the first two aligned cache blocks holding the packet data and initial next-buffer pointer is written to the L2 cache. All remaining cache blocks are not written to the L2 cache
如果需要调试PIP/IPD可以从这些寄存器入手。
这个时候的Qos的丢弃策略配置:
cvmx_helper_setup_red()
Per-QoS RED拥塞控制,所有queue都一样cvmx_helper_setup_red_queue()
queue的阀值都不一样
Qos相关的一些拥塞设置:
int cvmx_helper_setup_red(int pass_thresh, int drop_thresh) {
......
cvmx_write_csr(CVMX_IPD_ON_BP_DROP_PKTX(0), 0);
#define IPD_RED_AVG_DLY 1000
#define IPD_RED_PRB_DLY 1000
......
red_delay.s.avg_dly = IPD_RED_AVG_DLY;
red_delay.s.prb_dly = IPD_RED_PRB_DLY;
cvmx_write_csr(CVMX_IPD_RED_DELAY, red_delay.u64);
/*
* Only enable the gmx ports
cvmx_write_csr(CVMX_IPD_RED_BPID_ENABLEX(0), red_bpid_enable.u64);
}
int cvmx_helper_setup_red_queue(int queue, int pass_thresh, int drop_thresh) {
red_marks.s.drop = drop_thresh;
red_marks.s.pass = pass_thresh;
cvmx_write_csr(CVMX_IPD_QOSX_RED_MARKS(queue), red_marks.u64);
......
red_param.s.prb_con = (255ul<<24)/(red_marks.s.pass-red_marks.s.drop);
red_param.s.avg_con = 1;
red_param.s.new_con = 255;
red_param.s.use_pcnt = 1;
cvmx_write_csr(CVMX_IPD_RED_QUEX_PARAM(queue), red_param.u64)
......
}
- IPD_BPID(0..63)_MBUF_TH:
- IPD_ON_BP_DROP_PKT0:
- IPD_RED_DELAY:PRB_DL|YAVG_DLY:
- IPD_RED_BPID_ENABLE0:
2.3 SSO
2.3.1 DID system
DID:Device ID。文件cvmx-address.h中描述了OCTEON II的地址空间
#define CVMX_OCT_DID_PKT_SEND CVMX_FULL_DID(CVMX_OCT_DID_PKT,2ULL)
#define CVMX_OCT_DID_PKT 10ULL
#define CVMX_FULL_DID(did,subdid) (((did) << 3) | (subdid))
简单的换算CVMX_OCT_DID_TAG_SWTAG
:(12<<3 | 0) = 0110 0000得到是:
即为GET_WORK的操作。那这个DID是如何实现的?这个DID的address在OCTEON的那个地址空间?
2.3.2 SSO general
这部分是Cavium OCTEON的多核Soc最为复杂关键的部分。如何将大量的数据均衡的分配到不同的core上去,并且在硬件上需要做到保序的功能。主要功能:
- work queue:
- work secheduling/descheduling:
- ordering and synchronization of work:
介绍下调度的不同的tag类型:
- ORDERED
- ATOMIC
- UNSEHEDULED
SSO处理包的一般流程:
-
收到数据包:ORDERED
PIP/IPD收到包并且存储在FPA中,并且
commit_work()
给SSO -
SSO调度包到core:ORDERED
core调用
get_work()
得到packet,SSO使用schedule调度work到core。保证同一条流的的多个包在不同的core上并行处理,同时多条流并行处理。 -
锁住冲突区域:保证one-at-a-time访问:ATOMIC
-
解锁冲突区域恢复并行处理:ORDERED
- 发送数据包:ATOMIC
简单介绍下如上的状态图:packet一般有三种状态:进入SSO前的unschedule
deschedule应用: 当一个core在当前不能立刻完成当前的work的时候,SSO应该reschedule当前的work,但是这个work在SSO的调度中享有更高的优先级以便更快的被调度到新的core上,以下的几种情形非常有用:
- 可以传输work到另一个core上,可以作为一种实现work pipelining的的机制
- 可以避免某个core卡死在一个schedule太长时间
- 使当前work变成可中断的
如果能确定在deschedule后能够到哪一个core上也许更有意义。
在系统中一般使用到了ORDER类型来实现硬件上的保序。参考Programmer Guide简单的描述一下一个流在ORDER情况下是怎么通过SSO调度通过CPU的。
2.3.3 SSO operation
SSO有很多操作,这里讲一个简单的例子,建立一个SSO操作过程的印象:
- wait:如果被设置,只有到了work可用或者超市的时候才会返回结果
- indexed:如果被设置,返回的值使用到了index,多多个包的情形
- index: GET_WORKs的index entry
- no_work: 没有新的work_queue返回时这个值被置1
- pend_switch:
- addr: work_queue的指针地址
具体的代码的实现:
static inline cvmx_wqe_t * cvmx_pow_work_request_sync_nocheck(cvmx_pow_wait_t wait) {
ptr.u64 = 0;
ptr.swork.mem_region = CVMX_IO_SEG;
ptr.swork.is_io = 1;
ptr.swork.did = CVMX_OCT_DID_TAG_SWTAG;
ptr.swork.wait = wait;
result.u64 = cvmx_read_csr(ptr.u64);
return (cvmx_wqe_t*)cvmx_phys_to_ptr(result.s_work.addr);
}
如果no_work直接返回NULL,否则返回result.s_work.addr
的地址。
2.3.4 SSO code
SSO以前被称作POW,因此代码中很多还是以POW出现:
static inline void cvmx_pow_work_submit(cvmx_wqe_t *wqp, uint32_t tag, cvmx_pow_tag_type_t tag_type,
uint64_t qos, uint64_t grp) {
......
tag_req.s_cn68xx_add.op = CVMX_POW_TAG_OP_ADDWQ;
tag_req.s_cn68xx_add.type = tag_type;
tag_req.s_cn68xx_add.tag = tag;
tag_req.s_cn68xx_add.qos = qos;
tag_req.s_cn68xx_add.grp = grp;
......
ptr.u64 = 0;
ptr.sio.mem_region = CVMX_IO_SEG;
ptr.sio.is_io = 1;
ptr.sio.did = CVMX_OCT_DID_TAG_TAG1;
ptr.sio.offset = cvmx_ptr_to_phys(wqp);
.....
CVMX_SYNCWS;
cvmx_write_io(ptr.u64, tag_req.u64);
}
这个submit在代码里面用的不是很多,主要用于内核发包。先将submit是怎么工作,再说说submit的时候一般过程。如前面所说:tag_tape
tag
qos
grp
是SSO内部作为调度的参数。
最近因为要做一个应用因此格外关注了这部分,一般情况下QLM会被配支撑XAUI接口,SDK这个时候一般会为它绑定一个pko port,相应的这个pko port上有多条queue, 如果说这个XAUI口链接的对端是switch的话,则从XAUI口发送的数据需要下行到GE的链路上去,这个时候如果没有速率控制可能就会在switch上丢包,仔细的看了下文档发现还是有port的速率限制的,只是做的比较粗糙,但聊胜于无总比用软件实现要强的多.
看了文档中关于PKO部分发现主要将queue和pko engine,queue的模式可配,engine的内部存储也是有一定的大小,这样对于内部工作的一个模型有个大概的了解.相对于那种空想内部的工作原理有了直观的认识.
2.4 PKO
需要格外关注cvmx_helper_cfg_init_pko_port_map()
这个函数,这个对interface做PKO的映射,映射完毕后还要设置使用PKO的引擎,这个代码写的比较绕,不容易看懂。还有一个地方__cvmx_helper_cfg_init_ipd2pko_cache()
这个也比较重要。
再来说说这部分的问题.首先PKO这部分没有精细的速率控制机制,虽然又一个pko port的rate limit但是统计也是基于秒的并不是那么准确,如果下行是switch没有足够的buffer来缓存CPU过来的报文就会出现丢包的现象,当然对于XAUI和起上的pko以及queue,本来可以做一些硬件资源抽象或者虚拟化后的精细控制,但是这样对于性能来说是个挑战,做不做都有利弊需要根据当时的情况来考虑.当然我直到这样的问题,在很久以前肯定已经有人碰到过,他们的解决方案?我在看solaris的代码的时候他们的工程师在处理10G链路的时候遇到同样的问题.
那他们怎么处理呢?在对NIC的硬件rx ring的抽象出软件的mac ring,在这些软件mac层进行资源的控制,好比现在的硬件queue对应的NIC上的rx ring,有时我在想OCTEON给我们带来了什么?当然好处是显而易见的,但这个东西并不是放在四海皆准的,当考虑到效能的时候,现在的OCTEON 68xx在吞吐或者MIPS上会被intel的4核的芯片超出很大一段距离.当我们尝试做这样的方案的时候回操作一致的反对,原因很简单,对于性能的损害我们并不能接受,有些时候反而制约了软件上的发展.一款芯片总有应用场景的限制,大概限制就在这里吧,抱着一个方案解决所有case的想法有点不大现实.我现在越来越倾向intel,现在是个好时机,如果在SDN方向上.
3 OCTEON Simple Executive
在一次和同事的交流中,他们说想把executive做成一个内核模块,以一个ko的形式来发布,当时觉得这个想法很不错,Cavium或许可以考虑这种方式,但是后来回去仔细想了想,或许这个想法只有一半好,另外一半可能就需要慎重考虑。
首先我们需要明确的观念是executive并非是最终使用的代码,离工业级的代码相去甚远,只能作为一个参考的sample。其中很多实现,不能满足实际应用场景,比如配置PKO port queue这些参数的过程确实不那么漂亮,但是如果要改,需要深入到那部分难读的代码中去,本身来说其逻辑非常简单,但是用了一种不那么聪明的实现方式,导致最后这部分几乎是不可配置的,如果贸然的修改,轻则导致网络部通,或者直接kernel crash。
其次我们需要executive主要干的什么事情。一般是在配置硬件单元,一半在提供get work和commit work的操作。根据Cavium的官方将其描述成一个thinOS,恐怕这个和thinOS还差很多。
最后需要指出的是在executive中怎么进行CPU model的区别的,将OCTEON分成OCTEON OCTEON_Plus和OCTEON II,举例来说OCTEON II中有很多款比如CN6880和CN6870。总的来说这两者并没有太大的区别,去问FAE的话他们也会这么说,但是具体到硬件单元呢?或许细小的差别比如XAUI的配置参数上,有时候这样的区别并不会在executive中及时的反映出来,因此在不同的款的管理中这部分有点混乱。
为什么说将其发布成ko只是一半好呢?从实现上来说data plane会在初始化部分调用executive中的初始化部分,然后在后面的包处理部分调用其中的get work例程,但是如果是ko,意味着每次调用都会陷入到内核中去即vmlinux。这样的过程是我们应该尽量避免的。
4 Intel 10G Network
4.1 10G Network Driver
Intel的10G网卡驱动分析,本文以Intel 82599控制器为例,内核代码则为IXGBE驱动。驱动提供两种中断模型来进行收包。包中断和NAPI模型,随着NAPI机制的成熟,默认的都会使用到NAPI来进行收包。需要明确的是由于82599主要针对的服务器市场为了提高性能提出了很多比较新的概念比如说IO虚拟化之类,接下来的介绍中会略有提及。
既然作为内核模块,最好的办法就是从module_init()
和module_exit()
来看,这两个点一般就是内核模块的初始化和退出的地方。内核模块的初始化都会在module_init调用的函数中实现。
比如:module_init(ixgbe_init_module)
这个ixgbe_init_module
的主要做的事情如下:
#ifdef CONFIG_IXGBE_DCA
dca_register_notify(&dca_notifier);
#endif
ret = pci_register_driver(&ixgbe_driver);
可以看到是使用PCI register来注册 ixgbe_driver 这个驱动,需要提到的是 CONFIG_IXGBE_DCA
这个宏的意义,DCA的意思是Directly Cache Access,这个听起来和DMA-Directly Memory Access 听起来很像,所实现的主要功能就是让网卡驱动能够直接访问chip上的cache。这是IOAT(Intel IO Acceleration Technology)的一种。会写一篇文章来介绍IOAT,是Intel提出一个体系的方案。
接下来看下ixgbe_driver
做了些什么事情:
.name = ixgbe_driver_name,
.id_table = ixgbe_pci_tbl,
.probe = ixgbe_probe,
.remove = __devexit_p(ixgbe_remove),
#ifdef CONFIG_PM
.suspend = ixgbe_suspend,
.resume = ixgbe_resume,
#endif
.shutdown = ixgbe_shutdown,
.err_handler = &ixgbe_err_handler
一个很标准的网卡驱动内需要做的事情,类似于代码里面的NIC初始化的时候对子卡的IOCTL的设置。一套很通用的机制。这里的CONFIG_PM
指的内核中的Power Management Support,这里是想让NIC在空闲的时候suspend而在使用的时候能够resume。
主要关注的是ixgbe_probe()
。这个才真正的进入到IXGBE的init部分。可能层数有点多,但是符合一般的NIC driver的注册初始化的流程。
要搞清楚ixgbe_probe()
怎么实现以及为什么这么实现,还要需要大致看下Intel 82599的datasheet。大致说一下82599里面自己感觉比较重要的部分:
在82599驱动的整个过程中有两个比较重要的结构体:struct pci_dev *pdev
和 struct ixgbe_adapter *adapter
这两个结构体贯穿整个驱动全过程。IXGBE作为一个PCI的设备,在probe的过程首先是做pci的初始化,把ixgbe挂载到pci设备链中。
驱动的核心在如下数组中:
static const struct ixgbe_info *ixgbe_info_tbl[] = {
[board_82598] = &ixgbe_82598_info,
[board_82599] = &ixgbe_82599_info,
[board_X540] = &ixgbe_X540_info,
};
通过硬件参数来匹配相应的硬件:
const struct ixgbe_info *ii = ixgbe_info_tbl[ent->driver_data];
然后就是如何将不同的网卡驱动挂载到对应的回调中,这里做的很简单,就是通过对应的netdev的结构取得adapter,然后所有的核心操作都是保存在adapter中的,最后将ii的所有回调拷贝给adapter就可以了。
struct net_device *netdev;
struct ixgbe_adapter *adapter = NULL;
struct ixgbe_hw *hw;
.....................................
adapter = netdev_priv(netdev);
pci_set_drvdata(pdev, adapter);
adapter->netdev = netdev;
adapter->pdev = pdev;
hw = &adapter->hw;
hw->back = adapter;
.......
memcpy(&hw->mac.ops, ii->mac_ops, sizeof(hw->mac.ops));
hw->mac.type = ii->mac;
/* EEPROM */
memcpy(&hw->eeprom.ops, ii->eeprom_ops, sizeof(hw->eeprom.ops));
.......
最后需要关注的就是设置网卡属性,这些属性一般来说都是通过ethtool 可以设置的属性(比如tso/checksum等),这里我们就截取一部分:
netdev->features = NETIF_F_SG |
NETIF_F_IP_CSUM |
NETIF_F_IPV6_CSUM |
NETIF_F_HW_VLAN_TX |
NETIF_F_HW_VLAN_RX |
NETIF_F_HW_VLAN_FILTER |
NETIF_F_TSO |
NETIF_F_TSO6 |
NETIF_F_RXHASH |
NETIF_F_RXCSUM;
netdev->hw_features = netdev->features;
switch (adapter->hw.mac.type) {
case ixgbe_mac_82599EB:
case ixgbe_mac_X540:
netdev->features |= NETIF_F_SCTP_CSUM;
netdev->hw_features |= NETIF_F_SCTP_CSUM |
NETIF_F_NTUPLE;
break;
default:
break;
}
netdev->hw_features |= NETIF_F_RXALL;
......
netdev->priv_flags |= IFF_UNICAST_FLT;
netdev->priv_flags |= IFF_SUPP_NOFCS;
if (adapter->flags & IXGBE_FLAG_SRIOV_ENABLED)
adapter->flags &= ~(IXGBE_FLAG_RSS_ENABLED |
IXGBE_FLAG_DCB_ENABLED);
......
if (pci_using_dac) {
netdev->features |= NETIF_F_HIGHDMA;
netdev->vlan_features |= NETIF_F_HIGHDMA;
}
if (adapter->flags2 & IXGBE_FLAG2_RSC_CAPABLE)
netdev->hw_features |= NETIF_F_LRO;
if (adapter->flags2 & IXGBE_FLAG2_RSC_ENABLED)
netdev->features |= NETIF_F_LRO;
然后我们来看下中断的注册,因为万兆网卡大部分都是多对列网卡(配合msix),因此对于上层软件来说,就好像有多个网卡一样,它们之间的数据是相互独立的,这里读的话主要是napi驱动的poll方法,后面我们会分析这个。
到了这里或许要问那么网卡是如何挂载回调给上层,从而上层来发送数据呢,这里是这样子的,每个网络设备都有一个回调函数表(比如ndo_start_xmit)来供上层调用,而在ixgbe中的话,就是ixgbe_netdev_ops,下面就是这个结构,不过只是截取了我们很感兴趣的几个地方。
不过这里注意,读回调并不在里面,这是因为写是软件主动的,而读则是硬件主动的。现在ixgbe是NAPI的,因此它的poll回调是ixgbe_poll,是中断注册时候通过netif_napi_add添加进去的。
static const struct net_device_ops ixgbe_netdev_ops = {
.ndo_open = ixgbe_open,
.ndo_stop = ixgbe_close,
.ndo_start_xmit = ixgbe_xmit_frame,
.ndo_select_queue = ixgbe_select_queue,
.ndo_set_rx_mode = ixgbe_set_rx_mode,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = ixgbe_set_mac,
.ndo_change_mtu = ixgbe_change_mtu,
.ndo_tx_timeout = ixgbe_tx_timeout,
......
.ndo_set_features = ixgbe_set_features,
.ndo_fix_features = ixgbe_fix_features,
};
这里我们最关注的其实就是ndo_start_xmit回调,这个回调就是驱动提供给协议栈的发送回调接口。我们来看这个函数.
它的实现很简单,就是选取对应的队列,然后调用ixgbe_xmit_frame_ring来发送数据。
static netdev_tx_t ixgbe_xmit_frame(struct sk_buff *skb,
struct net_device *netdev)
{
struct ixgbe_adapter *adapter = netdev_priv(netdev);
struct ixgbe_ring *tx_ring;
if (skb->len <= 0) {
dev_kfree_skb_any(skb);
return NETDEV_TX_OK;
}
/*
* The minimum packet size for olinfo paylen is 17 so pad the skb
* in order to meet this minimum size requirement.
*/
if (skb->len < 17) {
if (skb_padto(skb, 17))
return NETDEV_TX_OK;
skb->len = 17;
}
//取得对应的队列
tx_ring = adapter->tx_ring[skb->queue_mapping];
//发送数据
return ixgbe_xmit_frame_ring(skb, adapter, tx_ring);
}
而在ixgbe_xmit_frame_ring中,我们就关注两个地方,一个是tso(什么是TSO,请自行google),一个是如何发送.
tso = ixgbe_tso(tx_ring, first, &hdr_len);
if (tso < 0)
goto out_drop;
else if (!tso)
ixgbe_tx_csum(tx_ring, first);
/* add the ATR filter if ATR is on */
if (test_bit(__IXGBE_TX_FDIR_INIT_DONE, &tx_ring->state))
ixgbe_atr(tx_ring, first);
#ifdef IXGBE_FCOE
xmit_fcoe:
#endif /* IXGBE_FCOE */
ixgbe_tx_map(tx_ring, first, hdr_len);
调用ixgbe_tso处理完tso之后,就会调用ixgbe_tx_map来发送数据。而ixgbe_tx_map所做的最主要是两步,第一步请求DMA,第二步写寄存器,通知网卡发送数据.
dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);
if (dma_mapping_error(tx_ring->dev, dma))
goto dma_error;
/* record length, and DMA address */
dma_unmap_len_set(first, len, size);
dma_unmap_addr_set(first, dma, dma);
tx_desc->read.buffer_addr = cpu_to_le64(dma);
for (;;) {
while (unlikely(size > IXGBE_MAX_DATA_PER_TXD)) {
tx_desc->read.cmd_type_len =
cmd_type | cpu_to_le32(IXGBE_MAX_DATA_PER_TXD);
i++;
tx_desc++;
if (i == tx_ring->count) {
tx_desc = IXGBE_TX_DESC(tx_ring, 0);
i = 0;
}
dma += IXGBE_MAX_DATA_PER_TXD;
size -= IXGBE_MAX_DATA_PER_TXD;
tx_desc->read.buffer_addr = cpu_to_le64(dma);
tx_desc->read.olinfo_status = 0;
}
......
data_len -= size;
dma = skb_frag_dma_map(tx_ring->dev, frag, 0, size,
DMA_TO_DEVICE);
......
frag++;
}
......
tx_ring->next_to_use = i;
/* notify HW of packet */
writel(i, tx_ring->tail);
......
上面的操作是异步的,也就是说此时内核还不能释放SKB,而是网卡硬件发送完数据之后,会再次产生中断通知内核,然后内核才能释放内存.接下来我们来看这部分代码。
首先来看的是中断注册的代码,这里我们假设启用了MSIX,那么网卡的中断注册回调就是ixgbe_request_msix_irqs函数,这里我们可以看到调用request_irq函数来注册回调,并且每个队列都有自己的中断号。
static int ixgbe_request_msix_irqs(struct ixgbe_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
int q_vectors = adapter->num_msix_vectors - NON_Q_VECTORS;
int vector, err;
int ri = 0, ti = 0;
for (vector = 0; vector < q_vectors; vector++) {
struct ixgbe_q_vector *q_vector = adapter->q_vector[vector];
struct msix_entry *entry = &adapter->msix_entries[vector];
......
err = request_irq(entry->vector, &ixgbe_msix_clean_rings, 0,
q_vector->name, q_vector);
if (err) {
e_err(probe, "request_irq failed for MSIX interrupt "
"Error: %d\n", err);
goto free_queue_irqs;
}
/* If Flow Director is enabled, set interrupt affinity */
if (adapter->flags & IXGBE_FLAG_FDIR_HASH_CAPABLE) {
/* assign the mask for this irq */
irq_set_affinity_hint(entry->vector,
&q_vector->affinity_mask);
}
}
......
return 0;
free_queue_irqs:
......
return err;
}
而对应的中断回调是ixgbe_msix_clean_rings,而这个函数呢,做的事情很简单(需要熟悉NAPI的原理,我以前的blog有介绍),就是调用napi_schedule来重新加入软中断处理.
static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data)
{
struct ixgbe_q_vector *q_vector = data;
/* EIAM disabled interrupts (on this vector) for us */
if (q_vector->rx.ring || q_vector->tx.ring)
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
而NAPI驱动我们知道,最终是会调用网卡驱动挂载的poll回调,在ixgbe中,对应的回调就是ixgbe_poll,那么也就是说这个函数要做两个工作,一个是处理读,一个是处理写完之后的清理.
int ixgbe_poll(struct napi_struct *napi, int budget)
{
struct ixgbe_q_vector *q_vector =
container_of(napi, struct ixgbe_q_vector, napi);
struct ixgbe_adapter *adapter = q_vector->adapter;
struct ixgbe_ring *ring;
int per_ring_budget;
bool clean_complete = true;
#ifdef CONFIG_IXGBE_DCA
if (adapter->flags & IXGBE_FLAG_DCA_ENABLED)
ixgbe_update_dca(q_vector);
#endif
//清理写
ixgbe_for_each_ring(ring, q_vector->tx)
clean_complete &= !!ixgbe_clean_tx_irq(q_vector, ring);
/* attempt to distribute budget to each queue fairly, but don't allow
* the budget to go below 1 because we'll exit polling */
if (q_vector->rx.count > 1)
per_ring_budget = max(budget/q_vector->rx.count, 1);
else
per_ring_budget = budget;
//读数据,并清理已完成的
ixgbe_for_each_ring(ring, q_vector->rx)
clean_complete &= ixgbe_clean_rx_irq(q_vector, ring,
per_ring_budget);
/* If all work not completed, return budget and keep polling */
if (!clean_complete)
return budget;
/* all work done, exit the polling mode */
napi_complete(napi);
if (adapter->rx_itr_setting & 1)
ixgbe_set_itr(q_vector);
if (!test_bit(__IXGBE_DOWN, &adapter->state))
ixgbe_irq_enable_queues(adapter, ((u64)1 << q_vector->v_idx));
return 0;
}
4.2 DPDK Startup
用x86这样的通用处理器来做网络产品第一遇到的可能就是分流负载均衡这样的问题,可能最后的实现大家投殊途同归,甚至可以参考Cavium这些厂商做硬件协处理器的思路来用软件实现,比如算packet头部的hash,基于hash和CPU core的分流。DPDK本身做的事情也非常有点,比如memory pool或者buffer list,然后用UIO把igbe拿到用户空间来,但还是欠缺很多东西,比如Intel本身的IOAT技术,况且这个驱动只是个简化版的并不完全就是内核里面对应的igbe的驱动,缺乏ixgbe的驱动。
到底是NAPI结合中断的模型还是poll的模型?
后续
如果使用Intel平台的话,底层的主要任务可能就是实现一个类似DPDK的东西,并保证稳定,既然选择了就要接受他的一些缺点,可能看起来不是那么漂亮,但是底层的东西做的稳定压倒一切,就算是性能再好,不稳定再好的产品也没有表现的机会。在考量性价比以及功耗的情况下,Intel架构可能确实不如类似MIPS PowerPC的CPU,但是x86的起点低,有很多可用的东西一旦稳定下来就很有保证,并且Intel出的东西有一个稳定的roadmap。并且就目前来看的话,x86的性能也上来了,只是在应对一些网络特殊应用的时候可能不如专用的网络处理器。如果能够控制DP的复杂度的并且保证稳定的话,可能x86是非常值得考虑的一个平台。还有个问题就是接口,在网络处理器上一般是SGMII或者XAUI类似的接口效率非常高,标称10G绝对可以达到链路满负载,但是在x86多数是PCIE,但是PCIE这个东西,标准上数值非常高,但是一般情况下很难到达那个数据,并且PCIE使用起来并不是非常容易,不像XAUI那种硬件配好直接用,可能PCIE的优势在于同一的标准总线并且支持高级的特性比如IO虚拟化等等。其实觉得可以考虑异构的系统,x86和MIPS或者PowerPC结合这样的架构,x86做数据分析和呈现还是非常占优势的,并且未来的防火墙在包处理技术上可能趋于成熟,不能说哪一家会领先这个时代,主要看在流量分析整流这样的功能上的对比。可能还有FPGA ASIC这样的技术,其实这些都是些金主才玩的起的,不是巨大市场利润空间驱动的话还是不要碰为好。可能像Netscreen这样的公司玩ASIC,那个时候还没有Cavium Netlogic这样的公司,出了产品,别人可能也要投入ASIC才能和你竞争,但是现在你投入巨大的资金来做ASIC,而别的厂家使用已有的NP就可以达到类似的水准,那么投入和产出不协调,得不偿失了。
并且现在Cavium Netlogic的芯片提供已有的SDK,并不需要非常深厚的技术积累就可以快速的出产品。他们的这种做法可能会同质化某些通信产品,真正的竞争力在哪里?现在你可以做100G,别人当然也可以达到,这种技术上的绝对领先不复存在。当然如果屏蔽这些实现的细节,在别人看来,东西能不能满足需求,稳不稳定当然是首要关注的。
当然你无论采用哪种架构,这些细节都应该想上层的功能部件屏蔽,上层只负责处理上来的包,而不关心怎么上来的。如果上层APP c-plane和d-plane结合过于紧密,可能这种架构会绑架硬件,入上所说,在这个时代,高度定制化的硬件竞争力正在下降。就好象做的手机,量小的时候根本不能盈利,只有在量大的时候才能摊薄硬件成本,但是这种设备量能大到哪里去呢?
只是印象上的对比,说一个技术上的问题,如果选择系统的话,可能有人选择vxworks这样的历史悠久的实时系统,但是问题来了,他是商业的,可能有好的技术支持,但是可能相对复杂。并且和开源的社区不能很好的兼容,不能用开源社区的成果,并且真的需要实时特性吗?真的需要把延时控制在一个范围内?很多时候只是需要高性能的OS,可能在老牌的工业控制上用到vxworks,历史原因。现在搞这个东西的一般都是Linux了吧,前几年可能还有FreeBSD,但现在也都移到Linux上来了吧,可能还有些设备基于QNX,但是真的有需要在这些盒子里跑QNX?最多的还是Linux,这个时候问题来了,Linux的首先遇到的就是用户空间内核空间,当然这也不是Linux的错,x86的实现就是这样,在MIPS上就不一样了,有一个特别的段,大家都可以访问,数据包来直接扔到这里,用户空间快速处理转发,但是在x86上首先到达堆栈内核空间可能转几遍才到用户空间,当然内核这样设计也是为了结构上可能好看明白一点,但是作为产品来用,当然大改一番,这里就是x86系列的难点之一了可能。你不能把东西直接放到内核空间,GPL有问题,再者难度可能有点大并且非常容易出问题。当然你可以封一套代码做成内核模块来供用户空间的程序来用绕过这个问题。当然还有一些用法就是cpusched这样的东西来绑定进程或者对中断绑定,主要收包以poll为主。当然如果是以一个core进行专门的中断处理,可能思路就和NP的协处理器加core的思路一般了,专用部分来处理包。
好,看起来x86上思路明确,搞起工程起来应该技术风险不是很大,但是有个问题?x86上怎么去做包保序呢?软件来实现包保序?可能把软件的复杂度提高一个级别,并且效果不好,在NP上可能直接在硬件上直接支持保序的,可能这个就是x86爱莫能助了。可能在x86上做保序的话有点不现实,并且好像业界也没有人这么做?还不如减少过盒子的延时。
说说多核系统效能。Cavium典型的高频低能,32core的每core 1GHZ绝对不如人的双路Xeon 4核心每core 2GHZ,并且这样的Soc为了实现把包能够平均到多core上以及保证数据同步,core的数目越多总线设计难度越大,如果不是Cavium那群人是最开始NEC做MIPS的人绝对驾驭不了啊,就算是做出来,复杂度越高越容易出问题。相比Intel可能技术积累更加深厚,并且相比于Cavium这样的MIPS厂商提高core数目人主要提高core的效能。因此单chip上core数目有限但是效率相对来说会高一些。并且那么多人使用出问题的技术风险非常之低,保证软件设计没有问题。但是Cavium上出了问题,复杂的问题你根本无法区分是CPU问题还是软件设计问题,并且OCTEON上很多功能寄存器在文档中也没有详细的说明到底如何工作如何影响系统。这个时候只有去求Cavium的FAE了,但是人家只说一句我们的东西没问题,你再看看你的软件是不是配置有问题,就完全傻眼了。就算是完全揪住小辫子,人家不给workaround也不承认发出来,再傻眼。
说一下打板,曾经去工厂看过打板的过程。目测Cavium的板比x86的复杂多。并且x86怎么说也算是通用架构,参考服务器设计可能稳定性有所保证,但是Cavium板就不好说。
可能会说我用MIPS多核来实现一个防火墙,听起来比x86实现一个美好高端很多。但是你跟客户说我是并行多核架构,但是这看得见摸得着?什么是并行多核?能够看得到多个core?你专业但是为什么你的系统和别人用起来没什么区别啊?并且用起来性能也差不多啊?搞工程的人总是喜欢被自我陶醉。
本文地址:http://www.tech4cloud.com/2012/08/01/XG_Networks