Linux內核的固定映射:提升性能的秘密武器
在當今數字化時代,高效穩定的 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,0level2_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 的發展注入源源不斷的動力,我們也期待在后續的研究與實踐中,能進一步挖掘其潛力,見證它為操作系統領域帶來更多的驚喜與突破。





























