蜗窝科技

内存初始化代码分析(三):创建系统内存地址映射

linuxer · · 内存管理 · 阅读 27218 · 2016年11月存档

一、前言

经过内存初始化代码分析(一)和内存初始化代码分析(二)的过渡,我们终于来到了内存初始化的核心部分:paging_init。当然本文不能全部解析完该函数(那需要的篇幅太长了),我们只关注创建系统内存地址映射这部分代码实现,也就是解析paging_init中的map_mem函数。

同样的,我们选择的是4.4.6的内核代码,体系结构相关的代码来自ARM64。

二、准备阶段

在进入实际的代码分析之前,我们首先回头看看目前内存的状态。偌大的物理地址空间中,系统内存占据了一段或者几段地址空间,这些信息被保存在了memblock模块中的memory type类型的数组中,数组中每一个memory region描述了一段系统内存信息(base、size以及node id)。OK,系统内存就这么多,但是也并非所有的memory type类型的数组中区域都是free的,实际上,有些已经被使用或者保留使用的内存区域需要从memory type类型的一段或者几段地址空间中摘取下来,并定义在reserved type类型的数组中。实际上,在整个系统初始化过程中(更具体的说是内存管理模块完成初始化之前),我们都需要暂时使用memblock这样一个booting阶段的内存管理模块来暂时进行内存的管理(收集内存布局信息只是它的副业),每次分配的内存都是在reserved type数组中增加一个新的item或者扩展其中一个memory region的size而已。

通过memblock模块,我们已经收集了内存布局的信息(memory type类型的数组),也知道了free memory资源的信息(memory type类型的数组减去reserved type类型的数组,从集合论的角度看,reserved type类型的数组是memory type类型的数组的真子集),但是,要管理这些珍贵的系统内存,首先要能够访问他们啊(顺便说一句:memblock中的那些数组定义的地址都是物理地址),通过前面的分析文章,我们知道有两段内存已经是可见的(完成了地址映射),一个是kernel image段,另外一个是fdt段。而广大的系统内存区域仍然在黑暗之中,等待我们去拯救(进行地址映射)。

最后,我们思考这样一个问题:是否memory type类型的数组代表了整个的系统内存的地址空间呢?当然不是,有些驱动可能会保留一段系统内存区域为自己使用,同时也不希望OS管理这段内存(或者说对OS不可见),而是自己创建该段内存的地址映射。如果你对dts中的memory reserve节点比较熟悉的话,那么实际上这样的reserved memory region是有no-map属性的。这时候,内核初始化过程中,在解析该reserved-memory节点的时候,会将该段地址从memblock模块中移除。而在map_mem函数中,为所有memory type类型的数组创建地址映射的时候,有no-map属性的那段内存地址将不会创建地址映射,也就不在OS的控制范围内了。

三、概览

创建系统内存地址映射的代码在map_mem中,如下:

static void __init map_mem(void) {
struct memblock_region *reg;
phys_addr_t limit;

limit = PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE;---------------(1)
memblock_set_current_limit(limit);

for_each_memblock(memory, reg) {------------------------(2)
phys_addr_t start = reg->base;――确定该region的起始地址
phys_addr_t end = start + reg->size; ――确定该region的结束地址

if (start >= end)--参数检查
break;

if (ARM64_SWAPPER_USES_SECTION_MAPS) {----------------(3)
if (start < limit)
start = ALIGN(start, SECTION_SIZE);
if (end < limit) {
limit = end & SECTION_MASK;
memblock_set_current_limit(limit);
}
}
__map_memblock(start, end);-------------------------(4)
}

memblock_set_current_limit(MEMBLOCK_ALLOC_ANYWHERE);----------(5)
}

(1)首先限制了当前memblock的上限。之所以这么做是因为在进行mapping的时候,如果任何一级的Translation table不存在的话都需要进行页表内存的分配。而在这个时间点上,伙伴系统没有ready,无法动态分配。当然,这时候memblock已经ready了,但是如果分配的内存都还没有创建地址映射(整个物理内存布局已知并且保存在了memblock模块中的memblock模块中,但是并非所有系统内存的地址映射都已经建立好的,而我们map_mem函数的本意就是要创建所有系统内存的mapping),内核一旦访问memblock_alloc分配的物理内存,悲剧就会发生了。怎么破?这里采用了限定memblock上限的方法。一旦设定了上限,那么memblock_alloc分配的物理内存不会高于这个上限。

