内存管理我的理解是分为两个部分,一个是物理内存的管理,另一个部分是物理内存地址到虚拟地址的转换。
物理内存管理
内核中实现了很多机制和算法来进行物理内存的管理,比如大名鼎鼎的伙伴系统,以及slab分配器等等。我们知道随着Linux系统的运行,内存是不断的趋于碎片化的,内存碎片分为两种类型,一种为外碎片,所谓外碎片就是以页为单位的内存之间的碎片化,另一种为内碎片,内碎片是指同一个页面内的碎片化,那么如果来优化这种内存碎片问题呢?
-
伙伴系统
伙伴系统可以用来减少外碎片的,通过更加合理的分配以页为单位的内存,可以减少外碎片的产生,以尽量保持系统内存的连续性。 -
slab分配器
slab是用于优化内碎片问题的,通过把小块内存以对象的方式管理起来,并且创建slab缓存,方便同种类型对象的分配和释放,减少了内碎片的产生,同时这些小块内存会尽可能的保持在硬件cache中,所以极大提升了访问效率。
物理内存管理这块比较复杂,本文仅仅做一个简述,关于这两者的内核API以及实现,将在后续文章中再做介绍。
物理地址到虚拟地址的转换
本文只介绍内核中访问所有物理内存的方式,当前我们面对的问题是:如何物理内存映射到内核空间(3G-4G)这一段区域内?对于用户空间访问物理内存的话题,我们后续再开专门的文章进行介绍。
内核中把物理内存的低端区域作为直接映射区,高地址区域定义为高端内存,通过一个变量high_memory来界定他们的分界线。high_memory是一个虚拟地址,定义了高端内存被允许映射到内核的起始地址。
它在arm平台上的定义如下:
void * high_memory;
EXPORT_SYMBOL(high_memory);
arm_lowmem_limit = lowmem_limit;
high_memory = __va(arm_lowmem_limit - 1) + 1;
if (!memblock_limit)
memblock_limit = arm_lowmem_limit;
以我的测试板子为例:
Memory: 1030548K/1048576K available (5078K kernel code, 221K rwdata, 1624K rodata, 1584K init, 179K bss, 18028K reserved, 0K cma-reserved, 270336K highmem)
Virtual kernel memory layout:
vector : 0xffff0000 - 0xffff1000 ( 4 kB)
fixmap : 0xffc00000 - 0xfff00000 (3072 kB)
vmalloc : 0xf0000000 - 0xff000000 ( 240 MB)
lowmem : 0xc0000000 - 0xef800000 ( 760 MB)
pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)
modules : 0xbf000000 - 0xbfe00000 ( 14 MB)
.text : 0xc0008000 - 0xc0693dd8 (6704 kB)
.init : 0xc0694000 - 0xc0820000 (1584 kB)
.data : 0xc0820000 - 0xc0857708 ( 222 kB)
.bss : 0xc0857708 - 0xc0884700 ( 180 kB)
它的虚拟内存分布如上所示。这块信息的实现代码如下:
pr_notice("Virtual kernel memory layout:\n"
" vector : 0x%08lx - 0x%08lx (%4ld kB)\n"
#ifdef CONFIG_HAVE_TCM
" DTCM : 0x%08lx - 0x%08lx (%4ld kB)\n"
" ITCM : 0x%08lx - 0x%08lx (%4ld kB)\n"
#endif
" fixmap : 0x%08lx - 0x%08lx (%4ld kB)\n",
MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) +
(PAGE_SIZE)),
#ifdef CONFIG_HAVE_TCM
MLK(DTCM_OFFSET, (unsigned long) dtcm_end),
MLK(ITCM_OFFSET, (unsigned long) itcm_end),
#endif
MLK(FIXADDR_START, FIXADDR_END));
#ifdef CONFIG_ENABLE_VMALLOC_SAVING
print_vmalloc_lowmem_info();
#else
printk(KERN_NOTICE
" vmalloc : 0x%08lx - 0x%08lx (%4ld MB)\n"
" lowmem : 0x%08lx - 0x%08lx (%4ld MB)\n",
MLM(VMALLOC_START, VMALLOC_END),
MLM(PAGE_OFFSET, (unsigned long)high_memory));
#endif
printk(KERN_NOTICE
#ifdef CONFIG_HIGHMEM
" pkmap : 0x%08lx - 0x%08lx (%4ld MB)\n"
#endif
#ifdef CONFIG_MODULES
" modules : 0x%08lx - 0x%08lx (%4ld MB)\n"
#endif
" .text : 0x%p" " - 0x%p" " (%4d kB)\n"
" .init : 0x%p" " - 0x%p" " (%4d kB)\n"
" .data : 0x%p" " - 0x%p" " (%4d kB)\n"
" .bss : 0x%p" " - 0x%p" " (%4d kB)\n",
#ifdef CONFIG_HIGHMEM
MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) *
(PAGE_SIZE)),
#endif
#ifdef CONFIG_MODULES
MLM(MODULES_VADDR, MODULES_END),
#endif
MLK_ROUNDUP(_text, _etext),
MLK_ROUNDUP(__init_begin, __init_end),
MLK_ROUNDUP(_sdata, _edata),
MLK_ROUNDUP(__bss_start, __bss_stop));
我们通过上面机器的启动log打印出来的memory layout可以知道,在3G以下的区域也是被内核数据所占用了,可是上面不是说用户空间是0-3G吗?这里不会被用户所占用导致冲突吗?
实际上,用户空间的映射区定义如下:
00001000 TASK_SIZE-1 User space mappings
Per-thread mappings are placed here via
the mmap() system call.
这里TASK_SIZE实际上并不是PAGE_OFFSET-1,而是中间间隔了一段区域(16M):
/*
* TASK_SIZE - the maximum size of a user space task.
* TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area
*/
#define TASK_SIZE (UL(CONFIG_PAGE_OFFSET) - UL(SZ_16M))
#define TASK_UNMAPPED_BASE ALIGN(TASK_SIZE / 3, SZ_16M)
低端内存映射
内核空间1G的虚拟空间,其中有一部分用于直接映射,线性映射区,在arm32平台上,物理地址[0:760M]这部分内存被线性的映射到[3G:3G+760M]的虚拟地址上,剩余的264M虚拟地址做什么呢?
是保留给高端内存映射使用的,这部分是能够动态分配和释放的,因为平台上实际的物理内存可能会超过1G,那么内核必须要具有能够寻址到整个物理内存的能力。线性映射区在启动时就完成了页表的创建,没有必要再过多介绍。
测试平台上的线性映射区域:
lowmem : 0xc0000000 - 0xef800000 ( 760 MB)
对应的解释如下:
PAGE_OFFSET high_memory-1 Kernel direct-mapped RAM region.
This maps the platforms RAM, and typically
maps all platform RAM in a 1:1 relationship.
高端内存映射
针对高端内存的映射,内核又划分了多个区域,因为需要在264M有限的区域内去访问除了760M之外的所有物理内存,所以这部分相比线性映射区将变得更加复杂。内核有三种方式用于将高端内存映射到内核空间,分别是pkmap、fixmap和vmalloc。
- pkmap
测试平台上的数据如下:
pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)
说明:
PKMAP_BASE PAGE_OFFSET-1 Permanent kernel mappings
One way of mapping HIGHMEM pages into kernel
space.
永久内核映射区,映射高端内存到内核空间的一种方式。pkmap在用于映射高端物理内存的,当我们从伙伴系统中分配到高端内存后,是无法直接操作的,必须要经过map操作,此时就可以使用pkmap,它对应的
内核API为
void *kmap(struct page *page);
传入的是一个物理内存页对应的struct page结构体,返回一个虚拟地址。使用方法如下:
使用alloc_pages()在高端存储器区得到struct page结构,然后调用kmap(struct *page)在内核地址空间[PKMAP_BASE : PAGE_OFFSET-1]中建立永久映射,如果page结构对应的是低端物理内存的页,该函数仅仅返回该页对应的虚拟地址。
另外需要注意kmap()可能引起睡眠,所以不能用在中断和持有锁的代码中使用。从使用方法上我们知道,它是针对struct page来进行的操作,所以至少会映射一个page。
- fixmap
测试平台上的数据如下:
fixmap : 0xffc00000 - 0xfff00000 (3072 kB)
说明:
ffc00000 ffefffff Fixmap mapping region. Addresses provided
by fix_to_virt() will be located here.
fixmap也叫临时映射区,他是一个固定的一块虚拟空间用于映射不同的物理地址,并且是在申请时使用,不用时释放。它于pkmap的区别在于这块地址的映射不会引起睡眠,是可以在中断和持有锁的代码中运行的,它的内核API如下:
void *kmap_atomic(struct page *page);
- vmalloc
测试平台上的数据如下:
vmalloc : 0xf0000000 - 0xff000000 ( 240 MB)
lowmem : 0xc0000000 - 0xef800000 ( 760 MB)
说明:
VMALLOC_START VMALLOC_END-1 vmalloc() / ioremap() space.
Memory returned by vmalloc/ioremap will
be dynamically placed in this region.
Machine specific static mappings are also
located here through iotable_init().
VMALLOC_START is based upon the value
of the high_memory variable, and VMALLOC_END
is equal to 0xff800000.
从平台打印的数据来看,vmalloc和lowmem线性映射区并没有完全紧靠着,而是中间有一个hole空洞(8M),这个8M的空间是为了捕获越界访问的。
vmalloc会分配非连续物理内存,这里的非连续指的是物理内存不连续,虚拟地址是连续的,优先使用高端内存来分配物理页,如果分配失败,才会从Normal zone分配。这个接口和上面的都不同,它会自动分配物理内存,然后完成映射后直接返回虚拟地址,而上面两个都是只进行映射。
void *vmalloc(unsigned long size);