Linux Kernel:内存管理之固定映射 (Fixmap)

656 阅读23分钟

本文采用 Linux 内核 v3.10 版本 x86_64架构

一、前言

固定映射的线性地址(Fixed-mapped linear addresses)是一组特殊的线性地址,这些线性地址在编译时就已经确定,但是其映射的物理地址是在系统启动时确定的。内核文件 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.

内核为 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_pgtlevel2_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 区域在页表中的位置如下图所示:

fixmap.png

二、Fixmap 空间分配

固定映射区可以看做由多个页组成的数组,数组的索引定义在枚举类型 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 函数还无法使用。

2.1 永久映射区

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

// 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_PAGEfixed_addresses 的第一个元素,其值为 0;VSYSCALL_FIRST_PAGE经过计算后,其值为 2047。也就是说,vsyscalls 区域拥有 2048 个页,即 2048×4K=8M2048 \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_0FIX_IO_APIC_BASE_END 对应的 512KB 空间分配给 I/O APIC;元素 FIX_RO_IDT 对应的 4KB 空间分配给中断描述符表(IDT)。

2.2 临时映射区

在永久映射区的下面,是临时映射区。临时映射区主要用于内核启动时供 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_ENDFIX_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)

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

2.3 固定映射区内存布局

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

Fixmap_Memory_Layout.png

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

三、APIs

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 会在编译时检查给定条件是否为真,如果条件为真,则在打印错误信息后将进程挂起。该宏的实现细节,我们在 Linux Kernel:中断和异常处理程序的早期初始化(续) 的第 2.2 节详细介绍过,此处不再赘述。

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

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

每个索引对应一个页,把索引值左移 PAGE_SHIFT 后,就得到索引对应的页基地址到 FIXADDR_TOP 的偏移量;然后用 FIXADDR_TOP 减去该偏移量,得到页基地址。

计算过程请参考下图:

fix_to_virt.png

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

各标志位的详细说明、分页相关的数据结构,请参考 Linux Kernel:内存管理之分页(Paging) 第 1.3 节、4.1.3 节和 4.2 节,此处不再赘述。

__set_fixmap 函数实现细节,详见 3.6 __set_fixmap 小节。

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 时,该表项是无效的。

__set_fixmap 函数实现细节,详见 3.6 __set_fixmap 小节。

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)状态。具体可查看 Intel SDM Volume 3A. Chapter 4.9.2 以及 Chapter 12 Memory Cache Control。

3.6 __set_fixmap

__set_fixmap 的实现涉及到较多内核分页相关知识 -- 原理、数据结构、APIs 等,我在 Linux Kernel:内存管理之分页(Paging)里对其做了汇总,不熟悉的同学可参考该文章。在下文中遇到分页相关的 API 或数据结构时,只会介绍其功能,不再详述实现过程。

__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,并将参数透传给该函数。

3.6.1 native_set_fixmap

native_set_fixmap 函数定义如下:

// file: arch/x86/mm/pgtable.c
void native_set_fixmap(enum fixed_addresses idx, phys_addr_t phys,
		       pgprot_t flags)
{
	__native_set_fixmap(idx, pfn_pte(phys >> PAGE_SHIFT, flags));
}

native_set_fixmap 函数内部,先是将物理地址右移 PAGE_SHIFT (扩展为 12)位得到页帧号;然后调用 pfn_pte 函数,将页帧号与页属性转换成页表项描述符 pte_t ;最后,调用 __native_set_fixmap 函数填充各级表项,将物理地址映射到虚拟地址。

3.6.2 pfn_pte

从函数名称也能看到,该函数实现的功能就是将页帧号转换成页表项。

pfnpage frame number 的缩写,即页帧号,用来表示物理地址的页编号。对应的,虚拟地址的页编号叫做页号( page number);页内地址叫做页偏移(page offset)。

Specifically, the upper bits of a linear address (called the page number) determine the upper bits of the physical address (called the page frame); the lower bits of the linear address (called the page offset) determine the lower bits of the physical address. The boundary between the page number and the page offset is determined by the page size.

pfn_pte 函数接收 2 个参数:物理页的页帧号以及页属性;返回值类型是结构体 pte_t,该结构体是对页表项(PTE)的包装。

函数实现如下:

// file: arch/x86/include/asm/pgtable.h
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
	return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) |
		     massage_pgprot(pgprot));
}

