通过延迟内存块初始化缩短Linux系统引导时间
发表于 2024-06-14 12:42:43

如果您是Linux系统内核的开发人员,并且正在寻找减少引导时间的方法,请关注一下延迟内存块初始化。我们的Linux系统内核团队一直在探索更快地引导设备的方法,从而在这一领域取得了一定进展。

在本篇博文中,我将说明以延迟方式对某些系统内存块进行初始化的背景,并引导您完成实现初始化的选项。(这是本人在2024年嵌入式开源峰会上所做演讲“减少引导时间的延迟内存块初始化”的内容摘要。详情和链接见下文。)

系统内存初始化

以下为系统内存初始化的可视化演示:

start_kernel: 启动内核

setup_arch: 架构初始化

memblock_init: 初始化内存块

paging_init: 初始化分页

bootmem_init: 初始化引导内存

mm_init: 初始化内存

中间的一串字:初始化整个12GB内存(在CPU单线程中完成)

12GB DDR: 12GB DDR内存

(注:除非另有说明,否则在上图以及本文中,我将使用12GB作为内存的标准量。)

在内核启动后,首先进行的是架构初始化,包含内存块初始化、分页初始化、引导内存初始化,然后才是内存初始化:

  • paging_init是内存管理的基础,在内核引导时调用。它为所有DDR内存设置页表。
  • bootmem_init通过创建memmap元数据(也称为分页结构体)的方式对所有内存块初始化。内存中的每个页面均由分页结构体映射,因此bootmem_init负责内存块初始化。它还负责对内存中的所有区段进行稀疏内存初始化。
  • mm_init对所有内核内存分配器进行初始化,例如:stack_depot_early_init(),kmem_cache_init(),kmemleak_init(),vmalloc_init()和mm_cache_init()。此处最令人感兴趣的分配器是mem_init(),因为它的执行时间取决于设备上的内存数量。mem_init()将引导后空闲的所有页作为空闲列表的一部分提供给系统分配程序。

问题是这些内存块(包括memblock_init)的执行时间取决于您所拥有的DDR内存的数量。DDR内存越大,初始化需要进行的工作和耗时就越多。所有四个初始化函数都需要对所有已安装的内存进行初始化,使用引导CPU以单线程方式执行,即使在对称多处理(SMP)系统上也是如此。因此,在具有数百GB(甚至TB)内存的设备上,要用更长的时间才能完成引导序列并启动用户空间。

因此,我们的想法是将某些内存块初始化推迟,从而减少引导时间并更快地进入用户空间。

延迟内存块初始化

通过只使用内存映射的一个子集来引导系统,可以减少引导时间。之所以有效,是因为您不需要使用所有可用内存来引导内核;该子集只需要足够的内存来进行内核和用户空间的初始化。稍后——也就是说,一旦进行了内核初始化,并且引导了次级CPU ——就可以以并行方式对其余的内存块进行初始化。

这种方法基于DEFERRED_STRUCT_PAGE_INIT。它已经出现在最近的五代Linux内核中,主要用于具有TB级系统内存的服务器。在加载所有驱动程序后,使用内核线程和延迟探测机制,以延迟方式对剩余内存进行初始化。

由于这发生在内核初始化结束时,因此可能会影响该时间窗口内的其他函数,从而对性能产生潜在的负面影响。正如我下面所描述的,这种影响微乎其微。

这是延迟内存初始化的可视化演示,描绘了DDR内存,时间和步骤:

HAL客户端初始化

9-10GB

延迟初始化

安卓核心与HAL初始化

HAL初始化完成。

所有内存块已完成初始化

用户空间实体对各个内存块初始化

2-3GB

内核初始化

安卓初始化

次级CPU启动

12GBDDR内存

时间

T0 – 设备启动

假设DDR内存大小为12 GB,前2到3个 GB足以对内核和用户空间进行初始化。使用bootmem_init,告诉内核只在那么多的内存中进行初始化。

如要对剩余的9或10 GB进行初始化,可以采用延迟方式进行初始化,或者使用内核线程进行初始化,或者通过用户空间进行初始化。如下所述,通过用户空间选项,您可以灵活地向系统中的不同节点和内存区域添加内存。您还可以向不同的区域添加不同大小的内存。

引导时间是CPU功率的函数,如下图所示:

左边的黑条描述了使用例如12GB内存进行引导的正常场景。所有工作都是引导CPU0在相对较低的功率下完成的,因此所花费的时间如顶部虚线所示。

从中间到右边,您可以看到延迟内存初始化是如何进行的。我们使用CPU0仅仅在内存的一个子集中进行内核初始化。一旦内核和对称多处理器被初始化,我们就可以使用更强大的CPU1和CPU2。我们使用这些CPU,以并行方式调度不同内存区域进行初始化。最后,一旦可以使用CPU3(一个更大更强的CPU),我们就用它来初始化剩余的内存。

对相同数量的内存进行初始化所需要的时间更少,因为我们已经将工作从CPU卸载到其他更强大的CPU上。对其进行初始化的时间越快,对剩余内存块进行初始化的效率就越高,也就能越快进入用户空间。

