Linux内存管理(五)内核态内存映射

Linux内存管理

Linux内存管理(一)Linux进程空间管理

Linux内存管理(二)物理内存管理(上)

Linux内存管理(三)物理内存管理(下)

Linux内存管理(四)用户态内存映射

Linux内存管理(五)内核态内存映射

Linux内存管理(五)内核态内存映射


首先,需要直到,内核态的内存映射机制,主要包含以下几个部分

  • 内核态内存映射函数 vmalloc、kmap_atomic 是如何工作的
  • 内核态页表放在哪里?是怎么工作的?swapper_pg_dir 是怎么回事?
  • 出现了内核态缺页异常应该怎么办?

一、内核页表

和用户态页表不同,在系统初始化的时候,就创建了内核页表了

swapper_pg_dir 是内核页表的根,可以从它开始找线索,其定义如下

extern pud_t level3_kernel_pgt[512];
extern pud_t level3_ident_pgt[512];
extern pmd_t level2_kernel_pgt[512];
extern pmd_t level2_fixmap_pgt[512];
extern pmd_t level2_ident_pgt[512];
extern pte_t level1_fixmap_pgt[512];
extern pgd_t init_top_pgt[];


#define swapper_pg_dir init_top_pgt

swpper_pg_dir 是指向内核最顶级的目录 pgd,同时出现的还有几个页表目录。

对应 64 位系统的虚拟地址空间的布局,其中 xxx_ident_pgt 对应的是直接映射区,xxx_kernel_pgt 对应的是内核代码区,xxx_fixmap_pgt 对应的是固定映射区

它们通过下面的代码初始化

__INITDATA


NEXT_PAGE(init_top_pgt)
	.quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
	.org    init_top_pgt + PGD_PAGE_OFFSET*8, 0
	.quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
	.org    init_top_pgt + PGD_START_KERNEL*8, 0
	/* (2^48-(2*1024*1024*1024))/(2^39) = 511 */
	.quad   level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE


NEXT_PAGE(level3_ident_pgt)
	.quad	level2_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
	.fill	511, 8, 0
NEXT_PAGE(level2_ident_pgt)
	/* Since I easily can, map the first 1G.
	 * Don't set NX because code runs from these pages.
	 */
	PMDS(0, __PAGE_KERNEL_IDENT_LARGE_EXEC, PTRS_PER_PMD)


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)


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	51

内核页表的顶级目录 init_top_pgt,定义在 __INITDATA 里面,__INITDATA 是一个宏定义,它标记着一个区域。

可以看到,页表的根其实是全局变量,这使得在初始化的时候,甚至内存管理还没有初始化的时候,很容易可以定位到

接下来,定义 init_top_pgt 包含哪些项,这些汇编代码较难看懂。可以简单地认为 quad 是声明一项内容,org 是跳到某个位置

所以 init_top_pgt 中一共有三项,第一项是 level3_ident_pgt,也即直接映射区页表的三级目录。为什么要减去 __START_KERNEL_map?因为 level3_ident_pgt 是定义在内核代码里的,而写代码的时候,用的都是虚拟地址,并不知道将来要加载的物理地址位置

因为 level3_ident_pgt 是在虚拟地址的内核代码段里的,而 __START_KERNEL_map 是虚拟地址空间的内核代码段的起始地址。这样 level3_ident_pgt 减去 __START_KERNEL_map 才是物理地址

第一项定义完后,接下来跳到 PGD_PAGE_OFFSET 偏移位置,再定义一项。从定义可以看出,这一项是 __PGD_PAGE_OFFSET 对应的。__PGD_PAGE_OFFSET 是虚拟地址空间中内核的起始地址。第二项也指向 level3_ident_pgt,直接映射区

PGD_PAGE_OFFSET = pgd_index(__PAGE_OFFSET_BASE)
PGD_START_KERNEL = pgd_index(__START_KERNEL_map)
L3_START_KERNEL = pud_index(__START_KERNEL_map)