pfn_pte 函数执行流程如下:

  • 将页帧号 page_nr 左移 PAGE_SHIFT 位,得到物理页的基地址。

  • 通过 massage_pgprot 函数,屏蔽掉页表项中不支持的标志位

  • 将物理页基地址和屏蔽后的标志位组合成页表项格式

  • 将组合后的页表项通过 __pte 宏转换成 pte_t 格式

massage_pgprot 函数实现如下:

// file: arch/x86/include/asm/pgtable.h
/*
 * Mask out unsupported bits in a present pgprot.  Non-present pgprots
 * can use those bits for other purposes, so leave them be.
 */
static inline pgprotval_t massage_pgprot(pgprot_t pgprot)
{
	pgprotval_t protval = pgprot_val(pgprot);

	if (protval & _PAGE_PRESENT)
		protval &= __supported_pte_mask;

	return protval;
}

massage_pgprot 函数执行流程如下:

  • 通过 pgprot_val 函数,获取到结构体 pgprot_t 内封装的属性值。其返回值为 pgprotval_t 类型,该类型实际是 unsigned long 类型的别名。
  • 将页属性与 _PAGE_PRESENT (存在位,位 0)进行与操作,判断该页表项是否有效。
    • 如果页表项有效,则屏蔽掉页属性里不支持的标志位
    • 如果页表项无效,什么也不做。根据注释说明,当页表项的 Present 位为 0 时,那些不支持的标志位有其它用途,所以没有屏蔽掉。
  • 将(屏蔽后的)页属性返回。

pgprot_val宏只是简单返回结构体中的成员 pgprot,其定义如下:

// file: arch/x86/include/asm/pgtable_types.h
#define pgprot_val(x)	((x).pgprot)

__supported_pte_mask 定义如下:

// file: arch/x86/mm/init_64.c
pteval_t __supported_pte_mask __read_mostly = ~_PAGE_IOMAP;
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_IOMAP	(_AT(pteval_t, 1) << _PAGE_BIT_IOMAP)
#define _PAGE_BIT_IOMAP		10	/* flag used to indicate IO mapping */

可以看到,__supported_pte_mask屏蔽掉了页表项的 IO-mapping 标志位(位 10)。正常情况下,各级表项的位 10 是被忽略的,是无效位,所以被屏蔽掉了。

3.6.2 __native_set_fixmap

通过 pfn_pte 函数将物理地址及页属性组装成页表项之后,接下来就需要将页表项写入到页表中。由于页表处于转换路径的底层,我们无法直接得到页表地址,需要从全局页目录开始,经过多级查找后,才能获取到页表地址。在查找过程中,如果发现某一级表项无效,说明表项引用的下级页表不存在,那么就要为下级页表分配单页大小的内存,并将内存的页帧号与页属性组装成表项,写入对应的页表里。

上述工作就是在 __native_set_fixmap 函数里完成的。__native_set_fixmap 函数接收 2 个参数:固定映射区的索引以及页表项。

// file: arch/x86/mm/pgtable.c
int fixmaps_set;


void __native_set_fixmap(enum fixed_addresses idx, pte_t pte)
{
	unsigned long address = __fix_to_virt(idx);

	if (idx >= __end_of_fixed_addresses) {
		BUG();
		return;
	}
	set_pte_vaddr(address, pte);
	fixmaps_set++;
}

__native_set_fixmap 函数执行流程如下:

  • 调用 __fix_to_virt 函数获取索引对应的固定映射区的虚拟地址;
  • 检查传入的索引值是否有效。如果大于等于最大值 __end_of_fixed_addresses,说明传入的参数无效,报错并返回;
  • 调用 set_pte_vaddr 函数完成实际映射功能;
  • fixmaps_set 自增 1。fixmaps_set 是个全局变量,用来统计设置次数。

__native_set_fixmap 函数的主要功能通过 set_pte_vaddr 函数来完成,下面我们来分析下该函数。

3.6.3 set_pte_vaddr

set_pte_vaddr 函数实现如下:

// file: arch/x86/mm/init_64.c
void set_pte_vaddr(unsigned long vaddr, pte_t pteval)
{
	pgd_t *pgd;
	pud_t *pud_page;

	pr_debug("set_pte_vaddr %lx to %lx\n", vaddr, native_pte_val(pteval));

	pgd = pgd_offset_k(vaddr);
	if (pgd_none(*pgd)) {
		printk(KERN_ERR
			"PGD FIXMAP MISSING, it should be setup in head.S!\n");
		return;
	}
	pud_page = (pud_t*)pgd_page_vaddr(*pgd);
	set_pte_vaddr_pud(pud_page, vaddr, pteval);
}

