一、传统协议栈之数据包从NIC到内核1、从NIC到内存概括地说,网络数据包进入内存(接收数据包)的流程为:网线--> Rj45网口--> MDI 差分线--> bcm5461(PHY芯片进行数模转换) --> MII总线--> TSEC的DMA Engine 会自动检查下一个可用的Rx bd--> 把网络数据包DMA 到Rx bd 所指向的内存,即skb->data1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;对应以上4步,来看它的具体实现:1、分配环形DMA缓冲区Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表2、建立DMA映射内核通过调用dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction) 建立映射关系。
struct device *dev,描述一个设备;buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;size:缓存大小;direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。
3、这一步由硬件完成;4、取消映射ma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用dma_sync_single_for_cpu() 让CPU在取消映射前,就可以访问DMA缓冲区中的内容。
2、从驱动到网络协议栈网络驱动收包大致有3种情况:no NAPI:mac每收到一个以太网包,都会产生一个接收中断给cpu,即完全靠中断方式来收包缺点是当网络流量很大时,cpu大部分时间都耗在了处理mac的中断。
netpoll:在网络和I/O子系统尚不能完整可用时,模拟了来自指定设备的中断,即轮询收包。
缺点是实时性差NAPI:采用中断+ 轮询的方式:mac收到一个包来后会产生接收中断,但是马上关闭。
直到收够了netdev_max_backlog个包(默认300),或者收完mac上所有包后,才再打开接收中断通过sysctl来修改dev_max_backlog或者通过proc修改/proc/sys/net/core/netdev_max_backlog以NAPI为例,NAPI 相关数据结构每个网络设备(MAC层)都有自己的net_device数据结构,这个结构上有napi_struct。
每当收到数据包时,网络设备驱动会把自己的napi_struct挂到CPU私有变量上。
这样在软中断时,net_rx_action会遍历cpu私有变量的poll_list,执行上面所挂的napi_struct结构的poll钩子函数,将数据包从驱动传到网络协议栈。
接收到一个完整的以太网数据包后,TSEC会根据event mask触发一个Rx 外部中断。
cpu保存现场,根据中断向量,开始执行外部中断处理函数do_IRQ()do_IRQ 伪代码{上半部处理硬中断查看中断源寄存器,得知是网络外设产生了外部中断执行网络设备的rx中断handler(设备不同,函数不同,但流程类似,TSEC是gfar_receive)1. mask 掉rx event,再来数据包就不会产生rx中断2. 给napi_struct.state加上NAPI_STATE_SCHED 状态3. 挂网络设备自己的napi_struct结构到cpu私有变量_get_cpu_var(softnet_data).poll_list4. 触发网络接收软中断下半部处理软中断依次执行所有软中断handler,包括timer,tasklet等等执行网络接收的软中断handler net_rx_action1. 遍历cpu私有变量_get_cpu_var(softnet_data).poll_list2. 取出poll_list上面挂的napi_struct 结构,执行钩子函数napi_struct.poll()(设备不同,钩子函数不同,流程类似,TSEC是gfar_poll)3. 若poll钩子函数处理完所有包,则打开rx event mask,再来数据包的话会产生rx中断4. 调用napi_complete(napi_struct *n)把napi_struct 结构从_get_cpu_var(softnet_data).poll_list 上移走同时去掉napi_struct.state 的NAPI_STATE_SCHED 状态}gfar_process_frame()-->skb->protocol = eth_type_trans(skb, dev); //确定网络层包类型,IP、ARP、VLAN等等-->RECEIVE(skb) //调用netif_receive_skb(skb)进入协议栈进入函数netif_receive_skb()后,skb正式开始协议栈之旅。
二、DPDK之数据包从NIC到应用程序DPDK是一套数据收发库。
当一个数据包进入网卡产生中断后,响应这个中断的驱动是DPDK 安装的驱动。
这个驱动会通过UIO机制直接让用户态可以直接操作这个数据包。
在用户态用户可以写一个程序通过DPDK提供的API处理这个数据包,比如直接在用户态写一个二层转发实现,或者在用户态直接实现一个vRouter等。
1、网卡初始化网卡驱动模型一般包含三层,即,PCI总线设备、网卡设备以及网卡设备的私有数据结构,即将设备的共性一层层的抽象,PCI总线设备包含网卡设备,网卡设备又包含其私有数据结构。
在DPDK中,首先会注册设备驱动,然后查找当前系统有哪些PCI设备,并通过PCI_ID 为PCI设备找到对应的驱动,最后调用驱动初始化设备。
一、网卡驱动注册使用attribute的constructor属性,在MAIN函数执行前,就执行rte_eal_driver_register()函数,将pmd_igb_drv驱动挂到全局dev_driver_list链表上。
二、扫描当前系统有哪些PCI设备调用rte_eal_init()--->rte_eal_pci_init()函数,查找当前系统中有哪些网卡,分别是什么类型,并将它们挂到全局链表pci_device_list上。
1、首先初始化全局链表pci_driver_list、pci_device_list。
用于挂载PCI驱动及PCI设备。
2、pci_scan()通过读取/sys/bus/pci/devices/目录下的信息,扫描当前系统的PCI设备,并初始化,并按照PCI地址从大到小的顺序挂在到pci_debice_list上。
三、PCI驱动注册调用rte_eal_init()--->rte_eal_dev_init()函数,遍历dev_driver_list链表,执行网卡驱动对应的init的回调函数,注册PCI驱动。
四、网卡初始化调用rte_eal_init()--->rte_eal_pci_probe()函数,遍历pci_device_list和pci_driver_list链表,根据PCI_ID,将pci_device与pci_driver绑定,并调用pci_driver的init回调函数rte_eth_dev_init(),初始化PCI设备。
2、DPDK组件结构及功能DPDK是INTEL提供的提升数据面报文快速处理速率的应用程序开发包,它主要利用以下几个方面的支持特点来优化报文处理过程,从而加快报文处理速率:1、使用大页缓存支持来提高内存访问效率。
2、利用UIO支持,提供应用空间下驱动程序的支持,也就是说网卡驱动是运行在用户空间的,减下了报文在用户空间和应用空间的多次拷贝。
3、利用LINUX亲和性支持,把控制面线程及各个数据面线程绑定到不同的CPU核,节省了线程在各个CPU核来回调度。
4、提供内存池和无锁环形缓存管理,加快内存访问效率。
整个DPDK系统由许多不同组件组成,各组件为应用程序和其它组件提供调用接口,其结构图如下图所示:环境抽象层:为DPDK其它组件和应用程序提供一个屏蔽具体平台特性的统一接口,EAL 提供的功能主要有:DPDK加载和启动;支持多核或多线程执行类型;CPU核亲和性处理;原子操作和锁操作接口;时钟参考;PCI总线访问接口;跟踪和调试接口;CPU特性采集接口;中断和告警接口等。
2、堆内存管理组件(Malloc lib):堆内存管理组件为应用程序提供从大页内存分配堆内存的接口。
当需要分配大量内存小块时(如用于存储列表中每个表项指针的内存),使用这些接口可以减少TLB缺页。
3、环缓冲区管理组件:环缓冲区管理组件为应用程序和其它组件提供一个无锁的多生产者多消费者FIFO队列API。
4、内存池管理组件:为应用程序和其它组件提供分配内存池的接口,内存池是一个由固定大小的多个内存块组成的内存容器,可用于存储相同对像实体,如报文缓存块等。
内存池由内存池的名称(一个字符串)来唯一标识,它由一个环缓中区和一组核本地缓存队列组成,每个核从自已的缓存队列分配内存块,当本地缓存队列减少到一定程度时,从内存环缓冲区中申请内存块来补充本地队列。
5、网络报文缓存块管理组件:提供应用程序创建和释放用于存储报文信息的缓存块的接口,这些MBUF存储在一内存池中。
提供两种类型的MBUF,一种用于存储一般信息,一种用于存储报文数据。
6、定时器组件:提供一些异步周期执行的接口(也可以只执行一次),可以指定某个函数在规定的时间异步的执行,就像LIBC中的timer定时器,但是这里的定时器需要应用程序在主循环中周期调用rte_timer_manage来使定时器得到执行,使用起来没有那么方便。
定时器组件的时间参考来自EAL层提供的时间接口。