目录
0. 引言 1. 页表 2. 结构化内存管理 3. 物理内存的管理 4. SLAB分配器 5. 处理器高速缓存和TLB控制 6. 内存管理的概念 7. 内存覆盖与内存交换 8. 内存连续分配管理方式 9. 内存非连续分配管理方式 10. 虚拟内存的概念、特征及其实现 11. 请求分页管理方式实现虚拟内存 12. 页面置换算法 13. 页面分配策略 14. 页面抖动和工作集 15. 缺页异常的处理 16. 堆与内存管理
0. 引言
有两种类型的计算机,分别以不同的方法管理物理内存
1. UMA计算机(一致内存访问 uniform memory access) 将可用内存以连续方式组织起来,SMP系统中的每个处理器访问各个内存区都是同样快 2. NUMA计算机(非一致内存访问 non-uniform memory access) 多处理器计算机,系统的各个CPU都有本地内存,可支持高速访问,各个处理器之间通过总线连接起来,以支持对其他CPU的本地内存的访问,但是跨CPU内存访问比本地CPU内存访问要慢 1) 基于Alpha的WildFire服务器 2) IMB的NUMA-Q计算机
0x1: (N)UMA模型中的内存组织
Linux支持的各种不同体系结构在内存管理方面差别很大,由于Linux内核良好的封装、以及其中的兼容层,这些差别被很好的隐藏起来了(下层的代码对上层是透明的),两个主要的问题是
1. 页表中不同数目的间接层(向上透明的多级页表) 2. NUMA和UMA系统的划分
内核对一致(UMA)和非一致(NUMA)内存访问系统使用相同的数据结构,因此针对各种不同形式的内存布局,各个算法几乎没有差别。在UMA系统上,只使用一个NUMA节点来管理整个系统内存,而内存管理的其他部分则认为它们是在处理一个只有单节点的NUMA系统(这也是Linux内核中常见的兼容思想)
上图表明了内存划分的大致情况
1. 首先,内存划分为"结点",每个结点关联到系统中的一个处理器,在内核中表示为pg_data_t的实例,各个内存节点保存在一个单链表中,供内核遍历 2. 各个结点又划分为"内存域",是内存的进一步划分,各个内存域都关联了一个数组,用来组织属于该内存域的物理内存页(页帧),对每个页帧,都分配一个struct page实例以及所需的管理数据 1) CONFIG_ZONE_DMA 2) 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. 2.1) parisc、ia64、sparc: <4G 2.2) s390: <2G 2.3) arm: Various 2.4) alpha: Unlimited or 0-16MB. 2.5) i386、x86_64、multiple other arches: <16M. ZONE_DMA标记适合DMA的内存域,该区域的长度依赖于处理器类型,在IA-32计算机上,一般的限制是16MB,这是古老的ISA设备强加的边界,但更现代的计算机也可能受这个限制的影响 3) CONFIG_ZONE_DMA32 4) 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标记了使用32位地址字可寻址、适合DMA的内存域。显然,只有在64位系统上两种DMA内存域上才有差别,在32位计算机上,这个内存域是空的,即长度为0MB,在Alpha和AMD64系统上,该内存域的长度可能从0到4GB 5) ZONE_NORMAL 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标记了可直接映射到内核段的普通内存域,这是在所有体系结构上保证都会存在的唯一内存域,但无法保证该地址范围对应了实际的物理内存,例如 1. 如果AMD64系统有2GB内存,那么所有内存都属于ZONE_DMA32,而ZONE_NORMAL则为空 6) CONFIG_HIGHMEM 7) ZONE_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标记了超出内核段的物理内存 8) ZONE_MOVABLE 9) __MAX_NR_ZONES 1) 对可用于(ISA设备的)DMA操作的内存区是有限制的,只有前16MB可用 2) 通用的"普通"内存区 3) 高端内存区域无法直接映射
内核引入了下列常量来枚举系统中的所有内存域
\linux-2.6.32.63\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标记适合DMA的内存域,该区域的长度依赖于处理器类型,在IA-32计算机上,一般的限制是16MB,这是古老的ISA设备强加的边界,但更现代的计算机也可能受这个限制的影响 */ 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标记了使用32位地址字可寻址、适合DMA的内存域。显然,只有在64位系统上两种DMA内存域上才有差别,在32位计算机上,这个内存域是空的,即长度为0MB,在Alpha和AMD64系统上,该内存域的长度可能从0到4GB */ 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标记了可直接映射到内核段的普通内存域,这是在所有体系结构上保证都会存在的唯一内存域,但无法保证该地址范围对应了实际的物理内存,例如 1. 如果AMD64系统有2GB内存,那么所有内存都属于ZONE_DMA32,而ZONE_NORMAL则为空 */ 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标记了超出内核段的物理内存 */ ZONE_HIGHMEM, #endif //内核定义了一个伪内存域ZONE_MOVABLE,在防止物理内存碎片的机制中需要使用该内存域 ZONE_MOVABLE, //__MAX_NR_ZONES充当结束标记,在内核想要迭代系统中的所有内存区域时,会用到该常量 __MAX_NR_ZONES };
根据编译时的配置,可能无须考虑某些内存域,例如
1. 在64位系统中,并不需要高端内存域 2. 如果支持了只能访问4GB以下内存的32位外设外,才需要DMA32内存域
处于性能考虑,在为进程分配内存时,内核总是试图在当前运行的CPU相关联的NUMA结点上进行(UMA只有一个结点)。但这并不总是可行的,例如,该结点的内存可能已经用尽,对这个情况,每个节点都提供了一个备用列表(借助struct node_zonelists),该列表包含了其他结点(和相关的内存域),可用于代替当前结点分配内存,列表项的位置越靠后,就越不适合分配
0x2: 数据结构
1. 结点管理
pg_date_t用于表示结点的基本元素
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x3: struct pg_data_t
2. 结点状态管理
如果系统中结点多于一个(NUMA),内核会维护一个位图,用于提供各个结点的状态信息,状态是用位掩码指定的,可使用下列值
\linux-2.6.32.63\include\linux\nodemask.h
/* * Bitmasks that are kept for all the nodes. */ enum node_states { /* The node could become online at some point 结点在某个时刻可能变为联机 */ N_POSSIBLE, /* The node is online 结点是联机的 */ N_ONLINE, /* The node has regular memory 结点有普通内存域 */ N_NORMAL_MEMORY, #ifdef CONFIG_HIGHMEM /* The node has regular or high memory 结点有普通、或高端内存域 如果结点有普通或高端内存则使用N_HIGH_MEMORY,否则使用N_NORMAL_MEMORY */ N_HIGH_MEMORY, #else N_HIGH_MEMORY = N_NORMAL_MEMORY, #endif /* The node has one or more cpus 结点有一个、或多个CPU */ N_CPU, NR_NODE_STATES };
状态N_POSSIBLE、N_ONLINE、N_CPU用于CPU和内存的热插拔。对内存管理有必要的标志是N_HIGH_MEMORY、N_NORMAL_MEMORY
两个辅助函数用来设置或清除位域或特定结点中的一个比特位
\linux-2.6.32.63\include\linux\nodemask.h
static inline void node_set_state(int node, enum node_states state) { __node_set(node, &node_states[state]); } static inline void node_clear_state(int node, enum node_states state) { __node_clear(node, &node_states[state]); } //宏for_each_node_state用来迭代处于特定状态的所有结 #define for_each_node_state(__node, __state) \ for_each_node_mask((__node), node_states[__state])
如果内核编译为只支持单个结点(平坦内存模型),则没有结点位图,上述操作该位图的函数则变为空操作
3. 内存域
内存划分为"结点",每个结点关联到系统中的一个处理器,各个结点又划分为"内存域",是内存的进一步划分
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x4: struct zone
4. 冷热页
struct zone的pageset成员用于实现冷热页分配器(hot-n-cold allocator),在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的
尽管内存域可能属于一个特定的NUMA结点,因而关联到某个特定的CPU,但其他CPU的高速缓存仍然可以包含该内存域中的页。实际上,每个处理器都可以访问系统中所有的页,尽管速度不同。因此,特定于内存域的数据结构不仅要考虑到所属NUMA结点相关的CPU,还必须考虑到系统中其他的CPU
pageset是一个数组,其容量与系统能够容纳的CPU数目的最大值相同,并不是系统中实际存在的CPU数目
struct zone { .. struct per_cpu_pageset pageset[NR_CPUS]; .. } //NR_CPUS是一个可以在编译时配置的宏常数,在单处理器系统上其值总是1,针对SMP系统编译的内核中,其值可能在2~32/64(在64位系统上是64)之间
struct per_cpu_pageset
struct per_cpu_pageset { /* pcp[0]: 热页 pcp[1]: 冷夜 */ struct per_cpu_pages pcp; #ifdef CONFIG_NUMA s8 expire; #endif #ifdef CONFIG_SMP s8 stat_threshold; s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; #endif } ____cacheline_aligned_in_smp; struct per_cpu_pages { /* number of pages in the list 列表中页数,count记录与该列表相关的页的数目 */ int count; /* high watermark, emptying needed 页数上限水印,在需要的情况下清空列表,如果count的值超过了high,则表明列表中的页太多了,对容量过低的状态没有显式使用水印,如果列表中没有成员,则重新填充 */ int high; /* chunk size for buddy add/remove 如果可能,CPU的高速缓存不是用单个页来填充的,而是用多个页组成的块,batch添加/删除多页块的时候,块的大小(即页数)的参考值 */ int batch; /* Lists of pages, one per migrate type stored on the pcp-lists 页的链表 lists是一个双链表,保存了当前CPU的冷页或热页,可使用内核的标准方法处理 */ struct list_head lists[MIGRATE_PCPTYPES]; };
下图说明了在双处理器系统上per-CPU缓存的数据结构是如何填充的
5. 页帧
页帧代表系统内存的最小单位,对内存中的每个页都会创建struct page的一个实例,内核需要保证该结构尽可能小,否则可能会出现"内存描述元信息占用了大量内存"的情况,在典型的系统中,由于页的数目巨大,因此对struct page结构的小的改动,也可能导致保存所有page实例所需的物理内存暴涨
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x5: struct page
在安全攻防产品的研发中,我们会大量用到cache的机制。在学习和使用cache缓存的时候,经常会遇到cache的更新和替换的问题,如何有效对cache进行清理、替换,同时要保证cache在清理后还要保持较高的命中率。通过对比我们发现,操作系统的内存管理调度策略和cache的动态更新策略本质是类似的,通过学习操作系统的内存管理策略,我们可以得到很多关于cache更新的策略思想
Relevant Link:
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=27&ved=0CDwQFjAGOBQ&url=%68%74%74%70%3a%2f%2f%6f%61%2e%70%61%70%65%72%2e%65%64%75%2e%63%6e%2f%66%69%6c%65%2e%6a%73%70%3f%75%72%6c%74%69%74%6c%65%3d%25%45%36%25%39%36%25%38%37%25%45%34%25%42%42%25%42%36%43%61%63%68%65%25%45%38%25%38%37%25%41%41%25%45%39%25%38%30%25%38%32%25%45%35%25%42%41%25%39%34%25%45%37%25%41%44%25%39%36%25%45%37%25%39%35%25%41%35%25%45%37%25%41%30%25%39%34%25%45%37%25%41%39%25%42%36&ei=veo1VN_RJZfj8AWBnoCACQ&usg=AFQjCNHVjRFlRvV-0O1tYyb4Inv33Pop4A&bvm=bv.76943099,d.dGc&cad=rjt http://www.cnblogs.com/hanyan225/archive/2011/07/28/2119628.html
1. 页表
页表寻址和传统的(DOS时代)的线性寻址的好处在于
1. 页表用于建立用户进程的虚拟地址空间和系统物理内存(页帧)之间的关联 2. 页表用于向每个进程提供一致的虚拟地址空间,应用程序看到的地址空间是一个连续的内存区 3. 页表也将虚拟内存页映射到物理内存,因而支持共享内存的实现(同一个物理页同时映射到不同进程的虚拟地址空间) 4. 层次化的页表用于支持对大地址空间的快速、高效的管理 5. 可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间,即将进程中某些不常用的虚拟内存进行"解关联",将对应的页表映射删除,从而释放出这部分物理内存,让其他进程可以用于映射
内核内存管理总是"假定"使用四级页表,而不管底层处理器是否如此,在IA-32系统中,该体系结构只使用两级分页系统(在不使用PAE扩展的情况下),因此,第三、第四级页表必须由特定于体系结构的代码模拟,页表管理分为两个部分
1. 第一部分依赖于体系结构: 所有数据结构和操作数据结构的函数都是定义在特定于体系结构的文件中 2. 第二部分是体系结构无关的 //需要注意的一点是,在Linux内核中,内存管理和体系结构的关联很密切
0x1: 数据结构
1. 内存地址的分解
根据四级页表结构的需要,虚拟内存地址分为5个部分(4个表项用于选择页、1个索引表示页内位置)。各个体系结构不仅地址字长度不同,而且地址字拆分的方式也不同,因此内核定义了宏,用于将地址分解为各个分量
BITS_PER_LONG定义用于unsigned long变量的比特位数目,因此也适用于用于指向虚拟地址空间的通用指针,需要明白的是,在不同的体系结构下,这个"BITS_PER_LONG"长度是不同的,以及用于各级页表的地址分隔长度也是不同的
1. PGD 2. PUD: PGDIR_SHIFT由PUD_SHIFT加上上层页表索引所需的比特位长度,对全局页目录中的一项所能寻址的的部分地址空间长度: 2(PGDIR_SHIFT)次方 3. PMD: PUD_SHIFT由PMD_SHIFT加上中间层页表索引所需的比特位长度 4. PTE: PMD_SHIFT指定了业内偏移量和最后一项页表项所需比特位的总数,该值减去PAGE_SHIFT,可得最后一项页表项索引所需比特位的数目。同时PMD_SHIFT表明了一个中间层页表项管理的部分地址空间的大小: 2(PMD_SHIFT)次方字节 5. Offset: 每个指针末端的几个比特位,用于指定所选页帧内部的位置,比特位的具体数目由PAGE_SHIFT指定(通过位移+掩码的形式来进行分段)
在各级页目录/页表中所能存储的指针数目,也可以通过宏定义确定
1. PTRS_PER_PGD: 指定了全局页目录中项的数目 2. PTRS_PER_PUD: 对应于上层页目录中项的数目 3. PTRS_PER_PMD: 对应于中间页目录 4. PTRS_PER_PTE: 页表中项的数目 /* 我们知道,Linux的四级页表实现是向下兼容的,即在两级页表的体系结构中,会将PTRS_PER_PMD、PTRS_PER_PTE定义为1,这使得内核的剩余部分感觉该体系结构也提供了四级页表转换结构 */
值2(N)次方的计算很容易通过从位置0左移n位计算而得到,同时Linux的内核的内存页管理的基本单位都是以2为底
2. 页表的格式
内核提供了4中数据结构,用来表示页表项的结构
\linux-2.6.32.63\include\asm-generic\page.h
/* These are used to make use of C type-checking.. */ 1. pgd_t: 全局页目录项 typedef struct { unsigned long pgd; } pgd_t; 3 pmd_t: 中间页目录项 typedef struct { unsigned long pmd[16]; } pmd_t; 4. pte_t: 直接页表项 typedef struct { unsigned long pte; } pte_t; pgprot_t: typedef struct { unsigned long pgprot; } pgprot_t; typedef struct page *pgtable_t;
内核同时还提供了用于分析页表项的标准函数,根据不同的体系结构,一些函数可能实现为宏而另一些则实现为内联函数
... #define pgd_val(x) ((x).pgd) //将pte_t等类型的变量转换为unsigned long #define pmd_val(x) ((&x)->pmd[0]) #define pte_val(x) ((x).pte) #define pgprot_val(x) ((x).pgprot) #define __pgd(x) ((pgd_t) { (x) } ) //pgd_val等函数的逆,将unsigned long整数转换为pgd_t等类型的变量 #define __pmd(x) ((pmd_t) { (x) } ) #define __pte(x) ((pte_t) { (x) } ) #define __pgprot(x) ((pgprot_t) { (x) } ) ...
PAGE_ALIGN是每种体系结构都必须定义的标准宏,它需要一个地址作为参?