该函数执行流程:

  • 通过宏 pgd_offset_k 计算出虚拟地址对应的全局页目录项的虚拟地址,并保存到变量 pgd 中。

  • 调用 pgd_none 函数,检查 pgd指向的全局页目录项是否为 0。如果为 0,说明全局页目录项无效,打印错误信息并返回。从前文可以知道,在页表创建时,已经将 fixmap 区域对应的全局页目录项 (level3_kernel_pgt )写到页表里了,所以不应该为 0;如果为 0 话,肯定是出错了!

  • 调用函数 pgd_page_vaddr 获取到全局页目录项(PGDE)中引用的上层页目录(PUD)的基(虚拟)地址,保存到变量 pud_page 中。

  • 调用 set_pte_vaddr_pud 函数填充上层页目录项及其下级表项。

set_pte_vaddr 中变量 pgdpud_page 示意图如下所示,这两个地址都是虚拟地址:

set_pte_vaddr.png

接下来,分析 set_pte_vaddr_pud 函数的执行过程。

3.6.4 set_pte_vaddr_pud

函数 set_pte_vaddr_pud 定义如下:

// file: arch/x86/mm/init_64.c
void set_pte_vaddr_pud(pud_t *pud_page, unsigned long vaddr, pte_t new_pte)
{
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;

	pud = pud_page + pud_index(vaddr);
	pmd = fill_pmd(pud, vaddr);
	pte = fill_pte(pmd, vaddr);

	set_pte(pte, new_pte);

	/*
	 * It's enough to flush this one mapping.
	 * (PGE mappings get flushed as well)
	 */
	__flush_tlb_one(vaddr);
}

该函数执行流程如下:

  • 调用 pud_index 函数计算出虚拟地址对应的上层页目录项索引。
  • 通过 pud_page + pud_index(vaddr) 计算出上层页目录项的虚拟地址,并赋值给 pud

此时,pud 指向的位置如下图所示:

set_pte_vaddr_pud.png
  • 调用 fill_pmd 函数,将中层页目录(PMD)关联到上层页目录项。
  • 调用 fill_pte 函数,将页表(PT)关联到中层页目录项。
  • 调用宏 set_pte 将页表项写入页表中。
  • 调用 __flush_tlb_one 刷新单条 TLB。

3.6.5 fill_pmd

fill_pmd 函数定义如下:

// file: arch/x86/mm/init_64.c
static pmd_t *fill_pmd(pud_t *pud, unsigned long vaddr)
{
	if (pud_none(*pud)) {
		pmd_t *pmd = (pmd_t *) spp_getpage();
		pud_populate(&init_mm, pud, pmd);
		if (pmd != pmd_offset(pud, 0))
			printk(KERN_ERR "PAGETABLE BUG #01! %p <-> %p\n",
			       pmd, pmd_offset(pud, 0));
	}
	return pmd_offset(pud, vaddr);
}

该函数接收 2 个参数,分别是:上层页目录项的虚拟地址、待映射的虚拟地址;返回值为 pmd_t 指针类型。

首先调用 pud_none 函数检查上层页目录项是否为 0。如果值为 0 ,说明上层页目录项无效,需要填充;否则,说明有效,可以直接使用。

如果上层页目录项无效,说明该项引用的中层页目录还不存在,则需要为中层页目录分配内存。从以前的文章中可以知道,每级页表都是一个页( 4KB) 的大小,所以调用 spp_getpage 函数从内存申请了一个页,并把返回地址转换成 pmd_t 类型的指针。

// file: arch/x86/mm/init_64.c
/*
 * NOTE: This function is marked __ref because it calls __init function
 * (alloc_bootmem_pages). It's safe to do it ONLY when after_bootmem == 0.
 */
static __ref void *spp_getpage(void)
{
	void *ptr;

	if (after_bootmem)
		ptr = (void *) get_zeroed_page(GFP_ATOMIC | __GFP_NOTRACK);
	else
		ptr = alloc_bootmem_pages(PAGE_SIZE);

	if (!ptr || ((unsigned long)ptr & ~PAGE_MASK)) {
		panic("set_pte_phys: cannot allocate page data %s\n",
			after_bootmem ? "after bootmem" : "");
	}

	pr_debug("spp_getpage %p\n", ptr);

	return ptr;
}

