Linux 内存管理窥探(10):内存初始化(create_mapping 创建映射)

在内存的初始化阶段,在初始化页表后,调用相关的函数来创建映射:

start_kernel() --> setup_arch() --> paging_init() --> map_lowmem() --> create_mapping()

 

1. create_mapping()

先来看看这个函数的真身:

/*
 * Create the page directory entries and any necessary
 * page tables for the mapping specified by `md'.  We
 * are able to cope here with varying sizes and address
 * offsets, and we take full advantage of sections and
 * supersections.
 */
static void __init create_mapping(struct map_desc *md)
{
	if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
		pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n",
			(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
		return;
	}

	if ((md->type == MT_DEVICE || md->type == MT_ROM) &&
	    md->virtual >= PAGE_OFFSET && md->virtual < FIXADDR_START &&
	    (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
		pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n",
			(long long)__pfn_to_phys((u64)md->pfn), md->virtual);
	}

	__create_mapping(&init_mm, md, early_alloc, false);
}

 

这个函数的入参是一个叫 md 的东东,这个玩意用来表征一个映射关系的结构:

struct map_desc {
	unsigned long virtual; // 虚拟地址
	unsigned long pfn;     // 物理地址起始帧号
	unsigned long length;  // 映射长度
	unsigned int type;     // 类型
};

这个好理解吧,给出一个虚拟地址起始地址,物理地址的起始帧号,以及映射的长度和类型即可。

 

2. __create_mapping()

这个关键函数,最后走到了 __create_mapping(),这个地方的入参比较多:

static void __init __create_mapping(struct mm_struct *mm, struct map_desc *md,
				    void *(*alloc)(unsigned long sz),
				    bool ng);

指定了 mm_struct 结构,然后和 map_desc 结构,同时指定了内存分配函数。因为在

create_mapping() --> __create_mapping() 的时候,其实传入的 alloc 分配函数,是 early_alloc ,也就是从 memblock 分配的,早期分配类型。同样的 mm_struct 结构 传入的是全局的 init_mm 指针。

我们接下来看这个 __create_mapping(&init_mm, md, early_alloc, false); 记住他的入参哈:

static void __init __create_mapping(struct mm_struct *mm, struct map_desc *md,
				    void *(*alloc)(unsigned long sz),
				    bool ng)
{
	unsigned long addr, length, end;
	phys_addr_t phys;
	const struct mem_type *type;
	pgd_t *pgd;

	type = &mem_types[md->type];

#ifndef CONFIG_ARM_LPAE
	/*
	 * Catch 36-bit addresses
	 */
	if (md->pfn >= 0x100000) {
		create_36bit_mapping(mm, md, type, ng);
		return;
	}
#endif

	addr = md->virtual & PAGE_MASK;
	phys = __pfn_to_phys(md->pfn);
	length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));

	if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
		pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n",
			(long long)__pfn_to_phys(md->pfn), addr);
		return;
	}

	pgd = pgd_offset(mm, addr);
	end = addr + length;
	do {
		unsigned long next = pgd_addr_end(addr, end);

		alloc_init_pud(pgd, addr, next, phys, type, alloc, ng);

		phys += next - addr;
		addr = next;
	} while (pgd++, addr != end);
}

 

2.1 pgd_offset 

首先做了一些数据的处理:虚拟地址的 Mask,长度对其等等,接下来便是调用这个 pdg_offset 拉:

#define pgd_offset(mm, addr)	((mm)->pgd + pgd_index(addr))

还记得之前的入参么?这里的 mm->pgd 根据入参来展开,就是 init_mm->pgd ,这个定义:

/*
 * For dynamically allocated mm_structs, there is a dynamically sized cpumask
 * at the end of the structure, the size of which depends on the maximum CPU
 * number the system can see. That way we allocate only as much memory for
 * mm_cpumask() as needed for the hundreds, or thousands of processes that
 * a system typically runs.
 *
 * Since there is only one init_mm in the entire system, keep it simple
 * and size this cpu_bitmask to NR_CPUS.
 */
struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir, // pgd 的入口
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	.mmap_sem	= __RWSEM_INITIALIZER(init_mm.mmap_sem),
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.arg_lock	=  __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	.user_ns	= &init_user_ns,
	.cpu_bitmap	= { [BITS_TO_LONGS(NR_CPUS)] = 0},
	INIT_MM_CONTEXT(init_mm)
};

