Linux - 用户态内存映射 和 内核态内存映射

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux - 用户态内存映射 和 内核态内存映射相关的知识,希望对你有一定的参考价值。

参考技术A

操作系统的内存管理,主要分为三个方面。
第一,物理内存的管理,相当于会议室管理员管理会议室。
第二,虚拟地址的管理,也即在项目组的视角,会议室的虚拟地址应该如何组织。
第三,虚拟地址和物理地址如何映射,也即会议室管理员如果管理映射表。

那么虚拟地址和物理地址如何映射呢?

每一个进程都有一个列表vm_area_struct,指向虚拟地址空间的不同的内存块,这个变量的名字叫mmap。

其实内存映射不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间。这个时候,访问内存空间就能够访问到文件里面的数据。而仅有物理内存和虚拟内存的映射,是一种特殊情况。

如果我们要申请小块内存,就用brk。brk函数之前已经解析过了,这里就不多说了。如果申请一大块内存,就要用mmap。对于堆的申请来讲,mmap是映射内存空间到物理内存。

另外,如果一个进程想映射一个文件到自己的虚拟内存空间,也要通过mmap系统调用。这个时候mmap是映射内存空间到物理内存再到文件。可见mmap这个系统调用是核心,我们现在来看mmap这个系统调用。

用户态的内存映射机制包含以下几个部分。

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

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

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

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

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

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

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

操作系统-内核态内存映射

内核态的内存映射机制,主要包含以下三个部分:

  • 内核态页表的工作原理。

  • 内核态内存映射函数vmalloc、kmap_atomic的工作原理。

  • 内核态缺页异常的处理方式。



内核页表

操作系统-内核态内存映射

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


我们从内核页表的根swapper_pg_dir开始找线索,在arch/x86/include/asm/pgtable_64.h中就能找到它的定义。

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

他们是从哪里初始化的呢?是在汇编语言的文件里面archx86kernelhead_64.S。这段代码比较难看懂,只要明白它是干什么的就行了。

__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, 0NEXT_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里面。页表的根其实是全局变量,这就使得我们初始化的时候,甚至内存管理还没有初始化的时候,很容易就可以定位到。


接下来是定义init_top_pgt包含哪些项,可以简单的认为,quad是声明了一项内容,org是跳到了某个位置。



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



接下来的代码就很类似,就是初始化个表项,然后指向下一级目录,最终形成下面这张图。

操作系统-内核态内存映射


如果是用户态进程页表,会有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 __meminitkernel_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



操作系统-内核态内存映射

vmalloc和kmap_atomic原理

操作系统-内核态内存映射

在用户态可以通过malloc函数分配内存,当然malloc在分配比较大的内存的时候,底层调用的是mmap,当然也可以直接通过mmap做内存映射,在内核里面也有相应的函数。


/** * 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);}
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)



操作系统-内核态内存映射

内核态缺页异常

操作系统-内核态内存映射

内核态的缺页异常还是会调用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架构分节点。每个节点里面再分区域。每个区域再分页。



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



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


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


对于用户态的内存分配,可以直接使用mmap系统调用分配,也可以调用malloc进行分配。需要注意的是,调用malloc的时候,如果分配小的内存,底层使用的是sys_brk系统调用;如果分配大的内存,底层还是调用sys_mmap系统调用。正常情况下,用户态的内存都是可以换出的,因而一旦发现内存中不存在,就会调用do_page_fault。


以上是关于Linux - 用户态内存映射 和 内核态内存映射的主要内容,如果未能解决你的问题,请参考以下文章

操作系统-内核态内存映射

内存映射:小块内存申请brk和申请大块内存的Mmap分析

程序员必读:Linux内存管理剖析

Linux操作系统 进程之间的通信

Linux mmap原理

linux进程为啥有用户栈和内核栈,