第二项定义完了以后,接下来跳到 PGD_START_KERNEL的位置,再定义一项。从定义中可以看到,这一项是 __START_KERNEL_map 对应的项,__START_KERNEL_map 是虚拟地址空间中内核代码段的起始地址。第三项指向 level3_kernel_pgt,内核代码区

接下来的代码就类似了,就是初始化整个表项,然后指向下一级目录,最终形成下图的布局

img

内核的页表定义完了,一开始这里面的页表映射到的物理内存范围比较小。如内核代码区 512M,直接映射区 1G。这个时候,只要能够映射基本的内核代码和数据结构就足够了。可以看出,还有很多空出的项,可以映射巨大的内核虚拟地址,等到需要使用时再进行映射

如果时内核态进程页表,会有 mm_struct 指向进程顶级页表 pgd,对应内核,也有一个全局的 mm_struct,指向 swapper_pg_dir

struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,
	.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),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	.user_ns	= &init_user_ns,
	INIT_MM_CONTEXT(init_mm)
};

定义完内核页表后,接下来是初始化内核页表

在系统启动的时候 start_kernel 会调用 setup_arch

void __init setup_arch(char **cmdline_p)
{
	/*
	 * copy kernel address range established so far and switch
	 * to the proper swapper page table
	 */
	clone_pgd_range(swapper_pg_dir     + KERNEL_PGD_BOUNDARY,
			initial_page_table + KERNEL_PGD_BOUNDARY,
			KERNEL_PGD_PTRS);


	load_cr3(swapper_pg_dir);
	__flush_tlb_all();
......
	init_mm.start_code = (unsigned long) _text;
	init_mm.end_code = (unsigned long) _etext;
	init_mm.end_data = (unsigned long) _edata;
	init_mm.brk = _brk_end;
......
	init_mem_mapping();
......
}

在 setup_arch 中,load_cr3(swapper_pg_dir) 说明内核页表要开始起作用了,并且刷新了 TLB,初始化 init_mm,最重要的是 init_mem_mapping,最终会调用 kernel_physical_mapping_init

/*
 * Create page table mapping for the physical memory for specific physical
 * addresses. The virtual and physical addresses have to be aligned on PMD level
 * down. It returns the last physical address mapped.
 */
