随笔-118  评论-133  文章-4  trackbacks-0

注:

    本文中都是基于ARM32来描述体系结构相关的内容

 

一、启动之前

  在详细描述linux kernel对内存的初始化过程之前,我们必须首先了解kernel在执行第一条语句之前所面临的处境。这时候的内存状况可以参考下图:


 注:

    还有一种旧的方案,不用DTB,Bootloader和Kernel之间通过ATAG传递参数,ATAG参数一般存放在Kernel image以下,TEXT_OFFSET里面,通过struct machine_desc的atag_offset参数来指定,比如s5p4418下是这样描述的:

 

bootloader有自己的方法来了解系统中memory的布局,然后,它会将绿色的kernel image和蓝色dtb image copy到了指定的内存位置上。kernel image最好是位于main memory起始地址偏移TEXT_OFFSET的位置,当然,TEXT_OFFSET需要和kernel协商好。kernel image是否一定位于起始的main memory(memory address最低)呢?也不一定,但是对于kernel而言,低于kernel image的内存,kernel是不会纳入到自己的内存管理系统中的。对于dtb image的位置,linux并没有特别的要求。由于这时候MMU是turn off的,因此CPU只能看到物理地址空间。

注:

    ARM32下,TEXT_OFFSET一般取0x00008000,取这个值是有深意的,因为上半部OFFSET为0x00004000的地方刚好用于存储一级页表(PGD),而下半部分前面也介绍了可能会用来存放bootloader传递的ATAG。

 

 二、汇编时代

一旦跳转到linux kernel执行,内核则完全掌控了内存系统的控制权,它需要做的事情首先就是要打开MMU,而为了打开MMU,必须要创建linux kernel正常运行需要的页表,这就是本节的主要内容。

在体系结构相关的汇编初始化阶段,我们会准备二段地址的页表:

第一段是identity mapping,其实就是把地址等于物理地址的那些虚拟地址mapping到物理地址上去,打开MMU相关的代码需要这样的mapping(别的CPU不知道,但是ARM ARCH强烈推荐这么做的)。

第二段是kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、rodata、data、bss等等)进行映射了(由于内核链接使用的虚拟地址,所以映射时也需要把链接使用的虚拟地址映射到对应的物理地址上)。具体的映射情况可以参考下图:

turn on MMU相关的代码被放入到一个特别的section,名字是.idmap.text,实际上对应上图中物理地址空间的IDMAP_TEXT这个block。这个区域的代码被mapping了两次,做为kernel image的一部分,它被映射到了__idmap_text_start开始的虚拟地址上去。此外,假设IDMAP_TEXT block的物理地址是A地址,那么它还被映射到了A地址开始的虚拟地址上去。虽然上图中表示的A地址似乎要大于PAGE_OFFSET,不过实际上不一定需要这样的关系,这和具体处理器的实现有关(比如s5p4418的内存起始物理地址PHYS_OFFSET为0x40000000,那IDMAP_TEXT block的物理地址应该是0x4xxxxxxx,比内核虚拟地址起始地址PAGE_OFFSET要小)

注:

    上图是基于ARM64的,所以idmap_pg_dir和swapper_pg_dir的位置和ARM32不同。ARM32下swapper_pg_dir对应地址是“KERNEL_RAM_VADDR - PG_DIR_SIZE”(比如0xc0008000-0x0004000=0xc0004000)。

编译器感知的是kernel image的虚拟地址(左侧),在内核的链接脚本中定义了若干的符号,都是虚拟地址。但是在内核刚开始,没有打开MMU之前,这些代码实际上是运行在物理地址上的,因此,内核起始刚开始的汇编代码基本上是PIC的,首先需要定位到页表的位置,然后在页表中填入kernel image mapping和identity mapping的页表项,如果传递了DTB,还会建立DTB的页表项,详细可参考linux内存管理----页表最初的初始化(从内核启动的第一段代码谈起)上的介绍。

一旦设定完了页表,那么打开MMU之后,kernel正式就会进入虚拟地址空间的世界,美中不足的是内核的虚拟世界没有那么大。原来拥有的整个物理地址空间都消失了,能看到的仅仅剩下kernel image mapping、identity mapping和DTB这几段地址空间是可见的。不过没有关系,这只是刚开始,内存初始化之路还很长。

 补充:内核内存初始化之路,充满了不确定性,一切起源于对内存布局的未知性(直到对ATAG/DTB数据的解释后)。所以内核只能基于目前的一些已知信息(比如内存物理地址偏移PHYS_OFFSET、内核虚拟地址偏移PAGE_OFFSET、mmu的代码片段、内核代码和数据、DTB存放起始地址)来准备好内存映像(段映射);然后加入一些约定,比如:内核映像放置于内存开始TEXT_OFFSET位置下(相当于预留了TEXT_OFFSET空间),对ATAG/DTB空间大小进行了约定(比如DTB不大于2M)。


