Linux内核的固定映射:提升性能的秘密武器

系统 Linux
Fixmap 作为 Linux Kernel 内存管理体系中的关键 “先锋”,在系统启动早期发挥着不可替代的作用。它以固定虚拟地址、灵活物理映射的独特方式,为内核突破初始化困境提供了可能,保障诸如早期控制台信息输出、设备树解析、早期 I/O 内存映射等关键任务顺利完成,是内核平稳起航的 “幕后英雄”。

在当今数字化时代,高效稳定的 Linux 内核是众多技术应用的基石。你是否好奇,如何让 Linux 内核在复杂任务中实现卓越性能?今天,我们要揭开其提升性能的秘密武器 —— 固定映射。它就像一位默默发力的幕后英雄,通过独特的机制,优化内核内存访问,让系统运行如丝般顺滑。下面,让我们一同走进固定映射的奇妙世界。

一、Fixmap固定映射简介

1.1Fixmap概述

在 Linux Kernel 的内存管理体系里,Fixmap(固定映射)可是个相当关键的角色。当系统启动,内核初始化前期,内存管理系统还在 “筹备” 阶段,大部分物理内存尚未建立页表,常规的内存操作函数(像 ioremap、kmalloc 等)都无法施展拳脚。这时候,Fixmap 就登场啦!它就像是内核提前备好的 “应急通道”,为特定模块提供了一种临时却可靠的物理内存映射机制,保障内核在初始化早期,也能顺利访问关键内存区域,完成诸如 early console、FDT+映射、early ioremap、建立 paging init 等重要任务,为系统的顺利启动和后续稳定运行 “保驾护航”。

固定映射的线性地址(Fixed-mapped linear addresses)是一组特殊的线性地址,这些线性地址在编译时就已经确定,但是其映射的物理地址是在系统启动时确定的。

内核为 fixmap 保留了地址空间,在页表创建时,就为它们创建了对应的表项:

NEXT_PAGE(level2_fixmap_pgt)
     .fill   506,8,0
     .quad   level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
     /* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
     .fill   5,8,0
 
 NEXT_PAGE(level1_fixmap_pgt)
     .fill   512,8,0

level2_fixmap_pgt 紧挨着 level2_kernel_pgt ,level2_kernel_pgt里保存了内核的 code+data+bss 段。

NEXT_PAGE(level3_kernel_pgt)
     .fill   L3_START_KERNEL,8,0
     /* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */
     .quad   level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE
     .quad   level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
 
 NEXT_PAGE(level2_kernel_pgt)
     /*
      * 512 MB kernel mapping. We spend a full page on this pagetable
      * anyway.
      *
      * The kernel code+data+bss must not be bigger than that.
      *
      * (NOTE: at +512MB starts the module area, see MODULES_VADDR.
      *  If you want to increase this then increase MODULES_VADDR
      *  too.)
      */
     PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
         KERNEL_IMAGE_SIZE/PMD_SIZE)

页表创建时,fixmap 区域在页表中的位置如下图所示:

图片图片

1.2为何 Linux Kernel 需要 Fixmap?

⑴内核启动初期的困境

咱们把时间拉回到内核启动初期,这时候内存管理系统还在 “筹备” 阶段,大部分物理内存尚未建立页表,常规的内存操作函数(像 ioremap、kmalloc 等)都无法施展拳脚。想象一下,内核就像是一个刚搬进毛坯房的住户,虽然房子(物理内存)有了,但家具(页表等内存管理机制)还没置办齐,想找个东西(访问特定内存区域)都困难重重。

这时候要是想进行一些关键操作,比如初始化早期控制台(early console)来输出启动信息,或者读取设备树(FDT)获取硬件配置信息,根本没办法像正常运行阶段那样,通过灵活的虚拟地址去访问物理内存。没有这些关键信息,内核后续的初始化步骤就如同盲人摸象,根本无从下手,整个启动流程就会陷入僵局。

⑵Fixmap 如何巧妙化解难题

这时候,Fixmap 就像是内核提前备好的 “应急通道” 闪亮登场啦!它在内核编译的时候,就预留了一段固定的虚拟地址段。就好比在毛坯房里提前规划出几个固定的储物空间,不管房子(内存布局)怎么装修变动,这些储物空间(固定虚拟地址)的位置不变。当内核启动初期需要访问关键内存区域时,就能利用这段固定虚拟地址,快速建立起与物理内存的临时映射关系。

