将晦涩难懂的技术讲的通俗易懂
分类: linux
2023-06-24 11:35:30
——lvyilong316
dpdk的内存管理之前有专门分析过,但是其实dpdk在18.05和18.08版本对内存的管理发生了较大的变化,比如增加了动态内存管理,no_hugetlbfs的支持,单个文件段(single_file),内存模式(in_memory)等。本文直接针对相对较新的dpdk 22.11进行内存管理分析,主要分析一下新的动态内存管理方式。相对新的内存管理方式,我们将早期版本的dpdk(17.11或更早版本)的内存管理方式称之为legacy模式(静态管理)。
下面我们分析dpdk22.11中和内存管理相关的代码,还是从dpdk初始化函数rte_eal_init开始。
和内存相关的{banned}中国第一部分是关于iova模式的选择,代码如下,关于iova我们在前面文章中已经有所介绍。可以看到只有使用hugepage才可能支持pa,否则(no_hugetlbfs)无法支持pa模式。
点击(此处)折叠或打开
iova的选择主要有三种:rte_iova_pa,rte_iova_va,rte_iova_dc(都支持,依赖命令行参数选择)。具体选择哪种模式主要看rte_bus_get_iommu_class的返回情况。对于pci bus其函数是rte_pci_get_iommu_class,具体如下。rte_pci_get_iommu_class通过判断设备是否支持va(取决于设备硬件支持iommu且系统开启iommu),以及设备所绑定的驱动,如vfio,igb_uio等是否支持va,{banned}最佳终确定支持va还是pa,还是都支持dc(取决于命令行参数)。
点击(此处)折叠或打开
第二部分同样来自rte_eal_init函数中,对no_hugepagefs的支持,即可以支持不使用hugepage内存,比如使用普通的4k页。
点击(此处)折叠或打开
如果不是使用no_hugetlbfs,则说明是要用hugepage,所以要进行hugepage_info的初始化。关于hugepage_info在前面文章介绍dpdk 17.11内存管理时已经分析过,其结构体如下,针对系统中所有的hugepage_size(2m,1g)和每个numa node分别创建一个hugepage_info结构。这里不再重复分析。
接下来是rte_eal_memzone_init函数对memzone结构的初始化,如果是primary{banned}最佳终会调用rte_fbarray_init,而rte_fbarray_init主要是否分配rte_max_memzone个struct rte_memzone结构,并mmap在dpdk运行目录下的”fbarray_smemzone”文件。具体结构关系如下图,
关于memzone和后文出现的memseg两个结构是dpdk内存管理的关键,在介绍dpdk 17.11内存管理已经介绍过,两者都是表示物理内存连续的一块区域,不过memseg是初始化时系统找到的连续内存,而memzone是进程动态申请产生的,是从memseg中分配出来的。但是在dpdk 22.11中有所不同,因为memseg不再表示一段连续的物理页,而是表示一个单独的物理页,另一方面memzone表示的还是是一段连续的物理页部分。
下面进入我们真正的重头戏,rte_eal_memory_init函数,它主要负责内存管理初始化的一些操作。在介绍其具体流程前,我们先介绍一下关键的数据结构。
在dpdk17.11中有struct rte_memseg结构,用于描述一段物理连续的内存。而在dpdk22.11中增加了struct rte_memseg_list结构,其下面再挂载struct rte_memseg。如下图所示:
为什么多引入一个struct rte_memseg_list呢?这主要是为了将struct rte_memseg进行分组,首先,可以将每个memtype上的内存放在不同的rte_memseg_list上。这里又要介绍一下什么是memtype,它是由系统中的numa node和hugepage_size共同决定的,其个数计算方式如下:
点击(此处)折叠或打开
例如一个有两个numa node的系统,同时支持2m和1g两种hugepage_size,那么就有4种memtype。把每个memtype的rte_memseg分开组织便于查找和管理。其次,是不是一个memtype对应一个rte_memseg_list呢?其实也不是,因为dpdk22.11中有很多可配置的约束,如:max_mem_per_type(每种memtype允许的{banned}最佳大内存),max_segs_per_type(每种memtype允许的{banned}最佳大memseg数量),max_segs_per_list(每个memseg_list允许的{banned}最佳大memseg数量)等,这些约束可能导致一个memtype有多个memseg_list。详情可以参考eal_dynmem_memseg_lists_init函数。
另外一个不通点是memseg的含义,在dpdk 17.11的内存分析中我们了解到一个rte_memseg结构表示的是系统初始化是的一块物理连续的内存,一般对应多个物理连续的hugepage,而在dpdk 22.11中一个rte_memseg结构对应的是一个hugepage,由于一个hugepage当然也是物理连续的。
动态内存模式是新版本dpdk关于内存方面的一个{banned}最佳大的变化。当前只在linux和window系统上支持。与动态内存模式相对应的是原有的lagecy模式(静态内存模式)。我们熟悉dpdk 17.11及早期版本的都知道,一般应用程序启动会通过-m或--socket-mem参数指定应用程序使用的内存大小,之后dpdk应用程序就会reserve相应大小的内存,然后整个程序运行期间调用rte_malloc()或rte_memzone_reserve()等内存分配结构都是从这个reserve内存池中进行分配的,超过reserve内存的大小将无法分配。但在动态内存模式下,应用程序可以不再需要通过-m或--socket-mem来预留内存,应用程序启动是完全可以不占用什么内存,当调用rte_malloc()或rte_memzone_reserve()等接口时动态的从系统中分配内存,并注册到dpdk的内存管理中,同样在调用释放内存接口时也会动态的将内存进行释放(从dpdk内存管理中删除)。这样应用程序可以不需要事先估算需要的内存大小,而采用按需分配,更加灵活(不过对于使用hugepage时,系统还是需要预留足够的hugepage)。
不过在动态内存模式情况下-m或--socket-mem参数仍然可以使用,但是其语义和lagecy模式有所不同,动态模式下-m或--socket-mem参数指定的是应用程序预留的{banned}最佳小内存。这部分内存应用程序不会释放,当需要申请更多的内存时应用程序可以超出这部分预留内存动态添加,为了可以限制应用程序所能使用的{banned}最佳大内存,动态内存提供了--socket-limit参数来指定当前socket所能使用的内存大小上限。他们的关系如下图所示:
关于动态内存的实现流程,其相关代码流程图如下所示,代码分析不再详细展开,通过下面的流程图和数据结构图对照代码应该很好理解:
其中动态内存的初始化入口是eal_dynmem_hugepage_init,与之对应老的lagecy模式的初始化入口是eal_legacy_hugepage_init。lagecy模式我们不再分析,我们看一下动态内存初始化相关的数据结构,如下图所示:
除了前文在内存管理初始化中提到,新的dpdk内存管理是对每个页面(hugepage)创建一个memseg,并且将每个numa node的每个hugepage_size组成memseg_list,而不是像lagecy模式一样将多个连续的hugepage对应为一个memseg外。还有一个重要变化就是,我们这里预留的memseg个数和系统可用的hugepge相当,并且只有通过-m或--socket-mem指定大小的内存在启动中才会真的进行hugepage文件的创建和mmap,同时初始化对应的memseg,其他memseg都是未初始化的,为的就是系统动态分配内存时可用于临时创建对应hugepage文件并初始化对应memeseg。
通过上面的代码流程分析,在动态内存模式情况下我们发现还有其他几个关键功能和注意事项,下面一一介绍。
动态内存模式情况下的内存分配默认不保证是iova连续的。什么意思呢?换句话说内存分配只保证va连续(这是肯定的,例如分配一个数据结构肯定是一个连续的虚拟地址空间),但不能保证这段内存在io视角是连续的地址空间。再进一步解释就是,在va作为iova的情况,内存分配保证了va连续,自然就保证了iova连续;但在pa作为iova时,内存分配就只能保证va连续无法保证iova(pa)连续了。为什么呢?因为从动态内存的实现上也可以看出来,频繁的内存分配/释放,肯定会很少有连续的的大块物理内存的。那在lagecy(如dpdk 17.11)情况下为什么没有这个问题呢?我们可以用下图解释,在va作为iova的情况,无论是动态内存还是lagecy方式分配内存都是连续的va,底层也是连续的iova(尽管可能跨pa空间,pa不连续);但是在pa作为iova时,lagecy模式由于pa和va空间是对应的,在pa作为iova时,如下图有三块连续的pa空间,那么dpdk初始就有三个memseg,对应三个iova空间,所以应用程序分配一块内存(这种情况不能跨memseg),如果是va连续,一定是iova(pa)连续,但是在动态内存场景,如图中(dpdk 18.11),即使pa作为iova的情况,va的组织和iova也没有任何关联,这种情况分配一块内存(是可以跨物理page的,也就是可以跨memseg的),所以底层iova(pa)不一定是连续的。
总结一下就是:在va作为iova时,动态内存和lagecy模式分配的内存都一样,都可以保证iova连续(因为va是连续的),在pa作为iova时,lagecy分配内存可以保证iova(pa)连续,但是动态内存模式无法保证分配出的内存是iova(pa)连续的。
动态内存这个特点是比较有意义的,因为即使pa作为iova,我们正常的内存结构(如ring,mempool,哈希表等)也仅需要va连续内存,而不需要底层物理内存连续。但是有些特殊场景,比如网卡驱动需要参与dma的mbuf内存必须需要iova连续怎么办呢?有以下三种方案:
1. 使用vfio驱动(前提是设备支持iommu),一遍va作为iova;
2. 使用lagecy模式;
3. 在动态模式的情况下,分配内存使用rte_memzone_iova_contig,如 在调用rte_memzone_reserve()函数时指定rte_memzone_iova_contig作为flag,将保证底层iova连续(或者申请失败);
在dpdk 17.11中,有一个--huge-unlink选项可以在创建和映射大页面文件之后立即从hugetlbfs文件系统中删除它们。在dpdk 18.11后,这仍然有效,但是有一个新的eal命令行参数--in-memory,将激活所谓的内存模式,建议使用它代替--huge-unlink。
所谓内存模式,即dpdk不会在任何文件系统(hugetlbfs或其他文件系统)上创建文件。dpdk首先避免创建任何文件,而不是创建然后删除文件(因此仍然需要hugetlbfs文件系统)。实际上,在此模式下甚至不需要hugetlbfs挂载点,因此使用此模式使dpdk更加易于设置,在hugetlbfs挂载点不常见的环境中工作(例如云本地场景)。
此外,与--huge-unlink仅处理大页文件并且不会阻止eal创建任何其他文件不同的是,--in-memory模式还覆盖了eal创建的其他文件,这实际上允许dpdk运行和关闭只读文件系统,同时避免申请对该系统的写访问权。
从dpdk 22.11代码eal_memalloc_init中我们可知,在指定了in_memory参数后,会尝试memfd_create测试系统是否支持memfd共享内存方式,如果支持则dpdk内存直接采用memfd_create进行创建,否则就只能采用mmap创建匿名内存了。
在动态内存中我们还看到一个single_file_segments的参数,较旧的dpdk版本在hugetlbfs文件系统中的每个大页上存储一个文件,这适用于大多数用例,但有时会出现问题,特别是,vhost-user后端的virtio将与后端共享文件,并且有可共享文件描述符数量的硬性限制。当使用大页(例如1 gb的页面)时,它可以很好地工作,但是在页面大小较小的情况下(如2m页),文件数量会很快超过文件描述符限制。
为了解决此问题,版本dpdk 18.11中引入了一种新模式,即单文件段模式,该模式通过--single-file-segments eal命令行标志启用,这使得eal在hugetlbfs中创建的文件更少,并且使具有vhost-user后端的virtio甚至可以在{banned}最佳小页面大小下工作。此外注意,这个选项必须依赖memfd的支持。在指定这个参数后,原有一个memseg(hugepage)对应一个文件将变为一个memseg_list对应一个文件。
动态内存模式也支持了一些内存管理的回调机制,主要由两个api。
1. 内存映射更改时的回调:rte_mem_event_callback_register()
在lagecy模式下大页内存初始化完成后就固定不变了,但是在动态内存模式下应用的内存是会动态增加和减少的,对于有些模块是需要感知这些变化的,如vfio需要将整个内存pin住,所以就需要即使知道新增的内存,并将其pin住。因此dpdk提供了rte_mem_event_callback_register这个api,用于关系内存映射变化的模块注册相应函数。
2. 内存超限时的回调函数:rte_mem_alloc_validator_callback_register()
前面我们介绍过,在动态内存情况下可以通过--socket-limit参数来指定当前socket所能使用的内存大小上限。有时候我们不希望应用程序超出这个限制就一定返回失败,但又希望能够感知这种情况,因此可以通过rte_mem_alloc_validator_callback_register这个api注册回调函数,当应用程序申请的内存超过--socket-limit时注册函数就会被调用,我们可以在函数中输出一下警告信息,并做一些更温和的处理,如:可以接受超出限制的几百兆字节,但拒绝超出限制千兆字节的情况。
这也是动态内存模式新引入的一个特性,是通过--match-allocations这个参数指定的。他的作用是用于一些需要释放的的大页与分配的完全相同。什么意思呢?就是保证应用释放内存时和其当初申请的内存是完全一样的。这一点引用malloc_heap_free中代码更为直观:
点击(此处)折叠或打开
可以看到,如果释放的内存大小和当初申请的不同,就延时释放,当前这种模式会增加系统内存的消耗(因为内存可能无法及时释放)。
我们知道通过--huge-dir可以指定dpdk进程创建大页文件的目录,大页文件名称通常类似rtemap_0这种,通过--file-prefix可以指定大页文件的前缀,以便不同进程可以在同一个目录下创建不同的大页内存。在动态内存模式下大页文件是随时申请创建和删除的,如果在dpdk进程存在内存泄露或者进程crash,则这些大页文件可能会残留无法删除。当然我们可以使用--huge-unlink参数,这个参数可以在每次mmap完大页就立刻删除,但是它有可能删除其他进程创建的大页文件,因此针对这种情况动态内存情况建议使用--in-memory参数。
此外,dpdk进程默认每次启动都会删除当然目录的大页文件,然后花费大量时间创建hugepage以及初始化(清除大页信息)。为此dpdk提供了--huge-unlink=never参数,如果设置了这个启动参数,在启动时默认不会删除和清理原有大页,启动时会将原有memseg标记为rte_memseg_flag_dirty,在申请新内存时会将这部分内存进行清理。
通过--huge-worker-stack[=size]参数可以将dpdk线程的栈内存从hugepage中进行分配,还可以通过可选的size参数设置线程的栈大小,如果不指定size就使用系统的默认设置。
通过阅读内存相关代码,还有一些其他的内存管理的细节需要说明:
1. legacy模式不支持in_memory
点击(此处)折叠或打开
从以上代码中可以看出in_memory只支持在动态内存模式使用。
2. 动态内存模式不感知numa
在函数rte_eal_memseg_init中,有如下代码:
点击(此处)折叠或打开
可以看到,如果没有使用legacy的内存管理方式,则内存是无法感知numa的,因此编译指定numa_aware也是无效的,如果希望内存感知numa就需要用老的legacy模式管理内存。
3. 外部内存
{banned}最佳后就是在dpdk新版本中可以支持将外部内存注册给dpdk内存管理,比如自己应用通过非dpdk api malloc的内存或者mmap的内存。将这部分内存注册进dpdk的内存管理中,同样可以使用dpdk的内存api进行访问,详细使用方法可以参考dpdk代码中的 ./app/test/test_external_mem.c中的例子。