设定怎样的上限呢?基本思路就是在map_mem的调用过程中,不需要分配translation table,怎么做到呢?当然是尽量利用已经静态定义好的那些页表了。PHYS_OFFSET是物理内存的起始地址,SWAPPER_INIT_MAP_SIZE 是启动阶段kernel direct mapping的size。也就是说,从PHYS_OFFSET到PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE的区域,所有的页表(各个level的translation table)都已经OK,不需要分配,只需要把描述符写入页表即可。因此,如果将当前memblock分配的上限设定在这里将不会产生内存分配的动作(因为页表都已经ready)。

(2)对系统中所有的memory type的region建立对应的地址映射。由于reserved type的memory region是memory type的region的真子集,因此reserved memory 的地址映射也就一并建立了。

(3)如果不使用section map,那么我们在kernel direct mapping区域静态分配了PGD~PTE的页表,通过起始地址对齐以及对memblock limit的设定就可以保证在create_mapping()的时候不分配页表内存。但是在下面的情况下:

(A)Memory block的start或者end地址并没有对齐在2M上

(B)使用section map

在这种情况下,调用create_mapping()的时候会分配pte页表内存(没有对齐2M,无法进行section mapping)。怎么破?还好第一个memory block(也就是kernel image所在的block)的start address是必定对齐在2M地址上的,所以只要考虑end地址,这时候需要适当的缩小limit到end & SECTION_MASK就可以保证分配的页表内存是已经建立地址映射的了。

(4)__map_memblock代码如下:

static void __init __map_memblock(phys_addr_t start, phys_addr_t end)
{
create_mapping(start, __phys_to_virt(start), end - start,
PAGE_KERNEL_EXEC);
}

需要说明的是,在map_mem之后,所有之前通过__create_page_tables创建的描述符都被覆盖了,取而代之的是新的映射,并且memory attribute如下:

#define PAGE_KERNEL_EXEC __pgprot(_PAGE_DEFAULT | PTE_UXN | PTE_DIRTY | PTE_WRITE)

大部分memory attribute保持不变(例如MT_NORMAL、PTE_AF 、 PTE_SHARED等),有几个bit需要说明一下:PTE_UXN,Unprivileged Execute-never bit,也就是限制userspace从这里取指执行。PTE_DIRTY是一个软件设定的bit,硬件并不操作这个bit,OS软件用这个bit标识该entry是clean or dirty,如果是dirty的话,说明该page的数据已经被写入过,如果该page需要被swapped out,那么还需要保存dirty的数据才能回收该page。关于PTE_WRITE的解释todo。

(5)所有的系统内存的地址映射已经建立完毕,取消之前的上限,让memblock模块可以自由的分配内存。

四、填充PGD中的描述符

create_mapping实际上是调用底层的__create_mapping函数完成地址映射的,具体代码如下:

static void __init create_mapping(phys_addr_t phys, unsigned long virt,
phys_addr_t size, pgprot_t prot)
{
if (virt < VMALLOC_START) {
pr_warn(“BUG: not creating mapping for %pa at 0x%016lx - outside kernel range\n”,
&phys, virt);
return;
}
__create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
size, prot, early_alloc);
}

create_mapping的作用是将起始物理地址等于phys,大小是size的这一段物理内存mapping到起始虚拟地址是virt的虚拟地址空间,映射的memory attribute是prot。内核的虚拟地址空间从VMALLOC_START开始,低于这个地址就不对了,验证完虚拟地址,底层是调用__create_mapping函数,传递的参数情况是这样的,init_mm是内核空间的内存描述符,pgd_offset_k是根据给定的虚拟地址,在kernel space的pgd中找到对应的描述符的位置,early_alloc是在mapping过程中,如果需要分配内存的话(页表需要内存),调用该函数进行内存的分配。__create_mapping函数具体代码如下:

static void __create_mapping(struct mm_struct *mm, pgd_t *pgd,
phys_addr_t phys, unsigned long virt,
phys_addr_t size, pgprot_t prot,
void *(*alloc)(unsigned long size))
{
unsigned long addr, length, end, next;

addr = virt & PAGE_MASK;------------------------(1)
length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

end = addr + length;
do {----------------------------------(2)
next = pgd_addr_end(addr, end);--------------------(3)
alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);----------(4)
phys += next - addr;
} while (pgd++, addr = next, addr != end);
}

创建地址映射熟悉要明确地址空间,不同的进程有不同的地址空间,struct mm_struct就是描述一个进程的虚拟地址空间,当然,我们这里的场景是为内核虚拟地址空间而创建地址映射,因此传递的参数是init_mm。需要创建地址映射的起始虚拟地址是virt,该虚拟地址对应的PUD中的描述符是一个8B的内存,pgd就是指向这个描述符内存的指针。

(1)因为地址映射的最小单位是page,因此这里进行mapping的虚拟地址需要对齐到page size,同样的,长度也需要对齐到page size。经过对齐运算,(addr,length)定义的地址范围应该是囊括(virt,size)定义的地址范围,并且是对齐到page的。

(2)(addr,length)这个虚拟地址范围可能需要占据多个PGD entry,因此这里我们需要一个循环,不断的调用alloc_init_pud函数来完成(addr,length)这个虚拟地址范围的映射,当然,alloc_init_pud函数其实也会建立下游(例如PUD、PMD、PTE)翻译表的entry。

(3)pgd中的一个描述符只能mapping有限区域的虚拟地址(PGDIR_SIZE),pgd_addr_end的宏就是计算addr所在区域的end address。如果计算出来的end address小于传入的end地址参数,那么返回end参数值。也就是说,如果(addr,length)这个虚拟地址范围的mapping需要跨越多个pgd entry,那么next变量保存了下一个pgd entry的起始虚拟地址。

(4)这个函数有两个作用,一是填充pgd entry,二是创建后续的pud translation table(如果需要的话)并进行下游Translation table的建立。

五、分配PUD页表内存并填充相应的描述符

alloc_init_pud并非只是操作pud,实际上它是操作pgd的一个entry,并分配初始pud以及后续translation table的。填充PGD的entry需要给出对应PUD translation table的内存地址,如果PUD不存在,那么alloc_init_pud还需要分配PUD translation table(page size),只有得到PUD翻译表的物理内存地址,我们才能填充PGD entry。具体代码如下:

static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
void *(*alloc)(unsigned long size))
{
pud_t *pud;
unsigned long next;

if (pgd_none(*pgd)) {--------------------------(1)
pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
pgd_populate(mm, pgd, pud);
}
pud = pud_offset(pgd, addr); ---------------------(2)
do { --------------------------------(3)
next = pud_addr_end(addr, end);

if (use_1G_block(addr, next, phys)) { ----------------(4)
pud_t old_pud = *pud;
set_pud(pud, __pud(phys | pgprot_val(mk_sect_prot(prot)))); -----(5)

if (!pud_none(old_pud)) { ---------------------(6)
flush_tlb_all(); ------------------------(7)
if (pud_table(old_pud)) {
phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
if (!WARN_ON_ONCE(slab_is_available()))
memblock_free(table, PAGE_SIZE); ------------(8)
}
}
} else {
alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
}
phys += next - addr;
} while (pud++, addr = next, addr != end);
}

(1)如果当前pgd entry是全0的话,说明还没有对应的下级PUD页表内存,因此需要进行PUD页表内存的分配。需要说明的是这时候,伙伴系统没有ready,分配内存仍然使用memblock模块,pgd_populate用来建立pgd entry和PUD 页表内存的关系。

(2)至此,pud的页表内存已经有了,但是addr对应PUD中的哪一个描述符呢?pud_offset给出了答案,其返回的指针指向传入参数addr地址对应的pud 描述符内存,而我们随后的任务就是填充pud entry了。

(3)虽然(addr,end)之间的虚拟地址范围共享一个pgd entry,但是这个地址范围对应的pud entry可能有多个,通过循环,逐一填充pud entry,同时分配并初始化下一阶页表。