这个 swapper_pg_dir 是 pgd 的入口,在汇编阶段就定义好了存放位置,在 arch/arm/kernel/head.S 中:
 

/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */
#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#ifdef CONFIG_ARM_LPAE
	/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE	0x5000
#define PMD_ORDER	3
#else
#define PG_DIR_SIZE	0x4000
#define PMD_ORDER	2
#endif

	.globl	swapper_pg_dir
	.equ	swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

	.macro	pgtbl, rd, phys
	add	\rd, \phys, #TEXT_OFFSET
	sub	\rd, \rd, #PG_DIR_SIZE
	.endm

所以他的位置在 KERNEL_RAM_VADDR - PG_DIR_SIZE 的地方。

KERNEL_RAM_VADDR 定义在上面,在 PAGE_OFFSET + TEXT_OFFSET 的位置

对于没有大物理地址扩展的情况下,PG_DIR_SIZE 为 0x4000 大小。

我们接下来看 pgd_offset 这个函数的意义:

他返回了 pgd 表的入口(swapper_pg_dir)加上一个 pgd_index(addr),这个 pgd_index(addr) 就是通过虚拟地址,转换到对应的 pgd 表项的索引值:

/* to find an entry in a page-table-directory */
#define pgd_index(addr)		((addr) >> PGDIR_SHIFT)

好了,这个通过这个宏,咱们可以定位到了这个虚拟地址开始的地方的 pgd 的地址了。也就是虚拟地址对应到的那个 pgd 。

接着看 _create_mapping():

pgd = pgd_offset(mm, addr);
end = addr + length;

do {
	unsigned long next = pgd_addr_end(addr, end); ///  <---这里

	alloc_init_pud(pgd, addr, next, phys, type, alloc, ng);

	phys += next - addr;
	addr = next;
} while (pgd++, addr != end);

 

2.2 pgd_addr_end 

获取到了 addr 虚拟地址对应的 pgd 的位置后,在计算得到虚拟地址的结束地点,end = addr + length

在文件 include/asm-generic/pgtable.h 中:

/*
 * When walking page tables, get the address of the next boundary,
 * or the end address of the range if that comes earlier.  Although no
 * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
 */

#define pgd_addr_end(addr, end)						\
({	unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;	\
	(__boundary - 1 < (end) - 1)? __boundary: (end);		\
})

这里的定义,是取了 addr 为开始,PGDIR_SIZE 为步长,来进行 while 的循环。

这个 PGDIR_SIZE 的定义,在 pgtable-2level.h 中的定义为:

#define PMD_SHIFT		21
#define PGDIR_SHIFT		21

#define PMD_SIZE		(1UL << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PGDIR_SIZE		(1UL << PGDIR_SHIFT)
#define PGDIR_MASK		(~(PGDIR_SIZE-1))

也就是 0x01 << 21 的值。这个地方为何是 21 的 SHIFT,后面在聊

换言之,就是将虚拟地址到物理地址的地址映射关系,通过 N 次循环来对 pgd 表进行配置,每次循环都哦配置一个 pgd,那么循环次数怎么确定呢?当然需要看咱们的映射的时候,是否需要多个 pgd 啦,因为在 Linux 管理这些表的时候,pgd 的 OFFSET 是 0x01 << 21 开始的,所以,咱们就需要以这个为步长,来判断我们映射的地址是否需要更多的 pgd 表项。(不知道罗嗦这么多,讲清楚没)。

好了,那么我们假设映射的虚拟地址和物理地址的 Range 不大,那么一格  pgd 就能搞定,好,那么接下来接着这仅有的一次循环都做了些什么事情(猜测应该是配置 pte 表这些的,不急,慢慢来):

 

2.3 alloc_init_pud

pgd = pgd_offset(mm, addr);
end = addr + length;

do {
	unsigned long next = pgd_addr_end(addr, end);

	alloc_init_pud(pgd, addr, next, phys, type, alloc, ng); // <--- 看这里

	phys += next - addr;
	addr = next;
} while (pgd++, addr != end);