比如说,要初始化 early console,Fixmap 可以将预留的虚拟地址映射到串口相关的物理内存区域,这样内核就能顺利往控制台输出信息,让我们看到启动过程中的各种日志,了解内核的 “启动心声”;读取 FDT 时,同样通过 Fixmap 建立映射,精准找到存储硬件配置的物理内存,获取设备信息,为后续硬件初始化做好准备。有了 Fixmap 的 “搭桥牵线”,内核在启动早期那些艰难时刻,也能有条不紊地推进各项关键任务,逐步搭建起完整的运行环境,最终顺利 “长大成人”,进入稳定运行状态。

二、Fixmap的实现原理

2.1虚拟地址的精心规划

Fixmap 所占据的虚拟地址范围可是在编译阶段就被精心规划好了。在 ARM 架构下,通常是一段特定的高地址空间,比如 0xFFC00000 - 0xFFF00000 ,这段地址空间就像是内核专门预留的 “黄金地段”,为启动初期关键模块的内存映射需求随时待命。

而在 x86 架构中,又有所不同,它处于内核模块区域附近,与其他内存区域划分清晰,像在一些常见的内核配置下,会在靠近内核代码段和数据段的特定位置 “安营扎寨”,确保内核在启动早期,能迅速精准地找到这块 “应急宝地”,利用其完成关键物理内存的映射。这种因架构而异的地址规划,是充分考虑了不同硬件平台的内存管理特性、地址总线布局以及内核启动流程中的实际需求,量身定制的方案,只为保障系统顺利起航。

2.2Fixmap 空间分配

固定映射区可以看做由多个页组成的数组,数组的索引定义在枚举类型 fixed_addresses 中。每个索引表示一个固定映射的线性地址,这些地址是 4KB 对齐的,意味着每个地址都是页基地址。正常情况下,每个索引对应着一个 4KB 的页;当fixed_addresses 中两个相邻的索引不连续时,意味着低序索引对应着多个页。

枚举类型 fixed_addresses 定义如下:

// file: arch/x86/include/asm/fixmap.h
 /*
  * Here we define all the compile-time 'special' virtual
  * addresses. The point is to have a constant address at
  * compile time, but to set the physical address only
  * in the boot process.
  * for x86_32: We allocate these special addresses
  * from the end of virtual memory (0xfffff000) backwards.
  * Also this lets us do fail-safe vmalloc(), we
  * can guarantee that these special addresses and
  * vmalloc()-ed addresses never overlap.
  *
  * These 'compile-time allocated' memory buffers are
  * fixed-size 4k pages (or larger if used with an increment
  * higher than 1). Use set_fixmap(idx,phys) to associate
  * physical memory with fixmap indices.
  *
  * TLB entries of such buffers will not be flushed across
  * task switches.
  */
 enum fixed_addresses {
 #ifdef CONFIG_X86_32
     FIX_HOLE,
     FIX_VDSO,
 #else
     VSYSCALL_LAST_PAGE,
     VSYSCALL_FIRST_PAGE = VSYSCALL_LAST_PAGE
                 + ((VSYSCALL_END-VSYSCALL_START) >> PAGE_SHIFT) - 1,
     VVAR_PAGE,
     VSYSCALL_HPET,
 #endif
 #ifdef CONFIG_PARAVIRT_CLOCK
     PVCLOCK_FIXMAP_BEGIN,
     PVCLOCK_FIXMAP_END = PVCLOCK_FIXMAP_BEGIN+PVCLOCK_VSYSCALL_NR_PAGES-1,
 #endif
     FIX_DBGP_BASE,
     FIX_EARLYCON_MEM_BASE,
 #ifdef CONFIG_PROVIDE_OHCI1394_DMA_INIT
     FIX_OHCI1394_BASE,
 #endif
 #ifdef CONFIG_X86_LOCAL_APIC
     FIX_APIC_BASE,  /* local (CPU) APIC) -- required for SMP or not */
 #endif
 #ifdef CONFIG_X86_IO_APIC
     FIX_IO_APIC_BASE_0,
     FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS - 1,
 #endif
 #ifdef CONFIG_X86_VISWS_APIC
     FIX_CO_CPU, /* Cobalt timer */
     FIX_CO_APIC,    /* Cobalt APIC Redirection Table */
     FIX_LI_PCIA,    /* Lithium PCI Bridge A */
     FIX_LI_PCIB,    /* Lithium PCI Bridge B */
 #endif
     FIX_RO_IDT, /* Virtual mapping for read-only IDT */
 #ifdef CONFIG_X86_32
     FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
     FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
 #ifdef CONFIG_PCI_MMCONFIG
     FIX_PCIE_MCFG,
 #endif
 #endif
 #ifdef CONFIG_PARAVIRT
     FIX_PARAVIRT_BOOTMAP,
 #endif
     FIX_TEXT_POKE1, /* reserve 2 pages for text_poke() */
     FIX_TEXT_POKE0, /* first page is last, because allocation is backward */
 #ifdef  CONFIG_X86_INTEL_MID
     FIX_LNW_VRTC,
 #endif
     __end_of_permanent_fixed_addresses,
 
     /*
      * 256 temporary boot-time mappings, used by early_ioremap(),
      * before ioremap() is functional.
      *
      * If necessary we round it up to the next 256 pages boundary so
      * that we can have a single pgd entry and a single pte table:
      */
 #define NR_FIX_BTMAPS       64
 #define FIX_BTMAPS_SLOTS    4
 #define TOTAL_FIX_BTMAPS    (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
     FIX_BTMAP_END =
      (__end_of_permanent_fixed_addresses ^
       (__end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS - 1)) &
      -PTRS_PER_PTE
      ? __end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS -
        (__end_of_permanent_fixed_addresses & (TOTAL_FIX_BTMAPS - 1))
      : __end_of_permanent_fixed_addresses,
     FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
 #ifdef CONFIG_X86_32
     FIX_WP_TEST,
 #endif
 #ifdef CONFIG_INTEL_TXT
     FIX_TBOOT_BASE,
 #endif
     __end_of_fixed_addresses
 };