(4)如果没有可能存在的1G block地址映射,这里的代码逻辑和上一节中的类似,只不过不断的循环调用alloc_init_pud改成alloc_init_pmd即可。然而,ARM64的MMU硬件提供了灰常强大的功能,系统支持1G size的block mapping,如果能够应用会获取非常多的好处:不需要分配下级的translation table节省了内存,更重要的是大大降低了TLB miss,提高了性能。既然这么好,当然要使用,不过有什么条件呢?首先系统配置必须是4k的page size,这种配置下,一个PUD entry可以覆盖1G的memory block。此外,起止虚拟地址以及映射到的物理地址都必须要对齐在1G size上。

(5)填写一个PUD描述符,一次搞定1G size的address mapping,没有PMD和PTE的页表内存,没有对PMD 和PTE描述符的访问,多么简单,多么美妙啊。假设系统内存4G,并且物理地址对齐在1G上(虚拟地址PAGE_OFFSET本来就是对齐在1G的),那么4个PUD的描述符就搞定了内核空间的线性地址映射区间。

(6)如果pud entry是非空的,那么就说明之前已经有了对该段地址的mapping(也许是只映射了部分)。一个简单的例子就是起始阶段的kernel image mapping,在__create_page_tables创建pud 以及pmd中entry。如果不能进行section mapping,那么还建立了PTE中的描述符,现在这些描述符都没有用了,我们可以丢弃它们了。

(7)虽然建立了新的页表,但是旧的页表还残留在了TLB中,必须将其“赶尽杀绝”,清除出TLB。

(8)如果pud指向了一个table描述符,也就是说明该entry指向一个PMD table,那么需要释放其memory。

六、分配PMD页表内存并填充相应的描述符

1G block mapping虽好,不一定适合所有的系统,下面我一起看看PUD entry中填充的是block descriptor的情况(描述符指向PMD translation table):

static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
void *(*alloc)(unsigned long size))
{
pmd_t *pmd;
unsigned long next;

if (pud_none(*pud) || pud_sect(*pud)) {-------------------(1)
pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));---分配pmd页表内存
if (pud_sect(*pud)) {--------------------------(2)
split_pud(pud, pmd);
}
pud_populate(mm, pud, pmd);---------------------(3)
flush_tlb_all();
}
BUG_ON(pud_bad(*pud));

pmd = pmd_offset(pud, addr);-----------------------(4)
do {
next = pmd_addr_end(addr, end);
if (((addr | next | phys) & ~SECTION_MASK) == 0) {------------(5)
pmd_t old_pmd =*pmd;
set_pmd(pmd, __pmd(phys | pgprot_val(mk_sect_prot(prot))));

if (!pmd_none(old_pmd)) {----------------------(6)
flush_tlb_all();
if (pmd_table(old_pmd)) {
phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
if (!WARN_ON_ONCE(slab_is_available()))
memblock_free(table, PAGE_SIZE);
}
}
} else {
alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
prot, alloc);
}
phys += next - addr;
} while (pmd++, addr = next, addr != end);
}

(1)有两个场景需要分配PMD的页表内存,一个是该pud entry是空的,我们需要分配后续的PMD页表内存。另外一个是旧的pud entry是section 描述符,映射了1G的address block。但是现在由于种种原因,我们需要修改它,故需要remove这个1G block的section mapping。

(2)虽然是建立新的mapping,但是原来旧的1G mapping也要保留的,也许这次我们只是想更新部分地址映射呢。在这种情况下,我们先通过split_pud函数调用把一个1G block mapping转换成通过pmd进行mapping的形式(一个pud的section mapping描述符(1G size)变成了512个pmd中的section mapping描述符(2M size)。形式变了,味道不变,加量不加价,仍然是1G block的地址映射。

(3)修正pud entry,让其指向新的pmd页表内存,同时flush tlb的内容。

(4)下面这段代码的逻辑起始是和alloc_init_pud是类似的。如果不能进行2M的section mapping,那么就循环调用alloc_init_pte进行地址的mapping,这里我们就不多说了,重点看看2M section mapping的处理。

(5)如果满足2M section的要求,那么就调用set_pmd填充pmd entry。

(6)如果有旧的section mapping,并且指向一个PTE table,那么还需要释放这些不需要的PTE页表描述符占用的内存。

七、分配PTE页表内存并填充相应的描述符

static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
pgprot_t prot,
void *(*alloc)(unsigned long size))
{
pte_t *pte;

if (pmd_none(*pmd) || pmd_sect(*pmd)) {----------------(1)
pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
if (pmd_sect(*pmd))
split_pmd(pmd, pte);----------------------(2)
__pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);--------(3)
flush_tlb_all();
}
BUG_ON(pmd_bad(*pmd));

