Code前端首页关于Code前端联系我们

为什么Linux内核在启动时要做线性映射?

terry 2年前 (2023-09-28) 阅读数 60 #未命名

Linux内核启动后,对于32位系统,会线性映射从0到896M的所有低端内存(low memory),而不管这个内存段。需要什么。对于64位系统,内核映射所有物理内存(通常除非物理内存非常大)。这样做的目的是什么?先说结论,再分析代码。

这样做的原因是为了访问效率。当内核直接使用这个地址时,不需要重新映射。并且该地址是大页映射,因此减少了tlb丢失的机会。一般来说,x86和arm64都有1G或2M的大页。使用大页映射的另一个好处是页表开销会更小。

注:虽然Linux内核在启动时映射了所有物理内存(对于64位平台),但它并不占用这块内存,只是为了方便访问。

以下代码来自:linux-5.15,ARM64架构。首先,map_mem函数将遍历整个内存库并执行线性映射。

// arch/arm64/mm/mmu.c
 510 static void __init map_mem(pgd_t *pgdp)
 511 {
 512         static const u64 direct_map_end = _PAGE_END(VA_BITS_MIN);
 513         phys_addr_t kernel_start = __pa_symbol(_stext);
 514         phys_addr_t kernel_end = __pa_symbol(__init_begin);
 515         phys_addr_t start, end;
...
 550         /* map all the memory banks */
 551         for_each_mem_range(i, &start, &end) {
 552                 if (start >= end)
 553                         break;
 554                 /*
 555                  * The linear map must allow allocation tags reading/writing
 556                  * if MTE is present. Otherwise, it has the same attributes as
 557                  * PAGE_KERNEL.
 558                  */
 559                 __map_memblock(pgdp, start, end, pgprot_tagged(PAGE_KERNEL),
 560                                flags);
 561         }

下面只是一个转换函数。

// arch/arm64/mm/mmu.c
 478 static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start,
 479                                   phys_addr_t end, pgprot_t prot, int flags)
 480 {
 481         __create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start,
 482                              prot, early_pgtable_alloc, flags);
 483 }

创建全局页表pgd。

// arch/arm64/mm/mmu.c
 372 static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
 373                                  unsigned long virt, phys_addr_t size,
 374                                  pgprot_t prot,
 375                                  phys_addr_t (*pgtable_alloc)(int),
 376                                  int flags)
 377 {
 378         unsigned long addr, end, next;
 379         pgd_t *pgdp = pgd_offset_pgd(pgdir, virt);
 380
 381         /*
 382          * If the virtual and physical address don't have the same offset
 383          * within a page, we cannot map the region as the caller expects.
 384          */
 385         if (WARN_ON((phys ^ virt) & ~PAGE_MASK))
 386                 return;
 387
 388         phys &= PAGE_MASK;
 389         addr = virt & PAGE_MASK;
 390         end = PAGE_ALIGN(virt + size);
 391
 392         do {
 393                 next = pgd_addr_end(addr, end);
 394                 alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
 395                                flags);
 396                 phys += next - addr;
 397         } while (pgdp++, addr = next, addr != end);
 398 }

以下函数用于分配pud页表项。注意,它会调用use_1G_block来判断是否使用1G页表。如果条件为真,则映射完成,无需经过 PMD 和 PTE。请注意,对于典型的 ARM64 Linux 架构,pte 可以映射 2^9*4K = 2M 地址空间。

