Linux深入理解内存管理9(基于Linux6.6)---fixmap
一、Fixmap概念
1. Fixmap 的背景
在内核中,固定映射区域主要用于提供对一些特定物理内存区域的高效访问。例如:
- IO 内存:例如某些设备的内存映射。
- 高端内存(High Memory):大多数系统在运行时会使用内核虚拟内存来映射高端内存,而通过
fixmap
映射某些特定区域。 - 内核常量或标志数据:例如内核的内存映射表、CPU 寄存器映射等。
为了在不同的架构和平台上进行高效的内存访问,fixmap
提供了一种机制,使得内核可以直接访问这些区域而不需要复杂的页表转换。
2. Fixmap 的实现
fixmap
区域是通过内核的 页表 来实现的。内核通过在虚拟地址空间的某些固定位置映射物理内存的一部分区域来为这些物理内存提供快速访问。在 x86 架构中,fixmap
主要位于 高端虚拟地址(例如,0xffff000000000000
到 0xffffffff
)区域。
在内核中,fixmap
区域的映射是通过内核代码中名为 fixmap
的数据结构来管理的。内核会在启动过程中,根据不同的硬件架构和配置,初始化和使用这个区域来处理特定的内存映射需求。
3. Fixmap 的作用
fixmap
区域主要有以下几个关键作用:
(1)快速访问特定内存区域
通过 fixmap
,内核可以直接访问物理内存中的特定区域,而不需要通过普通的页表映射。这种方式提高了对硬件设备或特定内存区域的访问效率,尤其是在访问某些映射到物理地址的外设或 I/O 区域时。
(2)内存映射管理
对于一些大于常规内存管理范围的内存区域(如高端内存),fixmap
提供了一种机制来将这些区域映射到内核虚拟地址空间,确保内核能够高效地访问这些内存。
(3)内核空间与用户空间之间的桥梁
在某些情况下,fixmap
还可以用于支持内核与用户空间之间的内存映射。在一些系统调用或内存映射操作中,内核可能需要访问用户空间的内存。fixmap
区域通过提供固定的虚拟地址来实现这种访问。
(4)支持特定硬件架构
不同的硬件平台可能有不同的内存映射需求,fixmap
提供了一个抽象层,可以根据硬件平台的要求进行定制化的内存映射,确保不同架构的兼容性和性能。
4. Fixmap 和内核的虚拟内存管理
fixmap
和常规的虚拟内存管理相比,具有不同的特点。常规虚拟内存管理依赖于页表来进行虚拟地址到物理地址的转换,而 fixmap
的映射则是固定的,不会随系统运行时的内存变化而变化。这使得 fixmap
在访问频繁的物理内存区域时能够提供更高的效率和稳定性。
(1)固定地址空间的管理
由于 fixmap
的地址是固定的,它通常不会涉及常规的内存分配和回收机制。内核会在启动时为 fixmap
区域分配固定的虚拟地址,并在整个系统运行时保持这些地址映射的有效性。这使得内核可以在固定的位置访问映射的物理内存。
(2)映射范围的限制
fixmap
区域的大小是有限的,通常取决于平台的虚拟地址空间和内存大小。它用于映射一部分特定的、关键的物理内存区域,而不适用于大量的动态内存映射。对于大规模的内存区域映射,内核依然依赖于常规的页表映射机制。
5. Fixmap 映射
当通过创建页表后,开启MMU,进入到start_kernel的世界中,那么当内存管理子系统没有完全初始化成功时候,所面对的困难为:
- 无法访问所有的内存,只能访问到临时页表创建的kernel image附件的地址空间。
- 无法访问任何的硬件,这些硬件对应的地址空间还没有完成映射关系。
所以内核进入start_kernel就马上完成fixmap的建立,对于fixmap从字面的意思来说,fixed map指的是虚拟地址中的一段区域,在该区域中所有的线性地址是在编译阶段就确定好的,这个虚拟地址需要在boot阶段去映射到物理地址上,对于NXP芯片,其虚拟地址为:
[ 0.000000] Virtual kernel memory layout:
[ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB)
[ 0.000000] fixmap : 0xffc00000 - 0xfff00000 (3072 kB)
[ 0.000000] vmalloc : 0xe0800000 - 0xff800000 ( 496 MB)
[ 0.000000] lowmem : 0xc0000000 - 0xe0000000 ( 512 MB)
[ 0.000000] pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)
[ 0.000000] modules : 0xbf000000 - 0xbfe00000 ( 14 MB)
[ 0.000000] .text : 0xc0008000 - 0xc0a00000 (10208 kB)
[ 0.000000] .init : 0xc0e00000 - 0xc1000000 (2048 kB)
[ 0.000000] .data : 0xc1000000 - 0xc1074c00 ( 467 kB)
[ 0.000000] .bss : 0xc1076000 - 0xc10e8eec ( 460 kB)
当内核完全启动后,内存管理提供了各种各样的API来使各个模块完成物理地址到虚拟地址的映射功能,但是在内核启动的初期,有些模块就需要使用虚拟地址并mapping到指定的物理地址上,同时,这些模块也没有办法等到内核的内存管理模块完全初始化之后再进行映射功能,因此,内核就分配了fixmap这段地址空间,对于ARM32的为0xffc00000 - 0xfff00000这段虚拟地址空间,这段地址空间就用来实现前期某些特定的模块实现物理内存映射。
fixmap虚拟地址空间映射方式分为以下两部分:
- 永久映射,即建立的映射关系再kernel阶段不会改变,仅供特定模块使用。
- 临时映射,即模块使用前创建映射,使用后解除映射关系。
其fixmap地址又被按功能划分成几个更小的部分,每一部分都有特定的功能,其定义如下:
arch/arm/include/asm/fixmap.h
enum fixed_addresses {
FIX_EARLYCON_MEM_BASE,
__end_of_permanent_fixed_addresses,
FIX_KMAP_BEGIN = __end_of_permanent_fixed_addresses,
FIX_KMAP_END = FIX_KMAP_BEGIN + (KM_MAX_IDX * NR_CPUS) - 1,
/* Support writing RO kernel text via kprobes, jump labels, etc. */
FIX_TEXT_POKE0,
FIX_TEXT_POKE1,
__end_of_fixmap_region,
/*
* Share the kmap() region with early_ioremap(): this is guaranteed
* not to clash since early_ioremap() is only available before
* paging_init(), and kmap() only after.
*/
#define NR_FIX_BTMAPS 32
#define FIX_BTMAPS_SLOTS 7
#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
__end_of_early_ioremap_region
};
- FIX_EARLYCON_MEM_BASE:earlycon虚拟地址空间,对于实际的应用的场景就是串口相关的IO寄存器即可,占用的空间是很小的
- FIX_KMAP_BEGIN:永久内存映射kmap
- FIX_TEXT_POKE0/FIX_TEXT_POKE1:特定操作的映射区,暂时不清楚用途
- FIX_BTMAP_BEGIN:BITMAP空间:从定义上看,应该也属于永久映射区,后面代码中会用到。
二、Fixmap的初始化
在执行setup_arch中,会最先进行early_fixmap_init()来填充fixmap,其代码实现如下:
arch/arm/mm/mmu.c
void __init early_fixmap_init(void)
{
pmd_t *pmd;
/*
* The early fixmap range spans multiple pmds, for which
* we are not prepared:
*/
BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT)---解析1
!= FIXADDR_TOP >> PMD_SHIFT);
pmd = fixmap_pmd(FIXADDR_TOP);---解析2
pmd_populate_kernel(&init_mm, pmd, bm_pte);---解析3
pte_offset_fixmap = pte_offset_early_fixmap;---解析4
}
- 检查__end_of_early_ioremap_region的范围。
- FIXADDR_TOP的地址为0xfff00000UL-4K=ffeff000,通过pgd_offset_k(addr),获得FIXADDR_TOP地址对应pgd全局页表项中的entry,而后通过pud_offset找到对应的pud的页目录中的entry,最后通过pmd_offset找到对应的pmd的页表项。
- 将pmd的物理地址写到pte页表中,而 bm_pte[512]存放pte的页表的entry,对于pmd_populate_kernel有三个重要的参数。
- init_mm : init进程的内存描述符。
- pmd: ioremap固定映射开始处的页中间目录。
- bm_pte:初期ioremap页表入口数组定义bm_pte[512]。
early_fixmap_init只是建立了一个映射的框架,具体的物理地址和虚拟地址的映射没有直接给出,这个是由使用者具体使用的时候再去填充的对应的pte_entry。
三、Ioremap初始化
如果希望kernel启动早期使用ioremap操作,其实是不行的。必须借助early ioremap接口。early ioremap是基于fixmap实现。初始化在early_ioremap_init完成,其代码如下:
arch/arm/mm/ioremap.c
/*
* Must be called after early_fixmap_init
*/
void __init early_ioremap_init(void)
{
early_ioremap_setup();
}
mm/early_ioremap.c
void __init early_ioremap_setup(void)
{
int i;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
if (WARN_ON(prev_map[i]))
break;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}
slot_virt和其他数组定义在同一个源文件中:
static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;
slot_virt 包含了固定映射区域的虚拟地址,prev_map 数组包含了初期 ioremap 区域的地址,实际上初期 ioremap 会使用 512 个临时引导时映射,同时可以看到所有的数组都使用 __initdata 定义,这意味着这些内存都会在内核初始化结束后释放掉。
初期 ioremap 初始化完成后,就能使用它了。它提供了两个函数:
- early_ioremap
- early_iounmap
用于从IO物理地址映射/解除映射到虚拟地址,这两个函数都依赖于CONFIG_MMU,如果该宏没有定义,就直接返回物理地址,什么都不做;如果定义为y,early_ioremap 就会调用 __early_ioremap,它有三个参数:
- phys_addr - 要映射到虚拟地址上的 I/O 内存区域的基物理地址。
- size - I/O 内存区域的尺寸。
- prot - 页表入口位。
在 __early_ioremap 中首先遍历了所有初期 ioremap 固定映射槽并检查 prev_map 数组中第一个空闲元素,然后将这个值存在了 slot 变量中,计算需要映射的物理地址结尾 ,同时将映射的size填入到相应slot的prev_size中。
mm/early_ioremap.c
static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
unsigned long offset;
resource_size_t last_addr;
unsigned int nrpages;
enum fixed_addresses idx;
int i, slot;
WARN_ON(system_state >= SYSTEM_RUNNING);
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
if (WARN(slot < 0, "%s(%pa, %08lx) not found slot\n",
__func__, &phys_addr, size))
return NULL;
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (WARN_ON(!size || last_addr < phys_addr))
return NULL;
prev_size[slot] = size;
/*
* Mappings have to be page-aligned
*/
offset = offset_in_page(phys_addr);
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
/*
* Mappings have to fit in the FIX_BTMAP area.
*/
nrpages = size >> PAGE_SHIFT;
if (WARN_ON(nrpages > NR_FIX_BTMAPS))
return NULL;
/*
* Ok, go for it..
*/
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
if (after_paging_init)
__late_set_fixmap(idx, phys_addr, prot);
else
__early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
WARN(early_ioremap_debug, "%s(%pa, %08lx) [%d] => %08lx + %08lx\n",
__func__, &phys_addr, size, slot, offset, slot_virt[slot]);
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
}
接下来,会看到,根据起始物理地址的值计算其偏移量,将phys_addr的最后12位清0,也相当于页对齐操作,然后计算对齐后的size的值。
offset = offset_in_page(phys_addr);
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
接下来就需要获取新的ioremap区域所占用的页的数量然后计算固定映射的下标。
nrpages = size >> PAGE_SHIFT;
if (WARN_ON(nrpages > NR_FIX_BTMAPS))
return NULL;
现在用给定的物理地址填充固定映射区域,逐页操作,在bm_pte页表中设置该地址对应entry的值。mm/early_ioremap.c
static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
unsigned long offset;
resource_size_t last_addr;
unsigned int nrpages;
enum fixed_addresses idx;
int i, slot;
...
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
if (after_paging_init)
__late_set_fixmap(idx, phys_addr, prot);
else
__early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
}
将其其实地址的虚拟地址保持到prev_map相对应的slot中,并将该虚拟地址返回,那么就可以通过这个函数实现物理地址到虚拟地址的映射。
对于 early_iounmap ,它会解除对一个 I/O 内存区域的映射。这个函数有两个参数:基地址和 I/O 区域的大小,这看起来与 early_ioremap 很像。它同样遍历了固定映射槽并寻找给定地址的槽。这样它就获得了这个固定映射槽的下标,然后通过判断 after_paging_init 的值决定是调用 __late_clear_fixmap 还是 __early_set_fixmap 。当这个值是 0 时会调用 __early_set_fixmap。最终它会将 I/O 内存区域设为 NULL:
四、固定映射
内核使用set_fixmap(idx,phys)和set_fixmap_nocache(idx, phys)来建立固定线性地址与物理地址的映射。通过clear_fixmap(idx)解除固定线性地址的映射。
arch/arm/mm/mmu.c
/*
* To avoid TLB flush broadcasts, this uses local_flush_tlb_kernel_range().
* As a result, this can only be called with preemption disabled, as under
* stop_machine().
*/
void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot)
{
unsigned long vaddr = __fix_to_virt(idx);---解析1
pte_t *pte = pte_offset_fixmap(pmd_off_k(vaddr), vaddr);
/* Make sure fixmap region does not exceed available allocation. */
BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) < FIXADDR_START);---解析2
BUG_ON(idx >= __end_of_fixed_addresses);
/* We support only device mappings before pgprot_kernel is set. */
if (WARN_ON(pgprot_val(prot) != pgprot_val(FIXMAP_PAGE_IO) &&
pgprot_val(prot) && pgprot_val(pgprot_kernel) == 0))
return;
if (pgprot_val(prot))---解析3
set_pte_at(NULL, vaddr, pte,
pfn_pte(phys >> PAGE_SHIFT, prot));
else
pte_clear(NULL, vaddr, pte);
local_flush_tlb_kernel_range(vaddr, vaddr + PAGE_SIZE);
}
对于ARM32,只是对于earlycon才用了这种方式,set_fixmap_io(FIX_EARLYCON_MEM_BASE, paddr & PAGE_MASK);
arch/parisc/mm/fixmap.c
void notrace clear_fixmap(enum fixed_addresses idx)
{
unsigned long vaddr = __fix_to_virt(idx);
pte_t *pte = virt_to_kpte(vaddr);
if (WARN_ON(pte_none(*pte)))
return;
pte_clear(&init_mm, vaddr, pte);
flush_tlb_kernel_range(vaddr, vaddr + PAGE_SIZE);
}
五、总结
对于fixmap地址区域分为两部分,一部分是临时映射,各个内核模块可以使用,用完之后就释放了,典型的应用场景是early ioremap模块,linux在fix map区域的虚拟地址空间打开FIX_BTMAPS_SLOTS个的slot,内核模块通过对应的接口来申请或者示范对某个slot虚拟地址的使用,这部分还有KMAP,。另外一部分是永久映射,这部分主要应用场景early console模块。
1. Fixmap 区域概述
Fixmap 是内核虚拟地址空间中的一个固定区域,用于映射一些特殊的、固定的物理内存区域。该区域通常位于内核虚拟地址空间的高端部分,确保内核能够快速访问这些内存映射区域。
在 Linux 内核中,Fixmap 地址区域的划分通常包含两部分:
- 长期固定映射区域:用于映射一些常驻内存区域,如内核常用的硬件映射或设备内存。典型的例子是 I/O 内存映射、一些设备的控制寄存器等。
- 临时映射区域:这些区域是内核模块在运行时申请并释放的区域,通常用于动态映射物理内存,特别是在初始化阶段。
early ioremap
是典型的使用场景。
2. Fixmap 的临时映射(临时 Slot)
临时映射主要用于内核模块动态申请和释放映射区域。这些临时映射通常是在系统启动过程中或者特定的硬件操作期间使用的,特别是在 早期 I/O 映射(如 early ioremap
)中。
在内核启动早期,内存管理机制还没有完全初始化,early ioremap
是一种利用 fixmap
区域为设备提供内存映射的方法。
临时映射的具体实现
在 Fixmap 区域,内核为模块提供了一些插槽(slot),这些插槽可以用于临时映射物理地址。每个插槽都有一个固定的虚拟地址,内核模块通过接口申请某个插槽,并将物理地址映射到相应的虚拟地址上。
例如,Linux 内核通过宏 FIX_BTMAPS_SLOTS
来控制可用插槽的数量。这些插槽可以在内核模块中被临时申请,然后在不需要时释放。
- 插槽(Slot):Fixmap 区域被划分为若干个固定大小的插槽,内核模块可以使用这些插槽来映射特定的物理地址。
early ioremap
:在早期初始化过程中,内核使用 Fixmap 临时映射来访问特定的物理内存区域,尤其是在设备驱动初始化时。
Fixmap 和 KMAP 结合使用
KMAP
是内核内存管理中的一个重要概念,通常用于将高端物理内存映射到内核的虚拟地址空间,方便内核访问。这些内存区域在高端内存(High Memory)中,且内核无法直接访问(因为它们不在内核直接可访问的低端内存范围内)。
KMAP
与 Fixmap 的结合使用使得内核能够在需要时动态地创建映射,将高端物理内存映射到低端虚拟地址空间(通常是 Fixmap 区域)。这样,内核就可以高效地访问高端内存,同时还可以避免频繁的页表切换。
- KMAP 与临时映射:在 Fixmap 区域申请的临时插槽通常会使用
KMAP
来完成高端内存的映射。在临时使用之后,这些映射会被及时释放。
内核模块使用 Fixmap 插槽的流程:
- 申请映射:内核模块通过接口请求一个 Fixmap 插槽,并传递要映射的物理地址。
- 映射物理地址:内核通过
ioremap()
或类似的函数,将物理地址映射到选定的虚拟地址(插槽)。 - 使用映射:内核模块访问映射后的虚拟地址区域,执行必要的硬件访问操作。
- 释放映射:一旦不再需要映射,模块通过接口释放插槽,解除映射,确保内存的正确管理。
3. Fixmap 的长期映射与临时映射的区别
-
长期映射:
- 长期映射的虚拟地址在内核启动时固定。
- 一般用于映射设备内存、I/O 区域、内核常驻内存等。
- 这些映射不会在内核运行过程中释放或改变,确保内核可以随时访问这些固定区域。
-
临时映射:
- 临时映射主要用于内核模块或在特定条件下动态映射物理内存。
- 映射的虚拟地址和物理地址的关系较为灵活,可以根据内核的需求动态分配和释放。
- 内核模块通常通过接口申请临时映射,并在使用完成后释放对应的插槽。