接下来调用 alloc_init_pud 函数(其余的都是用于循环控制的,可以不管了)

他的代码实现是:

static void __init alloc_init_pud(pgd_t *pgd, unsigned long addr,
				  unsigned long end, phys_addr_t phys,
				  const struct mem_type *type,
				  void *(*alloc)(unsigned long sz), bool ng)
{
	pud_t *pud = pud_offset(pgd, addr);
	unsigned long next;

	do {
		next = pud_addr_end(addr, end);
		alloc_init_pmd(pud, addr, next, phys, type, alloc, ng);
		phys += next - addr;
	} while (pud++, addr = next, addr != end);
}

这里首先是调用了 pud_offset 函数,入参是 pgd 和 addr:

#define pud_offset(pgd, start)		(pgd)

直接返回 pgd,也就是说,没有 pud。

#define pud_addr_end(addr, end)		(end)

这个也没有,相当于直接调用了 alloc_init_pmd 函数

 

2.4 alloc_init_pmd

这个函数的实现为:

static void __init alloc_init_pmd(pud_t *pud, unsigned long addr,
				      unsigned long end, phys_addr_t phys,
				      const struct mem_type *type,
				      void *(*alloc)(unsigned long sz), bool ng)
{
	pmd_t *pmd = pmd_offset(pud, addr);
	unsigned long next;

	do {
		/*
		 * With LPAE, we must loop over to map
		 * all the pmds for the given range.
		 */
		next = pmd_addr_end(addr, end);

		/*
		 * Try a section mapping - addr, next and phys must all be
		 * aligned to a section boundary.
		 */
		if (type->prot_sect &&
				((addr | next | phys) & ~SECTION_MASK) == 0) {
			__map_init_section(pmd, addr, next, phys, type, ng);
		} else {
			alloc_init_pte(pmd, addr, next,
				       __phys_to_pfn(phys), type, alloc, ng);
		}

		phys += next - addr;

	} while (pmd++, addr = next, addr != end);
}

在 ARM32 的 2-Level 页表映射中(arch/arm/include/asm/pgtable-2level.h):

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
	return (pmd_t *)pud;
}

 从前面的分析可知,pud 没有,这里的 pud 其实就是 pgd 了。

#define pmd_addr_end(addr, end)			(end)

然后调用到了 alloc_init_pte 函数

 

2.5 alloc_init_pte

这个是个关键函数了:

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
				  unsigned long end, unsigned long pfn,
				  const struct mem_type *type,
				  void *(*alloc)(unsigned long sz),
				  bool ng)
{
	pte_t *pte = arm_pte_alloc(pmd, addr, type->prot_l1, alloc); // <--(1)先看这里
	do {
		set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)),
			    ng ? PTE_EXT_NG : 0);
		pfn++;
	} while (pte++, addr += PAGE_SIZE, addr != end);
}

这个函数进来首先进入了: arm_pte_alloc 函数,让我们先走进去瞧瞧。

 

2.6 arm_pte_alloc

static pte_t * __init arm_pte_alloc(pmd_t *pmd, unsigned long addr,
				unsigned long prot,
				void *(*alloc)(unsigned long sz))
{
	if (pmd_none(*pmd)) {
		pte_t *pte = alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
		__pmd_populate(pmd, __pa(pte), prot);
	}
	BUG_ON(pmd_bad(*pmd));
	return pte_offset_kernel(pmd, addr);
}

首先使用宏来判断 pmd 中的数据是不是空的,显然之前看过了,pmd 就是 pud,就是 pgd,最开始没初始化的时候呢,这个地方的值就没有,所以进到了这个 if 里面的语句。

接下来就是使用这个 alloc 来进行内存的分配了,这个 alloc 是一个内存分配的函数指针,从最最开始的函数,也就是 __create_mapping 传进来的,回头看,其实就是使用早期的分配函数 memblock 来进行内存分配。

这里分配的空间是 ==  PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE

在 pgtable-2level.h 中:

#define PTRS_PER_PTE		512
#define PTRS_PER_PMD		1
#define PTRS_PER_PGD		2048

#define PTE_HWTABLE_PTRS	(PTRS_PER_PTE)
#define PTE_HWTABLE_OFF		(PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE	(PTRS_PER_PTE * sizeof(u32))