309 static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
 310                            phys_addr_t phys, pgprot_t prot,
 311                            phys_addr_t (*pgtable_alloc)(int),
 312                            int flags)
 313 {
 314         unsigned long next;
 315         pud_t *pudp;
 316         p4d_t *p4dp = p4d_offset(pgdp, addr);
 317         p4d_t p4d = READ_ONCE(*p4dp);
 318
 319         if (p4d_none(p4d)) {
 320                 p4dval_t p4dval = P4D_TYPE_TABLE | P4D_TABLE_UXN;
 321                 phys_addr_t pud_phys;
 322
 323                 if (flags & NO_EXEC_MAPPINGS)
 324                         p4dval |= P4D_TABLE_PXN;
 325                 BUG_ON(!pgtable_alloc);
 326                 pud_phys = pgtable_alloc(PUD_SHIFT);
 327                 __p4d_populate(p4dp, pud_phys, p4dval);
 328                 p4d = READ_ONCE(*p4dp);
 329         }
 330         BUG_ON(p4d_bad(p4d));
 331
 332         /*
 333          * No need for locking during early boot. And it doesn't work as
 334          * expected with KASLR enabled.
 335          */
 336         if (system_state != SYSTEM_BOOTING)
 337                 mutex_lock(&fixmap_lock);
 338         pudp = pud_set_fixmap_offset(p4dp, addr);
 339         do {
 340                 pud_t old_pud = READ_ONCE(*pudp);
 341
 342                 next = pud_addr_end(addr, end);
 343
 344                 /*
 345                  * For 4K granule only, attempt to put down a 1GB block
 346                  */
 347                 if (use_1G_block(addr, next, phys) &&
 348                     (flags & NO_BLOCK_MAPPINGS) == 0) {
 349                         pud_set_huge(pudp, phys, prot);
 350
 351                         /*
 352                          * After the PUD entry has been populated once, we
 353                          * only allow updates to the permission attributes.
 354                          */
 355                         BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),
 356                                                       READ_ONCE(pud_val(*pudp))));
 357                 } else {
 358                         alloc_init_cont_pmd(pudp, addr, next, phys, prot,
 359                                             pgtable_alloc, flags);
 360
 361                         BUG_ON(pud_val(old_pud) != 0 &&
 362                                pud_val(old_pud) != 
 READ_ONCE(pud_val(*pudp)));
 363                 }
 364                 phys += next - addr;
 365         } while (pudp++, addr = next, addr != end);
 366
 367         pud_clear_fixmap();
 368         if (system_state != SYSTEM_BOOTING)
 369                 mutex_unlock(&fixmap_lock);
 370 }

以下函数用于分配pmd页表。如果当前地址与2M对齐匹配,则使用2M页表进行映射。请注意,对于典型的 Linux ARM64 架构,pmd 可以映射 2^9*2M = 1G 的地址空间。

 219 static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end,
 220                      phys_addr_t phys, pgprot_t prot,
 221                      phys_addr_t (*pgtable_alloc)(int), int flags)
 222 {
 223         unsigned long next;
 224         pmd_t *pmdp;
 225
 226         pmdp = pmd_set_fixmap_offset(pudp, addr);
 227         do {
 228                 pmd_t old_pmd = READ_ONCE(*pmdp);
 229
 230                 next = pmd_addr_end(addr, end);
 231
 232                 /* try section mapping first */
 233                 if (((addr | next | phys) & ~PMD_MASK) == 0 &&
 234                     (flags & NO_BLOCK_MAPPINGS) == 0) {
 235                         pmd_set_huge(pmdp, phys, prot);
 236
 237                         /*
 238                          * After the PMD entry has been populated once, we
 239                          * only allow updates to the permission attributes.
 240                          */
 241                         BUG_ON(!pgattr_change_is_safe(pmd_val(old_pmd),
 242                                                       READ_ONCE(pmd_val(*pmdp))));
 243                 } else {
 244                         alloc_init_cont_pte(pmdp, addr, next, phys, prot,
 245                                             pgtable_alloc, flags);
 246
 247                         BUG_ON(pmd_val(old_pmd) != 0 &&
 248                                pmd_val(old_pmd) != READ_ONCE(pmd_val(*pmdp)));
 249                 }
 250                 phys += next - addr;
 251         } while (pmdp++, addr = next, addr != end);
 252
 253         pmd_clear_fixmap();
 254 }

最后一个问题:64位arm平台上可以直接映射多少物理内存?下图是ARM64架构的4K页结构+4级页表。答案是:128T。 Linux 内核启动时为什么要做线性映射?

此图来自:https://www.kernel.org/doc/html/latest/arm64/memory.html来自公众号:人人极客社区作者简介:周文佳:为ARM、阿里巴巴子公司、HTC等公司工作。 10年以上工作经验,主要涉及系统软件开发,包括:系统库开发、指令集优化、Linux内核开发等。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门