固定映射区分为 2 个部分:永久映射区和临时映射区。永久映射是指建立的映射关系不会改变,每段区域只供特定模块使用。临时映射区主要是内核启动时供 early_ioremap 函数使用,此时内存管理子系统还没有就绪, ioremap 函数还无法使用。

⑴永久映射区

永久映射区起始地址和大小使用以下两个宏表示:

// file: arch/x86/include/asm/fixmap.h
 #define FIXADDR_SIZE    (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
 #define FIXADDR_START        (FIXADDR_TOP - FIXADDR_SIZE)

宏 FIXADDR_SIZE 表示永久映射区的大小。__end_of_permanent_fixed_addresses 是永久映射区的边界索引,PAGE_SHIFT (扩展为 12)决定了页的大小。由于每个索引对应着单页大小,__end_of_permanent_fixed_addresses << PAGE_SHIFT 就计算出了永久映射区的大小。索引 __end_of_permanent_fixed_addresses的值与内核配置相关,在我的系统中,__end_of_permanent_fixed_addresses的值为 2206,也就是说永久映射区为 2206 个页大小,即 8824 KB。

宏 FIXADDR_START 是永久映射区的起始地址,其计算方法是用FIXADDR_TOP减去该区域的大小。宏 FIXADDR_TOP 定义如下:

// file: arch/x86/include/asm/fixmap.h
 #define FIXADDR_TOP (VSYSCALL_END-PAGE_SIZE)

宏VSYSCALL_END其定义如下:

// file: arch/x86/include/uapi/asm/vsyscall.h
 #define VSYSCALL_END (-2UL << 20)

宏VSYSCALL_END 扩展为 0xffffffffffe00000,宏 FIXADDR_TOP 扩展为 0xffffffffffdff000。对比一下 Linux 内核内存布局:

Virtual memory map with 4 level page tables:
 
 0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm
 hole caused by [48:63] sign extension
 ffff800000000000 - ffff80ffffffffff (=40 bits) guard hole
 ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
 ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
 ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
 ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
 ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
 ... unused hole ...
 ffffffff80000000 - ffffffffa0000000 (=512 MB)  kernel text mapping, from phys 0
 ffffffffa0000000 - ffffffffff5fffff (=1525 MB) module mapping space
 ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
 ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole

可以看到,宏 VSYSCALL_END 表示的是 vsyscalls 区域的结束地址。永久映射区的最高地址空间,分配给了 vsyscalls 区域:

VSYSCALL_LAST_PAGE,
     VSYSCALL_FIRST_PAGE = VSYSCALL_LAST_PAGE
                 + ((VSYSCALL_END-VSYSCALL_START) >> PAGE_SHIFT) - 1,

在 x86-64 模式下,VSYSCALL_LAST_PAGE是 fixed_addresses 的第一个元素,其值为 0;VSYSCALL_FIRST_PAGE经过计算后,其值为 2047。也就是说,vsyscalls 区域拥有 2048 个页,即 2048 \times 4K = 8M 内存空间。

另外,在永久映射区,还为 Local APIC 、 I/O APIC 以及中断描述符表(IDT)分配了空间:

#ifdef CONFIG_X86_LOCAL_APIC
     FIX_APIC_BASE,  /* local (CPU) APIC) -- required for SMP or not */
 #endif
 #ifdef CONFIG_X86_IO_APIC
     FIX_IO_APIC_BASE_0,
     FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS - 1,
 #endif
 
 ...
 
 FIX_RO_IDT, /* Virtual mapping for read-only IDT */
 
 ...

宏 MAX_IO_APICS 扩展为 128,其定义如下:

// file: arch/x86/include/asm/apicdef.h
# define MAX_IO_APICS 128

其中,元素 FIX_APIC_BASE 对应的 4KB 空间分配给 Local APIC;元素FIX_IO_APIC_BASE_0 到 FIX_IO_APIC_BASE_END 对应的 512KB 空间分配给 I/O APIC;元素 FIX_RO_IDT 对应的 4KB 空间分配给中断描述符表(IDT)。

⑵临时映射区

在永久映射区的下面,是临时映射区。临时映射区主要用于内核启动时供 early_ioremap() 函数使用,此时内存管理子系统还未就绪,ioremap() 函数还无法使用。

/*
      * 256 temporary boot-time mappings, used by early_ioremap(),
      * before ioremap() is functional.
      *
      * If necessary we round it up to the next 256 pages boundary so
      * that we can have a single pgd entry and a single pte table:
      */
 #define NR_FIX_BTMAPS       64
 #define FIX_BTMAPS_SLOTS    4
 #define TOTAL_FIX_BTMAPS    (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
     FIX_BTMAP_END =
      (__end_of_permanent_fixed_addresses ^
       (__end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS - 1)) &
      -PTRS_PER_PTE
      ? __end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS -
        (__end_of_permanent_fixed_addresses & (TOTAL_FIX_BTMAPS - 1))
      : __end_of_permanent_fixed_addresses,
     FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
 #ifdef CONFIG_X86_32
     FIX_WP_TEST,
 #endif
 #ifdef CONFIG_INTEL_TXT
     FIX_TBOOT_BASE,
 #endif
     __end_of_fixed_addresses

临时映射区的索引位于 FIX_BTMAP_END 与 __end_of_fixed_addresses 之间,这部分空间仅在内核启动时使用。其中,从FIX_BTMAP_END 到 FIX_BTMAP_BEGIN 共分配了 256 个页的空间,供 early_ioremap() 使用。

因为临时映射区的存在,内核又单独定义了 2 个宏,表示启动时映射区的大小和起始地址:

// file: arch/x86/include/asm/fixmap.h
 #define FIXADDR_BOOT_SIZE   (__end_of_fixed_addresses << PAGE_SHIFT)
 #define FIXADDR_BOOT_START  (FIXADDR_TOP - FIXADDR_BOOT_SIZE)

其计算过程类似于永久映射区,不再赘述。

⑶固定映射区内存布局

固定映射区内存布局如下图所示:

图片图片

可以看到,除了 vsyscalls 区域之外,固定映射区的其它部分延伸到了模块映射区。

2.3页表的精细构建流程

Fixmap 初始化时,页表的构建可是个精细活儿。以 ARM64 架构为例,来看看代码层面的操作:

void __init early_fixmap_init(void)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    unsigned long addr = FIXADDR_START;

    // 首先获取对应虚拟地址在全局页目录(PGD)中的项
    pgd = pgd_offset_k(addr);
    if (pgd_none(*pgd))
        __pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);

    // 接着获取下一级页目录(PUD)项
    pud = fixmap_pud(addr);
    if (pud_none(*pud))
        __pud_populate(pud, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);

    // 再获取页中间目录(PMD)项
    pmd = fixmap_pmd(addr);
    __pmd_populate(pmd, __pa_symbol(bm_pte), PMD_TYPE_TABLE);

    BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));
    if ((pmd!= fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN))) || pmd!= fixmap_pmd(fix_to_virt(FIX_BTMAP_END)))
    {
        WARN_ON(1);
    }
}

从代码里可以清晰看到,先是以 FIXADDR_START 为起点,在全局页目录(PGD)里找到对应的项,如果该项为空,就用 __pgd_populate 函数建立与下一级页目录(PUD)的关联,将 bm_pud 对应的物理地址填充进去,并标记好页表类型为 PUD_TYPE_TABLE;接着在 PUD 中如法炮制,通过 fixmap_pud 找到对应项,为空时用 __pud_populate 关联到页中间目录(PMD),填充 bm_pmd 物理地址;

最后在 PMD 里用 __pmd_populate 关联到页表项(PTE),填充 bm_pte 物理地址,如此层层递进,就像搭积木一样,构建起从虚拟地址到物理地址的精准映射通道,让内核在早期能顺利访问特定物理内存,为系统启动的各项关键任务提供有力支撑。不同架构在细节上虽有差异,但都是围绕着如何快速、精准地搭建起这一临时却关键的内存映射架构展开,确保内核初始化一路绿灯。

三、Fixmap相关函数详解

3.1 fix_to_virt

fix_to_virt 函数的功能是获取索引值对应的固定映射地址。这个函数的实现很简单:

static __always_inline unsigned long fix_to_virt(const unsigned int idx)
 {
         BUILD_BUG_ON(idx >= __end_of_fixed_addresses);
         return __fix_to_virt(idx);
 }

首先检查入参是否符合要求。fixed_addresses 中元素的最大值为 __end_of_fixed_addresses,该值仅作为边界值使用,没有其它意义,所以入参不能大于或等于该边界值。宏 BUILD_BUG_ON 会在编译时检查给定条件是否为真,如果条件为真,则在打印错误信息后将进程挂起。

检查通过后,使用 __fix_to_virt 宏将索引值转换成虚拟地址,该宏定义如下:

#define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))

每个索引对应一个页,把索引值左移 PAGE_SHIFT 后,就得到索引对应的页基地址到 FIXADDR_TOP 的偏移量;然后用 FIXADDR_TOP 减去该偏移量,得到页基地址。计算过程请参考下图:

图片图片

3.2 virt_to_fix

virt_to_fix 函数实现的功能与 fix_to_virt 函数相反, 是将虚拟地址转换成固定映射区的索引值,其定义如下:

static inline unsigned long virt_to_fix(const unsigned long vaddr)
 {
         BUG_ON(vaddr >= FIXADDR_TOP || vaddr < FIXADDR_START);
         return __virt_to_fix(vaddr);
 }

函数执行时,首先检查待转换虚拟地址是否低于 FIXADDR_START 或者大于 FIXADDR_TOP 。如果条件为真,BUG_ON 会使程序陷入死循环。

检查通过后,调用宏 __virt_to_fix 将虚拟地址转换成索引值,该宏定义如下:

#define __virt_to_fix(x)        ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

宏 PAGE_MASK定义如下:

/* PAGE_SHIFT determines the page size */
 #define PAGE_SHIFT  12
 #define PAGE_SIZE   (_AC(1,UL) << PAGE_SHIFT)
 #define PAGE_MASK   (~(PAGE_SIZE-1))

PAGE_MASK的低 12 位为 0,其余位为 1,使用它可以清空地址的低 12 位,得到页基地址。__virt_to_fix 宏工作原理如下:

  • 使用 (x)&PAGE_MASK 清空给定地址的低 12 位,得到页基地址
  • 然后用FIXADDR_TOP减去上一步得到的页基地址,得到两者的地址差。由于两者都对齐到页基地址,相减之后的差值,低 12 位仍然为 0。
  • 将上一步得到的地址差,右移 PAGE_SHIFT (扩展为 12 )位后,得到了两者之间页号差。由于每个索引映射一个页,所以页号差就是索引差;而FIXADDR_TOP对应的索引值为 0,所以索引差就等于虚拟地址的索引值。