所以这里可以看到,分配了 512 + 512 个 pte,每个 pte 是 4个字节,也就是分配了 (512+512)×4 = 4K 的空间(1个page)。好了,聊到这里,插播一条重要的内容:

------------------------------------------------ 插播 ------------------------------------------------

这里分配 512 + 512 的原因:

1. 在 ARMv7-A 的处理器,处理器 MMU 支持的内存映射关系是:

也就是,最开始使用 va[31:20] 一共 12 bits 来表征 1 级表项的 index_1,va[19:12] 8 bits 表征 2级表项 index_2,也就是说,1 级表项一共有 2 的 12 次幂这么多个 entry,也就是 4096 个,2 级表项有 2 的 8 次幂个 entry,也就是 256 个。这个特性是 ARM 的 MMU 硬件特性。

 

2. Linux 页表

但是在 Linux 中呢,还记得么,那个 PGD 的 OFFSET 定义成为了 21,也就是 2048 个 pgd 条目,pte 定义了 512 个。这样不是和 ARM 的硬件定义不一样了么?那即便是这样,512 个,那为何在 alloc 分配的时候,分配了 2 个 512 呢?

我们看看 Linux 的 pgtable-2level.h 的部分代码注释:

/*
 * Hardware-wise, we have a two level page table structure, where the first
 * level has 4096 entries, and the second level has 256 entries.  Each entry
 * is one 32-bit word.  Most of the bits in the second level entry are used
 * by hardware, and there aren't any "accessed" and "dirty" bits.
 *
 * Linux on the other hand has a three level page table structure, which can
 * be wrapped to fit a two level page table structure easily - using the PGD
 * and PTE only.  However, Linux also expects one "PTE" table per page, and
 * at least a "dirty" bit.
 *
 * Therefore, we tweak the implementation slightly - we tell Linux that we
 * have 2048 entries in the first level, each of which is 8 bytes (iow, two
 * hardware pointers to the second level.)  The second level contains two
 * hardware PTE tables arranged contiguously, preceded by Linux versions
 * which contain the state information Linux needs.  We, therefore, end up
 * with 512 entries in the "PTE" level.
 *
 * This leads to the page tables having the following layout:
 *
 *    pgd             pte
 * |        |
 * +--------+
 * |        |       +------------+ +0
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +1024
 * +--------+ +0    | Linux pt 1 |
 * |        |-----> +------------+ +2048
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +3072
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +4096
 *
 * See L_PTE_xxx below for definitions of bits in the "Linux pt", and
 * PTE_xxx for definitions of bits appearing in the "h/w pt".
 *
 * PMD_xxx definitions refer to bits in the first level page table.
 *
 * ......
 */

这里说了他的原因,其实 Linux 使用了 2048 个 pgd entry,但是 each 是 8 bytes。其实是一样的。

针对二级页表呢,分配了 512 + 512 个,其实真正的 ARM MMU 的二级是 256 个,他们的对应关系如上面的简要的图所示,pgd 对应到了 h/w pt 0 和 h/w pt 1,他们都是 256 的(每个 pte 是 4 个 Bytes,所以图中看到是 1K 的 Step),另外的两个是 Linux OS 对页面的一些描述信息,同他们放到一起,正好组成了 4K ,即一个页面,不浪费~~。

------------------------------------------------ 插播结束 -----------------------------------------------

好了,分配好了内存后,继续往前走哦。

 

2.7 __pmd_populate

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
				  pmdval_t prot)
{
	pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;
	pmdp[0] = __pmd(pmdval);
#ifndef CONFIG_ARM_LPAE
	pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
#endif
	flush_pmd_entry(pmdp);
}

这里,入参pmdp 是 pmd,pte 是刚刚从上面分配的 4K 的还很热乎的物理地址,然后呢,软件将 pte + PTE_HWTABLE_OFF | prot 这个咋个理解呢?还是看 Linux 官方的那个注释的图解:

*    pgd             pte
 * |        |
 * +--------+
 * |        |       +------------+ +0
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +1024
 * +--------+ +0    | Linux pt 1 |
 * |        |-----> +------------+ +2048   ------- PTE_HWTABLE_OFF
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +3072
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +4096