spp_getpage 函数涉及到内存分配的细节,我们暂不深入探讨。

分配好内存之后,调用 pud_populate 函数,将已分配的物理页关联到上层页目录项,完成上层页目录项的填充。

然后,验证分配的内存地址和通过上层页目录项计算的地址是否一致,如果不一致,说明有 bug,打印错误信息。

		if (pmd != pmd_offset(pud, 0))
			printk(KERN_ERR "PAGETABLE BUG #01! %p <-> %p\n",
			       pmd, pmd_offset(pud, 0));

最后,调用 pmd_offset 函数,获取到指定虚拟地址(此处为固定映射的线性地址)对应的中层页目录项的虚拟地址并返回。

注意,在 fill_pmd函数的内部,变量 pmd 指向的是中层页目录的基地址;而在 set_pte_vaddr_pud函数中,变量 pmd 指向的是中层页目录的地址。

fill_pmd 函数执行后, set_pte_vaddr_pud 中各变量位置如下:

set_pte_vaddr_pud_1.png

3.6.6 fill_pte

fill_pte 函数的实现与 fill_pmd 类似,其定义如下:

// file: arch/x86/mm/init_64.c
static pte_t *fill_pte(pmd_t *pmd, unsigned long vaddr)
{
	if (pmd_none(*pmd)) {
		pte_t *pte = (pte_t *) spp_getpage();
		pmd_populate_kernel(&init_mm, pmd, pte);
		if (pte != pte_offset_kernel(pmd, 0))
			printk(KERN_ERR "PAGETABLE BUG #02!\n");
	}
	return pte_offset_kernel(pmd, vaddr);
}

首先调用函数 pmd_none 检查中层页目录项是否为 0。

如果中层页目录项为 0,说明其引用的页表(Page Table)还未创建。于是,调用 spp_getpage 函数分配一个页的内存,并将内存地址转换为 pte_t 类型的指针。

然后调用 pmd_populate_kernel 函数,填充中层页目录项。

填充完成后,检查分配的内存地址与函数 pte_offset_kernel 计算的地址是否一致,不一致的话,说明有 bug,打印错误信息。

		if (pte != pte_offset_kernel(pmd, 0))
			printk(KERN_ERR "PAGETABLE BUG #02!\n");

最后,调用 pte_offset_kernel 函数,获取到指定虚拟地址(此处为固定映射的线性地址)对应的页表项的虚拟地址并返回。

注意,在 fill_pte函数的内部,变量 pte 指向的是页表的基地址;而在 set_pte_vaddr_pud函数中,变量 pte 指向是页表项的地址。

fill_pte函数执行后, set_pte_vaddr_pud函数中各变量位置如下:

set_pte_vaddr_pud_2.png

3.6.7 set_pte

set_pte 宏的作用就是页表项写入页表中。

该宏定义如下:

// file: arch/x86/include/asm/pgtable.h
#define set_pte(ptep, pte)		native_set_pte(ptep, pte)

宏内部引用了 native_set_pte函数,该函数的实现非常简单,就是将页表项的值写入指针指向的地址。

// file: arch/x86/include/asm/pgtable_64.h
static inline void native_set_pte(pte_t *ptep, pte_t pte)
{
	*ptep = pte;
}

set_pte 执行后,所有层级的表项全部建立完成。

set_pte_vaddr_pud_3.png

3.6.8 __flush_tlb_one

__flush_tlb_one会清除指定地址的 TLB( Translation Lookaside Buffer ) 缓存,最终会调用 x86 汇编指令 invlpg 来实现该功能。

// file: arch/x86/include/asm/tlbflush.h
static inline void __flush_tlb_one(unsigned long addr)
{
		__flush_tlb_single(addr);
}
// file: arch/x86/include/asm/tlbflush.h
#define __flush_tlb_single(addr) __native_flush_tlb_single(addr)
// file: arch/x86/include/asm/tlbflush.h
static inline void __native_flush_tlb_single(unsigned long addr)
{
	asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}

invlpg 指令的详细信息,可参考 Intel SDM 手册。

四、参考资料

1、Intel 开发者手册:Intel 64 and IA-32 Architectures Software Developer Manuals Volume 3A, Chapter 4 Paging 以及 Chapter 12 Memory Cache Control

2、Fix-Mapped Addresses and ioremap

3、Fixed-mapped linear addresses

4、 Linux Kernel:内存管理之分页(Paging)

5、Linux Kernel:中断和异常处理程序的早期初始化(续)