将内存添加到所需区域

在上文中,我提到通过延迟内存初始化的用户空间选项,您可以灵活地将内存添加到系统中的不同节点和区域。下文说明了添加方法。

Linux内核将系统随机存储器划分为不同的内存区。其中包括ZONE_NORMAL(用于不可移动DMA分配的内存)、ZONE_DMA(用于支持32位寻址的硬件的DMA应用程序)和ZONE_MOVABLE(用于用户空间应用程序)。对于单一非一致内存访问(NUMA)节点,您可以将具有12 GB内存的系统配置为具有2 GB的DMA、5 GB的正常区和5 GB的可移动区。

但是,您不能使用内核(内核线程)来添加内存,因为内核并不了解这类配置,比如为每个区域分配多少内存。与其使用内核添加内存,不如使用用户空间,用户空间可以了解各种配置并使用内存热插拔框架。例如:

echo addr > /sys/devices/system/memory/probe

您可以通过用户空间运行echo addr进行探测,然后指定需要在给定地址上添加的内存量。内存的大小是固定的——对于Arm64系统通常是128MB——将其作为内存块的大小。

一旦添加了该内存,就可以根据需要对其进行初始化。

echo online_movable > /sys/devices/system/memory/memoryXXX/state

online_movable表示将刚刚添加的内存块初始化到可移动区域。

echo online_kernel > /sys/devices/system/memory/memoryXXX/state

online_kernel表示将刚刚添加的内存块初始化到正常区域。

如何限制引导内存并指定剩余内存

如要通知内核在引导期间内仅初始化一定数量的内存,比如2GB,我们可以使用mem= 命令行参数。一旦指定了该参数,就必须通知内核(或用户空间)随机存储器剩余多少内存——在本示例中,剩余内存为10 GB。这需要快速计算剩余内存。

设备树(DT)包含一个类似于下面的内存节点:

