一 前言node
http://www.dpdk.org/ dpdk 是 intel 开发的x86芯片上用于高性能网络处理的基础库,业内比较经常使用的模式是linux-app模式,即linux
利用该基础库,在用户层空间作数据包处理,有了这个基础库,能够方便地在写应用层的网络包处理高性能程序,目前该库已经开源。数据库
multicore framework 多核框架,dpdk库面向intel i3/i5/i7/ep 等多核架构芯片,内置了对多核的支持数组
huge page memory 内存管理,dpdk库基于linux hugepage实现了一套内存管理基础库,为应用层包处理作了不少优化缓存
ring buffers 共享队列,dpdk库提供的无锁多生产者-多消费者队列,是应用层包处理程序的基础组件网络
poll-mode drivers 轮询驱动,dpdk库基于linux uio实现的用户态网卡驱动数据结构
下面分几个模块说明上述组件的功能和实现。架构
二 内存管理app
1 hugepage技术框架
hugepage(2M/1G..)相对于普通的page(4K)来讲有几个特色,a hugepage 这种页面不受虚拟内存管理影响,不会被替换出内存,而
普通的4kpage 若是物理内存不够可能会被虚拟内存管理模块替换到交换区。 b 一样的内存大小,hugepage产生的页表项数目远少于4kpage.举一个例子,用户进程须要使用 4M 大小的内存,若是采用4Kpage, 须要1K的页表项存放虚拟地址到物理地址的映射关系,而
采用hugepage 2M 只须要产生2条页表项,这样会带来两个后果,一是使用hugepage的内存产生的页表比较少,这对于数据库系统等
动不动就须要映射很是大的数据到进程的应用来讲,页表的开销是很可观的,因此不少数据库系统都采用hugepage技术。二是tlb冲突率
大大减小,tlb 驻留在cpu的1级cache里,是芯片访问最快的缓存,通常只能容纳100多条页表项,若是采用hugepage,则能够极大减小
tlb miss 致使的开销:tlb命中,当即就获取到物理地址,若是不命中,须要查 rc3->进程页目录表pgd->进程页中间表pmd->进程页框->物理内存,若是这中间pmd或者页框被虚拟内存系统替换到交互区,则还须要交互区load回内存。。总之,tlb miss是性能大杀手,而采用
hugepage能够有效下降tlb miss
linux 使用hugepage的方式比较简单,
/sys/kernel/mm/hugepages/hugepages-2048kB/ 经过修改这个目录下的文件能够修改hugepage页面的大小和总数目
mount -t hugetlbfs nodev /mnt/huge linux将hugepage实现为一种文件系统hugetlbfs,须要将该文件系统mount到某个文件
mmap /mnt/huge 在用户进程里经过mmap 映射hugetlbfs mount 的目标文件,这个mmap返回的地址就是大页面的了
2 多进程共享
mmap 系统调用能够设置为共享的映射,dpdk的内存共享就依赖于此,在这多个进程中,分为两种角色,第一种是主进程(RTE_PROC_PRIMARY),第二种是从进程(RTE_PROC_SECONDARY)。主进程只有一个,必须在从进程以前启动,
负责执行DPDK库环境的初始化,从进程attach到主进程初始化的DPDK上,主进程先mmap hugetlbfs 文件,构建内存管理相关结构
将这些结构存入hugetlbfs 上的配置文件rte_config,而后其余进程mmap rte_config文件,获取内存管理结构,dpdk采用了必定的
技巧,使得最终一样的共享物理内存在不一样进程内部对应的虚拟地址是彻底同样的,意味着一个进程内部的基于dpdk的共享数据和指向
这些共享数据的指针,能够在不一样进程间通用。
内存全局配置结构rte_config
rte_config 是每一个程序私有的数据结构,这些东西都是每一个程序的私有配置。
lcore_role:这个DPDK程序使用-c参数设置的它同时跑在哪几个核上。
master_lcore:DPDK的架构上,每一个程序分配的lcore_role 有一个主核,对使用者来讲影响不大。
lcore_count:这个程序可使用的核数。
process_type:DPDK多进程:一个程序是主程序,不然初始化DPDK内存表,其余从程序使用这个表。RTE_PROC_PRIMARY/RTE_PROC_SECONDARY
mem_config:指向设备各个DPDK程序共享的内存配置结构,这个结构被mmap到文件/var/run/.rte_config,经过这个方式多进程实现对mem_config结构的共享。
Hugepage页表数组
这个是struct hugepage数组,每一个struct hugepage 都表明一个hugepage 页面,存储的每一个页面的物理地址和程序的虚拟地址的映射关系。而后,把整个数组映射到文件/var/run /. rte_hugepage_info,一样这个文件也是设备共享的,主/从进程都能访问它。
file_id: 每一个文件在hugepage 文件系统中都有一个编号,就是数组1-N
filepath:%s/%smap_%file_id mount 的hugepage文件系统中的文件路径名
size; 这个hugepage页面的size,2M仍是1G
socket_id:这个页面属于那个CPU socket 。
Physaddr:这个hugepage 页面的物理地址
orig_va:它和final_va同样都是指这个huagepage页面的虚拟地址。这个地址是主程序初始化huagepage用的,后来就没用了。
final_va:这个最终这个页面映射到主/从程序中的虚拟地址。
首先由于整个数组都映射到文件里面,全部的程序之间都是共享的。主程序负责初始化这个数组,首先在它内部经过mmap把全部的hugepage物理页面都映射到虚存空间里面,而后把这种映射关系保存到这个文件里面。从程序启动的时候,读取这个文件,而后在它内存也建立和它如出一辙的映射,这样的话,整个DPDK管理的内存在全部的程序里面都是可见,并且地址都同样。
在对各个页面的物理地址份配虚拟地址时,DPDK尽量把物理地址连续的页面分配连续的虚存地址上,这个东西仍是比较有用的,由于CPU/cache/内存控制器的等等看到的都是物理内存,咱们在访问内存时,若是物理地址连续的话,性能会高一些。
至于到底哪些地址是连续的,那些不是连续的,DPDK在这个结构之上又有一个新的结构rte_mem_config. Memseg来管理。由于rte_mem_config也映射到文件里面,全部的程序均可见rte_mem_config. Memseg结构。
rte_mem_config
这个数据结构mmap 到文件/var/run /.rte_config中,主/从进程经过这个文件访问实现对这个数据结构的共享。
在每一个程序内,使用rte_config .mem_config 访问这个结构。
memseg
memseg 数组是维护物理地址的,在上面讲到struct hugepage结构对每一个hugepage物理页面都存储了它在程序里面的虚存地址。memseg 数组的做用是将物理地址、虚拟地址都连续的hugepage,而且都在同一个socket,pagesize 也相同的hugepage页面集合,把它们都划在一个memseg结构里面,这样作的好处就是优化内存。
Rte_memseg这个结构也很简单:
phys_addr:这个memseg的包含的全部的hugepage页面的起始地址
addr:这些hugepage页面的起始的虚存地址
len:这个memseg的包含的空间size
hugepage_sz; 这些页面的size 2M /1G?
这些信息都是从hugepage页表数组里面得到的。
free_memseg
上面memseg是存储一个总体的这种映射的映射,最终这些地址是要被分配出去的。free_memseg记录了当前整个DPDK内存空闲的memseg段,注意,这是对全部进程而言的。初始化等于memseg表示全部的内存都是free的,后面随着分配内存,它愈来愈小。
必须得说明,rte_mem_config结构在各个程序里面都是共享的,因此对任何成员的修改都必须加锁。
mlock :读写锁,用来保护memseg结构。
rte_memzone_reserve
通常状况下,各个功能分配内存都使用这个函数分配一个memzone
const struct rte_memzone *
rte_memzone_reserve(const char *name, size_t len, int socket_id,
unsigned flags)
{
一、这里就在free_memseg数组里面找知足socket_id的要求,最小的memseg,从这里面划出来一块内存,固然这时候free_memseg须要更新把这部份内存刨出去。
二、把这块内存记录到memzone里面。
}
上面提到rte_memzone_reserve分配内存后,同时在rte_mem_config.memzone数组里面分配一个元素保存它。对于从memseg中分配内存,以memzone为单位来分配,对于全部的分配状况,都记录在memzone数组里面,固然这个数组是多进程共享,你们都能看到。
在rte_mem_config结构里面memzone_idx 变量记录当前分配的memzone,每申请一次这个变量+1。
这个rte_memzone结构以下:
Name:给这片内存起个名字。
phys_addr:这个memzone 分配的内存区的物理地址
addr:这个memzone 分配的内存区的虚拟地址
len:这个zone的空间长度
hugepage_sz:这个zone的页面size
socket_id:页面的socket号
rte_memzone 是dpdk内存管理最终向客户程序提供的基础接口,经过 rte_memzone_reverse 能够获取基于
dpdk hugepage 的属于同一个物理cpu的物理内存连续的虚拟内存也连续的一块地址。rte_ring/rte_malloc/rte_mempool等
组件都是依赖于rte_memzone 组件实现的。
3 dpdk 内存初始化源码解析
入口:
rte_eal_init(int argc,char ** argv) dpdk 运行环境初始化入口函数
—— eal_hugepage_info_init 这4个是内存相关的初始化函数
——rte_config_init
——rte_eal_memory_init
——rte_eal_memzone_init
3.1 eal_hugepage_info_init
这个函数比较简单,主要是从 /sys/kernel/mm/hugepages 目录下面读取目录名和文件名,从而获取系统的hugetlbfs文件系统数,
以及每一个 hugetlbfs 的大页面数目和每一个页面大小,并保存在一个文件里,这个函数,只有主进程会调用。存放在internal_config结构里
3.2 rte_config_init
构造 rte_config 结构
rte_config_init
——rte_eal_config_create 主进程执行
——rte_eal_config_attach 从进程执行
rte_eal_config_create 和 rte_eal_config_attach 作的事情比较简单,就是将 /var/run/.config 文件shared 型
mmap 到本身的进程空间的 rte_config.mem_config结构上,这样主进程和从进程均可以访问这块内存,
rte_eal_config_attach
3.3 rte_eal_memory_init
rte_eal_memory_init
——rte_eal_hugepage_init 主进程执行,dpdk 内存初始化核心函数
——rte_eal_hugepage_attach 从进程执行
rte_eal_hugepage_init 函数分几个步骤:
/*
* Prepare physical memory mapping: fill configuration structure with
* these infos, return 0 on success.
* 1. map N huge pages in separate files in hugetlbfs
* 2. find associated physical addr
* 3. find associated NUMA socket ID
* 4. sort all huge pages by physical address
* 5. remap these N huge pages in the correct order
* 6. unmap the first mapping
* 7. fill memsegs in configuration with contiguous zones
*/
函数一开始,将rte_config_init函数获取的配置结构放到本地变量 mcfg 上,而后检查系统是否开启hugetlbfs,若是
不开启,则直接经过系统的malloc函数申请配置须要的内存,而后跳出这个函数。
接下来主要是构建 hugepage 结构的数组 tmp_hp(上图)
下面就是重点了。。
构建hugepage 结构数组分下面几步
首先,循环遍历系统全部的hugetlbfs 文件系统,通常来讲,一个系统只会使用一种hugetlbfs ,因此这一层的循环能够认为
没有做用,一种 hugetlbfs 文件系统对应的基础数据包括:页面大小,好比2M,页面数目,好比2K个页面
其次,将特定的hugetlbfs的所有页面映射到本进程,放到本进程的 hugepage 数组管理,这个过程主要由 map_all_hugepages函数完成,
第一次映射的虚拟地址存放在 hugepage结构的 orig_va变量
第三,遍历hugepage数组,找到每一个虚拟地址对应的物理地址和所属的物理cpu,将这些信息也记入 hugepage数组,物理地址
记录在hugepage结构的phyaddr变量,物理cpu号记录在 hugepage结构的socket_id变量
第四,跟据物理地址大小对hugepage数组作排序
第五,根据排序结果从新映射,这个也是由函数 map_all_hugepages完成,从新映射后的虚拟地址存放在hugepage结构的final_va变量
第六,将第一次映射关系解除,即将orig_va 变量对应的虚拟地址空间返回给内核
下面看 map_all_hugepages的实现过程
这个函数是复用的,共有两次调用。
对于第一次调用,就是根据hugetlbfs 文件系统的页面数m,构造
m个文件名称并建立文件,每一个文件对应一个大页面,而后经过mmap系统调用映射到进程的一块虚拟地址
空间,并将虚拟地址存放在hugepage结构的orig_va地址上。若是该hugetlbfs有1K个页面,最终会在
hugetlbfs 挂载的目录上生成 1K 个文件,这1K 个文件mmap到进程的虚拟地址由进程内部的hugepage数组维护
对于第二次调用,因为hugepage数组已经基于物理地址排序,这些有序的物理地址可能有2种状况,一种是连续的,
另外一种是不连续的,这时候的调用会遍历这个hugepage数组,而后统计连续物理地址的最大内存,这个统计有什么好处?
由于第二次的映射须要保证物理内存连续的其虚拟内存也是连续的,在获取了最大连续物理内存大小后,好比是100个页面大小,
会调用 get_virtual_area 函数向内涵申请100个页面大小的虚拟空间,若是成功,说明虚拟地址能够知足,而后循环100次,
每次映射mmap的首个参数就是get_virtual_area函数返回的虚拟地址+i*页面大小,这样,这100个页面的虚拟地址和物理地址
都是连续的,虚拟地址存放到final_va 变量上。
下面看 find_physaddr的实现过程
这个函数的做用就是找到hugepage数组里每一个虚拟地址对应的物理地址,并存放到 phyaddr变量上,最终实现由函数
rte_mem_virt2phy(const void * virt)函数实现,其原理至关于页表查找,主要是经过linux的页表文件 /proc/self/pagemap 实现
/proc/self/pagemap 页表文件记录了本进程的页表,即本进程虚拟地址到物理地址的映射关系,主要是经过虚拟地址的前面若干位
定位到物理页框,而后物理页框+虚拟地址偏移构成物理地址,其实现以下
下面看 find_numasocket的实现过程
这个函数的做用是找到hugepage数组里每一个虚拟地址对应的物理cpu号,基本原理是经过linux提供的 /proc/self/numa_maps 文件,
/proc/self/numa_maps 文件记录了本 进程的虚拟地址与物理cpu号(多核系统)的对应关系,在遍历的时候将非huge page的虚拟地址
过滤掉,剩下的虚拟地址与hugepage数组里的orig_va 比较,实现以下
sort_by_physaddr 根据hugepage结构的phyaddr 排序,比较简单
unmap_all_hugepages_orig 调用 mumap 系统调用将 hugepage结构的orig_va 虚拟地址返回给内核
上面几步就完成了hugepage数组的构造,如今这个数组对应了某个hugetlbfs系统的大页面,数组的每个节点是一个
hugepage结构,该结构的phyaddr存放着该页面的物理内存地址,final_va存放着phyaddr映射到进程空间的虚拟地址,
socket_id存放着物理cpu号,若是多个hugepage结构的final_va虚拟地址是连续的,则其 phyaddr物理地址也是连续的。
下面是rte_eal_hugepage_init函数的余下部分,主要分两个方面,一是将hugepage数组里 属于同一个物理cpu,物理内存连续
的多个hugepage 用一层 memseg 结构管理起来。 一个memseg 结构维护的内存必然是同一个物理cpu上的,虚拟地址和物理
地址都连续的内存,最终的memzone 接口是经过操做memseg实现的;2是将 hugepage数组和memseg数组的信息记录到共享文件里,
方便从进程获取;
遍历hugepage数组,将物理地址连续的hugepage放到一个memseg结构上,同时将该memseg id 放到 hugepage结构
的 memseg_id 变量上
下面是建立文件 hugepage_info 到共享内存上,而后hugepage数组的信息拷贝到这块共享内存上,并释放hugepage数组,
其余进程经过映射 hugepage_info 文件就能够获取 hugepage数组,从而管理hugepage共享内存