3.3 set_fixmap

宏 set_fixmap 的作用是将物理地址映射到索引对应的虚拟地址。该宏接收 2 个参数,分别是索引值以及待映射的物理地址。

// file: arch/x86/include/asm/fixmap.h
 #define set_fixmap(idx, phys)               \
     __set_fixmap(idx, phys, PAGE_KERNEL)

其内部调用了 __set_fixmap 函数来实现具体功能,该函数接收 3 个参数,分别是:索引值、待映射的物理地址以及页属性。宏 PAGE_KERNEL 表示页属性,其本质是多个标志位组合成的位图,其定义如下:

// file: arch/x86/include/asm/fixmap.h
 #define PAGE_KERNEL         __pgprot(__PAGE_KERNEL)
 #define __PAGE_KERNEL       (__PAGE_KERNEL_EXEC | _PAGE_NX)
 #define __PAGE_KERNEL_EXEC                      \
     (_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL)

宏 __pgprot 作用,是将表示位图的基本类型 unsigned long,包装成结构体 pgprot_t。

3.4 clear_fixmap

宏 clear_fixmap 的功能与 set_fixmap 相反,会清除索引与物理地址的映射关系。

// file: arch/x86/include/asm/fixmap.h
 #define clear_fixmap(idx)           \
     __set_fixmap(idx, 0, __pgprot(0))

clear_fixmap 内部也是调用 __set_fixmap 函数通过将页属性设置为 0 来实现清除映射的。当表项的存在 (Present) 位为 0 时,该表项是无效的。

3.5 set_fixmap_nocache

宏 set_fixmap_nocache 实现的功能与set_fixmap类似,也是建立索引与物理地址的映射关系。不过与 set_fixmap不同的是,通过set_fixmap_nocache映射的页面,是不会被缓存的。

// file: arch/x86/include/asm/fixmap.h
 /*
  * Some hardware wants to get fixmapped without caching.
  */
 #define set_fixmap_nocache(idx, phys)           \
     __set_fixmap(idx, phys, PAGE_KERNEL_NOCACHE)

宏 PAGE_KERNEL_NOCACHE 是页标志位组合,其定义如下:

// file: arch/x86/include/asm/pgtable_types.h
 #define PAGE_KERNEL_NOCACHE     __pgprot(__PAGE_KERNEL_NOCACHE)
 #define __PAGE_KERNEL_NOCACHE       (__PAGE_KERNEL | _PAGE_PCD | _PAGE_PWT)

可以看到,该宏除了包含 __PAGE_KERNEL中的各种标志以外,还包括 _PAGE_PCD (位 4)和 _PAGE_PWT (位 3)标志。

// file: arch/x86/include/asm/pgtable_types.h
 #define _PAGE_PWT   (_AT(pteval_t, 1) << _PAGE_BIT_PWT)
 #define _PAGE_PCD   (_AT(pteval_t, 1) << _PAGE_BIT_PCD)
 
 #define _PAGE_BIT_PWT       3   /* page write through */
 #define _PAGE_BIT_PCD       4   /* page cache disabled */

PWT 标志、PCD 标志、PAT 标志与内存类型范围寄存器( Memory-Type Range Registers,MTRR)一起,共同决定了页面的缓存类型。当把 PWT 标志位 和 PCD 标志位都设置为 1 时,不管 PAT 标志与 MTRR 是什么状态,此时的缓存类型均为不可缓存( Uncacheable ,UC)状态。

3.6 __set_fixmap

__set_fixmap 的实现涉及到较多内核分页相关知识 -- 原理、数据结构、APIs 等,__set_fixmap 实现的功能是将物理地址映射到索引对应的虚拟地址空间。下面我们来看下 __set_fixmap 函数的实现细节。该函数接收 3 个参数,分别是:索引值 ,需要映射的物理地址以及页属性。

// file: arch/x86/include/asm/fixmap.h
 static inline void __set_fixmap(enum fixed_addresses idx,
                 phys_addr_t phys, pgprot_t flags)
 {
     native_set_fixmap(idx, phys, flags);
 }

__set_fixmap 函数内部调用了native_set_fixmap,并将参数透传给该函数。

四、Fixmap的典型应用场景实例

