为什么Linux内核在启动时要做线性映射?
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。 ![]()
此图来自:https://www.kernel.org/doc/html/latest/arm64/memory.html来自公众号:人人极客社区作者简介:周文佳:为ARM、阿里巴巴子公司、HTC等公司工作。 10年以上工作经验,主要涉及系统软件开发,包括:系统库开发、指令集优化、Linux内核开发等。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网