三、内存布局

在内核开启 MMU 之后,整个内存世界实际上还处于一片黑暗之中,毕竟这时候内核并不知道当前系统中物理内存的信息,只是为内核 image、dtb 建立了页表,内核只有访问这两部分才是安全的。

紧接着,memblock 内存分配器就开始工作了,这是内核中静态定义的一个内存管理器,其首要工作就是将所有物理内存纳入管理,首先从通过扫描 dtb 获取物理内存的相关信息,看清楚整个物理内存世界,然后将那些已经被使用的内存设为保留,比如内核镜像所在内存、dtb 内存、页表内存,至于其它的内存都是空闲可支配的。

收集内存布局的信息主要来自下面几条途径: 

(1)choosen node。该节点有一个bootargs属性,该属性定义了内核的启动参数,而在启动参数中,可能包括了mem=nn[KMG]这样的参数项。initrd-start和initrd-end参数定义了initial ramdisk image的物理地址范围。

(2)memory node。这个节点主要定义了系统中的物理内存布局。主要的布局信息是通过reg属性来定义的,该属性定义了若干的起始地址和size条目。

(3)DTB header中的memreserve域。对于dts而言,这个域是定义在root node之外的一行字符串,例如:/memreserve/ 0x05e00000 0x00100000;,memreserve之后的两个值分别定义了起始地址和size。对于dtb而言,memreserve这个字符串被DTC解析并称为DTB header中的一部分。更具体的信息可以参考device tree基础文档,了解DTB的结构。

(4)reserved-memory node。这个节点及其子节点定义了系统中保留的内存地址区域。保留内存有两种,一种是静态定义的,用reg属性定义的address和size。另外一种是动态定义的,只是通过size属性定义了保留内存区域的长度,或者通过alignment属性定义对齐属性,动态定义类型的子节点的属性不能精准的定义出保留内存区域的起始地址和长度。在建立地址映射方面,可以通过no-map属性来控制保留内存区域的地址映射关系的建立。

  通过对DTB中上述信息的解析,其实内核已经基本对内存布局有数了,但是如何来管理这些信息呢?这也就是著名的memblock模块,主要负责在初始化阶段用来管理物理内存。一个参考性的示意图如下:

内核在收集了若干和memory相关的信息后,会调用memblock模块的接口API(例如:memblock_add、memblock_reserve、memblock_remove等)来管理这些内存布局的信息。内核需要动态管理起来的内存资源被保存在memblock的memory type的数组中(上图中的绿色block,按照地址的大小顺序排列),而那些需要预留的,不需要内核管理的内存被保存在memblock的reserved type的数组中(上图中的青色block,也是按照地址的大小顺序排列),详细可参考inux内存管理----memblock

四、内存映射paging_init

最重要的是map_lowmem,借助于memblock,把可用(非reserved)的物理内存地址小于lowmem_limit的内存映射到内核空间,实际的内存映射工作在create_mapping中完成。

/* arch/arm/mm/mmu.c */
static void __init map_lowmem(void)
{
 
 for_each_memblock(memory, reg) {
  start 
= reg->base;
  
end = start + reg->size;

  
if (end > lowmem_limit)
   
end = lowmem_limit;
  
if (start >= end)
   break;

  map.pfn 
= __phys_to_pfn(start);
  map.virtual 
= __phys_to_virt(start);
  map.length 
= end - start;
  map.type 
= MT_MEMORY;

  create_mapping(
&map, false);
 }
}

 详细可参考linux内存管理----paging_init


五、结束

目前为止,所有为内存管理做的准备工作已经完成:收集了整个内存布局的信息,memblock模块中已经保存了所有需要管理memory region的信息,同时,系统也为所有的lowmem(reserved除外)创建了地址映射。虽然整个内存管理系统没有ready,但是通过memblock模块已经可以在随后的初始化过程中进行动态内存的分配。 有了这些基础,随后就是真正的内存管理系统的初始化了,我们下回分解。

参考资料:

 

1、内存初始化(上)

2、linux内存管理----页表最初的初始化(从内核启动的第一段代码谈起)

3、linux内存管理----memblock

4、linux内存管理----paging_init

posted on 2022-12-29 09:43 lfc 阅读(174) 评论(0)  编辑 收藏 引用
只有注册用户登录后才能发表评论。