- 以secondary方式
- 以primary方式
- 以primary方式
- 以secondary方式
接上次内容继续对rte_eal_init()所做的工作进行分析。
18.检查是否允许直接物理地址访问
rte_eal_using_phys_addrs()会去检查当前系统是否允许在进程虚拟地址空间直接访问物理地址。需要有两个支持条件:存在大页内存和能够进行虚拟地址到物理地址的转换。
该func就是检查internal_config中的no_hugetlbfs是否置位,置位表示不使用大页内存,默认情况下是不置位的,如果想置位,需要通过DPDK启动参数–no-huge来指定。
该func的作用是随机传入一个进程中的虚拟内存地址,返回该虚拟内存地址映射到的物理地址。在这里,通过传入地址为0的虚拟地址,如果返回的结果不是无效的物理地址的话,则表示当前系统是允许的。
要实现从虚拟地址获得物理地址,需要借助/proc文件系统的支持。假设被寻找物理地址的虚拟地址值为virtaddr,首先要确保能够打开/proc/self/pagemap文件,获取到系统默认的页面大小pagesize。接下来获取virtaddr在虚拟地址空间的页面索引值vrit_pfn,即virt_pfn = virtaddr/pagesize。因为页面是虚拟内存映射到物理内存的最小单位,故需要获取到页面索引。
获取到virt_pfn之后,即可根据此值访问pagemap文件了。在Linux 64位操作系统中,需要54比特位空间存放物理页面的索引(参考Linux文档pagemap.txt得知),向上对齐为8字节空间。那么在pagemap中,offset为virt_pfn * 8的位置,即为索引为virt_pfn的虚拟内存页面映射到的物理内存页面的索引phy_page。
获取到phy_page之后,即可根据此值,还原出virtaddr映射到的物理地址phyaddr。特殊情况下,通过pagemap文件无法获取到phy_page的值,那么表示该系统不支持虚拟地址到物理地址的转换。
19.iova模式的处理
Internal_config中iova_mode指明设备访问物理内存的方式,一共有三种:RTE_IOVA_PA、RTE_IOVA_VA和RTE_IOVA_DC,分别表示直接访问内存物理地址,通过进程虚拟地址访问,不关心访问方式。默认情况下不关心具体访问方式,其他两种情况,需要通过DPDK启动参数–iova-mode来指定。
接下来详细介绍一下DPDK的iova模式。
DPDK是用户态的SDK,使用该SDK访问的是进程的虚拟地址,常规情况下,用户态进行IO操作需要通过系统调用进入内核态,在内核态完成设备与内存之间的交互,此种操作效率低下。为解决此问题,DPDK提供了一些API,能够实现在用户态模式下实现IO操作。然而硬件设备是不能直接操作进程的虚拟地址空间的,能够操作的是物理地址,为此DPDK提供了IO虚拟地址IOVA来支持硬件设备可以直接操作进程的虚拟地址。
DPDK统一通过IOVA实现与硬件设备的数据交换,但需要区分IOVA是寻址的是内存物理地址还是进程的虚拟地址的情况,这就分区出了iova_pa和iova_va两种情况。
该模式下,分配到DPDK存储区的iova都是实际的物理地址,当进程初始化iova时,虚拟地址空间会完成到物理内存页面的映射,这样当分配内存时,不会引发缺页异常的情况。但这种模式也存在一定的缺点,首先是需要在进程启动时需要root用户的特权,否则无法访问系统的页面映射,也就无法获取到内存区域的真实物理地址。其次是虚拟内存的分配受到物理内存的限制,当物理内存不是连续的内存片段时,iova的虚拟内存也只能是多个片段,不能在逻辑上将物理内存合并。如下图所示:
极端情况下,如果物理内存过于破碎,片段太多,会耗尽表示iova虚拟内存的数据结构,导致初始化失败。解决该问题的方法一般是使用1GB或者2MB的大页内存,并在机器启动时显式指定保留一定数量的大页内存。
相比PA模式,VA模式下虚拟内存映射物理内存不会受到物理内存片段过多的影响,DPDK的eal层会依靠内核的基础设施将多个物理内存片段映射为一个连续的虚拟内存片段,该过程不需要root权限也可以执行。如下图所示:
internal_config中的iova_mode默认为RTE_IOVA_DC,即不关心是哪种。在这种情况下,DPDK初始化过程中需要自行判断该采取哪种模式。首先调用rte_bus_get_iommu_class()来获取iova的模式,该func中遍历rte_bus_list中每个bus,如果bus定义了get_iommu_class这个方法,那么就调用该方法获取该bus需要PA还是VA。如果所有的bus都是PA或者都是VA,那么get_iommu_class就返回具体的模式,如果有的bus需要PA模式而有的需要VA模式,那么仍然是返回DC模式。
调用了get_iommu_class之后如果仍然是DC模式,那么就需要最终确定选择PA还是VA了。如果不支持直接物理地址访问,选择VA;如果内核模块rte_kni有加载,选择PA;如果iommu是使能的,选择VA;除此之外的情况,选择PA。
确定iommu是不是使能的是通过调用is_iommu_enabled()实现,该func通过打开/sys/kernel/iommu_groups目录,统计其中的文件数量再确定。
未完待续……
往期回顾:
作者:大空新一
接上次内容继续对rte_eal_init所做的工作进行分析。
将DPDK进程的启动参数保留下来
在启动DPDK进程时会传入启动参数,这些参数会分成两部分,一部分是DPDK的eal配置参数,如占用的CPU处理核的列表,分配的大页内存大小,网卡的黑白名单等;另外一部分是进程自定义参数,由开发者自行定义参数列表和含义。参数列表中eal配置参数放在前面,进程自定义参数在后面,中间通过“–”进行分割。
eal_save_args()中,分配两段内存空间并交给两个全局的变量eal_args和eal_app_args,后将两部分参数分别存放在这两段内存空间当中。
CPU处理核信息初始化
存在一个全局数组变量lcore_config,类型为struct lcore_config,大小由宏定义RTE_MAX_LCORE指定。在rte_eal_cpu_init()中,首先依次遍历数组中的每个元素并对struct lcore_config中的一些元素赋默认值。该结构体的数据结构包含如下内容:
要理解该结构体中一些元素的含义,首先需要了解一下CPU的结构。在Linux中执行lscpu命令,可得到如下输出内容:
该例子的主机中,有一个socket。socket可以对应主板上的一个CPU插槽,就是一块CPU芯片。对于一般的PC机来说,只有一个socket,大型服务器一般有多个socket。一个socket中一般会有多个core,即一个独立的处理单元,在单线程模式下,一个独立的处理单元包含一套标准的寄存器和L1级缓存,同时刻只能运行一个线程;而多线程模式下,一套处理单元有两套寄存器,同时刻可以运行两个线程,这两个线程共享L1级缓存。上例中所显示的主机的CPU有8个处理单元,因为开启了多线程,每个处理单元线程数是2,这样逻辑上讲一共有16个线程(或CPU)。
对应到lcore_config中的相关结构,socket_id即为socket的编号,core_id对应的是处理单元的编号,而DPDK的处理核lcore则对应的是线程(或CPU)的编号。
在遍历lcore_config的每个元素时的索引就是lcore的编号,遍历时,根据lcore的编号去系统中查找对应的线程(或CPU)。如果该线程没有被启用,则将lcore_config结构体中的core_index置为负数,并将rte_config中的lcore_role中对应的值置为ROLE_OFF;线程被启用时,core_index设置为相对索引,lcore_role设为ROLE_RTE。
遍历完lcore之后,也就能够统计出涉及到的socket有哪些了,会根据这些信息设置rte_config中的numa_node信息。
需要另外指出的是,rte_eal_cpu_init()中涉及到的某些func,是通过直接访问sys文件系统实现的。
如eal_cpu_socket_id()是遍历/sys/devices/system/node/nodeX目录中如果存在cpuY目录,则lcoreY的socket id是X;eal_cpu_detected()是遍历/sys/devices/system/cpu/cpuX/topology/core_id是否存在。
DPDK参数解析
eal_parse_args()解析DPDK参数,比较重要的几个参数有:
-a:allow list,指定哪些网卡设备是允许被DPDK接管的,传入的参数值是设备的pci总线编号。
-b:block list,指定哪些网卡设备是进制被DPDK禁止的,传入值同上。
-c:指定DPDK进程所占用的处理核是哪些,用bitmask的方式进行表示。
-m:指定分配多大的内存空间给DPDK的内存池,以MB为单位。
解析之后的信息存放在internal_config这个全局变量或者其他的变量当中。DPDK涉及到的参数比较多,后续会根据需要补充说明各参数的含义。
插件初始化
如果是Windows系统,是不支持插件动态载入的,eal_plugins_init()的内容为空。如果是Linux系统时,如果能通过执行系统调用dlopen打开动态链接库librte_eal.so.XX,则调用eal_plugin_add将默认动态库的目录default_solib_dir添加到全局变量solib_list当中(此外还可以通过DPDK的启动参数-d,将自定义目录添加到solib_list中)。
由于在初始化阶段加入solib_list中的可能是动态库本身也可能是动态库所在的目录,eal_plugins_init()会在遍历solib_list中将目录中的*.so文件全部遍历之后加入solib_list中;如果已经是*.so本身,则设置solib的lib_handle。
初始化跟踪功能
调用eal_trace_init()初始化跟踪功能,该功能是对一些事件和变量进行跟踪,用于多线程同步和时间度量等。该功能比较复杂,后续分析过程中会详细介绍,此处先略过。
解析设备
前面提到DPDK的参数时,-a和-b参数分别指定网卡设备的白黑名单,这些信息是存放在devopt_list这个全局变量当中的,eal_option_device_parse()会遍历其中的每个设备,并转换成新的数据结构存放在devargs_list当中。
未完待续
往期回顾: