本文共 5498 字,大约阅读时间需要 18 分钟。
Linux内核在启动时会打印出内核内存空间的布局图.
编译器在编译目标文件并且链接完成之后,就可以知道内核映像文件最终的大小,接下来打包成二进制文件,该操作由arch/arm/kernel/vmlinux.ld.S控制,其中也划定了内核的内存布局。
\kernel\msm-3.18\arch\arm\kernel\vmlinux.lds.S
内核image本身占据的内存空间从_text段到_end段,并且分为如下几个段。
❑ 代码段:_text和_etext为代码段的起始和结束地址,包含了编译后的内核代码。 ❑ init段:__init_begin和__init_end为init段的起始和结束地址,包含了大部分模块初始化的数据。 ❑ 数据段:_sdata和_edata为数据段的起始和结束地址,保存大部分内核的变量。 ❑ BSS段:__bss_start和__bss_stop为BSS段的开始和结束地址,包含初始化为0的所有静态全局变量。上述几个段的大小在编译链接时根据内核配置来确定,因为每种配置的代码段和数据段长度都不相同,这取决于要编译哪些内核模块,但是起始地址_text总是相同的。
内核编译完成之后,会生成一个System.map文件,查询这个文件可以找到这些地址的具体数值。用户空间和内核空间使用3:1的划分方法时,内核空间只有1GB大小。这1GB的映射空间,其中有一部分用于直接映射物理地址,这个区域称为线性映射区。
在ARM32平台上,物理地址[0:760MB]的这一部分内存被线性映射到[3GB:3GB+ 760MB]的虚拟地址上。
线性映射区的虚拟地址和物理地址相差PAGE_OFFSET,即3GB。 内核中有相关的宏来实现线性映射区虚拟地址到物理地址的查找过程,例如__pa(x)和__va(x)。// \kernel\msm-3.18\arch\arm\include\asm\memory.h/* * Drivers should NOT use these either. */#define __pa(x) __virt_to_phys((unsigned long)(x))#define __va(x) ((void *)__phys_to_virt((phys_addr_t)(x)))/* PAGE_OFFSET - the virtual address of the start of the kernel image */#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)static inline phys_addr_t __virt_to_phys(unsigned long x){ return (phys_addr_t)x - PAGE_OFFSET + PHYS_OFFSET;}static inline unsigned long __phys_to_virt(phys_addr_t x){ return x - PHYS_OFFSET + PAGE_OFFSET;}
__pa()把线性映射区的虚拟地址转换为物理地址,转换公式很简单,即用虚拟地址减去PAGE_OFFSET(3GB),然后加上PHYS_OFFSET(这个值在有的ARM平台上为0,在ARM Vexpress平台该值为0x6000_0000)。
那高端内存的起始地址(760MB)是如何确定的呢?
在内核初始化内存时,在sanity_check_meminfo()函数中确定高端内存的起始地址,全局变量high_memory来存放高端内存的起始地址。 vmalloc_min计算出来的结果是0x2F80_0000,即760MB。为什么内核只线性映射760MB呢?
剩下的264MB的虚拟地址空间用来做什么呢?
那是保留给vmalloc、fixmap和高端向量表等使用的。 内核很多驱动使用vmalloc来分配连续虚拟地址的内存,因为有的驱动不需要连续物理地址的内存; 除此以外,vmalloc还可以用于高端内存的临时映射。 一个32bit系统中实际支持的内存数量会超过内核线性映射的长度,但是内核要具有对所有内存的寻找能力。vmalloc区域在ARM32内核中,从VMALLOC_START开始到VMALLOC_END结束,即从0xf000_0000到0xff00_0000,大小为240MB。
在VMALLOC_START开始之前有一个8MB的洞,用于捕捉越界访问。内核通常把物理内存低于760MB的称为线性映射内存(NormalMemory),而高于760MB以上的称为高端内存(High Memory)。
由于32位系统的寻址能力只有4GB,对于物理内存高于760MB而低于4GB的情况,我们可以从保留的240MB的虚拟地址空间中划出一部分用于动态映射高端内存,这样内核就可以访问到全部的4GB内存了。如果物理内存高于4GB,那么在ARMv7-A架构中就要使用LPE机制来扩展物理内存访问了。
用于映射高端内存的虚拟地址空间有限,所以又可以划分为两部分,一部分为临时映射区,另一部分为固定映射区, PKMAP指向的就是固定映射区。
ARM64架构处理器采用48位物理寻址机制,最大可以寻找256TB的物理地址空间。对于目前的应用来说已经足够了,不需要扩展到64位的物理寻址。
虚拟地址也同样最大支持48位寻址,所以在处理器架构设计上,把虚拟地址空间划分为两个空间,每个空间最大支持256TB。 Linux内核在大多数体系结构上都把两个地址空间划分为用户空间和内核空间。❑ 用户空间:0x0000_0000_0000_0000到0x0000_ffff_ffff_ffff。
❑ 内核空间:0xffff_0000_0000_0000到 0xffff_ffff_ffff_ffff。64位Linux内核中没有高端内存这个概念了,因为48位的寻址空间已经足够大了。
(1)用户空间:0x0000_0000_0000_0000到0x0000_ffff_ffff_ffff,一共有256TB。
(2)非规范区域。 (3)内核空间:0xffff_0000_0000_0000到0xffff_ffff_ffff_ffff,一共有256TB。内核空间又做了如下细分。
❑ vmalloc区域:0xffff000000000000到0xffff7bffbfff0000,大小为126974GB。 ❑ vmemmap区域:0xffff7bffc0000000到0xffff7fffc0000000,大小为4096GB。 ❑ PCI I/O区域:0xffff7ffffae00000到0xffff7ffffbe00000,大小为16MB。 ❑ Modules区域:0xffff7ffffc000000到0xffff800000000000,大小为64MB。 ❑ normal memory线性映射区:0xffff800000000000到0xffffffffffffffff,大小为128TB。释放页面的核心函数是free_page(),最终还是调用__free_pages()函数。
__free_pages()函数会分两种情况,对于order等于0的情况,做特殊处理;对于order大于0的情况,属于正常处理流程。void __free_pages(struct page *page, unsigned int order){ if(put_page_testzero(page)){ if(order == 0) free_hot_cold_page(page, false); else __free_pages_ok(page, order); }}
首先来看order大于0的情况。
__free_pages()函数内部调用__free_pages_ok(),最后调用__free_one_page()函数。 因此释放内存页面到伙伴系统,最终还是通过__free_one_page()来实现。 该函数不仅可以释放内存页面到伙伴系统,还会处理空闲页面的合并工作。释放内存页面的核心功能是把页面添加到伙伴系统中适当的free_area链表中。
在释放内存块时,会查询相邻的内存块是否空闲,如果也空闲,那么就会合并成一个大的内存块,放置到高一阶的空闲链表free_area中。如果还能继续合并邻近的内存块,那么就会继续合并,转移到更高阶的空闲链表中,这个过程会一直重复下去,直至所有可能合并的内存块都已经合并。static inline void __free_one_page(struct page *page, unsigned long pfn, struct zone *zone, unsigned int order, int migratetype){ unsigned long page_idx; unsigned long combined_idx; unsigned long uninitiaized_var(buddy_idx); struct page *buddy; int max_order = MAX_ORDER; page_idx = pfn & ( (1 << max_order) - 1 ); while(order < max_order -1){ buddy_idx = __find_buddy_index(page_idx, order); buddy = page + (buddy_idx - page_idx); if( !page_is_buddy(page, buddy, order) ) break; // our buddy is free or it is CONFIG_DEBUG_PAGEALLOC guard page, merge wirh it and move up one order if( page_is_guard(buddy) ){ clear_page_guard(zone, buddy, order, migratetype); }else{ list_del(&buddy->lru); zone->free_area[order].nr_free --; rmv_page_order(buddy); } combind_idx = buddy_idx & page_idx; page = page + (combined_idx - page_idx); page_idx = combined_idx; order++; }}
这段代码是合并相邻伙伴块的核心代码。
我们以一个实际例子来说明这段代码的逻辑, 假设现在要释放一个内存块A,大小为2个page,内存块的page的开始页帧号是0x8e010, order为1,如图2.8所示。(1)首先计算得出page_idx等于0x10。也就是说,这个内存块位于pageblock的0x10的位置。
(2)在第一次while循环中,计算buddy_idx。
page_idx ^ (1<<order) ====> page_idx为0x10, order为1,最后计算结果为0x12。(3)那么buddy就是内存块A的临近内存块B了,内存块B在pageblock的起始地址为0x12。
(4)接下来通过page_is_buddy()函数来检查内存块B是不是空闲的内存块。内存块在buddy中并且order也相同,该函数返回1。
(5)如果发现内存块B也是空闲内存,并且order也等于1,那么我们找到了一块志同道合的空闲伙伴块,把它从空闲链表中摘下来,以便和内存块A合并到高一阶的空闲链表中。(6)这时combined_idx指向内存块A的起始地址。order++表示继续在附近寻找有没有可能合并的相邻的内存块,这次要查找的order等于2,也就是4个page大小的内存块。
(7)重复步骤(2),查找附近有没有志同道合的order为2的内存块。(8)如果在0x14位置的内存块C不满足合并条件,例如内存块C不是空闲页面,或者内存块C的order不等于2。如图2.8所示,内存块C的order等于3,显然不符合我们的条件。如果没找到order为2的内存块,那么只能合并内存块A和B了,然后把这个内存块添加到空闲页表中。
list_add(&page->lru, &zone->free_are[order].free_list[migratetype]);
__free_pages()对于order等于0的情况,作为特殊情况来处理,
zone中有一个变量zone->pageset为每个CPU初始化一个percpu变量structper_cpu_pageset。当释放order等于0的页面时,首先页面释放到per_cpu_page->list对应的链表中。
转载地址:http://qvqbf.baihongyu.com/