/ {
#address-cells = <2>;
#size-cells = <2>;
memory { device_type = "memory"; reg = <0x0 0x0 0x3 0x00000000>; };

通常按照DDR内存的大小,由引导加载程序对其进行初始化。它利用分区的大小和起始地址填充reg属性(即内存分区表)。例如,reg中的单个条目可以指定在0地址处有12 GB的内存。

在内核驱动程序中,您可以扫描内存节点以查找reg属性;其中给出了随附内存的大小。然后,bootmem块地址指示内核初始化结束的位置,此处为2GB。因为您知道bootmem_dram_end是12 GB,所以您知道还有10 GB的内存。无论您使用内核还是用户空间,这些内存都是以延迟方式添加。

用于延迟内存初始化的内核和用户空间选项

首先,我将说明内核方法——使用mem=参数和一个名为add_memory_driver_managed()的内存热插拔函数——以及为什么它的灵活性较差。

内存热插拔具有以下命令行参数:

  • memhp_default_state=online表示如果您添加了指定数量的内存,就可以自动对其进行初始化,并且内核本身不需要使这些分页在线。
  • movable_node(或内核节点)指定了需要默认添加的内存区域。
  • 使用内核驱动程序的限制在于,您只能将内存添加到所要求的内存区域,并且无法在运行时更改您的选择。在以上示例中,对于10GB的延迟内存初始化,您必须将所有10GB内存作为可移动区域或正常区域添加;您不能将其分为多个区域。

幸运的是,用户空间方法克服了这一限制。该方法仍然使用内核,通过DT节点进行读取,获取mem=值并确定剩余内存的大小。但是,该方法随后通过sys/kernel/deferred_mem_init中的sysfs节点将这些数据暴露给用户空间:

  • sys/kernel/deferred_mem_init/memblock_end_of_dram在我们的示例中为2GB。
  • sys/kernel/deferred_mem_init/bootmem_end_of_dram处于12GB的末尾。
  • Sys /kernel/deferred_mem_init/deferred_kernelcore是针对内核核心添加到内核区域的数量。
  • Sys /kernel/deferred_mem_init/deferred_movablecore是针对移动核心添加到移动区域的数量。

采用用户空间方法确保您能够灵活地将内存添加到所需要的区域,而内核方法则无法确保这种灵活性。这种方式唯一的缺点是必须等到用户空间初始化完成才能进行。

这是一张DDR内存的说明图;一旦您将内存添加到所需要的区域:

Dram_size = 12GB

5GB

移动内核

12GB DDR

5GB

内核核心

10GB

延迟初始化

mem=2GB

(bootmem_size)

2 GB

  • bootmem_size = 2GB,因为mem= param设置为2GB。
  • dram_size = 12GB,即挂载到系统上的内存数量。
  • movablecore-size = 5GB,在DT属性中指定。
  • 这样可留下12 – (2 – 5)= 5GB的剩余内存。这将分配到正常区域。可通过内存热插拔,由用户空间将其添加至内核核心。

结果:减少引导时间

我们已经在配有12GBDDR内存的高通SM8550主板上,作为延迟机制的组成部分实现了此项功能。如上文示例所述,我们在引导期间将其限制为2 GB,从而测量到通过延迟方式添加8 GB可以节省160-200毫秒的时间。每千兆字节的内存大约减少20到30毫秒的引导时间。时间的节省呈线性,所以在有更多内存的系统上,您可以节省更多的时间。

我们通过分析paging_init和bootmem_init测量了该项功能的结果,发现该功能减少了加载内核和内核模块的时间。我们的脚本捕获了所有阶段的整体引导性能,从引导加载程序初始化到内核初始化,再到内核模块加载,再到启动本地守护进程;然后从用户空间初始化阶段开始,包括安卓服务和用户界面应用程序。

局限性和观察结果

我们获得的结果对于性能没有任何影响,因为在引导过程中没有任何进程或任务需要使用整个12 GB内存。不过,还是要考虑一下局限性和观察结果:

  • 测量结果表明,如使用较少的内存进行初始化,pagint_ing、bootmem_init和mm_init均会减少。但请注意,这些函数均在time_init之前执行,因此不能使用内核时间来衡量性能指标。我们的测量结果取决于其他硬件时间戳和时钟。
  • 如前所述,此项功能需要内存热插拔支持,而在Arm和x86架构中提供了内存热插拔支持。
  • 尽管您可以采用延迟方式对剩余的DDR内存进行初始化,但您不一定能够实现完全并行。例如,如果您需要将5GB的内存要添加到正常区域,您不能将其分成5个1GB区段,将其分配到不同线程中的不同CPU,并尝试实现并行。每个区域都会被锁定,试图向同一区域添加内存的其他线程必须等待,直到解除了区域锁定。另一方面,如果您试图将内存添加到不同节点的不同区域,那么您可以实现并行。例如,一个CPU将内存添加到可移动区域,另一个CPU将内存添加到正常区域。
  • 我们的示例假定2 GB足以在具有12 GB内存的系统上引导内核或用户空间。然而,我们发现2GB并不总是足够的。更成熟的安卓版本和OEM版本通常需要4-5 GB的引导空间。这一数字因每一代芯片组或系统而有所不同,所以要做好试验准备。
  • 该项功能需要健壮的代码和错误处理。如您利用有限的内存引导系统,并且您的延迟内存代码出现问题,则无法对剩余的(例如:10 GB)内存进行初始化。您的用户永远被限制为2 GB的DDR内存。在这种情况下,由于内核承担了大部分责任,因此应该由内核检查剩余的内存是否被添加到系统中。用户空间是否正确地添加了内存?如果没有,那么内核应该接管并使用任何可用方法添加剩余的内存。
  • 该项功能有可能会干扰在引导序列中早期运行的任务。在我们的测量结果中,我们发现一旦用户空间达到上限,任何正在运行的任务都不会受到性能影响。

我们的升级路径  密切关注

我们还没有升级此项功能。关于所实现的确切引导时间节省和并行程度,我们进行了长时间的探讨,而我们得出结论的是确实存在益处。我们计划对内核方法进行升级,因为如果开发人员或原始设备制造商实施了自己的用户空间方法,那么这就不是一个端到端的解决方案。因此,我们将不得不依赖于开发人员实现用户空间的方式。

我们的计划是纠正本人提到的局限性来升级内核方法,允许内核将内存添加到特定区域,并具有用户空间所具有的灵活性。这有其自身的局限性,因为我们的应用程序接口必须确保不会执行有关不连续区域(例如:正常,然后可移动,然后正常)的调用。我们将寻求获得上游社区对本公司方法的评价。

最后,我们计划将其作为一个单独的内核驱动程序予以实施,该内核驱动程序结合了三个命令行参数:

  • mem =在引导过程中限制内存
  • deferred_mem.Kernelcore =nn%作为百分比的组成部分
  • deferred_mem.Movablecore =nn%作为另一个百分比

可以使用配置或命令行参数启用或禁用内核驱动程序,而不是依赖于用户空间的实现。

如要获得更多具体内容,请参见我在2024年嵌入式开源峰会上的演讲,以及幻灯片集和视频链接:为减少引导时间的延迟内存块初始化

在所发布内容中表达的观点仅为原作者的个人观点,并不代表高通公司或其子公司(以下简称为“高通公司”)的观点。所提供的内容仅供参考之用,而并不意味着高通公司或任何其他方的赞同或表述。本网站同样可以提供非高通公司网站和资源的链接或参考。高通公司对于可能通过本网站引用、访问、或链接的任何非高通公司网站或第三方资源并没有做出任何类型的任何声明、保证、或其他承诺。

CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
微博关注
【免责声明:CSDN本栏目发布信息,目的在于传播更多信息,丰富网络文化,稿件仅代表作者个人观点,与CSDN无关。其原创性以及文中陈述文字和文字内容未经本网证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本网不做任何保证或者承诺,请读者仅作参考,并请自行核实相关内容。您若对该稿件有任何怀疑或质疑,请立即与CSDN联系,我们将迅速给您回应并做处理。】