这个 PTR_HWTABLE_OFF=512,每个pte 4个 Bytes,所以就是 2048 的位置咯 (复制很多遍,不过这里为了清晰,在搞一遍):

#define PTRS_PER_PTE		512
#define PTRS_PER_PMD		1
#define PTRS_PER_PGD		2048

#define PTE_HWTABLE_PTRS	(PTRS_PER_PTE)
#define PTE_HWTABLE_OFF		(PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE	(PTRS_PER_PTE * sizeof(u32))

然后,这个 pmd,pud 都是指向的 pgd,所以,这里呢,把我们分配的 pte 的4K页面的中间位置的地址,交给了pgd,这就是 table walk 的一个逆过程(正向的是,硬件根据 pgd 来寻找 pte 的基地址,这里就赋值给了他)。

然后呢,与上来 prot,应该是一些属性,这个还没太搞懂呢,以后研究清楚了在补上。

最后调用 flush_pmd_entry,来刷 Flush a PMD entry,这个暂时也没太搞清楚,以后清楚了在补上。

好了,让我们在回到刚刚的地方,调用完这个分配 pte 后

 

2.8 pte_offset_kernel

从 __pmd_populate 返回后,会调用到 pte_offset_kernel :

#define pte_offset_kernel(pmd,addr)	(pmd_page_vaddr(*(pmd)) + pte_index(addr))

这里,pmd 就是 pud 也就是 pgd,这里已经被 pte 的物理基地址填充过了

入参的 addr 是虚拟地址

#define pte_index(addr)		(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

获取 pte 的 index。

返回了相应的 PTE 表项。

 

2.9 set_pte_ext

从 arm_pte_alloc 函数返回到 alloc_init_pte 后,继续调用 set_pte_ext,这个和结构体系相关,在 ARMv7-A架构的处理器,在:

arch/arm/mm/proc-v7-2level.S

/*
 *	cpu_v7_set_pte_ext(ptep, pte)
 *
 *	Set a level 2 translation table entry.
 *
 *	- ptep  - pointer to level 2 translation table entry
 *		  (hardware version is stored at +2048 bytes)
 *	- pte   - PTE value to store
 *	- ext	- value for extended PTE bits
 */
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
	str	r1, [r0]			@ linux version

	bic	r3, r1, #0x000003f0
	bic	r3, r3, #PTE_TYPE_MASK
	orr	r3, r3, r2
	orr	r3, r3, #PTE_EXT_AP0 | 2

	tst	r1, #1 << 4
	orrne	r3, r3, #PTE_EXT_TEX(1)

	eor	r1, r1, #L_PTE_DIRTY
	tst	r1, #L_PTE_RDONLY | L_PTE_DIRTY
	orrne	r3, r3, #PTE_EXT_APX

	tst	r1, #L_PTE_USER
	orrne	r3, r3, #PTE_EXT_AP1

	tst	r1, #L_PTE_XN
	orrne	r3, r3, #PTE_EXT_XN

	tst	r1, #L_PTE_YOUNG
	tstne	r1, #L_PTE_VALID
	eorne	r1, r1, #L_PTE_NONE
	tstne	r1, #L_PTE_NONE
	moveq	r3, #0

 ARM(	str	r3, [r0, #2048]! ) // 写入页表
 THUMB(	add	r0, r0, #2048 )
 THUMB(	str	r3, [r0] )
	ALT_SMP(W(nop))
	ALT_UP (mcr	p15, 0, r0, c7, c10, 1)		@ flush_pte
#endif
	bx	lr
ENDPROC(cpu_v7_set_pte_ext)

cpu_v7_set_pte_ext 中 r0 代表了入参第一个,即 pte 指针,(这里请注意,ARM+Linux 上的页表结构 r0+2048),r0 代表了 Linux 版本页面地址。接着设置了一些标志位。最后写入 pte 的页表,完成 pte 页表的初始化。

至此,整个 table walk 的逆过程完成,这个过程,根据需要创建映射的虚拟地址,物理地址,以及长度,来分配了物理的 pte 页面,并且设置了 pgd 到 pte 的关系,以及根据需要映射的相关地址,来设置了 pte 的值,同时设置了相关的属性。