4.1早期控制台(Early Console)的信息输出保障

在系统启动最初阶段,控制台驱动可能还没完全初始化,但内核需要及时输出启动信息,这些信息对于调试、了解系统启动状态至关重要,就好比建筑开工前,工头得先找个地方记录施工进度和问题。这时候,Fixmap 就派上用场啦!它会将一段预留的虚拟地址,映射到串口相关的物理内存区域。

串口作为早期控制台输出信息的重要硬件,内核通过 Fixmap 建立的映射,就能顺利地往控制台输出各种日志,像内核初始化到哪一步了、内存检测结果如何、硬件初始化有没有报错等等。这些日志就像内核启动过程中的 “日记本”,让开发人员能实时追踪内核的 “启动心声”,一旦出现问题,能迅速定位根源,保障启动流程顺利推进。

4.2设备树(Device Tree)的高效解析支撑

设备树(Device Tree)是内核了解硬件配置信息的关键数据源,它详细记录了系统中有哪些硬件设备、设备的参数、连接关系等信息,就像是内核的 “硬件地图”。在内核启动初期读取设备树时,常规的内存映射机制还没就位,Fixmap 再次登场。它把设备树所在的物理地址,精准映射到内核可访问的虚拟地址空间。

这样一来,内核就能轻松 “读懂” 设备树,知晓系统中有哪些 CPU 核心、内存布局怎样、有哪些外接设备如 USB 控制器、网卡等,以及它们对应的中断号、寄存器地址等关键参数。基于这些信息,内核才能有条不紊地进行后续硬件初始化工作,为各个硬件设备加载合适的驱动,让它们协同工作,保障系统稳定运行。

4.3早期 I/O 内存映射(Early Ioremap)的得力助手

在系统启动的早期,有些硬件设备的 I/O 内存区域需要提前访问,以便进行初始化设置,像显卡要初始化显示模式、硬盘控制器要设置初始工作参数等,但这时候常规的 ioremap 函数还不能用,因为内存管理系统的相关页表还不完善。Fixmap 就充当了 “临时桥梁”,它为特定的 I/O 内存区域建立临时映射,让内核可以直接通过固定的虚拟地址访问到这些关键的 I/O 内存。

例如,对于一些早期启动就需要配置的硬件寄存器,内核借助 Fixmap 临时映射其所在的 I/O 内存,写入初始化命令,使硬件进入准备状态,确保后续系统启动过程中,硬件能及时响应内核指令,跟上启动节奏,为整个系统的顺利起航提供有力保障。

五、Fixmap与其他内存映射方式的异同对比

5.1与直接映射(Direct Mapping)的区别剖析

直接映射通常是将内核的虚拟地址空间与物理内存按固定偏移量进行一一对应,比如在常见的 32 位系统中,内核空间起始的一段虚拟地址直接对应物理内存的低地址部分,这就像给每个物理内存页在虚拟地址空间里安排了一个固定的 “座位”,只要知道虚拟地址,通过简单计算就能快速定位物理地址,访问速度极快,常用于内核代码段、数据段等频繁读写的区域。

而 Fixmap 则不同,它更像是内核预留的 “机动部队”,虚拟地址虽然在编译时固定,但映射的物理内存页不固定,在内核启动早期,哪里需要紧急访问,就临时将 Fixmap 的虚拟地址映射过去,像前面提到的早期控制台、设备树读取等场景。并且,Fixmap 的地址范围相对较小,是专门为那些启动关键阶段的临时需求开辟的 “特区”,不像直接映射覆盖大片连续的内核虚拟地址空间。直接映射全程 “在岗”,保障内核稳定运行期的常规内存访问;Fixmap 则是在内核初始化前期 “冲锋陷阵”,解决燃眉之急,二者分工明确,保障内核不同阶段的内存需求。

5.2与动态映射(如 Vmalloc)的优势比较

Vmalloc 是内核用于分配连续虚拟地址空间的 “利器”,它的优势在于能灵活地按需分配大块连续虚拟内存,这些虚拟地址对应的物理内存可以不连续,适用于一些对虚拟地址连续性有要求,但物理内存布局复杂的场景,比如加载大型内核模块时,模块可能分散在各处物理内存,Vmalloc 能为其构建连续的虚拟访问视图。不过,Vmalloc 的建立过程相对复杂,需要遍历内核的页表结构,寻找合适的物理页并建立映射,耗时较长。