unsigned long __meminit
kernel_physical_mapping_init(unsigned long paddr_start,
			     unsigned long paddr_end,
			     unsigned long page_size_mask)
{
	unsigned long vaddr, vaddr_start, vaddr_end, vaddr_next, paddr_last;


	paddr_last = paddr_end;
	vaddr = (unsigned long)__va(paddr_start);
	vaddr_end = (unsigned long)__va(paddr_end);
	vaddr_start = vaddr;


	for (; vaddr < vaddr_end; vaddr = vaddr_next) {
		pgd_t *pgd = pgd_offset_k(vaddr);
		p4d_t *p4d;


		vaddr_next = (vaddr & PGDIR_MASK) + PGDIR_SIZE;


		if (pgd_val(*pgd)) {
			p4d = (p4d_t *)pgd_page_vaddr(*pgd);
			paddr_last = phys_p4d_init(p4d, __pa(vaddr),
						   __pa(vaddr_end),
						   page_size_mask);
			continue;
		}


		p4d = alloc_low_page();
		paddr_last = phys_p4d_init(p4d, __pa(vaddr), __pa(vaddr_end),
					   page_size_mask);


		p4d_populate(&init_mm, p4d_offset(pgd, vaddr), (pud_t *) p4d);
	}
	__flush_tlb_all();


	return paddr_l

在 kernel_physical_mapping_init 中,先通过 __va 将物理地址转化位虚拟地址,然后再创建物理地址和虚拟地址的映射页表

可以使用 __va 和 __pa 直接进行物理地址和虚拟地址的转换,为什么还需要建立页表?这是因为硬件的需求,在开启保护模式后,CPU 总是需要通过页表将虚拟地址转换为物理地址

二、vmalloc 和 kmap_atomic 原理

在虚拟地址的内核空间中,有一段 vmalloc 区域,从 VMALLOC_START 开始到 VMALLOC_END,可以用于映射物理内存

/**
 *	vmalloc  -  allocate virtually contiguous memory
 *	@size:		allocation size
 *	Allocate enough pages to cover @size from the page level
 *	allocator and map them into contiguous kernel virtual space.
 *
 *	For tight control over page level allocator and protection flags
 *	use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
	return __vmalloc_node_flags(size, NUMA_NO_NODE,
				    GFP_KERNEL);
}


static void *__vmalloc_node(unsigned long size, unsigned long align,
			    gfp_t gfp_mask, pgprot_t prot,
			    int node, const void *caller)
{
	return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
				gfp_mask, prot, 0, node, caller);
}

下面来看看内核 kmap_atomic 的实现

void *kmap_atomic_prot(struct page *page, pgprot_t prot)
{
......
	if (!PageHighMem(page))
		return page_address(page);
......
	vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
	set_pte(kmap_pte-idx, mk_pte(page, prot));
......
	return (void *)vaddr;
}


void *kmap_atomic(struct page *page)
{
	return kmap_atomic_prot(page, kmap_prot);
}


static __always_inline void *lowmem_page_address(const struct page *page)
{
	return page_to_virt(page);
}


#define page_to_virt(x)	__va(PFN_PHYS(page_to_pfn(x)

如果是32位,由于内存空间较小,所以物理内存有高端地址区,就需要调用 set_pte 通过内核页表进行临时映射;如果是64位没有高端地址,就调用 page_address,里面会调用 lowmem_page_address,直接通过 _va 转换到低端内存

三、内核缺页异常

可以看到 vmalloc 和 kmap_atomic 的不同,kmap_atomic 发现没有页表的时候,会直接创建页表;而 vmalloc 只是分配虚拟地址空间,当访问该地址的时候,会发生缺页中断

内核态的缺页还是会调用 do_page_fault,然后调用 vmalloc_fault,这个函数主要用于关联内核页表

/*
 * 32-bit:
 *
 *   Handle a fault on the vmalloc or module mapping area
 */
static noinline int vmalloc_fault(unsigned long address)
{
	unsigned long pgd_paddr;
	pmd_t *pmd_k;
	pte_t *pte_k;


	/* Make sure we are in vmalloc area: */
	if (!(address >= VMALLOC_START && address < VMALLOC_END))
		return -1;


	/*
	 * Synchronize this task's top level page-table
	 * with the 'reference' page table.
	 *
	 * Do _not_ use "current" here. We might be inside
	 * an interrupt in the middle of a task switch..
	 */
	pgd_paddr = read_cr3_pa();
	pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);
	if (!pmd_k)
		return -1;


	pte_k = pte_offset_kernel(pmd_k, address);
	if (!pte_present(*pte_k))
		return -1;


	return 0

四、总结

下面对整个内存管理体系进行总结

物理内存根据 NUMA 架构分节点。每个节点里面再分区域,每个区域再分页

物理页面通过伙伴系统进行管理。分配的物理页面要变成虚拟地址让上层可以访问,kswapd 可以根据物理页面的使用情况对页面进行换入和换出

对内存的分配需求,可能来自内核态,也可能来自用户态

对于内核态,kmalloc 在分配大内存的时候,以及 vmalloc 分配不连续物理页的时候,直接使用伙伴系统,分配后转换位虚拟地址,需要的时候通过内核页表进行访问

对于 kmem_cache 以及 kmalloc 分配小块内存,则使用 slub 分配器,将从伙伴系统中分配到的大块内存,切分成一小块一小块进行分配

kmem_cache 和 kmalloc 不会被换出,应为用这两个函数分配到的内存,多用于维护内核的关键数据结构。内核态中 vmalloc 分配的部分会被换出,因而当访问的时候,发现不在,就会调用 do_page_fault

对于用户态的内存分配,通过调用 malloc 或者 mmap。调用 malloc 的时候,如果分配小块内存,就使用 sys_brk 系统调用;如果分配大内存块,就使用 sys_mmap。正常情况下,用于态的内存是可以换出的,因而一旦发现内存中不存在,就会调用 do_page_fault

img

发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102511127
今日推荐