【计算子系统】内存管理之六:初始化

  初始化过程往往是比较冗长且乏味的,如果一接触就开始学习这块内容会让人烦闷。在了解内存管理几个核心功能模块后,我们再回头看看内存管理的初始化过程,相信大家会有新的收获。计算子系统相关内容目录点此进入

低级阶段-汇编实现

  我们可以把整个内存初始化过程大体分成低级阶段和高级阶段两个过程。低级阶段主要是用低级汇编语言实现的,又可细分成三步:

  首先是BIOS,它在计算机启动的最初阶段检测了物理内存的分布情况,并在x86实模式的高端地址处(1M内存以下)记录了这些内存分布信息(称为e820表)。e820表是一个数组,每一项记录了一段连续内存信息,包含起始地址、结束地址和内存类型。内存类型分为普通内存(RAM)、保留内存(RESERVED,如BIOS内存)、ACPI表空间等。此外,对于NUMA结构系统,BIOS还将产生ACPI的SRAT表,用来记录每个numa节点的内存分布。

  接着BIOS通过引导GRUB,再由GRUB将内核实模式部分和保护模式部分加载进内存。之后GRUB跳转到内核实模式部分执行,此时内核通过int指令获取e820表信息,由此得知物理内存分布。

  在低级阶段的最后,内核进入保护模式,设置了高端虚拟地址到物理地址的线性映射,并将栈空间设定在了0号进程(BSP启动核对应的IDLE进程)的栈空间,随后就进入了高级阶段。

高级阶段-C语言实现

  高级阶段的入口函数是start_kernel,与内存管理相关的部分主要setup_arch中。

  首先,我们会看到一些以memblock_打头的函数,这是干嘛的?我们应该知道,完整的内存初始化过程完成之前,正常的内存申请和释放功能是没法使用的。但是内核初始化过程中也需要动态分配内存,这就产生了矛盾。内核的做法是在初始化阶段使用一个简便的内存管理方法,这就是memblock。它从e820表中获知内存的分布情况,并以简单的连续分配方法来管理内存。所以在内核初始化阶段,它使用memblock来进行内存的申请和释放。

  接着在init_mem_mapping中,内核将direct mapping区线性映射到整个物理空间。如此一来,内核便可访问所有物理内存了。大家可以回顾下Linux 3.10/Documentation/x86/x86_64/mm.txt。

  再接着在initmem_init中,通过读取ACPI的SRAT表获知NUMA信息,并将这些信息更新到memblock中,此时内核就得知了完整的内存分布信息:有多少段内存,每段内存分别属于哪个NUMA节点。这里内核会为每个节点创建struct pglist_data结构,用来记录内存分布信息。

  随后进入了和分页相关的初始化过程paging_init。这里又涉及内核的sparse_memory特性,这又是什么鬼?内核在管理内存时,是需要分配独立的内存页来记录内存信息的,比如struct page数组。早期的内核是按最大物理内存量固定分配,对于小内存场景,这种方法问题不大。然而当前系统内存越来越大(x86_64最大支持2^46),而且内存可能动态增加(热插内存条)时,固定分配的方法就不适用了。sparse memory则以更灵活的方式来分配管理内存,如下图所示:

  内核将所有内存(最大2^46)划分成区(128M),并通过一个数组mem_section来记录每个区的信息。对于数组mem_section,是按4K页为粒度来分配空间,本质可以视为一个两维数组。内核通过memblock记录的信息为可用内存动态分配管理空间,不可用的区间将置为空,或者将section_mem_map低位清零(代表对应的区不存在)。在此过程中,内核也会对struct page数组分配空间,并将地址记录到section_mem_map中。

  完成sparse memory的初始化后,内核通过free_area_init_nodes来初始化内核NUMA节点的空闲内存信息。此时页管理系统没有任何空闲内存。那么空闲内存是怎么来的?这就回到start_kernel中,它先通过build_all_zonelists建立分配zone序列,再通过mem_init调用free_all_bootmem,这里会把memblock中的空闲内存释放到页管理系统中。此后,内核就可以使用正常的alloc_pages函数来分配页了。

  在2017年的最后几天里,终于把内核中有关内存管理方面的基础内容分析完了,但整个内存管理涉及的知识面非常广,还包括:反向映射、大页内存管理、KSM、cgroup_memory等多个方面。要想真正精通内存管理,需要坚持长期学习,并不断总结与实践。2018,继续努力!


转载请注明:吴斌的博客 » 【计算子系统】内存管理之六:内存初始化