1 概述
内存管理的实现涵盖了许多领域:
内存中的物理内存页的管理;
分配大块内存的伙伴系统;
分配较小块内存的slab,slub,和slob分配器;
分配非连续内存块的vmalloc机制;
进程的地址空间。
Linux内核一般将处理器的虚拟地址空间分为两个部分。底部比较大的部分用于用户进程,顶部则用于内核。在上下文切换时候会改变下半部分,但内核部分不会改变,一般内核与用户部分比例为1:3。
可用的物理内存将映射到内核的地址空间。访问内存时,如果所用的虚拟地址与内核区域的起始地址之间的偏移量不超出可用物理内存的长度,那么该虚拟地址会自动关联到物理页桢。如果超过,则用高端内存方法来管理多余的内存。
有两种类型计算机,分别以不同的方法管理物理内存。
UMA计算机(uniform memory access)将可用内存以连续方式组织起来,SMP系统中的每个处理器访问各个内存区都是同样快。
NUMA计算机(non-uniform memory access)总是多处理器计算机。系统的各个CPU都有本地内存,可支持特别快速的访问。各个处理器之间通过总线连接起来,以支持对其他CPU的本地内存的访问,当然比访问本地内存慢。
2 (N)UMA模型中的内存组织
概述
内存划分为结点,每个结点关联一个处理器,在内核中表示为pg_data_t的实例。
各个结点又划分为内存域,是内存的进一步划分。一个结点由3个内存域组成。
在include/linux/mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
/*
* ZONE_DMA is used when there are devices that are not able
* to do DMA to all of addressable memory (ZONE_NORMAL). Then we
* carve out the portion of memory that is needed for these devices.
* The range is arch specific.
*
* Some examples
*
* Architecture Limit
* ---------------------------
* parisc, ia64, sparc <4G
* s390 <2G
* arm Various
* alpha Unlimited or 0-16MB.
*
* i386, x86_64 and multiple other arches
* <16M.
*/
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
/*
* x86_64 needs two ZONE_DMAs because it supports devices that are
* only able to do DMA to the lower 16M but also 32 bit devices that
* can only do DMA areas below 4G.
*/
ZONE_DMA32,
#endif
/*
* Normal addressable memory is in ZONE_NORMAL. DMA operations can be
* performed on pages in ZONE_NORMAL if the DMA devices support
* transfers to all addressable memory.
*/
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
/*
* A memory area that is only addressable by the kernel through
* mapping portions into its own address space. This is for example
* used by i386 to allow the kernel to address the memory beyond
* 900MB. The kernel will set up special mappings (page
* table entries on i386) for each page that the kernel needs to
* access.
*/
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
__MAX_NR_ZONES
};
ZONE_DMA:标记适合DMA的内存域。一般为16MiB。
ZONE_DMA32:标记了使用32位地址字可寻址。适合DMA的内存域。(只在64位系统上有用,32位系统为0MiB)
ZONE_NORMAL:标记了可直接映射到内核段的普通内存域。
3 页表
层次化的页表用于支持大地址空间的快速,高效的管理。
页表管理分为两个部分,第一部分依赖于体系结构,第二部分是体系结构无关的。
1)数据结构
内存地址分解
根据四级页表结构的需要,虚拟内存地址分为5部分(4个表项用于选择页,1个索引表示页内位置)
BITS_PER_LONG
PGDIR_SHIF PUD_SHIFT PMD_SHIFT PAGE_SHIFT
-------------------------------------------------------------
PAGE_SHIFT------------
PMD_SHIFT -------------------------
PUD_SHIFT---------------------------------------
PGDIR_SHIF---------------------------------------------------
PMD_SHIFT指定了页内偏移量和最后一级页表项所需比特位的总数。
PAGE_ALIGN是一个每种体系结构都必须定义的标准宏,他需要一个地址作为参数,并将该地址舍入下一页的起始位置。如果页大小为4096,则宏总能被4096整除。
初始化内存管理
在内存管理上下文中,初始化可以有多种含义。在许多CPU上,必须显式设置适于Linux内核的内存模式,还必须建立内存管理的数据结构,以及其他很多事物。因为内核在内存管理完全初始化之前就需要使用内存,在系统启动过程期间,使用了一个额外的简化形式的内存管理模块,然后又丢弃掉。
系统启动
对相关函数的初始化是从全局启动例程start_kernel中开始的,我们只看与内存管理相关的函数。
setup_arch是一个特定于体系结构的设置函数,其中一项任务是负责初始化自举分配器。
setup_per_areas初始化源代码中定义的静态per-cpu变量,setup_per_cpu_areas的目的是为系统的各个CPU分配器创建一份这些数据的副本。
build_all_zonelists建立结点和内存域的数据结构。
mem_init,用于停用bootmem分配器并迁移到实际的内存管理函数。
kmem_cache_init初始化内核内部用于小块内存区的分配器。
setup_per_cpu_pageset从struct zone 为pageset数组的第一个数组元素分配内存。换句话说,为第一个系统处理器分配。
特定于体系结构的设置
1) 内核在内存中的布局
在讨论各个具体的内存初始化操作之前,我们需要弄清楚,在启动装载程序将内核复制到内存,而初始化例程的汇编程序部分已经执行完毕后,此时内存中的具体布局。在默认情况下,内核被装载到物理内存中的一个固定位置,该位置在编译时确定。
物理内存中前4KiB,一般会忽略,因为通常留给BIOS使用,接下来的640KiB原则上是可用的,但由于这块区域之后的区域由系统保留用于映射各种ROM(通常为系统BIOS和显卡的ROM)。不可能向映射ROM的区域写入数据。当然如果内核大小要是小于640KiB。则可以用这块区域。为了解决这些问题IA-32内核使用0x100000作为起始地址。
内核编译时候生成1个System.map并保存在源代码目录下。
/proc/iomem也提供了有关物理内存划分的各个段的一些信息。
chechunli@chechunli-desktop:~ $ cat /proc/iomem
00000000-0009efff : System RAM
0009f000-0009ffff : reserved
000a0000-000bffff : Video RAM area
000c0000-000cebff : Video ROM
000cec00-000cffff : pnp 00:0a
000f0000-000fffff : System ROM
00100000-7becffff : System RAM
00100000-00321899 : Kernel code
0032189a-0041cdc3 : Kernel data
0047f000-004f5a7f : Kernel bss
7bed0000-7bed2fff : ACPI Non-volatile Storage
7bed3000-7bedffff : ACPI Tables
7bee0000-7befffff : reserved
7c000000-7fffffff : reserved
88000000-8801ffff : 0000:00:05.0
e0000000-efffffff : 0000:00:05.0
f0000000-f3ffffff : reserved
fb000000-fbffffff : 0000:00:05.0
fc000000-fcffffff : 0000:00:05.0
fc000000-fcffffff : nvidia
fd700000-fd7fffff : PCI Bus #01
fd800000-fd8fffff : PCI Bus #01
fd900000-fd9fffff : PCI Bus #03
fda00000-fdafffff : PCI Bus #04
fdb00000-fdbfffff : PCI Bus #04
fdc00000-fdcfffff : PCI Bus #03
fdd00000-fddfffff : PCI Bus #02
fde00000-fdefffff : PCI Bus #02
fdef0000-fdefffff : 0000:02:00.0
fdef0000-fdefffff : tg3
fe024000-fe027fff : 0000:00:10.1
fe024000-fe027fff : ICH HD audio
fe02c000-fe02cfff : 0000:00:0f.0
fe02c000-fe02cfff : sata_nv
fe02d000-fe02dfff : 0000:00:0e.0
fe02d000-fe02dfff : sata_nv
fe02e000-fe02e0ff : 0000:00:0b.1
fe02e000-fe02e0ff : ehci_hcd
fe02f000-fe02ffff : 0000:00:0b.0
fe02f000-fe02ffff : ohci_hcd
fec00000-ffffffff : reserved
feff0000-feff03ff : HPET 0
2)初始化步骤
在内核已经载入内存,而初始化的汇编程序部分已经执行完毕后,内核必须执行哪些特定于系统的步骤呢?
setup_arch
|->machine_specific_memory_setup
|->parse_early_param
|->setup_memory
|->paging_init
|->pagetable_init
|->zone_sizes_init
|->add_active_range
|->free_area_init_nodes
首先调用machine_specific_memory_setup创建一个列表,包括系统占据的内存区和空闲内存区。
在系统启动时,找到的内存区由内核函数print_memory_map显示。
chechunli@chechunli-desktop:~ $ dmesg
......
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
[ 0.000000] BIOS-e820: 000000000009f000 - 00000000000a0000 (reserved)
[ 0.000000] BIOS-e820: 00000000000f0000 - 0000000000100000 (reserved)
[ 0.000000] BIOS-e820: 0000000000100000 - 000000007bed0000 (usable)
[ 0.000000] BIOS-e820: 000000007bed0000 - 000000007bed3000 (ACPI NVS)
[ 0.000000] BIOS-e820: 000000007bed3000 - 000000007bee0000 (ACPI data)
[ 0.000000] BIOS-e820: 000000007bee0000 - 000000007bf00000 (reserved)
[ 0.000000] BIOS-e820: 000000007c000000 - 0000000080000000 (reserved)
[ 0.000000] BIOS-e820: 00000000f0000000 - 00000000f4000000 (reserved)
[ 0.000000] BIOS-e820: 00000000fec00000 - 0000000100000000 (reserved)
[ 0.000000] 1086MB HIGHMEM available.
[ 0.000000] 896MB LOWMEM available.
......
内核接下来用parse_cmdline_early分析命令行,主要是关注类似mem=XXX,highmem=XXX这样的参数。
下一个步骤是setup_memory的执行,他的作用是确定每个结点可用的物理内存页的数目。初始化bootmem分配器,接下来分配各种内存区。
paging_init初始化内核页表并启用内存分页,通过调用pagetable_init,该函数确保了直接映射到内核地址空间的物理内存被初始化。低端内存中的所有页桢都直接映射到page_offset之上的虚拟内存区。这使得内核无需处理页表,即可寻址相当一部分内存。
调用zone_sizes_init会初始化系统中所有结点的pgdat_t实例。使用add_active_arange对可用的物理内存建立一个相对简单的列表。体系结构无关的函数free_area_init_nodes接下来使用该信息建立完备的内核数据结构。
3)分页机制的初始化
paging_init负责建立只能用于内核的页表,用户空间无法访问。这样作的目的是:
䒈 在用户应用程序的执行切换到核心态时,内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用。
䒈 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作。
地址空间的划分
按3:1的比例划分,内核地址空间自身又分为各个段。
high_memory PKMAP_BASE FIXADDR_START
-----------|-|---------|-|-----|-----|
0xC000000 vmalloc_start vmalloc_end
直接映射的所有物理页桢 8M VMALLOC 持久映射 固定映射
地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。
由于内核的虚拟地址空间只有1G,最多只能映射1G,IA-32最大的内存配置可以达到4G,那么如何处理剩下的内存呢,这里还要注意一点,如果物理内存超过896M,则内核无法直接映射全部物理内存。因为内核必须保留地址空间最后的128M用于以下目的。
第一,虚拟内存中连续,但是物理内存中不连续的内存区,可以在vmalloc区域分配。
第二,持久映射用于将高端内存域中的非持久页映射到内核。
第三,固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页桢可以自由选择。
在high_memory与vmalloc_start之间的8M是VMALLOC_OFFSET,这个缺口可用作针对任何内核故障的保护措施。如果访问越界地址,则访问失败并声称一个异常,报告该错误。
vmalloc从能被VMALLOC_OFFSET整除的地方开始。
vmalloc从哪里结束取决于高端内存支持,没有启动,直接接固定映射,要是启动了接持久映射,在这之间,总是会留下两页作为vmalloc区域与这两个区域之间的保护措施。
阅读(967) | 评论(0) | 转发(0) |