pte = pte_offset_kernel(pmd, addr);
do {
set_pte(pte, pfn_pte(pfn, prot));-------------------(4)
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
}

(1)走到这个函数,说明后续需要建立PTE这一个level的页表描述符,因此,需要分配PTE页表内存,场景有两个,一个是从来没有进行过映射,另外一个是已经建立映射,但是是section mapping,不符合要求。

(2)如果之前有section mapping,那么我们需要将其分裂成512个pte中的page descriptor。

(3)让pmd entry指向新的pte页表内存。需要说明的是:如果之前pmd entry是空的,那么新的pte页表中有512个invalid descriptor,如果之前有section mapping,那么实际上这个新的PTE页表已经通过split_pmd填充了512个page descritor。

(4)循环设定(addr,end)这段地址区域的pte中的page descriptor。

参考文献:

1、ARMv8技术手册

2、Linux 4.4.6内核源代码

原创文章,转发请注明出处。蜗窝科技

标签:create_mapping

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(22 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • tigger

    hello linuxer ================================================================================== 。PHYS_OFFSET是物理内存的起始地址,SWAPPER_INIT_MAP_SIZE 是启动阶段kernel direct mapping的size。也就是说,从PHYS_OFFSET到PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE的区域,所有的页表(各个level的translation table)都已经OK,不需要分配,只需要把描述符写入页表即可。因此,如果将当前memblock分配的上限设定在这里将不会产生内存分配的动作(因为页表都已经ready)。 ================================================================================== 我看到SWAPPER_INIT_MAP_SIZE,如果系统是4K page ,使用section page的话,这个大小是PUD_SHIFT,这个pud size,这个4K大小。是这样的吗?
    • linuxer

      @tigger:这个问题看来以前是漏掉了,呵呵~~~ 你说的pud size是指pud translation table size吧,当然是4K了,因为translation table size是跟随page size的。
  • langyijiang

    hello linuxer 目前项目中DDR是512M的,设备树中使用448M作为系统内存,留下64M做特殊用处,当在内核中特殊处理完成以后,我想将64M空间返还给内核,请问有什么策略吗? 望回复!!!!!!!!!
    • linuxer

      @langyijiang:使用free_reserved_area这个接口函数可以将reserved page归还给伙伴系统。
      • langyijiang

        @linuxer:不好意思,这段时间有事耽搁了,没看到回复哈。 这64M在系统起来的时候,并没有被MMU管理,使用这个接口释放内存不会是系统崩溃么?
        • Murphy

          @langyijiang:不会,前提是要将这64M内存在内核中设为保留
  • Murphy

    Hi linuxer, 在ARM的页表中有一个domain区域,在我的理解中用户进程的0~3G区间的全局页目录中的domain区域应该是 DOMAIN_USER ,但我在内核中没找到设定用户进程页目录domain区域的地方,不知能否告知下?
  • 时间为贵

    @linuxer memory type类型的数组减去reserved type类型的数组,从集合论的角度看,reserved type类型的数组是memory type类型的数组的真子集 ==》为什么会是真子集?在membloc_reserve的时候不会将对应region从memory type中移除吗?
    • wowo

      @时间为贵:至于是不是子集,可以先从debugfs中看看结论,例如: flo:/ $ cat /sys/kernel/debug/memblock/memory 0: 0x80200000..0x88dfffff 1: 0x8a000000..0x8d9fffff 2: 0x8ec00000..0x8effffff 3: 0x8f700000..0x8fdfffff 4: 0x90000000..0x9fdfffff 5: 0xa5d00000..0xfe9fefff flo:/ $ cat /sys/kernel/debug/memblock/reserved 0: 0x80204000..0x80207fff 1: 0x80208180..0x81580efb 2: 0x82200000..0x823c42c4 3: 0x88d00000..0x88dfffff 4: 0xa9fdb000..0xa9ffefff 5: 0xa9fff8c0..0xaaffffff 至于为什么不移除,也很好理解,memory就是所有的memory,reserve就是从所有的memory里面reserve一点。
  • 时间为贵

    @wowo 如此,前面文章中的部分表述是否不太合适? ==> 内存初始化代码分析(二):内存布局 ...... (3)size等于0的memory region表示这是一个动态分配region,base address尚未定义,因此我们需要通过__reserved_mem_alloc_size函数对节点进行分析(size、alignment等属性),然后调用memblock的alloc接口函数进行memory block的分配,最终的结果是确定base address和size,并将这段memory region从memory type的数组中移到reserved type的数组中。当然,如果定义了no-map属性,那么这段memory会从系统中之间删除(memory type和reserved type数组中都没有这段memory的定义)。
  • 神品江湖

    hello linuxer ==================================================================================================== 如果当前pgd entry是全0的话,说明还没有对应的下级PUD页表内存,因此需要进行PUD页表内存的分配。需要说明的是这时候,伙伴系统没有ready,分配内存仍然使用memblock模块,pgd_populate用来建立pgd entry和PUD 页表内存的关系。 ==================================================================================================== 上文说分配内存仍然使用memblock模块,memblock模块分配的是物理内存,这个内存是没有建立页表映射的,那如何访问它呢?
    • vedic

      @神品江湖:页表存储的就是物理地址, memblock里面记录的也是物理地址, 所以刚好
    • 明天

      @神品江湖:我理解memblock申请的内存是物理地址,要将该地址填充到页表中,页表就是需要物理地址的,所以该地址也就可以通过页表访问了
  • cs350203

    hell linuxer 一直不是很明白memblock_reserve 和memblock_remove memblock_reserve是被操作系统保留了,但是还会在系统中建立映射关系,而memblock_remove表示这段memory会从系统中之间删除,mam_mem不会建立映射关系,换句话税就是被memblock_remove 的内存是不是就没办法访问了。但是我实际测试发现用memblock_remove溢出一段物理内存后,在系统起来之后用/dev/mem还能访问,难道是我用/dev/mem访问重新建立了页表关系吗!
    • linuxer

      @cs350203:其实你可以自己在内核代码中找到答案的......
  • casio8040

    有点疑问,如果是三级映射在create_mapping中也要alloc_init_pud,分配PUD页表内存并填充相应的描述符,这有用吗,三级映射不是没有pud吗?
  • kidesire

    你好,感谢分享。有些疑问求教下,我原本的理解是,假设内核现在需要访问虚拟地址为virt对应的物理地址为phys上的内容, 那么MMU通过存放内核页表的寄存器得的内核页表swapper_pg_dir的虚拟地址即PGD页表基地址,然后根据virt计算对应的PGD entry(PGD页表基地址 + virt计算得到的offset, 后面PUD/PMD类似),从PGD entry中得到PUD页表基地址的虚拟地址,然后根据virt计算对应的PUD entry, 从PUD entry中得到PMD页表基地址的虚拟地址,然后根据virt计算对应的PMD entry,从PMD entry中得到PTE页表基地址的虚拟地址,然后根据virt计算PTE entry,从PTE entry中得到phys所在的物理页帧地址,加上virt计算得到的偏移后得到virt对应的物理地址,执行访问指令.经过上述描述,我一直有一个问题不解,PGD/PUD/PMD 的各个entry中存放的应该是下级页表基地址的虚拟地址才对,文章中和源码中却是 phys|port.不知我的思考哪里出了问题, 还望指点,万分感谢.
  • kidesire

    由于我一直以来认为mmu是通过页表的虚拟地址来访问页表的,这个想法可能是错的.由于一直以来的可能错误的想法给我阅读源码和博客时造成困惑, 因此重置固有想法,再进行思考. 假设mmu访问页表是通过物理地址直接访问的而不通过虚拟地址访问页表,也即ttbr0,ttbr1存储的是页表的物理地址, 以及各个pgd/pud/pmd/pte的entry中存储的也是物理地址相关值, 那么此问题就迎刃而解了. 因此我再次思考, mmu可以直接读取物理地址, 不需要通过虚拟地址转换. 并且重新查看__enable_mmu代码, 发现, ENTRY(__enable_mmu) ... adrp x2, idmap_pg_dir phys_to_ttbr x1, x1 phys_to_ttbr x2, x2 msr ttbr0_el1, x2 // load TTBR0 msr ttbr1_el1, x1 // load TTBR1 ... ENDPROC(__enable_mmu) #ifdef CONFIG_ARM64_PA_BITS_52 #define phys_to_ttbr(addr) (((addr) | ((addr) >> 46)) & TTBR_BADDR_MASK_52) #else #define phys_to_ttbr(addr) (addr) #endif ttbr中确实为物理地址. 同时在paging_init实现中各级页表的entry存放的也是物理地址相关值. 不知以上描述是否正确或者说是否准确.
    • smcdef

      @kidesire:正确,如果MMU通过虚拟地址访问的话,岂不是陷入了“先有鸡还是先有蛋”的问题。
    • smcdef

      @kidesire:正确,如果MMU通过虚拟地址访问的话,岂不是陷入了“先有鸡还是先有蛋”的问题。
  • kidesire

    文章很好, 受益匪浅. 从研究内核的角度出发, 有一处细节不知是否还有更精确的描述: "从集合论的角度看,reserved type类型的数组是memory type类型的数组的真子集", 有两个例子(如有考虑不全的地方, 希望指出): 一. 如果reserved-memory中有内除区A带有no-map标签, 那么这段地址会从memory region中移除. 这种情况下A在reserved-memory region中, A不在memory region中, 这时"reserved type类型的数组是memory type类型的数组的真子集"说法不符合当前情况. 二. 以下是源码https://elixir.bootlin.com/linux/latest/source/arch/arm64/boot/dts/hisilicon/hi6220-hikey.dts memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x05e00000>, <0x00000000 0x05f00000 0x00000000 0x00001000>, <0x00000000 0x05f02000 0x00000000 0x00efd000>, <0x00000000 0x06e00000 0x00000000 0x0060f000>, <0x00000000 0x07410000 0x00000000 0x1aaf0000>, <0x00000000 0x22000000 0x00000000 0x1c000000>; }; reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; ramoops@21f00000 { compatible = "ramoops"; reg = <0x0 0x21f00000 0x0 0x00100000>; record-size = <0x00020000>; console-size = <0x00020000>; ftrace-size = <0x00020000>; }; /* global autoconfigured region for contiguous allocations */ linux,cma { compatible = "shared-dma-pool"; reusable; size = <0x00000000 0x08000000>; linux,cma-default; }; }; 此源码中ramoops的保留内存区域不在memory表达的范围. 以上是我的观察, 期待指导.
    • zxq

      @kidesire:我也发现了这个问题,别的不说,你可以看一下这个函数int __init __weak early_init_dt_alloc_reserved_memory_arch(phys_addr_t size, phys_addr_t align, phys_addr_t start, phys_addr_t end, bool nomap, phys_addr_t *res_base) { phys_addr_t base; /* * We use __memblock_alloc_base() because memblock_alloc_base() * panic()s on allocation failure. */ end = !end ? MEMBLOCK_ALLOC_ANYWHERE : end; base = __memblock_alloc_base(size, align, end); if (!base) return -ENOMEM; /* * Check if the allocated region fits in to start..end window */ if (base < start) { //__memblock_alloc_base不保证返回的base大于start,这里再次检查 memblock_free(base, size); return -ENOMEM; } *res_base = base; /* * 如果nomap属性,则将其从memory中移除,但是这里并没有从reserve区域移除, * 那么reserve不就不是memory的真子集了吗? */ if (nomap) return memblock_remove(base, size); return 0; } __memblock_alloc_base调用将找到的base区域添加到了reserve区域,但是在函数最后,如果有nomap属性,则将其从memory区域移除了,这样看reserve区域并不满足是memory区域真子集的说法了。