2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

在系统启动的汇编阶段,我们建立了临时页表,开启了MMU,进入了虚拟空间的世界,进入到start_kernel,内核要访问内存,要访问IO地址空间,那么就必须要位物理地址建立页表,以实现物理地址和虚拟地址之间的映射。

在内核初始化前期,内存管理系统还未初始化,现在除了临时页表外,主要的还是kernel image空间,其余的物理内存都没有建立页表,那么对于内存管理相关的API接口都无法使用,内核提出了fix mapped address的概念用来解决这些问题,本文主要针对ARM(IMX6ull)体系结构进行分析。

1. fixmap概念

当我们通过创建页表后,开启MMU,进入到start_kernel的世界中,那么当内存管理子系统没有完全初始化成功时候,我们所面对的困难为:

  1. 我们无法访问所有的内存,只能访问到临时页表创建的kernel image附件的地址空间
  2. 我们无法访问任何的硬件,这些硬件对应的地址空间还没有完成映射关系

所以内核进入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地址又被按功能划分成几个更小的部分,每一部分都有特定的功能,其定义如下:

    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_TYPE_NR * 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
    }
  1. FIX_EARLYCON_MEM_BASE:earlycon虚拟地址空间,对于实际的应用的场景就是串口相关的IO寄存器即可,占用的空间是很小的
  2. FIX_KMAP_BEGIN:永久内存映射kmap
  3. FIX_TEXT_POKE0/FIX_TEXT_POKE1:特定操作的映射区,暂时不清楚用途
  4. FIX_BTMAP_BEGIN:BITMAP空间:从定义上看,应该也属于永久映射区,后面代码中会用到。

2. fixmap的初始化

在执行setup_arch中,会最先进行early_fixmap_init()来填充fixmap,其代码实现如下

    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)
    }
  1. 检查__end_of_early_ioremap_region的范围

  2. FIXADDR_TOP的地址为0xfff00000UL-4K=ffeff000,通过pgd_offset_k(addr),获得FIXADDR_TOP地址对应pgd全局页表项中的entry,而后通过pud_offset找到对应的pud的页目录中的entry,最后通过pmd_offset找到对应的pmd的页表项

  3. 将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。

3. ioremap初始化

如果你希望kernel启动早期使用ioremap操作,其实是不行的。我们必须借助early ioremap接口。early ioremap是基于fixmap实现。初始化在early_ioremap_init完成,其代码如下

    void __init early_ioremap_init(void)
    {
    	early_ioremap_setup();
    }
    
    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中

    	for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
    		if (!prev_map[i]) {
    			slot = i;
    			break;
    		}
    	}
    
    	if (WARN(slot < 0, "%s(%08llx, %08lx) not found slot\n",
    		 __func__, (u64)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;

接下来,我们会看到,根据起始物理地址的值计算其偏移量,将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的值

    	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

4. 固定映射

内核使用set_fixmap(idx,phys)和set_fixmap_nocache(idx, phys)来建立固定线性地址与物理地址的映射。通过clear_fixmap(idx)解除固定线性地址的映射,其代码流程为

    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(FIXADDR_START + (__end_of_fixed_addresses * PAGE_SIZE) >      --------------(2)
    		     FIXADDR_END);
    	BUG_ON(idx >= __end_of_fixed_addresses);
    
    	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);

5. 总结

对于fixmap地址区域分为两部分,一部分是临时映射,各个内核模块可以使用,用完之后就释放了,典型的应用场景是early ioremap模块,linux在fix map区域的虚拟地址空间打开FIX_BTMAPS_SLOTS个的slot,内核模块通过对应的接口来申请或者示范对某个slot虚拟地址的使用,这部分还有KMAP,后面章节在分析。另外一部分是永久映射,这部分主要应用场景early console模块。

6. 参考文档

http://www.wowotech.net/memory_management/fixmap.html

https://www.cntofu.com/book/114/MM/linux-mm-2.md

阅读全文