而 Fixmap 在映射建立上堪称 “闪电侠”,由于虚拟地址固定且预先规划好页表层级,在内核启动早期,几乎是瞬间就能完成特定物理内存的映射,让内核迅速开展关键任务,不耽误 “启动工期”。而且,Fixmap 映射的地址稳定性强,只要内核不重启,相关虚拟地址对应的用途不变,这对于一些依赖固定地址的硬件设备初始化至关重要;Vmalloc 分配的虚拟地址在复杂的内存管理操作下,有重新映射的可能,地址稳定性相对较弱。所以,在对启动速度、地址稳定性要求极高的内核初始化场景,Fixmap 完胜;在常规运行阶段,面对复杂多样的大块内存分配需求,Vmalloc 则大显身手。

六、全文总结

Fixmap 作为 Linux Kernel 内存管理体系中的关键 “先锋”,在系统启动早期发挥着不可替代的作用。它以固定虚拟地址、灵活物理映射的独特方式,为内核突破初始化困境提供了可能,保障诸如早期控制台信息输出、设备树解析、早期 I/O 内存映射等关键任务顺利完成,是内核平稳起航的 “幕后英雄”。与直接映射、动态映射(Vmalloc)等方式相比,Fixmap 凭借其启动初期快速响应、地址稳定的优势,在内核启动流程中牢牢占据一席之地。随着硬件技术不断演进、内核功能日益复杂,Fixmap 或许也将面临新挑战与优化契机,但其为内核关键阶段内存管理需求 “兜底” 的核心价值,将持续助力 Linux 系统稳定高效运行,为开源世界蓬勃发展筑牢根基。

从功能特性来看,Fixmap 通过在编译时预留特定的虚拟地址范围,为物理内存建立起固定的映射关系。这一特性在 Linux Kernel 的多个关键环节发挥着不可或缺的作用。在系统启动初期,当常规内存管理机制尚未完备时,Fixmap 为内核提供了稳定的虚拟地址到物理地址的映射,确保了内核能够顺利启动并完成关键的初始化操作,诸如早期控制台信息输出、设备树解析以及早期 I/O 内存映射等,为后续系统的正常运行筑牢根基。

从系统性能与稳定性角度分析,Fixmap 使得内核在访问特定内存区域时,能够避开复杂的动态映射流程,从而显著提升访问效率,尤其在对时间和稳定性要求极高的场景下,这种优势更为凸显。同时,其固定的映射方式减少了因动态映射可能引发的错误与不确定性,有力地增强了系统的稳定性。

尽管在现代复杂的内存管理生态中,存在多种内存映射方式,但 Fixmap 凭借其独特的机制,与其他映射方式相互配合,共同构建起一个高效、稳定的 Linux 内核内存管理体系。展望未来,随着硬件技术的不断革新和操作系统功能的持续拓展,Fixmap 有望在更多新的应用场景中展现其价值,为 Linux Kernel 的发展注入源源不断的动力,我们也期待在后续的研究与实践中,能进一步挖掘其潜力,见证它为操作系统领域带来更多的惊喜与突破。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2025-01-03 16:32:13

SpringBoot虚拟线程Java

2013-10-16 09:28:14

亚马逊AWSSDN

2013-10-16 09:33:36

亚马逊AWSSDN

2024-01-31 08:04:43

PygmentsPython

2011-08-11 17:05:26

2014-01-07 10:46:39

2022-02-11 10:47:17

CIOIT团队企业

2019-11-27 10:38:37

数据分析数据准备工具

2009-07-28 10:36:58

云计算Google秘密武器

2024-07-11 08:34:48

2023-05-08 14:54:00

AI任务HuggingGPT

2019-11-27 10:40:34

数据工具CIO

2010-09-02 16:09:43

Linux

2024-12-18 16:00:00

C++性能优化consteval

2019-02-27 09:44:01

CIO秘密武器顾问

2011-06-02 10:24:11

iTravel苹果

2023-02-24 10:26:34

语音AI人工智能

2023-09-25 15:29:44

Go并发Goroutines

2023-11-20 07:39:07

2023-02-13 08:00:00

深度学习数据算法
点赞
收藏

51CTO技术栈公众号