目录html
0. 引言 1. 页表 2. 结构化内存管理 3. 物理内存的管理 4. SLAB分配器 5. 处理器高速缓存和TLB控制 6. 内存管理的概念 7. 内存覆盖与内存交换 8. 内存连续分配管理方式 9. 内存非连续分配管理方式 10. 虚拟内存的概念、特征及其实现 11. 请求分页管理方式实现虚拟内存 12. 页面置换算法 13. 页面分配策略 14. 页面抖动和工做集 15. 缺页异常的处理 16. 堆与内存管理
0. 引言前端
有两种类型的计算机,分别以不一样的方法管理物理内存node
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支持的各类不一样体系结构在内存管理方面差异很大,因为Linux内核良好的封装、以及其中的兼容层,这些差异被很好的隐藏起来了(下层的代码对上层是透明的),两个主要的问题是ios
1. 页表中不一样数目的间接层(向上透明的多级页表) 2. NUMA和UMA系统的划分
内核对一致(UMA)和非一致(NUMA)内存访问系统使用相同的数据结构,所以针对各类不一样形式的内存布局,各个算法几乎没有差异。在UMA系统上,只使用一个NUMA节点来管理整个系统内存,而内存管理的其余部分则认为它们是在处理一个只有单节点的NUMA系统(这也是Linux内核中常见的兼容思想)程序员
上图代表了内存划分的大体状况web
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、ia6四、sparc: <4G 2.2) s390: <2G 2.3) arm: Various 2.4) alpha: Unlimited or 0-16MB. 2.5) i38六、x86_6四、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扩展的状况下),所以,第3、第四级页表必须由特定于体系结构的代码模拟,页表管理分为两个部分
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是每种体系结构都必须定义的标准宏,它须要一个地址做为参数,并将地址"舍入"到下一页的起始处,即老是返回页的整倍数。为了用好处理器的高速缓存资源,将地址对齐到页边界是很重要的
从某种程度上来讲,四级页表的索引是体现(基于)对内存地址的分段索引之上的,即将一个内存地址切成几段,每一段分别表明不一样层次的索引信息
3. 特定于PTE的信息
最后一级页表中的项不只包含了指向页的内存位置的指针,还在多余比特位包含了与页有关的附加信息,尽管这些数据是特定于CPU的,可是它们提供了有关"页访问控制"的一些信息
1. _PAGE_PRESENT 指定了虚拟内存页是否存在于内存中,由于页可能被换出到交换区 1) 若是页不在内存中,那么页表项的结构一般会有所不一样,由于不须要描述页在内存中的位置 2) 若是页存在于内存中,就须要信息来标识并找到换出的页 2. _PAGE_ACCESSED CPU每次访问页时,会自动设置_PAGE_ACCESSED,内核会按期检查该比特位,以确认页使用的活跃程序(不常用的页,比较适合换出)。在读或写访问以后会设置该比特位 3. _PAGE_DIRTY 表示该页是不是"脏的",即页的内容是否已经被修改过 4. _PAGE_FILE 数值和_PAGE_DIRTY相同,但用于不一样的上下文,即页不在内存中的时候,显然不存在的页不多是脏的(由于它不可能被进程修改),所以能够从新解释该比特位 1) 若是没有设置,则该项指向一个换出页的位置 2) 若是该项属于非线性文件映射,则须要设置_PAGE_FILE 5. _PAGE_USER 若是设置了_PAGE_USER,则容许用户空间代码访问该页,不然只有内核才能访问(或CPU处于系统状态的时候) 6. _PAGE_READ、_PAGE_WRITE、_PAGE_EXECUTE 指定了普通的用户进程是否容许读取、写入、执行该页中的机器代码 //内核内存中的页必须防止用户进程写入,对于访问权限粒度不是很是细的体系结构而言,若是没有进一步的准则能够区分读写访问权限,则会定义_PAGE_RW常数,用于同时容许或禁止读写访问 7. _PAGE_BIT_NX IA-32、AMD64提供了_PAGE_BIT_NX,用于将页标记为"不可执行"的(在IA-32系统上,只有启用了"可寻址64GB内存的页面地址扩展(page address extension PAE)"时,才能使用该保护位)。它能够有效防止执行栈页上的代码,不然,恶意代码可能经过缓冲区溢出手段在栈上执行代码,致使程序的安全漏洞 8. __pgprot、pte_modidy() 每一个体系结构都必须提供两个东西,使得内存管理子系统可以修改pte_t项中额外的比特位 1) 保存额外的比特位的__pgprot数据结构 2) 以及修改这些比特位的pte_modidy()函数
内核还定义了各类函数,用于查询和设置内存页与体系结构相关的状态,某些处理器可能缺乏对一些给定特性的硬件支持,所以并不是全部的处理器都定义了全部函数
\linux-2.6.32.63\arch\x86\include\asm\pgtable.h
1. pte_present: 检查页表项指向的页是否存在于内存中,该函数能够用于检测一页是否已经换出 2. pte_read: 从用户空间是否能够读取该页 3. pte_write: 检查内核是否能够写入到该页 4. pte_exec: 检查该页中的数据是否能够做为二进制代码执行 5. pte_dirty: 检查与页表项相关的页是不是脏的,即其内容在上次内核检查以后是否以已经修改过,须要注意的是,只有在pte_present确认了该页可用的状况下(即存在于内存中),才能调用该函数 6. pte_file: 用于非线性映射,经过操做页表提供了文件内容的一种不一样视图,该函数检查页表项是否属于这样的一个映射,要注意的是,只有在pte_present返回false时,才能调用pte_file,即与该页表项相关的页再也不内存中 7. pte_young: 访问位(一般是_PAGE_ACCESSED)是否设置 8. pte_rdprotect: 清除该页的读权限 9. pte_wrprotect: 清除该页的写权限 10. pte_exprotect: 清除执行该页中的二进制数据的权限 11. pte_mkread: 设置读权限 12. pte_mkwrite: 设置写权限 13. pte_mkexec: 容许执行页的内容 14. pte_mkdirty: 将页标记为脏 15. pte_mkclean: "清除"页,一般指清除_PAGE_DIRTY位 16. pte_mkyoung: 设置访问位,在大多数体系结构上是_PAGE_ACCESSED 17. pte_mkold: 清除访问位 //这些函数常常分为3组,分别用于设置、删除、查询某个特定的属性
0x2: 页表项的建立和操做
下列为用于建立新页表项的全部函数
\linux-2.6.32.63\arch\x86\include\asm\pgtable.h
1. mk_pte #define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot)) 建立一个页表项,必须将page实例和所需的页访问权限(__pgprot)做为参数传递 2. pte_page #define pte_page(pte) pfn_to_page(pte_pfn(pte)) 得到页表项描述的页对应的page实例地址 3. pte_alloc 4. pte_free 5. set_pte 6. pgd_alloc \linux-2.6.32.63\arch\x86\mm\pgtable.c 分配并初始化一个可容纳完整页表的内存 7. pgd_free: 释放页表占据的内存 8. set_pgd: 设置页表中某项的值 9. pud_alloc 10. pud_free 11. set_pud 12. pmd_alloc 13. pmd_free 14. set_pmd
2. 结构化内存管理
在内存管理的上下文中,初始化(initiation)能够有多种含义,在许多CPU上,必须显示设置适合Linux内核的内存模型(例如在IA-32系统上须要切换到保护模式,而后内核才能检测可用内存和寄存器)。在初始化过程当中,还必须创建内存管理的数据结构,以及其余不少事务。由于内核在内存管理彻底初始化以前就须要使用内存,在系统启动过程期间,使用了一个额外的简化形式的内存管理模块,而后又丢弃掉
0x1: 创建数据结构
对相关数据结构的初始化是从全局启动例程start_kernel中开始的,该例程在加载内核并激活各个子系统以后执行,因为内存管理是内核一个很是重要的部分,所以在特定于体系结构的设置步骤中检测内存并肯定系统中内存的分配状况后,会当即执行内存管理的初始化。此时,已经对各类系统内存模式生成了一个pgdat_t实例,用于保存诸如"结点中内存数量"以及内存在各个"内存域"之间分配状况的信息。全部平台都实现了特定于体系结构的NODE_DATA宏,用于经过结点编号查询与一个NUMA结点相关的pgdat_t实例
1. 先决条件
因为大部分系统都只有一个"内存结点",为了确保内存管理代码是可移植的(一样能够适用于UMA和NUMA系统),内核在"\linux-2.6.32.63\mm\page_alloc.c"中定义了一个pg_data_t实例管理全部的物系统内存,这不是特定于CPU的实现,大多数体系结构都采用了该方案
linux-2.6.32.63\arch\x86\include\asm\mmzone_64.h #define NODE_DATA(nid) (node_data[nid]) \linux-2.6.32.63\arch\m32r\mm\discontig.c struct pglist_data *node_data[MAX_NUMNODES]; EXPORT_SYMBOL(node_data);
尽管该宏有一个形式参数用于选择NUMA结点,但在UMA系统中只有一个伪结点,所以老是返回一样的数据。内核也能够依赖于下述事实: 体系结构相关的初始化代码将"MAX_NUMNODES"设置为系统中结点的数目,在UMA系统上由于只有一个(形式上的)结点,所以该值老是1,在编译期间,预处理器会为特定的配置选择正确的值
2. 系统启动
下图给出的start_kernel的代码流程图,其中包括了与内存管理相关的系统初始化函数
1. setup_arch 特定于体系结构的设置函数,其中一项任务是负责初始化自举分配器 2. setup_per_cpu_areas 1) 在SMP系统上,setup_per_cpu_areas初始化源代码中定义的静态per-cpu变量,这种变量对系统中的每一个CPU都有一个独立的副本(这类变量保存在内核二进制映像的一个独立的段中),setup_per_cpu_areas的目的是为系统的各个CPU分别建立一份这些数据的副本 2) 在非SMP系统上,该函数是一个空操做 3. build_all_zonelists 创建"结点"和"内存域"的数据结构 4. mem_init 另外一个特定于体系结构的函数,用于停用bootmem分配器并迁移到实际的内存管理函数 5. setup_per_cpu_pageset 为"strcut zone->struct per_cpu_pageset pageset[NR_CPUS]"数组的第一个数组元素分配内存。分配第一个数组元素,即意味着为第一个系统处理器分配,系统的全部内存域都会考虑进来 该函数还负责设置冷热分配器的限制 //SMP系统上对应于其余CPU的pageset数组成员,将会在相应的CPU激活时初始化
3. 结点和内存域初始化
build_all_zonelists创建管理结点及其内存域所需的数据结构,该函数能够经过内核的宏和抽象机制实现,而不用考虑具体的NUMA或UMA系统,由于执行的函数实际上有两种形式,一种用于NUMA系统,另外一种用于UMA系统。Linux内核常常采用这种技术
#ifdef CONFIG_WORK_HARD void do_work() { //start } #else void do_work() { //stop } #endif
\linux-2.6.32.63\mm\page_alloc.c
void build_all_zonelists(void) { /* 在当前处理的结点和系统中其余结点的内存域之间创建一种等级次序,接下来,依据这种次序分配内存 考虑下面的例子 内核想要分配高端内存,它首先试图在当前结点的高端内存域找到一个大小适当的空闲段,若是失败,则查看该节点的普通域,若是还失败,则试图在该结点的DMA内存域执行分配 若是在3个本地内存域都没法找到空闲内存,则查看其余结点,在这种状况下,备选结点应该尽量靠近主结点,以最小化因为访问非本地内存引发的性能损失 内核将内存域分为层次结构,首先试图分配"廉价的"内存,若是失败,则根据访问速度和容量,逐渐尝试分配"更昂贵的"内存 1. 高端内存是最"廉价的",由于内核没有部分依赖于从该内存域分配的内存,若是高端内存域用尽,对内核没有任何反作用,这也是优先分配高端内存的缘由 2. 普通内存域的状况有所不一样,许多内核数据结构必须保存在该内存域,而不能放置到高端内存域。所以若是普通内存彻底用尽,那么内核会面临紧急状况,因此只要高端内存域的内存域没有用尽,都不会从普通内存域分配内存 3. 最昂贵的是DMA内存域,由于它用于外设和系统之间的数据传输,所以从该内存域分配内存是最后一招(通常状况下不会从DMA中分配内存) */ set_zonelist_order(); if (system_state == SYSTEM_BOOTING) { __build_all_zonelists(NULL); mminit_verify_zonelist(); cpuset_init_current_mems_allowed(); } else { /* we have to stop all cpus to guarantee there is no user of zonelist */ stop_machine(__build_all_zonelists, NULL, NULL); /* cpuset refresh routine should be here */ } vm_total_pages = nr_free_pagecache_pages(); /* * Disable grouping by mobility if the number of pages in the * system is too low to allow the mechanism to work. It would be * more accurate, but expensive to check per-zone. This check is * made on memory-hotadd so a system can start with mobility * disabled and enable it later */ if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES)) page_group_by_mobility_disabled = 1; else page_group_by_mobility_disabled = 0; printk("Built %i zonelists in %s order, mobility grouping %s. " "Total pages: %ld\n", nr_online_nodes, zonelist_order_name[current_zonelist_order], page_group_by_mobility_disabled ? "off" : "on", vm_total_pages); #ifdef CONFIG_NUMA printk("Policy zone: %s\n", zone_names[policy_zone]); #endif } //将全部工做都委托给__build_all_zonelists /* non-NUMA variant of zonelist performance cache - just NULL zlcache_ptr */ static void build_zonelist_cache(pg_data_t *pgdat) { pgdat->node_zonelists[0].zlcache_ptr = NULL; } #endif /* CONFIG_NUMA */ /* return values int ....just for stop_machine() */ static int __build_all_zonelists(void *dummy) { int nid; #ifdef CONFIG_NUMA memset(node_load, 0, sizeof(node_load)); #endif //对系统中的各个NUMA结点分别调用build_zonelists for_each_online_node(nid) {
//pg_data_t *pgdat包含告终点内存配置的全部现存信息,且新建的数据机构也会放置在其中 pg_data_t *pgdat = NODE_DATA(nid); build_zonelists(pgdat); build_zonelist_cache(pgdat); } return 0; }
build_zonelists(pgdat);
static void build_zonelists(pg_data_t *pgdat) { int node, local_node; enum zone_type j; struct zonelist *zonelist; local_node = pgdat->node_id; zonelist = &pgdat->node_zonelists[0]; j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1); /* * Now we build the zonelist so that it contains the zones * of all the other nodes. * We don't want to pressure a particular node, so when * building the zones for node N, we make sure that the * zones coming right after the local ones are those from * node N+1 (modulo N) */ //迭代全部的结点内存域,每一个循环在node_zonelist数组中找到第i个zonelist,对第i个内存域计算备用列表 for (node = local_node + 1; node < MAX_NUMNODES; node++) { if (!node_online(node)) continue; //实际工做委托给build_zonelists_node j = build_zonelists_node(NODE_DATA(node), zonelist, j, MAX_NR_ZONES - 1); } for (node = 0; node < local_node; node++) { if (!node_online(node)) continue; j = build_zonelists_node(NODE_DATA(node), zonelist, j, MAX_NR_ZONES - 1); } zonelist->_zonerefs[j].zone = NULL; zonelist->_zonerefs[j].zone_idx = 0; }
build_zonelists_node
/* * Builds allocation fallback zone lists. * * Add all populated zones of a node to the zonelist. */ /* 备用列表的各项是根据zone_type参数排序的,该参数指定了最优选择哪一个内存域,该参数的初始值是外层循环的控制变量i,咱们知道其值多是 1. ZONE_HIGHMEM 2. ZONE_NORMAL 3. ZONE_DMA / ZONE_DMA32 nr_zones表示从备用列表中的哪一个位置开始填充新项,因为列表中尚没有项,所以调用者传递了0 */ static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_zones, enum zone_type zone_type) { struct zone *zone; BUG_ON(zone_type >= MAX_NR_ZONES); zone_type++; do { //在每一步结束时,都将内存域类型减一,即设置为一个更"昂贵"的内存域类型(例如从ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA) zone_type--; zone = pgdat->node_zones + zone_type; if (populated_zone(zone)) { zoneref_set_zone(zone, &zonelist->_zonerefs[nr_zones++]); check_highest_zone(zone_type); } } while (zone_type); return nr_zones; }
0x2: 特定于体系结构的设置
在IA-32系统上内存管理的初始化在一些细节方面很是微秒,其中必须克服一些与处理器体系结构相关的问题,例如
1. 将处理器从普通模式切换到保护模式 2. 授予CPU访问32位地址空间的权限 3. 兼容16位8086处理器 4. 分页在默认状况下没有启用,必须手动激活,这涉及处理器的CR0寄存器
1. 内核在内存中的布局
在学习各个具体的内存初始化操做以前,咱们先来学习启动装载程序将内核复制到内存,而初始化例程的汇编程序部分也已经执行完毕后,此时内存中的具体布局,内核被装载到物理内存中的一个固定位置,该位置在编译时肯定,配置选项PHYSICAL_START用于肯定内核在内存中的位置,会受到配置选项PHYSICAL_ALIGN设置的物理对齐方式的影响
1. 0 ~ 0x1000(4KB): 第一个页帧,通常会忽略,觉得一般保留给BIOS使用 2. 0x1000 ~ 0x9c80(640KB): 原则上是可用的,但也不用于内核加载,缘由是该区域以后紧邻的区域由系统保留,用于映射各类ROM(一般是系统BIOS和显卡ROM),不可能向映射ROM的区域写入数据。但内核老是会装载到一个连续的内存区中,若是要从4KB处做为起始位置来装载内核映像,则要求内核必须小于640KB 3. 0x9c80 ~ 0x100000: 可用内存区域 4. 0x100000 ~ end: 为了解决内核加载的问题,IA-32内核使用0x100000做为起始地址,今后处开始,有足够的连续内存区,可容纳整个内核,内核占据的内存分为几个段,其边界保存在变量中 1) _text ~ _etext: 内核代码段的起始和结束地址,包含了编译后的内核代码 2) _etext ~ _edata: 数据段,保存了大部份内核变量 3) _edata ~ _end: 初始化数据在内核启动过程结束后再也不须要(例如,包含初始化为0的全部静态全局变量的BSS段),保存在最后一段,在内核初始化完成后,其中的大部分数据均可以从内存删除,给应用程序留出更多空间 //准确的数值依内核配置而异,由于每种配置的代码段和数据段长度都不相同,这取决于启动和禁用了内核的哪些部分,只有起始地址(_text)是相同的
每次编译内核时,都生成一个文件System.map并保存在源码目录下,其中包括了
1. 全局变量 2. 内核定义的导出函数 3. 例程函数地址 4. 内存分段常数的值 1) _text 2) _etext 3) _edata 4) _end // /proc/iomem也提供了有关物理内存划分的各个段的一些信息
2. 初始化步骤
在内核已经载入内存、而初始化的汇编程序部分已经执行完毕后,内核必须执行一些特定于系统的步骤
1. machine_specific_memory_setup 建立一个列表,包括系统占据的内存区和空闲内存区,BIOS提供的映射给出了在这种状况下使用的各个内存区,在系统启动时,找到的内存区由内核函数print_memory_map显示 dmesg BIOS-provided physical RAM map: BIOS-e820: 0000000000000000 - 000000000009ec00 (usable) BIOS-e820: 000000000009ec00 - 00000000000a0000 (reserved) BIOS-e820: 00000000000dc000 - 0000000000100000 (reserved) BIOS-e820: 0000000000100000 - 000000007fee0000 (usable) BIOS-e820: 000000007fee0000 - 000000007feff000 (ACPI data) BIOS-e820: 000000007feff000 - 000000007ff00000 (ACPI NVS) BIOS-e820: 000000007ff00000 - 0000000080000000 (usable) BIOS-e820: 00000000f0000000 - 00000000f8000000 (reserved) BIOS-e820: 00000000fec00000 - 00000000fec10000 (reserved) BIOS-e820: 00000000fee00000 - 00000000fee01000 (reserved) BIOS-e820: 00000000fffe0000 - 0000000100000000 (reserved) 2. parse_cmdline_early 分析命令行,从本质上来说,内核也是一个进程,须要被连接进系统中,载入内核映像的时候也有参数传入,例如 1) mem=XXX[KkmM] 2) highmem=XXX[KkmM] 3) memmap=XXX[KkmM] 若是内核计算的值或BIOS提供的值不正确,管理员能够修改可用内存的数量或手工划定内存区 3. setup_memory 1) 肯定(每一个结点)可用的物理内存页的数目 2) 初始化bootmem分配器 3) 分配各类内存区,例如运行第一个用户空间过程所需的最初的RAM磁盘 4. paging_init 初始化内核页表并启用内存分页,由于IA-32计算机上默认状况下分页是禁用的,若是内核编译了PAE支持,并且处理器也支持Execute Disabled Protection,则启用该特性 1) pagetable_init: 该函数确保了直接映射到内核地址空间的物理内存被初始化,低端内存中的全部页帧都直接映射到PAGE_OFFSET之上的虚拟内存区,这使得内核无需处理页表,便可寻址至关一部分可用内存 5. zone_sizes_init 初始化系统中全部结点pgdat_t实例 1) add_active_range: 对可用的物理内存创建一个相对简单的列表 2) free_area_init_nodes: 这是体系结构无关的函数,创建完备的内核数据结构
3. 分页机制的初始化
咱们知道,paging_init负责创建只能用于内核的页表,用于空间没法访问,这对管理用户态应用程序和内核访问内存的方式有深远的影响
\linux-2.6.32.63\arch\x86\mm\init_32.c
/* paging_init() sets up the page tables - note that the first 8MB are already mapped by head.S. This routines also unmaps the page at virtual kernel address 0, so that we can trap those pesky NULL-reference errors in the kernel. */ void __init paging_init(void) { //初始化系统的页表 pagetable_init(); //在pagetable_init完成页表初始化以后,则将CR3寄存器设置为指向全局页目录(swapper_pg_dir)的指针,此时必须激活新的页表 //因为TLB缓存项仍然包含了启动时分配的一些内存地址数据,此时也必须刷出,与上下文切换期间相反,设置了_PAGE_GLOBAL位的页也要刷出 __flush_tlb_all(); //kmap_init初始化全局变量kmap_pte,在从高端内存内存域将页映射到内核地址空间时,会使用该变量存入相应内存区的页表项,此外,用于高端内存内核映射的第一个固定映射内存区的地址保存在全局变量kmem_vstart中 kmap_init(); /* * NOTE: at this point the bootmem allocator is fully available. */ sparse_init(); zone_sizes_init(); } static void __init pagetable_init(void) { //以swapper_pg_dir为基础初始化系统的页表 pgd_t *pgd_base = swapper_pg_dir; //创建固定映射和持久内核,用适当的值填充页表 permanent_kmaps_init(pgd_base); }
咱们以前学习过per-CPU(冷热)缓存,咱们接下来学习内核如何处理相关数据结构的初始化,以及用于控制缓存填充行为的"水印"的计算
zone_pcp_init负责初始化该缓存,该函数由free_area_init_nodes
\linux-2.6.32.63\mm\page_alloc.c
static __meminit void zone_pcp_init(struct zone *zone) { int cpu; //用zone_batchsize算出批量大小(用于计算最小和最大填充水平的基础)后,代码将遍历系统中的全部CPU unsigned long batch = zone_batchsize(zone); for (cpu = 0; cpu < NR_CPUS; cpu++) { #ifdef CONFIG_NUMA /* Early boot. Slab allocator not functional yet */ zone_pcp(zone, cpu) = &boot_pageset[cpu]; setup_pageset(&boot_pageset[cpu],0); #else //调用setup_pageset填充每一个per_cpu_pageset实例的常量,使用了zone_pcp宏来选择与当前CPU相关的内存域的pageset实例 setup_pageset(zone_pcp(zone,cpu), batch); #endif } if (zone->present_pages) printk(KERN_DEBUG " %s zone: %lu pages, LIFO batch:%lu\n", zone->name, zone->present_pages, batch); } static int zone_batchsize(struct zone *zone) { #ifdef CONFIG_MMU int batch; /* * The per-cpu-pages pools are set to around 1000th of the * size of the zone. But no more than 1/2 of a meg. * * OK, so we don't know how big the cache is. So guess. */ batch = zone->present_pages / 1024; if (batch * PAGE_SIZE > 512 * 1024) batch = (512 * 1024) / PAGE_SIZE; batch /= 4; /* We effectively *= 4 below */ if (batch < 1) batch = 1; /* * Clamp the batch to a 2^n - 1 value. Having a power * of 2 value was found to be more likely to have * suboptimal cache aliasing properties in some cases. * * For example if 2 tasks are alternately allocating * batches of pages, one task can end up with a lot * of pages of one half of the possible page colors * and the other with pages of the other colors. */ batch = rounddown_pow_of_two(batch + batch/2) - 1; return batch; #else /* The deferral and batching of frees should be suppressed under NOMMU * conditions. * * The problem is that NOMMU needs to be able to allocate large chunks * of contiguous memory as there's no hardware page translation to * assemble apparent contiguous memory from discontiguous pages. * * Queueing large contiguous runs of pages for batching, however, * causes the pages to actually be freed in smaller chunks. As there * can be a significant delay between the individual batches being * recycled, this leads to the once large chunks of space being * fragmented and becoming unavailable for high-order allocations. */ return 0; #endif }
上述代码计算获得的batch,大约至关于内存域中页数的0.25%,根据经验,缓存大小是主内存的千分之一,考虑到当前系统每一个CPU配备的物理内存大约在1GB ~ 2GB,该规则是有意义的,这样,计算获得的批量大小使得冷热页缓存中的页有可能放置到CPU的L2缓存中
在zone_pcp_init结束时,会输出各个内存域的页数以及计算出的批量大小
dmesg | grep LIFO DMA zone: 56 pages used for memmap DMA zone: 101 pages reserved DMA zone: 3825 pages, LIFO batch:0 DMA32 zone: 7112 pages used for memmap DMA32 zone: 513048 pages, LIFO batch:31
4. 注册活动内存区
活动内存区就是不包含空洞的内存区,必须使用add_active_range在全局变量early_node_map中注册内存区
5. AMD64地址空间的设置
AMD64系统地址空间的设置在某些方面比IA-32要容易,但在另外一些方面要困难,虽然64位地址空间避免了古怪的高端内存区域,但有另外一个因素使状况复杂化,即64位地址空间跨度太大,基本没有应用程序须要这个
0x3: 启动过程期间的内存管理
在内核加载启动过程当中,尽管内存管理还没有初始化,但内核仍然须要分配内存以建立各类数据结构,bootmem分配器用于在启动阶段早期分配内存。显然,对该分配器的需求集中在简单性方面,而不是性能和通用性,所以Linux内核实现了一个"最早适配(first-fit)"分配器用于在启动阶段管理内存
1. bootmem使用一个"位图(bitmap)"来管理页,位图比特位的数目与系统中物理内存页的数目相同 1) 比特位为1: 表示已经使用该页 2) 比特位为0: 表示空闲页 2. 在须要分配内存时,分配器逐位扫描位图,直至找到一个可以提供足够"连续页"的位置,即所谓的最早最佳(first-best)或最早适配位置 /* 该过程不是很高效(缘由在于每次都须要遍历操做),由于每次分配都必须从头扫描比特链,所以在内核彻底初始化以后,不能将该分配器用于内存管理。伙伴系统(连同slab、slub、slob分配器)是一个更好的技术方案 */
1. 数据结构
最早适配分配器也必须管理一些数据,内核为系统中每一个结点都提供了一个bootmem_data结构的实例,该结构所需的内存没法动态分配,必须在编译时分配给内核。在UMA系统上该分配的实现与CPU无关(NUMA系统采用了特定于体系结构的解决方案)
\linux-2.6.32.63\include\linux\bootmem.h
/* node_bootmem_map is a map pointer - the bits represent all physical memory pages (including holes) on the node. */ typedef struct bootmem_data { //node_min_pfn保存了系统中第一个页的编号,大多数体系结构下都是零 unsigned long node_min_pfn; //node_low_pfn是能够直接管理的物理地址空间中最后一页的编号(即ZONE_NORMAL的结束页) unsigned long node_low_pfn; //node_bootmem_map是指向存储分配位图的指针,在IA-32系统上,该内存区(位图)紧接在内核映像以后,对应的地址保存在_end变量中,该变量在连接期间自动地插入到内核映像中 void *node_bootmem_map; unsigned long last_end_off; unsigned long hint_idx; /* 内存不连续的系统可能须要多个bootmem分配器,一个典型的例子是NUMA计算机,其中每一个结点注册了一个bootmem分配器,但若是物理地址空间中散布者空洞,也能够为每一个连续内存区注册一个bootmem分配器 注册新的自举分配器(bootmem)能够使用init_bootmem_core,全部注册的分配器保存在一个链表中,表头是全局变量bdata_list */ struct list_head list; } bootmem_data_t; extern bootmem_data_t bootmem_node_data[];
2. 初始化
bootmem分配器的初始化是一个特定于体系结构的过程,同时还取决于计算机的内存布局,在IA-32使用setup_memory实现
setup_memory在setup_arch中被调用
/source/arch/m32r/kernel/setup.c
#ifndef CONFIG_DISCONTIGMEM static unsigned long __init setup_memory(void) { unsigned long start_pfn, max_low_pfn, bootmap_size; //setup_memory分析检测到的内存区,以找到低端内存区中最大的页帧号,由于高端内存处理太过麻烦,所以对bootmem分配器无用 start_pfn = PFN_UP( __pa(_end) ); //全局变量max_low_pfn保存了可映射的最高页的编号,内核会在启动日志中报告找到的内存数量 max_low_pfn = PFN_DOWN( __pa(memory_end) ); /* * Initialize the boot-time allocator (with low memory only): */ bootmap_size = init_bootmem_node(NODE_DATA(0), start_pfn, CONFIG_MEMORY_START>>PAGE_SHIFT, max_low_pfn); /* * Register fully available low RAM pages with the bootmem allocator. */ { unsigned long curr_pfn; unsigned long last_pfn; unsigned long pages; /* * We are rounding up the start address of usable memory: */ curr_pfn = PFN_UP(__pa(memory_start)); /* * ... and at the end of the usable range downwards: */ last_pfn = PFN_DOWN(__pa(memory_end)); if (last_pfn > max_low_pfn) last_pfn = max_low_pfn; pages = last_pfn - curr_pfn; free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(pages)); } /* Reserve the kernel text and Reserve the bootmem bitmap. We do this in two steps first step was init_bootmem()), because this catches the (definitely buggy) case of us accidentally initializing the bootmem allocator with an invalid RAM area. 因为bootmem分配器须要一些内存页用于管理分配位图,必须首先调用reserve_bootmem分配这些内存页 但还有一些其余的内存区已经在使用中,必须相应地标记出来,所以还须要用reserve_bootmem注册相应的页,须要注册的内存区的准确数目,高度依赖于内核配置,例如 1. 须要保留0页,由于在许多计算机上该页是一个特殊的BIOS页,有些特定于计算机的功能须要该页才能运做正常 */ reserve_bootmem(CONFIG_MEMORY_START + PAGE_SIZE, (PFN_PHYS(start_pfn) + bootmap_size + PAGE_SIZE - 1) - CONFIG_MEMORY_START, BOOTMEM_DEFAULT); /* reserve physical page 0 - it's a special BIOS page on many boxes, enabling clean reboots, SMP operation, laptop functions. */ reserve_bootmem(CONFIG_MEMORY_START, PAGE_SIZE, BOOTMEM_DEFAULT); /* * reserve memory hole */ #ifdef CONFIG_MEMHOLE reserve_bootmem(CONFIG_MEMHOLE_START, CONFIG_MEMHOLE_SIZE, BOOTMEM_DEFAULT); #endif #ifdef CONFIG_BLK_DEV_INITRD if (LOADER_TYPE && INITRD_START) { if (INITRD_START + INITRD_SIZE <= (max_low_pfn << PAGE_SHIFT)) { reserve_bootmem(INITRD_START, INITRD_SIZE, BOOTMEM_DEFAULT); initrd_start = INITRD_START + PAGE_OFFSET; initrd_end = initrd_start + INITRD_SIZE; printk("initrd:start[%08lx],size[%08lx]\n", initrd_start, INITRD_SIZE); } else { printk("initrd extends beyond end of memory " "(0x%08lx > 0x%08lx)\ndisabling initrd\n", INITRD_START + INITRD_SIZE, max_low_pfn << PAGE_SHIFT); initrd_start = 0; } } #endif return max_low_pfn; } #else /* CONFIG_DISCONTIGMEM */ extern unsigned long setup_memory(void); #endif /* CONFIG_DISCONTIGMEM */
3. 对内核的接口
1. 分配内存
内核提供了各类函数,用于在初始化期间分配内存,在UMA系统上有下列函数可用 1) alloc_bootmem(size) 2) alloc_bootmem_pages(size) 按指定的大小在ZONE_NORMAL内存域分配内存,数据是对齐的,这使得内存或者从可适用于L1高速缓存的理想位置开始,或者从边界开始 /* 须要注意的是,用alloc_bootmem/alloc_bootmem_pages函数申请指定大小的内存。若是须要在其余地方调用这块内存,能够将alloc_bootmem返回的内存首地址经过EXPORT_SYMBOL导出,而后就能够使用这块内存了,这种内存分配不是否是经过动态分配得到的,而是相似于"内核引导参数"在编译时就预留出来的内核内存 这种内存分配方式的缺点是,申请内存的代码必须在连接到内核中的代码里才能使用,所以必须从新编译内核,并且内存管理系统看不到这部份内存,须要用户自行管理 */ 3) alloc_bootmem_low 4) alloc_bootmem_low_pages alloc_bootmem_low、alloc_bootmem_low_pages和alloc_bootmem、alloc_bootmem_pages相似,区别在于alloc_bootmem_low系列只是从ZONE_DMA内存域分配内存,所以,只有须要DMA内存时,才能使用上述API函数
这些函数都是__alloc_bootmem的前端,__alloc_bootmem将实际工做委托给__alloc_bootmem_nopanic,因为能够注册多个bootmem分配器(这些分配器都保存在一个全局链表中),__alloc_bootmem_core会遍历全部的分配器,直至分配成功为止
/source/mm/bootmem.c
/* 1. size: 所需内存区的长度 2. align: 数据的对齐方式 3. goal: 开始搜索适当空闲内存区的起始地址 1) SMP_CACHE_BYTES: 对齐数据,使之在大多数体系结构上可以理想地置于L1高速缓存中 2) PAGE_SIZE: 将数据对齐到页边界,适用于分配一个或多个整页 */ static void * __init ___alloc_bootmem(unsigned long size, unsigned long align, unsigned long goal, unsigned long limit) { void *mem = ___alloc_bootmem_nopanic(size, align, goal, limit); if (mem) return mem; /* * Whoops, we cannot satisfy the allocation request. */ printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size); panic("Out of memory"); return NULL; } static void * __init ___alloc_bootmem_nopanic(unsigned long size, unsigned long align, unsigned long goal, unsigned long limit) { bootmem_data_t *bdata; void *region; restart: region = alloc_arch_preferred_bootmem(NULL, size, align, goal, limit); if (region) return region; list_for_each_entry(bdata, &bdata_list, list) { if (goal && bdata->node_low_pfn <= PFN_DOWN(goal)) continue; if (limit && bdata->node_min_pfn >= PFN_DOWN(limit)) break; /* alloc_bootmem_core韩素的功能相对而言很普遍,在启动期间不须要过高的效率,该函数主要实现了最早适配算法 1. 从goal开始,扫描位图,查找知足分配请求的空闲内存页 2. 若是目标页紧接着上一次分配的页,内核会判断所需的内存(包括对齐数据所需的padding空间)是否可以在上一页分配或从上一页开始分配 3. 新分配的页在位图对应的比特位设置1 */ region = alloc_bootmem_core(bdata, size, align, goal, limit); if (region) return region; } if (goal) { goal = 0; goto restart; } return NULL; }
2. 释放内存
内核提供了free_bootmem函数来释放内存
/** * free_bootmem - mark a page range as usable * @addr: starting address of the range * @size: size of the range in bytes * * Partial pages will be considered reserved and left as they are. * * The range must be contiguous but may span node boundaries. */ void __init free_bootmem(unsigned long addr, unsigned long size) { unsigned long start, end; kmemleak_free_part(__va(addr), size); start = PFN_UP(addr); end = PFN_DOWN(addr + size); mark_bootmem(start, end, 0, 0); }
4. 停用bootmem分配器
在系统初始化进行到伙伴分配器可以承担内存管理的责任后,必须停用bootmem分配器,在UMA和NUMA系统上,停用分别由free_all_bootmem、free_all_bootmem_node完成,在伙伴系统创建以后,特定于体系结构的初始化代码须要调用这两个函数
/** * free_all_bootmem - release free pages to the buddy allocator * * Returns the number of pages actually released. */ unsigned long __init free_all_bootmem(void) { return free_all_bootmem_core(NODE_DATA(0)->bdata); } static unsigned long __init free_all_bootmem_core(bootmem_data_t *bdata) { int aligned; struct page *page; unsigned long start, end, pages, count = 0; if (!bdata->node_bootmem_map) return 0; start = bdata->node_min_pfn; end = bdata->node_low_pfn; /* * If the start is aligned to the machines wordsize, we might * be able to free pages in bulks of that order. */ aligned = !(start & (BITS_PER_LONG - 1)); bdebug("nid=%td start=%lx end=%lx aligned=%d\n", bdata - bootmem_node_data, start, end, aligned); //扫描bootmem分配器的页位图,释放每一个未用的页 while (start < end) { unsigned long *map, idx, vec; map = bdata->node_bootmem_map; idx = start - bdata->node_min_pfn; vec = ~map[idx / BITS_PER_LONG]; if (aligned && vec == ~0UL && start + BITS_PER_LONG < end) { int order = ilog2(BITS_PER_LONG); //到伙伴系统的接口是__free_pages_bootmem函数,该函数对每一个空闲页调用,该函数内部依赖于标准函数__free_page,它使得这些页并入伙伴系统的数据结构,在其中做为空闲页管理,可用于分配 __free_pages_bootmem(pfn_to_page(start), order); count += BITS_PER_LONG; } else { unsigned long off = 0; while (vec && off < BITS_PER_LONG) { if (vec & 1) { page = pfn_to_page(start + off); __free_pages_bootmem(page, 0); count++; } vec >>= 1; off++; } } start += BITS_PER_LONG; } page = virt_to_page(bdata->node_bootmem_map); pages = bdata->node_low_pfn - bdata->node_min_pfn; pages = bootmem_bootmap_pages(pages); count += pages; while (pages--) __free_pages_bootmem(page++, 0); bdebug("nid=%td released=%lx\n", bdata - bootmem_node_data, count); return count; }
在页位图已经彻底扫描后,它占据的内存空间也必须释放,此后,只有伙伴系统可用于内存分配
5. 释放初始化内存
许多内核代码和数据表只在系统初始化阶段须要,例如
1. 对于连接到内核中的驱动程序而言,则没必要要在内核内存中保持其数据结构的"初始化例程(init函数)",在结构创建后,这些例程就不须要了 2. 驱动程序用于检测其设备的硬件数据库,在相关的设备已经识别以后,就再也不须要了
内核提供了两个属性(__init、__initcall)用于标记初始化函数和数据,这些必须置于函数或数据的声明以前,例如
int __init hyper_hopper_probe(struc net_device *dev); static char stilllocking_msg[] _initdata = "found.\n";
__init、__initdata不能使用普通的C语言实现,所以内核必须再一次借助于特殊的GNU C编译器语句。初始化函数实现的背后,其通常性的思想在于,将数据保持在内核映像的一个特定部分,在启动结束时能够彻底从内存删除
\linux-2.6.32.63\include\linux\init.h
/* These are for everybody (although not all archs will actually discard it in modules) */ #define __init __section(.init.text) __cold notrace #define __initdata __section(.init.data) #define __initconst __section(.init.rodata) #define __exitdata __section(.exit.data) #define __exit_call __used __section(.exitcall.exit)
为从内存中释放初始化数据,内核没必要知道数据的性质,惟一相关的信息是这些数据和函数在内存中开始和结束的地址。因为该信息在编译时没法获得,它是内核在连接时插入的,为提供该信息,内核定义一对变量__init_begin、__init_end
\linux-2.6.32.63\arch\x86\mm\init.c
void free_initmem(void) { free_init_pages("unused kernel memory", (unsigned long)(&__init_begin), (unsigned long)(&__init_end)); }
free_initmem负责释放用于初始化的内存区,并将相关的页返回给伙伴系统,在启动过程恰好结束时会调用该函数,紧接其后init做为系统中第一个进程启动,启动日志包含了一条信息,代表释放了多少内存
dmesg
...
Freeing unused kernel memory: 1292k freed
Freeing unused kernel memory: 788k freed
Freeing unused kernel memory: 1568k freed
...
3. 物理内存的管理
在内核初始化完成后,内存管理的责任由伙伴系统承担,伙伴系统基于一种相对简单却十分强大高效的算法,它结合优秀内存分配器的两个关键特征: 速度和效率
0x1: 伙伴系统的结构
struct zone { .. //不一样长度的空闲区域 struct free_area free_area[MAX_ORDER]; .. }
相关数据结构,请参阅另外一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x4: struct zone
struct free_area是一个辅助数据结构
\linux-2.6.32.63\include\linux\mmzone.h
struct free_area { //用于链接空闲页的链表,页链表包含大小相同的连续内存区,其中又定义了多个(MIGRATE_TYPES 个)页链表 struct list_head free_list[MIGRATE_TYPES]; /* 指定了当前内存区中空闲页块的数目 1. 对0阶内存区逐页计算 2. 对1阶内存区计算页对的数目 3. 对2阶内存区计算4页集合的数目 ..依次类推 */ unsigned long nr_free; };
阶是伙伴系统中一个很是重要的概念,它描述了内存分配的数量单位,内存块的长度是2(order)次方,其中order的范围: 0 ~ MAX_ORDER
\linux-2.6.32.63\include\linux\mmzone.h
/* Free memory management - zoned buddy allocator. */ #ifndef CONFIG_FORCE_MAX_ZONEORDER #define MAX_ORDER 11 #else #define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER #endif #define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))
该常数一般设置为11,这意味着一次分配能够请求的页数最大为2(11)次方 = 2048
struct zone->struct free_area free_area[]数组中各个元素的索引也解释为阶,用于指定对应链表中的连续内存区中包含多少个页帧
1. 第0个链表包含的内存区为单页: 2(0)次方 = 1 2. 第1个链表管理的内存区为两页: 2(1)次方 = 2 3. 第3个链表管理的内存区为4页 ..
内存区中第一页的链表元素,用于将内存区维持在链表中
伙伴之间(指单页一组、两页一组、4页一组中的每一个内存块)没必要是彼此链接的,若是一个内存区在分配期间分解成两半,内核会自动将"未用"的一半加入到对应的链表中(加入大小对应的,对应阶的链表中)。若是在将来的某个时刻,因为内存释放等缘故,两个内存区都处于空闲状态,可经过其地址判断其是否为伙伴,管理成本十分低廉,是伙伴系统的一个主要优势
基于伙伴系统的内存管理专一于某个结点的某个内存域,例如DMA或高端内存域,但全部内存域和结点的伙伴系统都经过"备用分配列表"链接起来
在首选的内存域(或结点)没法知足内存分配请求时,首先尝试同一结点的另外一个内存域(从廉价到昂贵),接下来再尝试另外一个结点,直至知足请求
有关伙伴系统当前状态信息能够在/proc/buddyinfo中得到
Node 0, zone DMA 0 4 3 2 1 1 0 0 1 1 3 Node 0, zone DMA32 475 196 63 49 25 28 9 3 5 4 153 //给出了各个内存域中每一个分配阶中空闲项的数目,从左至右,阶依次升高
0x2: 避免碎片
1. 根据可移动性组织页
伙伴系统的性能很是高效,但在Linux内存管理方面,有一个长期存在的问题,在系统启动并长期运行后,物理内存会产生不少碎片
1. 左侧的地址空间中散布着空闲页,尽管大约25%的物理内存仍然未分配,但最大的连续空闲页只有一页,这对用户空间程序没有问题,由于用户空间的内存是经过页表映射的,不管空闲页在物理内存中的分布如何,应用程序看到的内存都是"连续"的 2. 右图给出的情形中,空闲页和使用页的数目与左图相同,但全部空闲页都位于一个连续内存中
可是对于内核来讲,碎片是一个大问题,对于内核映射来讲,因为(大多数)物理内存须要一致映射到地址空间的内核部分,那么在左图的场景中,没法映射比一页更大的内存区,尽管许多时候都分配的是比较小的内存,但也有时候须要分配多于一页的内存
须要明白的是,文件系统也有碎片,该领域的碎片问题主要经过碎片合并工具解决,它们分析文件系统,从新排序已分配存储块,从而创建较大的连续存储区。理论上,该方法对物理内存也是可能的,但因为许多物理内存页不能移动到任意位置,阻碍了该方法的实施
所以,内核的策略方法是"反碎片(anti-愤然哥们他提on)",即试图从最初开始尽量防止碎片。在学习反碎片策略的工做原理,咱们须要明白内核将已分配页划分为下面3种不一样类型
1. 不可移动页 在内存中有固定位置,不能移动到其余地方,核心内核分配的大多数内存属于该类别 2. 可回收页 不能直接移动,但能够删除,其内容能够从某些源从新生成,例如映射自文件的数据属于该类别 kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存,这是一个复杂的过程,属于页面回收的范畴,简单来讲内核会在可回收页占据了太多内存时进行回收 另外,在内存短缺(即分配失败)时也能够发起页面回收 3. 可移动页 能够随意移动,属于用户空间应用程序的页属于该类别,它们是经过页表映射的,若是它们复制到新位置,页表项能够相应地更新,应用程序不会注意到任何区别
页的可移动性,依赖该页属于3种类别的哪种,内核使用反碎片技术,即基于将具备相同可移动性的页分组的思想,根据页的可移动性,将其分配到不一样的列表中,这样,不可移动页不能位于可移动内存区的中间,避免了碎片的发生(这是一种聚类的思想)
须要注意的是,从最初开始,内存并未划分为可移动性不一样的区,这些是在运行时造成的
数据结构
尽管内核使用的反碎片技术颇有效果,但它对伙伴系统的代码和数据结构几乎没有影响,内核定义了一些宏来表示不一样的迁移类型
\linux-2.6.32.63\include\linux\mmzone.h
#define MIGRATE_UNMOVABLE 0 // #define MIGRATE_RECLAIMABLE 1 // #define MIGRATE_MOVABLE 2 #define MIGRATE_PCPTYPES 3 /* the number of types on the pcp lists */ #define MIGRATE_RESERVE 3 // 若是向具备特定可移动特性的列表请求分配内存失败,这种紧急情能够从MIGRATE_RESERVE分配内存 #define MIGRATE_ISOLATE 4 /* can't allocate from here MIGRATE_ISOLATE是一个特殊的虚拟区域,用于跨越NUMA结点移动物理内存页 */ #define MIGRATE_TYPES 5 // MIGRATE_TYPES用于表示迁移类型的数目,不表明具体的区域
对伙伴系统数据结构的主要调整,是将空闲列表分解为MIGRATE_TYPES个列表
struct free_area { //用于链接空闲页的链表,页链表包含大小相同的连续内存区,其中又定义了多个(MIGRATE_TYPES 个)页链表 struct list_head free_list[MIGRATE_TYPES]; /* 指定了当前内存区中空闲页块的数目 1. 对0阶内存区逐页计算 2. 对1阶内存区计算页对的数目 3. 对2阶内存区计算4页集合的数目 ..依次类推 */ unsigned long nr_free; };
若是内核没法知足针对某一给定迁移类型的分配请求,内核在这种状况下会提供一个备用列表,规定了在指定列表中没法知足分配请求时,接下来使用哪种迁移类型
\linux-2.6.32.63\mm\page_alloc.c
/* * This array describes the order lists are fallen back to when the free lists for the desirable migrate type are depleted */ static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE }, [MIGRATE_RESERVE] = { MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE }, /* Never used */ };
全局变量和辅助函数
尽管页可移动性分组特性老是编译到内核中,但只有在系统中有足够内存能够分配到多个迁移类型对应的链表时,才是有意义的。因为每一个迁移链表都应该有适当数量的内存,内核须要定义"适当"的概念,这是经过两个全局变量提供的
1. pageblock_order: 表示内核认为是"大"的一个分配阶 2. page_nr_pages: 表示该分配阶对应的页数
若是体系结构提供了巨型页机制,则pageblock_order一般定义为巨型页对应的分配阶
1. 在IA-32体系结构上,巨型页长度是4MB,所以每一个巨型页由1024个普通页组成,而HUGETLB_PAGE_ORDER则定义为10 2. IA-64体系结构容许设置可变的普通和巨型页长度,所以HUGETLB_PAGE_ORDER的值取决于内核配置 3. 若是体系结构不支持巨型页,则将其定义为第二高的分配阶
\linux-2.6.32.63\include\linux\pageblock-flags.h
#ifdef CONFIG_HUGETLB_PAGE #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE /* Huge page sizes are variable */ extern int pageblock_order; #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ /* Huge pages are a constant size */ #define pageblock_order HUGETLB_PAGE_ORDER #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ #else /* CONFIG_HUGETLB_PAGE */ /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ #define pageblock_order (MAX_ORDER-1) #endif /* CONFIG_HUGETLB_PAGE */
内核提供了两个标志(分配掩码),用于代表给定的分配内存属于何种迁移类型
1. __GFP_MOVALE: 分配的内存是可移动的 2. __GFP_RECLAIMABLE: 分配的内存是可回收的 //若是这些标志都没有设置,则分配的内存假定为不可移动的
在初始化期间,内核自动确保对内存域(struct zone)中的每一个不一样的迁移类型分组,在各个迁移链表之间,当前的页面分配状态能够从/proc/pagetypeinfo中得到
Page block order: 9 Pages per block: 512 Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10 Node 0, zone DMA, type Unmovable 0 4 3 2 1 1 0 0 1 0 0 Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0 Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 0 3 Node 0, zone DMA, type Reserve 0 0 0 0 0 0 0 0 0 1 0 Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0 Node 0, zone DMA32, type Unmovable 100 92 31 21 14 5 2 0 1 1 0 Node 0, zone DMA32, type Reclaimable 0 1 0 0 1 1 0 1 1 1 0 Node 0, zone DMA32, type Movable 4 4 3 1 4 19 2 3 1 4 152 Node 0, zone DMA32, type Reserve 1 0 1 1 1 1 1 1 1 1 0 Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0 Number of blocks type Unmovable Reclaimable Movable Reserve Isolate Node 0, zone DMA 1 0 6 1 0 Node 0, zone DMA32 74 42 898 2 0
初始化基于可移动性的分组
在内核子系统初始化期间,memmap_init_zone负责处理内存域的page实例,该函数将全部的页最初都标记为可移动的。总而言之,这种作法避免了启动期间内核分配的内存(常常在系统的整个运行时间都不释放)散布到物理内存的各处,从而使其余类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一
2. 虚拟可移动内存域
根据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另外一种组织该问题的手段,即虚拟内存域ZONE_MOVABLE,与可移动性分组相反,ZONE_MOVABLE特性必须由管理员显式激活。基本思想很简单,可用的物理内存划分为两个内存域: 一个用于可移动分配、另外一个用于不可移动分配。这会自动防止不可移动页向可移动内存域引入碎片(而且这个碎片仍是没法清除的)
管理员必须指定用于不可移动分配的内存数量、以及可移动分配的内存数量
0x3: 初始化内存域和结点数据结构
截至目前,咱们已经在特定于体系结构的代码中看到了内核如何检测系统中的可用内存。与高层数据结构(如内存域和结点)的关联,则须要根据该信息构建,咱们知道,体系结构相关代码须要在启动期间创建如下信息
1. 系统中各个内存域的页帧边界,保存在max_zone_pfn数组 2. 各结点页帧的分配状况,保存在全局变量early_node_map中
1. 管理数据结构的建立
从内核版本2.6.10开始提供了一个通用框架,用于将上述信息转换为伙伴系统预期的结点和内存域数据结构,setup_arch->zone_sizes_init->free_area_init_nodes完成主要的工做
在创建结点和内存域内存管理数据结构时,特定于体系结构的代码和通用内核代码之间的相关做用
\linux-3.15.5\mm\page_alloc.c
/** * free_area_init_nodes - Initialise all pg_data_t and zone data * @max_zone_pfn: an array of max PFNs for each zone * * This will call free_area_init_node() for each active node in the system. * Using the page ranges provided by add_active_range(), the size of each * zone in each node and their holes is calculated. If the maximum PFN * between two adjacent zones match, it is assumed that the zone is empty. * For example, if arch_max_dma_pfn == arch_max_dma32_pfn, it is assumed * that arch_max_dma32_pfn has no pages. It is also assumed that a zone * starts where the previous one ended. For example, ZONE_DMA32 starts * at arch_max_dma_pfn. */ void __init free_area_init_nodes(unsigned long *max_zone_pfn) { unsigned long nid; int i; /* Sort early_node_map as initialisation assumes it is sorted 根据结点的第一个页帧start_pfn,对early_node_map中的各项进行排序 */ sort_node_map(); /* Record where the zone boundaries are 计算各个内存域可以使用的最低和最高的页帧编号 */ memset(arch_zone_lowest_possible_pfn, 0, sizeof(arch_zone_lowest_possible_pfn)); memset(arch_zone_highest_possible_pfn, 0, sizeof(arch_zone_highest_possible_pfn)); //辅助函数find_min_pfn_with_active_regions用于找到注册的最低内存域中可用的编号最小的页帧,该内存域不必定是ZONE_DMA arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; //接下来构建其余内存域的页帧区间,方法很直接,第n个内存域的最小页帧 = 第n-1个内存域的最大页帧 for (i = 1; i < MAX_NR_ZONES; i++) { if (i == ZONE_MOVABLE) continue; arch_zone_lowest_possible_pfn[i] = arch_zone_highest_possible_pfn[i-1]; arch_zone_highest_possible_pfn[i] = max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); } //由于ZONE_MOVABLE是一个虚拟内存域,不与真正的硬件内存域相关联,该内存域的边界老是设置为0 arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; /* Find the PFNs that ZONE_MOVABLE begins at in each node */ memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); find_zone_movable_pfns_for_nodes(zone_movable_pfn); /* Print out the zone ranges */ printk("Zone PFN ranges:\n"); for (i = 0; i < MAX_NR_ZONES; i++) { if (i == ZONE_MOVABLE) continue; printk(" %-8s %0#10lx -> %0#10lx\n", zone_names[i], arch_zone_lowest_possible_pfn[i], arch_zone_highest_possible_pfn[i]); } /* Print out the PFNs ZONE_MOVABLE begins at in each node */ printk("Movable zone start PFN for each node\n"); for (i = 0; i < MAX_NUMNODES; i++) { if (zone_movable_pfn[i]) printk(" Node %d: %lu\n", i, zone_movable_pfn[i]); } /* Print out the early_node_map[] */ printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries); for (i = 0; i < nr_nodemap_entries; i++) printk(" %3d: %0#10lx -> %0#10lx\n", early_node_map[i].nid, early_node_map[i].start_pfn, early_node_map[i].end_pfn); /* Initialise every node */ mminit_verify_pageflags_layout(); setup_nr_node_ids(); for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); free_area_init_node(nid, NULL, find_min_pfn_for_node(nid), NULL); /* Any memory on that node */ if (pgdat->node_present_pages) node_set_state(nid, N_HIGH_MEMORY); check_for_regular_memory(pgdat); } }
2. 对各个结点建立数据结构
\linux-3.15.5\mm\page_alloc.c
在内存域边界已经肯定后,free_area_init_nodes分别对各个内存域调用free_area_init_node建立数据结构
void __paginginit free_area_init_node(int nid, unsigned long *zones_size, unsigned long node_start_pfn, unsigned long *zholes_size) { pg_data_t *pgdat = NODE_DATA(nid); pgdat->node_id = nid; pgdat->node_start_pfn = node_start_pfn; /* 累积各个内存域的页数,计算结点中页的生总数,对连续内存模型而言,这能够经过zones_size_init完成,但calculate_node_totalpages还考虑了内存域中的空洞 在系统启动初始化期间,内核会输出一段简短的消息 dmesg On node 0 totalpages: 524142 */ calculate_node_totalpages(pgdat, zones_size, zholes_size); /* alloc_node_mem_map负责初始化一个简单但很是重要的数据结构,即系统中的各个物理内存页,都对应着一个strcut page实例,该结构的初始化由alloc_node_mem_map执行 */ alloc_node_mem_map(pgdat); #ifdef CONFIG_FLAT_NODE_MEM_MAP printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", nid, (unsigned long)pgdat, (unsigned long)pgdat->node_mem_map); #endif //初始化内存域数据结构涉及的繁重工做由free_area_init_core执行,它会依次遍历结点的全部内存域 free_area_init_core(pgdat, zones_size, zholes_size); }
0x4: 分配器API
就伙伴系统的接口而言,NUMA或UMA体系结构是没有差异的,两者的调用语法都是相同的,全部函数的共同点是: 只能分配2的整数幂个页,所以,接口中不像C标准库的malloc函数或bootmem分配器那样任意指定了所需内存大小做为参数,相反,必须指定的是分配阶,伙伴系统将在内存中分配2(order)次方页。内核中细粒度的分配只能借助SLAB分配器(或者SLUB、SLOB分配器),它们都基于伙伴系统
1. alloc_pages(mask, order) 分配2(order)次方页并返回一个struct page的实例,表示分配的内存块的起始页 2. get_zeroed_page(mask) 分配一页并返回一个struct page实例,页对应的内存填充0(全部其余函数分配以后页的内容是未定义的) 3. __get_free_pages(mask, order) 返回分配内存块的虚拟地址 4. get_dma_pages(gfp_mask, order) 用来得到适用于DMA的页 /* 在空闲内存没法知足请求以致于分配失败的状况下,全部上述函数都返回空指针(alloc_page)、或者0(get_zeroed_page、__get_free_pages、get_dma_pages) 所以内核在每次分配以后都必须检查返回的结果,这种惯例与设计良好的用户层应用程序相似,但在内核中忽略检查会致使严重得多的故障 */
内核除了伙伴系统函数以外,还提供了其余内存管理函数,它们以伙伴系统为基础,但并不属于伙伴分配器自身,这些函数包括
1. vmalloc 2. vmalloc_32 //使用页表将不连续的内存映射到内核地址空间中,使之看上去是连续的 3. kmalloc类型的函数 //用于分配小于一整页的内存区
有4个函数用于释放再也不使用的页
1. free_page(struct page *) 2. free_pages(struct page *, order); //用于将一个2(order)次方页返回(释放)给内存管理子系统,内存区的起始地址由指向该内存区的第一个page实例的的指针表示 3. __free_page(addr) 4. __free_pages(addr, order); //语义相似于一、2函数,但在表示须要释放的内存区时,使用了虚拟内存地址而不是page实例
1. 分配掩码
伙伴系统提供的内存分配接口函数中强制使用的mask参数
内存域修饰符
Linux将内存划分为内存域,内核提供了所谓的"内存域修饰符(zone modifier)",在掩码的最低4个比特位定义来指定从哪一个内存域中分配所需的页
\linux-2.6.32.63\include\linux\gfp.h
/* * GFP bitmasks.. * * Zone modifiers (see linux/mmzone.h - low three bits) * * Do not put any conditional on these. If necessary modify the definitions without the underscores and use the consistently. The definitions here may be used in bit comparisons. */ #define __GFP_DMA ((__force gfp_t)0x01u) #define __GFP_HIGHMEM ((__force gfp_t)0x02u) #define __GFP_DMA32 ((__force gfp_t)0x04u) #define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
缩写GFP表明得到空闲页(get free page),__GFP_MOVABLE不表明物理内存页,但通知内核应该在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配。值得注意的是,没有__GFP_NORMAL常数,但内存分配的主要负担却落到ZONE_NORMAL内存域,内核考虑到这一点,提供了gfp_zone()函数来计算与给定分配标志"兼容"的最高(最廉价)内存域,那么内存分配能够从该内存域或更低(从最廉价逐渐到最昂贵)的内存域进行
\linux-2.6.32.63\include\linux\gfp.h
/* 1. 若是__GFP_DMA、__GFP_HIGHMEM都没有设置,则首先扫描ZONE_NORMAL,而后是ZONE_DMA 2. 若是设置了__GFP_HIGHMEM,没有设置__GFP_DMA,则结果是从__GFP_HIGHMEM开始扫描全部3个内存域 3. 若是设置了__GFP_DMA,那么无论__GFP_HIGHMEM设置与否都从__GFP_DMA开始扫描 总之遵循的一个基本原则是: 尽可能遵循掩码参数的指示,同时尽可能从最廉价的内存域开始分配 */ static inline enum zone_type gfp_zone(gfp_t flags) { enum zone_type z; int bit = flags & GFP_ZONEMASK; z = (GFP_ZONE_TABLE >> (bit * ZONES_SHIFT)) & ((1 << ZONES_SHIFT) - 1); if (__builtin_constant_p(bit)) MAYBE_BUILD_BUG_ON((GFP_ZONE_BAD >> bit) & 1); else { #ifdef CONFIG_DEBUG_VM BUG_ON((GFP_ZONE_BAD >> bit) & 1); #endif } return z; }
操做修饰符
/* * Action modifiers - doesn't change the zoning */ /* Can wait and reschedule 能够等待和重调度 __GFP_WAIT表示分配内存的请求能够中断,即调度器在该请求期间能够随意选择另外一个进程执行,或者该请求能够被另外一个更重要的事件中断 分配器还能够在返回内存以前,在队列上等待一个事件,相关进程会进入睡眠状态 */ #define __GFP_WAIT ((__force gfp_t)0x10u) /* Should access emergency pools 应该访问紧急分配池 若是请求很是重要,则设置__GFP_HIGH,即内核急切地须要内存时,在分配内存失败可能给内核带来严重后果时(例如威胁到系统稳定性或系统崩溃),老是会使用该标志 */ #define __GFP_HIGH ((__force gfp_t)0x20u) /* Can start physical IO 能够启动物理IO __GFP_IO说明在查找空闲内存期间内核能够进行I/O操做,实际上,这意味着若是内核在内存分配期间换出页,那么仅当设置该标志时,才能将选择的页写入硬盘 */ #define __GFP_IO ((__force gfp_t)0x40u) /* Can call down to low-level FS 能够调用底层文件系统 __GFP_FS容许内核执行VFS操做,在与VFS层有联系的内核子系统中必须禁用,由于这可能引发循环递归调用 */ #define __GFP_FS ((__force gfp_t)0x80u) #define __GFP_COLD ((__force gfp_t)0x100u) /* Cache-cold page required 须要分缓存的冷页(即不在CPU高速缓存中的"冷页") */ #define __GFP_NOWARN ((__force gfp_t)0x200u) /* Suppress page allocation failure warning 禁止分配失败告警*/ #define __GFP_REPEAT ((__force gfp_t)0x400u) /* Try hard to allocate the memory, but the allocation attempt _might_ fail. This depends upon the particular VM implementation 重试分配,但在重试若干次后会中止,可能失败 */ #define __GFP_NOFAIL ((__force gfp_t)0x800u) /* The VM implementation _must_ retry infinitely: the caller cannot handle allocation failures 一直重试,不会失败*/ #define __GFP_NORETRY ((__force gfp_t)0x1000u) /* The VM implementation must not retry indefinitely 不重试,可能失败 */ #define __GFP_COMP ((__force gfp_t)0x4000u) /* Add compound page metadata 增长复合页元数据 */ #define __GFP_ZERO ((__force gfp_t)0x8000u) /* Return zeroed page on success 成功则返回填充字节0的页 */ #define __GFP_NOMEMALLOC ((__force gfp_t)0x10000u) /* Don't use emergency reserves 不使用紧急分配链表 */ /* Enforce hardwall cpuset memory allocs 只容许在进程容许运行的CPU所关联的结点分配内存 __GFP_HARDWALL只在NUMA系统上有意义,它限制只在分配到当前进程的各个CPU所关联的结点分配内存,若是进程容许在全部CPU上运行(默认状况),该标志是无心义的,只有进程能够运行的CPU受限时,该标志才有效果 */ #define __GFP_HARDWALL ((__force gfp_t)0x20000u) /* No fallback, no policies 没有备用结点,没有策略 __GFP_THISNODE只有在NUMA系统上才有意义,若是设置该比特位,则内存分配失败的状况下不容许使用其余结点做为备用,须要保证在当前结点或者明确指定的结点上成功分配内存 */ #define __GFP_THISNODE ((__force gfp_t)0x40000u) #define __GFP_RECLAIMABLE ((__force gfp_t)0x80000u) /* Page is reclaimable 页是可回收的*/ #define __GFP_MOVABLE ((__force gfp_t)0x08u) /* Flag that this page will be movable by the page migration mechanism or reclaimed 页是可移动的 */ #ifdef CONFIG_KMEMCHECK #define __GFP_NOTRACK ((__force gfp_t)0x200000u) /* Don't track with kmemcheck */ #else #define __GFP_NOTRACK ((__force gfp_t)0) #endif
因为这些标志几乎老是组合使用,内核对此做了一些分组,包含了用于各类标准情形的适当的标志
/* This equals 0, but use constants in case they ever change */ #define GFP_NOWAIT (GFP_ATOMIC & ~__GFP_HIGH) /* GFP_ATOMIC means both !wait (__GFP_WAIT not set) and use emergency pool GFP_ATOMIC用于原子分配,在任何状况下都不能中断,可能使用紧急分配链表中的内存 */ #define GFP_ATOMIC (__GFP_HIGH) //GFP_NOIO明确禁止I/O操做,但可能被中断 #define GFP_NOIO (__GFP_WAIT) //GFP_NOIO明确禁止I/O操做、以及访问VFS层,但可能被中断 #define GFP_NOFS (__GFP_WAIT | __GFP_IO) //GFP_KERNEL是内核的默认配置,它的失败不会当即威胁系统稳定性,这是内核源代码中最常使用的标志 #define GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS) #define GFP_TEMPORARY (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE) #define GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL) /* GFP_HIGHUSER是GFP_USER的一个扩展,也用于用户空间,它容许分配没法直接映射的高端内存 使用高端内存是没有坏处的,由于用户进程的地址空间老是经过非线性页表组织的 */ #define GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | __GFP_HIGHMEM) #define GFP_HIGHUSER_MOVABLE (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | __GFP_HIGHMEM | __GFP_MOVABLE) #ifdef CONFIG_NUMA #define GFP_THISNODE (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY) #else #define GFP_THISNODE ((__force gfp_t)0) #endif /* This mask makes up all the page movable related flags */ #define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) /* Control page allocator reclaim behavior */ #define GFP_RECLAIM_MASK (__GFP_WAIT|__GFP_HIGH|__GFP_IO|__GFP_FS| __GFP_NOWARN|__GFP_REPEAT|__GFP_NOFAIL| __GFP_NORETRY|__GFP_NOMEMALLOC) /* Control slab gfp mask during early boot */ #define GFP_BOOT_MASK __GFP_BITS_MASK & ~(__GFP_WAIT|__GFP_IO|__GFP_FS) /* Control allocation constraints */ #define GFP_CONSTRAINT_MASK (__GFP_HARDWALL|__GFP_THISNODE) /* Do not use these with a slab allocator */ #define GFP_SLAB_BUG_MASK (__GFP_DMA32|__GFP_HIGHMEM|~__GFP_BITS_MASK) /* Flag - indicates that the buffer will be suitable for DMA. Ignored on some platforms, used as appropriate on others */ #define GFP_DMA __GFP_DMA /* 4GB DMA on some platforms */ #define GFP_DMA32 __GFP_DMA32
2. 内存分配宏
经过使用分配掩码和各个分配函数,内核提供了一个很是灵活的内存分配体系,尽管如此,全部接口函数均可以追溯到一个简单的基本函数: alloc_pages_node
1. alloc_page #define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) //分配0阶的内存页,即1页 2. alloc_pages #define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order) 3. __get_free_page #define __get_free_page(gfp_mask) __get_free_pages((gfp_mask),0) //分配0阶的内存页,即1页 4. __get_free_pages // unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) { struct page *page; /* * __get_free_pages() returns a 32-bit address, which cannot represent a highmem page */ VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); page = alloc_pages(gfp_mask, order); if (!page) return 0; return (unsigned long) page_address(page); } EXPORT_SYMBOL(__get_free_pages); 5. __get_dma_pages #define __get_dma_pages(gfp_mask, order) __get_free_pages((gfp_mask) | GFP_DMA,(order)) 6. get_zeroed_page unsigned long get_zeroed_page(gfp_t gfp_mask) { return __get_free_pages(gfp_mask | __GFP_ZERO, 0); } EXPORT_SYMBOL(get_zeroed_page);
这样,就完成了全部API函数到公共基础函数alloc_pages的统一
相似的,内存释放函数也能够规约到一个主要的函数: __free_pages,只是用不一样的参数调用
1. __free_pages void __free_pages(struct page *page, unsigned int order) { if (put_page_testzero(page)) { trace_mm_page_free_direct(page, order); if (order == 0) free_hot_page(page); else __free_pages_ok(page, order); } } 2. __free_page #define __free_page(page) __free_pages((page), 0) 3. free_page #define free_page(addr) free_pages((addr),0) 4. free_pages void free_pages(unsigned long addr, unsigned int order) { if (addr != 0) { VM_BUG_ON(!virt_addr_valid((void *)addr)); //virt_to_page将虚拟内存底子好转换为指向page实例的指针 __free_pages(virt_to_page((void *)addr), order); } } EXPORT_SYMBOL(free_pages); //free_pages和__free_pages之间的关系经过函数而不是宏创建,由于首先必须将虚拟地址转换为指向struct page的指针
0x5: 分配页
咱们知道,全部API函数都追溯到alloc_pages_node,该函数是伙伴系统主要实现的"发射台"
\linux-2.6.32.63\include\linux\gfp.h
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) { /* Unknown node is current node 若是指定负的结点ID(不存在),内核自动地使用当前执行CPU对应的结点ID */ if (nid < 0) nid = numa_node_id(); //node_zonelist用于选择分配内存的内存域 return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask)); }
在内核源代码中,__alloc_pages被称之为"伙伴系统的心脏",它处理的是实质性的内存分配
1. 选择页
辅助函数
首先咱们须要定义一些函数使用的标志,用于控制到达各个水印指定的临界状态时的行为
\linux-2.6.32.63\mm\page_alloc.c
/* The ALLOC_WMARK bits are used as an index to zone->watermark */ #define ALLOC_WMARK_MIN WMARK_MIN /* 使用pages_min水印 */ #define ALLOC_WMARK_LOW WMARK_LOW /* 使用pages_low水印 */ #define ALLOC_WMARK_HIGH WMARK_HIGH /* 使用pages_high水印 */ #define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all 彻底不检查水印 */ /* Mask to get the watermark bits */ #define ALLOC_WMARK_MASK (ALLOC_NO_WATERMARKS-1) #define ALLOC_HARDER 0x10 /* try to alloc harder 试图更努力地分配,即放宽限制 */ #define ALLOC_HIGH 0x20 /* __GFP_HIGH set 设置了__GFP_HIGH */ #define ALLOC_CPUSET 0x40 /* check for correct cpuset 检查内存结点是否对应着指定的CPU集合 */
设置的标志在zone_watermark_ok函数中检查,该函数根据设置的标志判断是否能从给定的内存域中分配内存
\linux-2.6.32.63\mm\page_alloc.c
/* * Return 1 if free pages are above 'mark'. This takes into account the order of the allocation. */ int zone_watermark_ok(struct zone *z, int order, unsigned long mark, int classzone_idx, int alloc_flags) { /* free_pages my go negative - that's OK */ long min = mark; long free_pages = zone_nr_free_pages(z) - (1 << order) + 1; int o; //在解释了ALLOC_HIGH、ALLOC_HARDER标志以后(将最小值标记下降到当前值的一半或四分之一,使得分配过程更加努力,即放宽限制) if (alloc_flags & ALLOC_HIGH) min -= min / 2; if (alloc_flags & ALLOC_HARDER) min -= min / 4; //检查空闲页的数目是否小于最小值 + lowmem_reserve中指定的紧急分配值之和 if (free_pages <= min + z->lowmem_reserve[classzone_idx]) return 0; //若是不小于,则遍历全部小于当前阶的分配阶,从free_pages减去当前分配阶的全部空闲页(左移o位是必要的,由于nr_free记载的是当前分配阶的空闲页块数目) for (o = 0; o < order; o++) { /* At the next order, this order's pages become unavailable 在下一阶,当前阶的页是不可用的 */ free_pages -= z->free_area[o].nr_free << o; /* Require fewer higher order pages to be free 所需高阶空闲页的数目相对较少 每升高一阶,所需空闲页的最小值折半 */ min >>= 1; //若是内核遍历全部的低端内存域以后,发现内存不足,则不进行内存分配 if (free_pages <= min) return 0; } return 1; }
get_page_from_freelist是伙伴系统使用的另外一个重要的辅助函数,它经过标志集和分配阶来判断是否能进行分配,若是能够,则发起实际的分配操做
/* * get_page_from_freelist goes through the zonelist trying to allocate a page. */ static struct page *get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order, struct zonelist *zonelist, int high_zoneidx, int alloc_flags, struct zone *preferred_zone, int migratetype) { struct zoneref *z; struct page *page = NULL; int classzone_idx; struct zone *zone; nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */ int zlc_active = 0; /* set if using zonelist_cache */ int did_zlc_setup = 0; /* just call zlc_setup() one time */ classzone_idx = zone_idx(preferred_zone); zonelist_scan: /* * Scan zonelist, looking for a zone with enough free. * See also cpuset_zone_allowed() comment in kernel/cpuset.c. */ for_each_zone_zonelist_nodemask(zone, z, zonelist, high_zoneidx, nodemask) { if (NUMA_BUILD && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes)) continue; //cpuset_zone_allowed_softwall用于检查给定内存域是否属于该进程容许运行的CPU if ((alloc_flags & ALLOC_CPUSET) && !cpuset_zone_allowed_softwall(zone, gfp_mask)) goto try_next_zone; BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK); if (!(alloc_flags & ALLOC_NO_WATERMARKS)) { unsigned long mark; int ret; mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK]; //zone_watermark_ok接下来检查所遍历到内存域是否有足够的空闲页,并试图分配一个连续的内存块(足够空闲页、连续内存二者必须同时知足才可继续,不然循环进行到备用列表中的写一个内存域) if (zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags)) goto try_this_zone; if (zone_reclaim_mode == 0) goto this_zone_full; ret = zone_reclaim(zone, gfp_mask, order); switch (ret) { case ZONE_RECLAIM_NOSCAN: /* did not scan */ goto try_next_zone; case ZONE_RECLAIM_FULL: /* scanned but unreclaimable */ goto this_zone_full; default: /* did we reclaim enough */ if (!zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags)) goto this_zone_full; } } try_this_zone: //若是内存域适用于当前的分配请求,则调用buffered_rmqueue试图从中分配所需数目的页 page = buffered_rmqueue(preferred_zone, zone, order, gfp_mask, migratetype); if (page) break; this_zone_full: if (NUMA_BUILD) zlc_mark_zone_full(zonelist, z); try_next_zone: if (NUMA_BUILD && !did_zlc_setup && nr_online_nodes > 1) { /* * we do zlc_setup after the first zone is tried but only * if there are multiple nodes make it worthwhile */ allowednodes = zlc_setup(zonelist, alloc_flags); zlc_active = 1; did_zlc_setup = 1; } } if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) { /* Disable zlc cache for second zonelist scan */ zlc_active = 0; goto zonelist_scan; } return page; }
分配控制
咱们知道,alloc_pages_node是内核伙伴系统内存分配最后规约的底层函数,而__alloc_pages又是其中伙伴系统的主函数,该函数的实现比较复杂,尤为是在可用内存太少或逐渐用完时。若是可用内存足够,则必要的工做会很快完成
\linux-2.6.32.63\include\linux\gfp.h
static inline struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist) { return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL); }
\linux-2.6.32.63\mm\page_alloc.c
/* * This is the 'heart' of the zoned buddy allocator. */ struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, nodemask_t *nodemask) { enum zone_type high_zoneidx = gfp_zone(gfp_mask); struct zone *preferred_zone; struct page *page; int migratetype = allocflags_to_migratetype(gfp_mask); gfp_mask &= gfp_allowed_mask; lockdep_trace_alloc(gfp_mask); might_sleep_if(gfp_mask & __GFP_WAIT); if (should_fail_alloc_page(gfp_mask, order)) return NULL; /* Check the zones suitable for the gfp_mask contain at least one valid zone. It's possible to have an empty zonelist as a result of GFP_THISNODE and a memoryless node 适合于gfp_mask的内存域列表 */ if (unlikely(!zonelist->_zonerefs->zone)) //若是没有在内存的结点上使用GFP_THISNODE,致使zonelist为空,就会发生这种状况 return NULL; /* The preferred zone is used for statistics later */ first_zones_zonelist(zonelist, high_zoneidx, nodemask, &preferred_zone); if (!preferred_zone) return NULL; /* First allocation attempt 在最简单的情形中,分配空闲内存区只涉及调用一次get_page_from_freelist,而后返回所需数目的页 */ page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order, zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET, preferred_zone, migratetype); if (unlikely(!page)) //第一次内存分配尝试不会特别积极,若是在某个内存域中没法找到空闲内存,则意味着内存相对较紧张,内核须要更多的工做量才能找到更多内存 page = __alloc_pages_slowpath(gfp_mask, order, zonelist, high_zoneidx, nodemask, preferred_zone, migratetype); trace_mm_page_alloc(page, order, gfp_mask, migratetype); return page; } EXPORT_SYMBOL(__alloc_pages_nodemask);
\linux-2.6.32.63\mm\page_alloc.c
static inline struct page *__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, enum zone_type high_zoneidx, nodemask_t *nodemask, struct zone *preferred_zone, int migratetype) { const gfp_t wait = gfp_mask & __GFP_WAIT; struct page *page = NULL; int alloc_flags; unsigned long pages_reclaimed = 0; unsigned long did_some_progress; struct task_struct *p = current; /* * In the slowpath, we sanity check order to avoid ever trying to * reclaim >= MAX_ORDER areas which will never succeed. Callers may * be using allocators in order of preference for an area that is * too large. */ if (order >= MAX_ORDER) { WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN)); return NULL; } /* * GFP_THISNODE (meaning __GFP_THISNODE, __GFP_NORETRY and * __GFP_NOWARN set) should not cause reclaim since the subsystem * (f.e. slab) using GFP_THISNODE may choose to trigger reclaim * using a larger set of nodes after it has established that the * allowed per node queues are empty and that nodes are * over allocated. */ if (NUMA_BUILD && (gfp_mask & GFP_THISNODE) == GFP_THISNODE) goto nopage; restart: //唤醒负责换出页的kswapd守护进程,空闲内存能够经过缩减内核缓存和非热点页面回收来得到,即写回或换出不多使用的页,这两种措施都是由该守护进程发起的 wake_all_kswapd(order, zonelist, high_zoneidx); /* OK, we're below the kswapd watermark and have kicked background reclaim. Now things get more complex, so set up alloc_flags according to how we want to proceed. 在交互守护进程唤醒后,内核开始新的尝试,这一次进行的搜索更为积极,对分配标志进行了调整,修改成一些在当前特定状况下更有可能分配成功的标志 同时,将水印下降到最小值,对实时进程和指定了__GFP_WAIT标志由于不能睡眠的调用,会设置ALLOC_HARDER */ alloc_flags = gfp_to_alloc_flags(gfp_mask); rebalance: /* This is the last chance, in general, before the goto nopage. 使用修改后的标志集,再一次调用get_page_from_freelist,试图得到所需的页 若是再次失败,内核会借助更强有力的措施 */ page = get_page_from_freelist(gfp_mask, nodemask, order, zonelist, high_zoneidx, alloc_flags & ~ALLOC_NO_WATERMARKS, preferred_zone, migratetype); if (page) goto got_pg; /* Allocate without watermarks if the context allows */ if (alloc_flags & ALLOC_NO_WATERMARKS) { page = __alloc_pages_high_priority(gfp_mask, order, zonelist, high_zoneidx, nodemask, preferred_zone, migratetype); if (page) goto got_pg; } /* Atomic allocations - we can't balance anything */ if (!wait) goto nopage; /* Avoid recursion of direct reclaim */ if (p->flags & PF_MEMALLOC) goto nopage; /* Avoid allocations with no watermarks from looping endlessly */ if (test_thread_flag(TIF_MEMDIE) && !(gfp_mask & __GFP_NOFAIL)) goto nopage; /* Try direct reclaim and then allocating 尝试回收页面,并继续分配 */ page = __alloc_pages_direct_reclaim(gfp_mask, order, zonelist, high_zoneidx, nodemask, alloc_flags, preferred_zone, migratetype, &did_some_progress); if (page) goto got_pg; /* * If we failed to make any progress reclaiming, then we are running out of options and have to consider going OOM */ if (!did_some_progress) { if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) { if (oom_killer_disabled) goto nopage; page = __alloc_pages_may_oom(gfp_mask, order, zonelist, high_zoneidx, nodemask, preferred_zone, migratetype); if (page) goto got_pg; /* The OOM killer does not trigger for high-order ~__GFP_NOFAIL allocations so if no progress is being made, there are no other options and retrying is unlikely to help. */ if (order > PAGE_ALLOC_COSTLY_ORDER && !(gfp_mask & __GFP_NOFAIL)) goto nopage; goto restart; } } /* Check if we should retry the allocation */ pages_reclaimed += did_some_progress; if (should_alloc_retry(gfp_mask, order, pages_reclaimed)) { /* Wait for some write requests to complete then retry 等待块设备层队列释放,这样内核就有机会换出页 */ congestion_wait(BLK_RW_ASYNC, HZ/50); goto rebalance; } nopage: //不管如何都没法分配内存了,只能打印OOM事件信息了 if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) { printk(KERN_WARNING "%s: page allocation failure. order:%d, mode:0x%x\n", p->comm, order, gfp_mask); dump_stack(); show_mem(); } return page; got_pg: if (kmemcheck_enabled) kmemcheck_pagealloc_alloc(page, order, gfp_mask); return page; }
2. 移除选择的页
若是内核找到适当的内存域,且有足够的空闲页可供分配,那么还有两件事须要完成
1. 必须检查这些页是不是"连续"的,由于到目前为止,只知道有许多空闲页 2. 必须按伙伴系统的方式从free_lists移除这些页,这可能须要分解并重排内存区
内核将该工做委托给"get_page_from_freelist->buffered_rmqueue"完成
/* Really, prep_compound_page() should be called from __rmqueue_bulk(). But we cheat by calling it from here, in the order > 0 path. Saves a branch or two. */ static inline struct page *buffered_rmqueue(struct zone *preferred_zone, struct zone *zone, int order, gfp_t gfp_flags, int migratetype) { unsigned long flags; struct page *page; //若是分配标志设定了__GFP_COLD,那么必须从per-CPU缓存取得冷页 int cold = !!(gfp_flags & __GFP_COLD); int cpu; again: cpu = get_cpu(); //若是只分配一页,内核会进行优化,即分配阶为0 = 2(1)次方的状况,该页不是从伙伴系统直接取得,而是取自per-CPU的页缓存 if (likely(order == 0)) { struct per_cpu_pages *pcp; struct list_head *list; //在只请求一页时,内核试图借助于per-CPU缓存加速请求的处理,若是缓存为空,内核可借机检查缓存的填充水平 pcp = &zone_pcp(zone, cpu)->pcp; list = &pcp->lists[migratetype]; local_irq_save(flags); if (list_empty(list)) { //在针对当前处理器选择了适当的per-CPU列表(热页或冷页列表)以后,调用rmqueue_bulk从新填充缓存 pcp->count += rmqueue_bulk(zone, 0, pcp->batch, list, migratetype, cold); if (unlikely(list_empty(list))) goto failed; } if (cold) page = list_entry(list->prev, struct page, lru); else page = list_entry(list->next, struct page, lru); list_del(&page->lru); pcp->count--; } else { //须要分配多页 if (unlikely(gfp_flags & __GFP_NOFAIL)) { /* * __GFP_NOFAIL is not to be used in new code. * * All __GFP_NOFAIL callers should be fixed so that they * properly detect and handle allocation failures. * * We most definitely don't want callers attempting to * allocate greater than order-1 page units with * __GFP_NOFAIL. */ WARN_ON_ONCE(order > 1); } spin_lock_irqsave(&zone->lock, flags); //__rmqueue会从内存域的伙伴列表中选择适当的内存块,若是有必要,该函数会自动分解大块内存,将未用的部分放回列表中 page = __rmqueue(zone, order, migratetype); spin_unlock(&zone->lock); //若是内存域中有足够的空闲页知足分配请求,但页不是连续的,这种状况,__rmqueue调用失败并返回NULL指针 if (!page) goto failed; __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order)); } __count_zone_vm_events(PGALLOC, zone, 1 << order); zone_statistics(preferred_zone, zone); local_irq_restore(flags); put_cpu(); VM_BUG_ON(bad_range(zone, page)); /* 因为全部失败情形都跳转到标号failed处理,这能够确保内核到达当前点以后,page指向一系列有效的页 在返回指针前,prep_new_page须要作一些准备工做,以便内核可以处理这些页,若是所选择的页出了问题,该函数返回正值,在这种状况下,分配将从头开始 */ if (prep_new_page(page, order, gfp_flags)) goto again; return page; failed: local_irq_restore(flags); put_cpu(); return NULL; }
__rmqueue辅助函数
内核使用了__rmqueue函数,该函数充当进入伙伴系统核心的"看门人"
\linux-2.6.32.63\mm\page_alloc.c
/* Do the hard work of removing an element from the buddy allocator. Call me with the zone->lock already held. */ static struct page *__rmqueue(struct zone *zone, unsigned int order, int migratetype) { struct page *page; retry_reserve: //根据传递进来的分配阶、用于获取页的内存域、迁移类型,__rmqueue_smallest扫描页的列表,直至找到适当的连续内存块 page = __rmqueue_smallest(zone, order, migratetype); if (unlikely(!page) && migratetype != MIGRATE_RESERVE) { //若是指定的迁移列表不能知足分配请求,则调用__rmqueue_fallback尝试其余的迁移列表,做为应急措施 page = __rmqueue_fallback(zone, order, migratetype); /* * Use MIGRATE_RESERVE rather than fail an allocation. goto * is used because __rmqueue_smallest is an inline function * and we want just one call site */ if (!page) { migratetype = MIGRATE_RESERVE; goto retry_reserve; } } trace_mm_page_alloc_zone_locked(page, order, migratetype); return page; }
__rmqueue_smallest的实现不是很长,本质上,它由一个循环组成,按递增顺序遍历内存域的各个特定迁移类型的空闲页列表,直至找到合适的一项
/* Go through the free lists for the given migratetype and remove the smallest available page from the freelists */ static inline struct page *__rmqueue_smallest(struct zone *zone, unsigned int order, int migratetype) { unsigned int current_order; struct free_area * area; struct page *page; /* Find a page of the appropriate size in the preferred list 在首选的列表中找到适当大小的页 搜索从指定分配阶对应的项开始,由于要求页必须是连续的,因此最好的状况也是当前对应分配阶中正好能找到一块内存区 1. 若是检查的列表中有一个元素(非空),那么它就是可用的,由于其中包含了所需数目的连续页 2. 不然,内核将选择下一个更高分配阶,进行相似的搜索 */ for (current_order = order; current_order < MAX_ORDER; ++current_order) { area = &(zone->free_area[current_order]); if (list_empty(&area->free_list[migratetype])) continue; page = list_entry(area->free_list[migratetype].next, struct page, lru); list_del(&page->lru); rmv_page_order(page); area->nr_free--; //若是须要分配的内存块长度小于所选择的连续页范围,即若是由于没有更小的适当的内存块可用,而从较高的分配阶分配了一块内存,那么该内存块必须按照伙伴系统的原理分裂成更小的块,即调用expand expand(zone, page, order, current_order, area, migratetype); return page; } return NULL; }
0x6: 释放页
__free_pages是一个基础函数,用于实现内核API中全部涉及内存释放的函数
\linux-2.6.32.63\mm\page_alloc.c
void __free_pages(struct page *page, unsigned int order) { if (put_page_testzero(page)) { trace_mm_page_free_direct(page, order); /* 首先判断所需释放的内存是单页仍是较大的内存块 1. 若是释放单页,则不还给伙伴系统(在申请内存的时候,若是是单页,也不从伙伴系统中申请,而是从per-CPU中申请),而是置于per-CPU缓存中,对极可能出如今CPU高速缓存的页,则放置到热页的列表中 free_hot_page简单的进行参数转换,随即调用free_hot_cold_page 若是free_hot_cold_page判断per-CPU缓存中页的数目超出了pcp->count,则将数量为pcp->batch的一批内存还给伙伴系统,该策略称之为"惰性合并(lazy coalescing)" 这种策略的原理在于: 若是单页直接返回给伙伴系统,那么会发生合,为了知足后来的分配请求又须要进行拆分,惰性合并策略阻止了大量可能白费的合并和拆分操做 */ if (order == 0) free_hot_page(page); else /* 若是释放多个页,则将工做委托给__free_pages_ok,即将相关的内存区添加到伙伴系统中适当的free_area列表,即合并到为一个连续的内存区,放置到高一阶的free_area列表中 若是还能合并一个进一步的伙伴对,那么继续进行合并,转移到更高阶的列表,该过程会一直重复下去,直至全部可能的伙伴对都已经合并,并将改变尽量向上传播 该策略的核心思想是: 尽可能创造出尽量多的大块连续内存 */ __free_pages_ok(page, order); } }
这里存在的一个问题是内核如何知道一个伙伴对的两个部分都位于空闲页的列表中,为了将内存块放回伙伴系统,内核必须计算"潜在伙伴"的地址,以及在有可能合并的状况下合并后内存块的索引,为此,内核提供了如下几个辅助函数
\linux-2.6.32.63\mm\page_alloc.c
/* * Locate the struct page for both the matching buddy in our pair (buddy1) and the combined O(n+1) page they form (page). * * 1) Any buddy B1 will have an order O twin B2 which satisfies the following equation: * B2 = B1 ^ (1 << O) * For example, if the starting buddy (buddy2) is #8 its order 1 buddy is #10: * B2 = 8 ^ (1 << 1) = 8 ^ 2 = 10 * * 2) Any buddy B will have an order O+1 parent P which satisfies the following equation: * P = B & ~(1 << O) * * Assumption: *_mem_map is contiguous at least up to MAX_ORDER */ static inline struct page *__page_find_buddy(struct page *page, unsigned long page_idx, unsigned int order) { unsigned long buddy_idx = page_idx ^ (1 << order); return page + (buddy_idx - page_idx); } static inline unsigned long __find_combined_index(unsigned long page_idx, unsigned int order) { return (page_idx & ~(1 << order)); } /* * This function checks whether a page is free && is the buddy, we can do coalesce a page and its buddy if * (a) the buddy is not in a hole && * (b) the buddy is in the buddy system && * (c) a page and its buddy have the same order && * (d) a page and its buddy are in the same zone. * * For recording whether a page is in the buddy system, we use PG_buddy. Setting, clearing, and testing PG_buddy is serialized by zone->lock. * For recording page's order, we use page_private(page). */ static inline int page_is_buddy(struct page *page, struct page *buddy, int order) { /* 伙伴的第一页若是在伙伴系统中,则对应的struct page实例会设置PG_buddy标志位,但这不足以做为合并两个伙伴的根据 在释放2(order)次方页的内存块时,内核必须确保第二个伙伴的2(order)次方页也包含在伙伴系统中 */ if (!pfn_valid_within(page_to_pfn(buddy))) return 0; if (page_zone_id(page) != page_zone_id(buddy)) return 0; if (PageBuddy(buddy) && page_order(buddy) == order) { VM_BUG_ON(page_count(buddy) != 0); return 1; } return 0; }
下面代码用于肯定一对伙伴是否可以合并(使用到了辅助函数)
/* * Freeing function for a buddy system allocator. * * The concept of a buddy system is to maintain direct-mapped table(containing bit values) for memory blocks of various "orders". * The bottom level table contains the map for the smallest allocatable units of memory (here, pages), and each level above it describes pairs of units from the levels below, hence, "buddies". * At a high level, all that happens here is marking the table entry at the bottom level available, and propagating the changes upward as necessary, * plus some accounting needed to play nicely with other parts of the VM system. * At each level, we keep a list of pages, which are heads of continuous free pages of length of (1 << order) and marked with PG_buddy. Page's order is recorded in page_private(page) field. * So when we are allocating or freeing one, we can derive the state of the other. * That is, if we allocate a small block, and both were free, the remainder of the region must be split into blocks. * If a block is freed, and its buddy is also free, then this triggers coalescing into a block of larger size. */ static inline void __free_one_page(struct page *page, struct zone *zone, unsigned int order, int migratetype) { unsigned long page_idx; if (unlikely(PageCompound(page))) if (unlikely(destroy_compound_page(page, order))) return; VM_BUG_ON(migratetype == -1); page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1); VM_BUG_ON(page_idx & ((1 << order) - 1)); VM_BUG_ON(bad_range(zone, page)); //例程试图释放分配阶为order的一个内存块,由于有可能不仅当前内存块可以与其直接伙伴合并,并且高阶的伙伴也能够合并,所以内核须要找到可能的"最大分配阶" while (order < MAX_ORDER-1) { unsigned long combined_idx; struct page *buddy; buddy = __page_find_buddy(page, page_idx, order); if (!page_is_buddy(page, buddy, order)) break; /* Our buddy is free, merge with it and move up one order. */ list_del(&buddy->lru); zone->free_area[order].nr_free--; rmv_page_order(buddy); combined_idx = __find_combined_index(page_idx, order); page = page + (combined_idx - page_idx); page_idx = combined_idx; order++; } set_page_order(page, order); list_add(&page->lru, &zone->free_area[order].free_list[migratetype]); zone->free_area[order].nr_free++; }
0x7: 内核中不连续页的分配
咱们知道,在内存分配中,物理上连续的映射对内核是最好的,但并不总能成功地申请到,在分配一大块内存时,可能不遗余力也没法找到连续的内存块。在用户空间这不是问题,由于Ring3的应用程序老是使用内核提供的虚拟内存分页机制(固然这会下降速度并占用TLB)
在内核遇到没法得到一整块连续的大块内存的时候用,也使用了一样的技术,内核分配了其虚拟地址空间的一部分,用于创建连续映射,例以下图的IA-32系统
vmalloc管理的是一段不连续的内存区域,这一段具备线性地址空间的全部性质,分配到其中的页可能位于物理内存中的任何地方,经过修改负责该区域的内核页表,便可作到这一点
每一个vmalloc分配的子区域都是"自包含"的,与其余vmalloc子区域经过一个内存页分隔(相似于直接映射和vmalloc区域之间的边界),不一样vmalloc子区域之间的分隔也是为了防止不正确的内存访问操做(这种状况只会由于内核故障而出现)
1. 用vmalloc分配内存
vmalloc是一个接口函数,内核代码使用它来分配在虚拟内存中连续但在物理内存中不必定连续的内存,使用vmalloc的最著名的实例是内核对模块(LKM)的实现,由于模块可能在任什么时候候加载,若是模块数据比较多,那么没法保证有足够的连续内存可用,特别是系统已经运行了比较长的时间的状况下,若是可以用小块内存拼接出足够的内存,那么使用vmalloc能够规避该问题
关于vmalloc函数的相关编程使用方法请参阅另外一篇文章 http://www.cnblogs.com/LittleHann/p/4113830.html
由于用于vmalloc的内存页老是必须映射在内核地址空间中,所以使用ZONE_HIGHMEM内存域的页是最优的选择,这使得内核能够节省更宝贵的较低端内存域,而又不会带来额外的好处,所以,vmalloc是内核出于自身的目的使用高端内存页的少数情形之一
数据结构
内核在管理虚拟内存中的vmalloc区域时,内核必须跟踪哪些子区域被使用、哪些是空闲的。为此定义了一个数据结构(strcut vm_struct),将全部使用的部分保存在一个链表中,注意和用户空间进程的虚拟地址存储结构struct vm_area_struct区分开来
\linux-2.6.32.63\include\linux\vmalloc.h
struct vm_struct { //next使得内核能够将vmalloc区域中的全部子区域保存在一个单链表上 struct vm_struct *next; //addr定义了分配的子区域在虚拟地址空间中的起始地址 void *addr; //size表示该区域的长度,能够根据该信息获得vmalloc区域的完成分配方案 unsigned long size; /* flags存储了与该内存区关联的标志联合,它只用于指定内存区类型 1. VM_ALLOC: 指定由vmalloc产生的子区域 2. VM_MAP: 用于表示将现存pages集合映射到连续的虚拟地址空间汇总 3. VM_TOREMAP: 表示将几乎随机的物理内存区域映射到vmalloc区域中,这是一个特定于体系结构的操做 */ unsigned long flags; //pages是指向page指针数组的指针,每一个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例 struct page **pages; //nr_pages指定pages中数组项的数目,即涉及的内存页数目 unsigned int nr_pages; //phys_addr仅当用ioremap映射到由物理地址描述的物理内存区域时才须要,该信息保存在phys_addr中 unsigned long phys_addr; void *caller; };
建立vm_area
在建立一个新的虚拟内存区以前,必须找到一个适当的位置,vm_area实例组成的一个链表,管理着vmalloc区域中已经创建的各个子区域,内核全局变量vmlist是表头
\linux-2.6.32.63\mm\vmalloc.c
/*** Old vmalloc interfaces ***/ DEFINE_RWLOCK(vmlist_lock); struct vm_struct *vmlist;
内核提供了辅助函数get_vm_area
/** * get_vm_area - reserve a contiguous kernel virtual area * @size: size of the area * @flags: %VM_IOREMAP for I/O mappings or VM_ALLOC * * Search an area of @size in the kernel virtual mapping area, and reserved it for out purposes. Returns the area descriptor on success or %NULL on failure. */ struct vm_struct *get_vm_area(unsigned long size, unsigned long flags) { return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END, -1, GFP_KERNEL, __builtin_return_address(0)); } static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long align, unsigned long flags, unsigned long start, unsigned long end, int node, gfp_t gfp_mask, void *caller) { static struct vmap_area *va; struct vm_struct *area; BUG_ON(in_interrupt()); if (flags & VM_IOREMAP) { int bit = fls(size); if (bit > IOREMAP_MAX_ORDER) bit = IOREMAP_MAX_ORDER; else if (bit < PAGE_SHIFT) bit = PAGE_SHIFT; align = 1ul << bit; } size = PAGE_ALIGN(size); if (unlikely(!size)) return NULL; area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); if (unlikely(!area)) return NULL; /* We always allocate a guard page. 老是分配一个警惕页,做为安全隙,内核首先适当提升须要分配的内存长度 */ size += PAGE_SIZE; va = alloc_vmap_area(size, align, start, end, node, gfp_mask); if (IS_ERR(va)) { kfree(area); return NULL; } /* * When this function is called from __vmalloc_node, * we do not add vm_struct to vmlist here to avoid * accessing uninitialized members of vm_struct such as * pages and nr_pages fields. They will be set later. * To distinguish it from others, we use a VM_UNLIST flag. */ if (flags & VM_UNLIST) setup_vmalloc_vm(area, va, flags, caller); else insert_vmalloc_vm(area, va, flags, caller); return area; }
remove_vm_area函数将一个现存的子区域从vmlloc地址空间中删除
\linux-2.6.32.63\mm\vmalloc.c
/** * remove_vm_area - find and remove a continuous kernel virtual area * @addr: base address * * Search for the kernel VM area starting at @addr, and remove it. This function returns the found VM area, but using it is NOT safe on SMP machines, except for its size or flags. */ struct vm_struct *remove_vm_area(const void *addr) { struct vmap_area *va; //依次扫描vmlist的链表元素,直至找到匹配者 va = find_vmap_area((unsigned long)addr); if (va && va->flags & VM_VM_AREA) { struct vm_struct *vm = va->private; if (!(vm->flags & VM_UNLIST)) { struct vm_struct *tmp, **p; /* * remove from list and disallow access to * this vm_struct before unmap. (address range * confliction is maintained by vmap.) */ write_lock(&vmlist_lock); for (p = &vmlist; (tmp = *p) != vm; p = &tmp->next) ; *p = tmp->next; write_unlock(&vmlist_lock); } vmap_debug_free_range(va->va_start, va->va_end); free_unmap_vmap_area(va); vm->size -= PAGE_SIZE; return vm; } return NULL; }
分配内存区
vmalloc发起对不连续的内存区的分配操做
/** * 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(size, 1, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL, -1, __builtin_return_address(0)); } EXPORT_SYMBOL(vmalloc); void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot) { return __vmalloc_node(size, 1, gfp_mask, prot, -1, __builtin_return_address(0)); } EXPORT_SYMBOL(__vmalloc); /** * __vmalloc_node - allocate virtually contiguous memory * @size: allocation size * @align: desired alignment * @gfp_mask: flags for the page level allocator * @prot: protection mask for the allocated pages * @node: node to use for allocation or -1 * @caller: caller's return address * * Allocate enough pages to cover @size from the page level * allocator with @gfp_mask flags. Map them into contiguous * kernel virtual space, using a pagetable protection of @prot. */ static void *__vmalloc_node(unsigned long size, unsigned long align, gfp_t gfp_mask, pgprot_t prot, int node, void *caller) { struct vm_struct *area; void *addr; unsigned long real_size = size; size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > totalram_pages) return NULL; //在vmlloc地址空间中找到一个适当的区域 area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNLIST, VMALLOC_START, VMALLOC_END, node, gfp_mask, caller); if (!area) return NULL; //从物理内存中分配各个页 addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller); /* In this function, newly allocated vm_struct is not added to vmlist at __get_vm_area_node(). so, it is added here. 将这些页连续地映射到vmalloc区域中 */ insert_vmalloc_vmlist(area); /* * A ref_count = 3 is needed because the vm_struct and vmap_area * structures allocated in the __get_vm_area_node() function contain * references to the virtual address of the vmalloc'ed block. */ kmemleak_alloc(addr, real_size, 3, gfp_mask); return addr; }
对于vmalloc的使用,咱们须要注意的是,要理解它的设计思想和使用场景
使用vmalloc从伙伴系统分配内存时,是逐页分配的,而不是一次分配一大块连续内存,这是vmalloc的一个关键方面。若是能够确信可以分配连续内存页,那么就没有必要使用vmalloc,毕竟该函数的全部目的就在于: 即便由于内存碎片的缘故,内存块中的页帧可能不是连续的,仍是依旧可以分配大的内存块。将分配单位拆分得尽量小(即以页为单位),能够确保在物理内存有严重碎片的状况下,vmalloc仍然能够工做
2. 备选映射方法
除了vmalloc以外,还有其余方法能够建立虚拟连续映射,这都基于__vmalloc函数或使用相似的机制
1. vmalloc_32 vmalloc_32的工做方式与vmalloc相同,但会确保所使用的物理内存老是能够用普通32位指针寻址 2. vmap 使用一个page数组做为起点,来建立虚拟连续内存区,与vmalloc相比,该函数所用的物理内存位置不是隐式分配的,而须要先行分配好,做为参数传递 3. ioremap ioremap是一个特定于处理器的函数,必须在全部体系结构上实现,它能够取自物理地址空间、由系统总线用于I/O操做的一个内存块,映射到内核的地址空间中 //该函数在设备驱动程序中使用不少,可将用于与外设通讯的地址区域暴露给内核的其余部分使用
3. 释放内存
有两个函数用于向内核释放内存,这两个函数都会规约到__vunmap
1. vfree用于释放vmalloc和vmalloc_32分配的区域 2. vunmap用于释放由vmap或ioremap建立的映射
\linux-2.6.32.63\mm\vmalloc.c
/* 1. addr: 表示要释放的区域的起始地址 2. deallocate_pages: 指定了是否将与该区域相关的物理内存页返回给伙伴系统 1) vfree: 将这个参数设置为1 2) vunmap: 将这个参数设置为0,即只删除映射,而不将相关的物理内存页返回给伙伴系统 */ static void __vunmap(const void *addr, int deallocate_pages) { struct vm_struct *area; if (!addr) return; if ((PAGE_SIZE-1) & (unsigned long)addr) { WARN(1, KERN_ERR "Trying to vfree() bad address (%p)\n", addr); return; } //没必要明确给出须要释放的区域长度,长度能够从vmlist中的信息导出,remove_vm_area扫描该链表,以找到相关项 area = remove_vm_area(addr); if (unlikely(!area)) { WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n", addr); return; } debug_check_no_locks_freed(addr, area->size); debug_check_no_obj_freed(addr, area->size); if (deallocate_pages) { int i; for (i = 0; i < area->nr_pages; i++) { struct page *page = area->pages[i]; BUG_ON(!page); __free_page(page); } //释放用于管理该内存区的内核数据结构 if (area->flags & VM_VPAGES) vfree(area->pages); else kfree(area->pages); } kfree(area); return; }
0x8: 内核映射
尽管vmalloc函数族能够用于从高端内存域向内核映射映射页帧,但这并非这些函数的实际用途,内核提供了其余函数用于将ZONE_HIGHMEM页帧显式映射到内核空间
1. 持久内核映射
若是须要将高端页帧长期映射(做为持久映射)到内核地址空间中,必须使用kmap函数,须要映射的页用指向page的指针指定,做为该函数的参数。该函数在有必要时建立一个映射(即若是该页确实是高端页),并返回数据的地址
若是没有启用高端支持,该函数的任务就比较简单,在这种状况下,全部页均可以直接访问,所以只须要返回页的地址,无需显式建立一个映射
若是确实存在高端页,状况会比较复杂,相似于vmalloc,内核首先必须创建高端页和所映射到的地址之间的关联,还必须在虚拟地址空间中分配一个区域以映射页帧,最后,内核必须记录该虚拟区域的哪些部分在使用中,哪些部分仍然是空闲的
2. 临时内核映射
kmap函数不能用于中断处理程序,由于它可能进入睡眠状态。若是pkmap数组中没有空闲位置,该函数会进入睡眠状态,直至情形有所改善。所以内核提供了一个备选的映射函数,其执行是原子的,逻辑上称kmap_atomic,该函数的一个主要优势就是它比普通的kmap快速,但它不能用于可能进入睡眠的代码,所以,他对于很快就须要一个临时页的简短代码,是很是理想的
3. 没有高端内存的计算机上的映射函数
在许多体系结构上不支持高端内存,由于不须要该特性,例如64位体系结构,但为了在使用内核映射函数的时候不须要老是区分高端内存和非高端内存体系结构,内核定义了几个在普通内存实现兼容函数的宏
\linux-2.6.32.63\include\linux\highmem.h
#ifdef CONFIG_HIGHMEM #include <asm/highmem.h> /* declarations for linux/mm/highmem.c */ unsigned int nr_free_highpages(void); extern unsigned long totalhigh_pages; void kmap_flush_unused(void); #else /* CONFIG_HIGHMEM */ static inline unsigned int nr_free_highpages(void) { return 0; } #define totalhigh_pages 0 #ifndef ARCH_HAS_KMAP static inline void *kmap(struct page *page) { might_sleep(); return page_address(page); } static inline void kunmap(struct page *page) { } static inline void *kmap_atomic(struct page *page, enum km_type idx) { pagefault_disable(); return page_address(page); } #define kmap_atomic_prot(page, idx, prot) kmap_atomic(page, idx) #define kunmap_atomic(addr, idx) do { pagefault_enable(); } while (0) #define kmap_atomic_pfn(pfn, idx) kmap_atomic(pfn_to_page(pfn), (idx)) #define kmap_atomic_to_page(ptr) virt_to_page(ptr) #define kmap_flush_unused() do {} while(0) #endif #endif /* CONFIG_HIGHMEM */
4. SLAB分配器
内核须要常常分配内存,但没法借助于标准库函数(例如C库中的malloc),由于标准库中的函数是基于伙伴系统提供的按页分配内存,但这个单位太大了(即 只能是2的n次方阶的内存块分配),若是仅仅须要一个10个字符的字符串分配空间,分配一个4KB或者更多空间的完整页面,这是彻底不可接受的。为此必须 引入新的管理机制,这会给内核带来更大的开销,为了最小化这个额外负担对系统性能的影响,该管理层的实现应该尽量紧凑以便不要对处理器的高速缓存和 TLB带来显著影响,同时,内核还必须保证内存利用的速度和效率,解决这些问题的一个较好的方案就是SLAB分配,它对许多种类的工做负荷都很是高效
须要明白的是,提供小内存块不是SLAB分配器的惟一任务,因为结构上的特色,它也用做一个缓存,主要针对常常分配并释放的对象,经过创建SLAB缓存,内核可以储备一些对象,供后续使用,即便在初始化阶段也是如此,例如
/* 为了管理与进程关联的文件系统数据,内核必须常常生成strcut fs_struct的新实例,此类型实例占据的内存块一样须要常常回收(在进程结束时),换句话说,内核趋向于很是有规律地分配并释放大小为sizeof(fs_struct)的内存块。SLAB分配器将释放的内存块保存在一个内部列表里,并不立刻返回给伙伴系统,在请求为该类对象分配一个新实例时,会使用最近释放的内存块,这带来2个优势 */ 1. 因为内核没必要使用伙伴系统算法,处理时间会变短(伙伴系统的阶次分配和递归的阶次合并相对较消耗时间) 2. 因为该内存块仍让是"新"的,所以其仍然驻留在CPU高速缓存的几率较高 3. 调用伙伴系统的操做对系统的数据和指令高速缓存有至关的影响,内核越浪费这些资源,这些资源对用户空间进程就越不可用,更轻量级的SLAB分配器在可能的状况下减小了对伙伴系统的调用,有助于防止"缓存污染" 4. 若是数据存储在伙伴系统直接提供的页中,那么其地址老是出如今2的幂次的整数倍附近,这对CPU高速缓存的利用有负面影响,因为这种地址分布,使得某些缓存行过分使用,而其余的则几乎为空,多处理器可能会加重这种不利状况,由于不一样的内存地址可能在不一样的总线上传输,多处理会致使某些总线拥塞,而其余的总线几乎没有使用
经过"SLAB着色(SLAB coloring)",SLAB分配器可以均匀的分布对象(让对象在分配的内存块区域中均匀分布,不要老是位于2的幂次起始位置),以实现均匀的缓存使用
常用的内核对象保存在CPU高速缓存中,这是咱们想要的结果。着色这个术语是隐喻性的,它与颜色无关,只是表示SLAB中的对象须要移动的特定偏移量,以便使对象放置到不一样的缓存行
值得注意的是,之因此称之为SLAB分配器,是由于各个缓存管理的对象,会合并为较大的组,覆盖一个或多个连续页帧,这种组称做SLAB,每一个缓存由这几个这种SLAB组成
0x1: 备选分配器
尽管SLAB分配器对许多可能的工做负荷都工做良好,但也有一些场景它没法提供最优性能,若是某些计算机处于当前硬件尺度的边界上,在此类计算机上使用SLAB分配器会出现一些问题,例如
1. 微小的嵌入式系统: SLAB分配器代码量和复杂性都过高 2. 配备有大量物理内存的大规模并行系统: SLAB分配器所需的大量元数据可能成为一个问题
为了解决这个问题,LINUX KERNEL增长SLAB分配器的2个替代品
1. SLOB分配器(simple linked list of block)进行了特别优化,以便减小代码量,它围绕一个简单的内存块链表展开,在分配内存时,使用了一样简单的最早适配算法 SLOB分配器只有大约600行代码,十分的简单 2. SLUB分配器经过将页帧打包为组,并经过struct page中未使用的字段来管理这些组,试图最小化所需的内存开销
因为SLAB分配器是大多数内核配置的默认选项,但有一点须要强调,内核的其他部分无须关注底层选择使用了哪一个分配器,全部分配器的前端接口都是相同的,每一个分配器都必须实现一组特定的函数,用于内存分配和缓存
1. kmalloc、_kmalloc、kmalloc_node: 用于通常的(用于特定结点)内存分配函数 2. kmem_cache_alloc、kmem_cache_alloc_node: 提供(特定于结点)特定类型的内核缓存
使用这些标准函数,内核能够提供更方便的函数,而不涉及内存在内部具体如何管理(例如kcalloc为数组分配内存、kzalloc分配一个填充字节0的内存区)
普通内核代码只须要包含slab.h,便可使用内存分配的全部标准内核函数,联编系统会保证使用编译时选择的分配器,来知足程序的内存分配请求
0x2: 内核中的内存管理
内核中通常的内存分配和释放函数与C标准库中等价函数的名称相似,用法也几乎相同
1. kmalloc(size, flags) 分配长度为size字节的一个内存区,并返回指向该内存区起始处的一个void指针,若是没有足够内存,则结果为NULL指针 2. kfree(*ptr) 释放*ptr指向的内存区 3. vmalloc 4. vfree 5. percpu_alloc 6. percpu_free //用于为各个系统CPU分配和释放所需内存区
kmalloc在内核源代码中的使用数以千计,但模式都是相同的,用kmalloc分配的内存区,首先经过类型转换为正确的类型,而后赋值到指针指针变量
info = (struct cdrom_info *) kmalloc( sizeof(struct cdrom_info), GFP_KERNEL );
从程序员的角度来讲,创建和使用缓存的任务不是特别困难,必须首先使用kmem_cache_create创建一个适当的缓存,接下来便可使用kmeme_cache_alloc、kmem_cache_free分配和释放其中包含的对象。SLAB分配器负责完成与伙伴系统的交互,来分配所需的页,全部活动缓存的列表保存在/proc/slabinfo中
cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> ip_conntrack_expect 0 0 136 28 1 : tunables 120 60 8 : slabdata 0 0 0 ip_conntrack 24 26 304 13 1 : tunables 54 27 8 : slabdata 2 2 0 ip_fib_alias 16 59 64 59 1 : tunables 120 60 8 : slabdata 1 1 0 ip_fib_hash 16 59 64 59 1 : tunables 120 60 8 : slabdata 1 1 0 bio_map_info 100 105 1064 7 2 : tunables 24 12 8 : slabdata 15 15 0 dm_mpath 0 0 1064 7 2 : tunables 24 12 8 : slabdata 0 0 0 jbd_4k 2 2 4096 1 1 : tunables 24 12 8 : slabdata 2 2 0 dm_uevent 0 0 2608 3 2 : tunables 24 12 8 : slabdata 0 0 0 dm_tio 0 0 24 144 1 : tunables 120 60 8 : slabdata 0 0 0 dm_io 0 0 48 77 1 : tunables 120 60 8 : slabdata 0 0 0 sgpool-128 32 32 4096 1 1 : tunables 24 12 8 : slabdata 32 32 0 sgpool-64 32 32 2048 2 1 : tunables 24 12 8 : slabdata 16 16 0 sgpool-32 32 32 1024 4 1 : tunables 54 27 8 : slabdata 8 8 0 sgpool-16 32 32 512 8 1 : tunables 54 27 8 : slabdata 4 4 0 sgpool-8 32 45 256 15 1 : tunables 120 60 8 : slabdata 3 3 0 scsi_io_context 0 0 112 34 1 : tunables 120 60 8 : slabdata 0 0 0 ext3_inode_cache 15169 15185 760 5 1 : tunables 54 27 8 : slabdata 3037 3037 0 ext3_xattr 0 0 88 44 1 : tunables 120 60 8 : slabdata 0 0 0 journal_handle 76 144 24 144 1 : tunables 120 60 8 : slabdata 1 1 0 journal_head 112 160 96 40 1 : tunables 120 60 8 : slabdata 4 4 0 revoke_table 2 202 16 202 1 : tunables 120 60 8 : slabdata 1 1 0 revoke_record 0 0 32 112 1 : tunables 120 60 8 : slabdata 0 0 0 uhci_urb_priv 0 0 56 67 1 : tunables 120 60 8 : slabdata 0 0 0 UNIX 44 44 704 11 2 : tunables 54 27 8 : slabdata 4 4 0 flow_cache 0 0 128 30 1 : tunables 120 60 8 : slabdata 0 0 0 cfq_ioc_pool 43 90 128 30 1 : tunables 120 60 8 : slabdata 3 3 0 cfq_pool 40 90 216 18 1 : tunables 120 60 8 : slabdata 5 5 0 crq_pool 62 96 80 48 1 : tunables 120 60 8 : slabdata 2 2 0 deadline_drq 0 0 80 48 1 : tunables 120 60 8 : slabdata 0 0 0 as_arq 0 0 96 40 1 : tunables 120 60 8 : slabdata 0 0 0 mqueue_inode_cache 1 4 896 4 1 : tunables 54 27 8 : slabdata 1 1 0 isofs_inode_cache 0 0 608 6 1 : tunables 54 27 8 : slabdata 0 0 0 hugetlbfs_inode_cache 1 7 576 7 1 : tunables 54 27 8 : slabdata 1 1 0 ext2_inode_cache 0 0 720 5 1 : tunables 54 27 8 : slabdata 0 0 0 ext2_xattr 0 0 88 44 1 : tunables 120 60 8 : slabdata 0 0 0 dnotify_cache 0 0 40 92 1 : tunables 120 60 8 : slabdata 0 0 0 dquot 0 0 256 15 1 : tunables 120 60 8 : slabdata 0 0 0 eventpoll_pwq 3 53 72 53 1 : tunables 120 60 8 : slabdata 1 1 0 eventpoll_epi 2 20 192 20 1 : tunables 120 60 8 : slabdata 1 1 0 inotify_event_cache 63 92 40 92 1 : tunables 120 60 8 : slabdata 1 1 0 inotify_watch_cache 11 53 72 53 1 : tunables 120 60 8 : slabdata 1 1 0 kioctx 0 0 320 12 1 : tunables 54 27 8 : slabdata 0 0 0 kiocb 0 0 256 15 1 : tunables 120 60 8 : slabdata 0 0 0 fasync_cache 0 0 24 144 1 : tunables 120 60 8 : slabdata 0 0 0 shmem_inode_cache 224 250 768 5 1 : tunables 54 27 8 : slabdata 50 50 0 posix_timers_cache 0 0 128 30 1 : tunables 120 60 8 : slabdata 0 0 0 uid_cache 2 30 128 30 1 : tunables 120 60 8 : slabdata 1 1 0 ip_mrt_cache 0 0 128 30 1 : tunables 120 60 8 : slabdata 0 0 0 tcp_bind_bucket 21 112 32 112 1 : tunables 120 60 8 : slabdata 1 1 0 inet_peer_cache 2 30 128 30 1 : tunables 120 60 8 : slabdata 1 1 0 secpath_cache 0 0 64 59 1 : tunables 120 60 8 : slabdata 0 0 0 xfrm_dst_cache 0 0 384 10 1 : tunables 54 27 8 : slabdata 0 0 0 ip_dst_cache 47 60 384 10 1 : tunables 54 27 8 : slabdata 6 6 0 arp_cache 3 15 256 15 1 : tunables 120 60 8 : slabdata 1 1 0 RAW 19 20 768 5 1 : tunables 54 27 8 : slabdata 4 4 0 UDP 5 5 768 5 1 : tunables 54 27 8 : slabdata 1 1 0 tw_sock_TCP 19 20 192 20 1 : tunables 120 60 8 : slabdata 1 1 0 request_sock_TCP 0 0 128 30 1 : tunables 120 60 8 : slabdata 0 0 0 TCP 10 10 1600 5 2 : tunables 24 12 8 : slabdata 2 2 0 blkdev_ioc 35 118 64 59 1 : tunables 120 60 8 : slabdata 2 2 0 blkdev_queue 23 25 1576 5 2 : tunables 24 12 8 : slabdata 5 5 0 blkdev_requests 41 70 272 14 1 : tunables 54 27 8 : slabdata 5 5 0 biovec-256 7 7 4096 1 1 : tunables 24 12 8 : slabdata 7 7 0 biovec-128 7 8 2048 2 1 : tunables 24 12 8 : slabdata 4 4 0 biovec-64 7 8 1024 4 1 : tunables 54 27 8 : slabdata 2 2 0 biovec-16 7 15 256 15 1 : tunables 120 60 8 : slabdata 1 1 0 biovec-4 7 59 64 59 1 : tunables 120 60 8 : slabdata 1 1 0 biovec-1 49 202 16 202 1 : tunables 120 60 8 : slabdata 1 1 0 bio 300 360 128 30 1 : tunables 120 60 8 : slabdata 12 12 0 utrace_engine_cache 0 0 64 59 1 : tunables 120 60 8 : slabdata 0 0 0 utrace_cache 0 0 64 59 1 : tunables 120 60 8 : slabdata 0 0 0 sock_inode_cache 96 96 640 6 1 : tunables 54 27 8 : slabdata 16 16 0 skbuff_fclone_cache 21 21 512 7 1 : tunables 54 27 8 : slabdata 3 3 0 skbuff_head_cache 498 555 256 15 1 : tunables 120 60 8 : slabdata 37 37 0 file_lock_cache 5 22 176 22 1 : tunables 120 60 8 : slabdata 1 1 0 Acpi-Operand 3248 3304 64 59 1 : tunables 120 60 8 : slabdata 56 56 0 Acpi-ParseExt 0 0 64 59 1 : tunables 120 60 8 : slabdata 0 0 0 Acpi-Parse 0 0 40 92 1 : tunables 120 60 8 : slabdata 0 0 0 Acpi-State 0 0 80 48 1 : tunables 120 60 8 : slabdata 0 0 0 Acpi-Namespace 2199 2240 32 112 1 : tunables 120 60 8 : slabdata 20 20 0 delayacct_cache 225 295 64 59 1 : tunables 120 60 8 : slabdata 5 5 0 taskstats_cache 13 53 72 53 1 : tunables 120 60 8 : slabdata 1 1 0 proc_inode_cache 1188 1212 592 6 1 : tunables 54 27 8 : slabdata 202 202 3 sigqueue 96 96 160 24 1 : tunables 120 60 8 : slabdata 4 4 0 radix_tree_node 6432 6433 536 7 1 : tunables 54 27 8 : slabdata 919 919 0 bdev_cache 24 28 832 4 1 : tunables 54 27 8 : slabdata 7 7 0 sysfs_dir_cache 3468 3520 88 44 1 : tunables 120 60 8 : slabdata 80 80 0 mnt_cache 28 30 256 15 1 : tunables 120 60 8 : slabdata 2 2 0 inode_cache 1267 1281 560 7 1 : tunables 54 27 8 : slabdata 183 183 0 dentry_cache 23148 23148 216 18 1 : tunables 120 60 8 : slabdata 1286 1286 0 filp 600 1185 256 15 1 : tunables 120 60 8 : slabdata 79 79 0 names_cache 25 25 4096 1 1 : tunables 24 12 8 : slabdata 25 25 0 avc_node 34 53 72 53 1 : tunables 120 60 8 : slabdata 1 1 0 selinux_inode_security 17931 18000 80 48 1 : tunables 120 60 8 : slabdata 375 375 0 key_jar 5 20 192 20 1 : tunables 120 60 8 : slabdata 1 1 0 idr_layer_cache 100 105 528 7 1 : tunables 54 27 8 : slabdata 15 15 0 buffer_head 43856 43880 96 40 1 : tunables 120 60 8 : slabdata 1097 1097 0 mm_struct 76 76 896 4 1 : tunables 54 27 8 : slabdata 19 19 0 vm_area_struct 1632 1914 176 22 1 : tunables 120 60 8 : slabdata 87 87 180 fs_cache 177 177 64 59 1 : tunables 120 60 8 : slabdata 3 3 60 files_cache 75 80 768 5 1 : tunables 54 27 8 : slabdata 16 16 0 signal_cache 122 144 832 9 2 : tunables 54 27 8 : slabdata 16 16 0 sighand_cache 87 87 2112 3 2 : tunables 24 12 8 : slabdata 29 29 0 task_struct 136 136 1888 2 1 : tunables 24 12 8 : slabdata 68 68 0 anon_vma 651 1440 24 144 1 : tunables 120 60 8 : slabdata 10 10 15 pid 242 295 64 59 1 : tunables 120 60 8 : slabdata 5 5 0 shared_policy_node 0 0 48 77 1 : tunables 120 60 8 : slabdata 0 0 0 numa_policy 38 144 24 144 1 : tunables 120 60 8 : slabdata 1 1 0 size-131072(DMA) 0 0 131072 1 32 : tunables 8 4 0 : slabdata 0 0 0 size-131072 0 0 131072 1 32 : tunables 8 4 0 : slabdata 0 0 0 size-65536(DMA) 0 0 65536 1 16 : tunables 8 4 0 : slabdata 0 0 0 size-65536 0 0 65536 1 16 : tunables 8 4 0 : slabdata 0 0 0 size-32768(DMA) 0 0 32768 1 8 : tunables 8 4 0 : slabdata 0 0 0 size-32768 3 3 32768 1 8 : tunables 8 4 0 : slabdata 3 3 0 size-16384(DMA) 0 0 16384 1 4 : tunables 8 4 0 : slabdata 0 0 0 size-16384 5 5 16384 1 4 : tunables 8 4 0 : slabdata 5 5 0 size-8192(DMA) 0 0 8192 1 2 : tunables 8 4 0 : slabdata 0 0 0 size-8192 13 13 8192 1 2 : tunables 8 4 0 : slabdata 13 13 0 size-4096(DMA) 0 0 4096 1 1 : tunables 24 12 8 : slabdata 0 0 0 size-4096 130 130 4096 1 1 : tunables 24 12 8 : slabdata 130 130 0 size-2048(DMA) 0 0 2048 2 1 : tunables 24 12 8 : slabdata 0 0 0 size-2048 237 244 2048 2 1 : tunables 24 12 8 : slabdata 122 122 0 size-1024(DMA) 0 0 1024 4 1 : tunables 54 27 8 : slabdata 0 0 0 size-1024 752 752 1024 4 1 : tunables 54 27 8 : slabdata 188 188 27 size-512(DMA) 0 0 512 8 1 : tunables 54 27 8 : slabdata 0 0 0 size-512 405 416 512 8 1 : tunables 54 27 8 : slabdata 52 52 0 size-256(DMA) 0 0 256 15 1 : tunables 120 60 8 : slabdata 0 0 0 size-256 1080 1080 256 15 1 : tunables 120 60 8 : slabdata 72 72 0 size-128(DMA) 0 0 128 30 1 : tunables 120 60 8 : slabdata 0 0 0 size-64(DMA) 0 0 64 59 1 : tunables 120 60 8 : slabdata 0 0 0 size-64 2011 2183 64 59 1 : tunables 120 60 8 : slabdata 37 37 0 size-32(DMA) 0 0 32 112 1 : tunables 120 60 8 : slabdata 0 0 0 size-128 1316 1380 128 30 1 : tunables 120 60 8 : slabdata 46 46 0 size-32 2155 2352 32 112 1 : tunables 120 60 8 : slabdata 21 21 0 kmem_cache 134 134 2688 1 1 : tunables 24 12 8 : slabdata 134 134 0
输出的各列信息包括
1. 用于标识各个缓存的字符串名称(确保不会建立相同的缓存) 2. 缓存中活动对象的数量 3. 缓存中对象的总数(已用、未用) 4. 所管理对象的长度,按字节计算 5. 一个SLAB中对象的数量 6. 每一个SLAB中页的数量 7. 活动SLAB的数量 8. 在内核决定向缓存分配更多内存时,所分配对象的数量。每次会分配一个较大的内存块,以减小与伙伴系统的交互,在缩小缓存时,也使用该值做为释放内存块的大小
0x3: SLAB分配的原理
SLAB分配器由一个紧密地交织的数据和内存结构的网络组成
1. 保存管理性数据的缓存对象 2. 保存被管理对象的各个SLAB
每一个缓存只负责一种对象类型(例如struct unix_sock实例),或提供通常性的缓冲区。各个缓存中SLAB的数目各有不一样,这与已经使用的页的数目、对象长度、被管理对象的数目有关
能够看到,系统中全部的缓存都保存在一个双链表中,这使得内核有机会依次遍历全部的缓存,这是有必要的,例如在即将发生内存不足时,内核可能须要缩减分配给缓存的内存数量
1. 缓存的精细结构
咱们更加仔细地研究一下缓存的结构,kmem_cache是Linux内核提供的快速内存缓冲接口,这些内存块要求是大小相同的,由于分配出的内存在接口释放时并不真正释放,而是做为缓存保留,下一次请求分配时就能够直接使用,省去了各类内存块初始化或释放的操做,所以分配速度很快,一般用于大数量的内存块分配的状况,如inode节点、skbuff头、netfilter的链接等,其实kmalloc也是从kmem_cache中分配的,可通
过/proc/slabinfo文件直接读取cache分配状况
struct kmem_cache { /* 1) per-cpu data, touched during every alloc/free 指向一个数组,每次分配/释放期间都会访问,其中包含了与系统CPU数目相同的数组项,每一个元素都是一个指针,指向一个数组缓存(array cache),其中包含了对应于特定系统CPU的管理数据 struct array_cache { unsigned int avail; unsigned int limit; unsigned int batchcount; unsigned int touched; spinlock_t lock; void *entry[0]; }; 为最好地利用CPU高速缓存(即尽量地去访问热点数据,CPU高速缓存是一种硬件机制,而内核须要作的是尽量遵循正确地使用方式操做内存,最大化地利用这个硬件机制) 这些per-CPU指针是很重要的,在分配和释放对象时,采用后进先出原理(LIFO last in first out),内核假定刚释放的对象仍然处于CPU高速缓存中,会尽快再次分配它(响应下一个分配请求) 仅当per-CPU缓存为空时,才会用SLAB中的空闲对象从新填充它们 */ struct array_cache *array[NR_CPUS]; /* 2) Cache tunables. Protected by cache_chain_mutex 可调整的缓存参数,由cache_chain_mutex保护 */ //batchcount指定了在per-CPU列表为空的状况下,从缓存的SLAB中获取对象的数目,它还表示在缓存增加时分配的对象数目 unsigned int batchcount; //limit指定了per-CPU列表中保存的对象的最大数目,若是超出该值,内核会将batchcount个对象返回到SLAB,若是接下来内核缩减缓存,则释放的内存从SLAB返回到伙伴系统 unsigned int limit; unsigned int shared; //buffer_size指定了缓存中管理的对象的长度 unsigned int buffer_size; u32 reciprocal_buffer_size; /* 3) touched by every alloc & free from the backend */ unsigned int flags; /* constant flags 常数标志*/ unsigned int num; /* # of objs per slab 每一个SLAB中对象的数量 */ /* 4) cache_grow/shrink 缓存的增加/缩减 */ /* order of pgs per slab (2^n) 每一个SLAB中页数,取以2为底的对数*/ unsigned int gfporder; /* force GFP flags, e.g. GFP_DMA 强制的GFP标志 */ gfp_t gfpflags; //colour指定了颜色的最大数目 size_t colour; /* cache colouring range 缓存着色范围,即基本偏移量乘以颜色值得到的绝对偏移量*/ unsigned int colour_off; /* colour offset 着色偏移 */ struct kmem_cache *slabp_cache; unsigned int slab_size; /* dynamic flags 动态标志集合,描述SLAB的"动态性质" */ unsigned int dflags; /* constructor func 构造函数*/ void (*ctor)(void *obj); /* 5) cache creation/removal 缓存建立/删除 */ //name是一个字符串,包含该缓存的名称 const char *name; //next是一个标准的链表元素,用于将kmem_cache的全部实例保存在全局链表cache_chain上 struct list_head next; /* 6) statistics 统计量 */ #ifdef CONFIG_DEBUG_SLAB unsigned long num_active; unsigned long num_allocations; unsigned long high_mark; unsigned long grown; unsigned long reaped; unsigned long errors; unsigned long max_freeable; unsigned long node_allocs; unsigned long node_frees; unsigned long node_overflow; atomic_t allochit; atomic_t allocmiss; atomic_t freehit; atomic_t freemiss; /* * If debugging is enabled, then the allocator can add additional * fields and/or padding to every object. buffer_size contains the total * object size including these internal fields, the following two * variables contain the offset to the user object and its size. */ int obj_offset; int obj_size; #endif /* CONFIG_DEBUG_SLAB */ /* We put nodelists[] at the end of kmem_cache, because we want to size this array to nr_node_ids slots instead of MAX_NUMNODES We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache is statically defined, so we reserve the max number of nodes. 每一个内存结点都对应3个表头,用于组织SLAB的链表 1. 第一个链表: 包含彻底用尽的SLAB 2. 第二个链表: 包含部分空闲的SLAB 3. 第三个链表: 空闲的SLAB */ struct kmem_list3 *nodelists[MAX_NUMNODES]; /* * Do not add fields after nodelists[] */ };
这样,对象分配的体系结构就造成了一个三级的层次结构,分配成本和操做对CPU高速缓存和TLB的负面影响逐级升高
1. 仍然处于CPU高速缓存中的per-CPU对象 2. 现存SLAB中未使用的对象 3. 刚使用伙伴系统分配的新SLAB中未使用的对象
2. SLAB的精细结构
对象在SLAB中并不是连续排列,而是按照一个至关复杂的方案分布
/* * The slab lists for all objects. */ struct kmem_list3 { /* 管理结构位于每一个SLAB的起始处,保存了全部的管理数据(和用于链接缓存链表的链表元素) /* struct list_head slabs_partial; /* partial list first, better asm code */ struct list_head slabs_full; struct list_head slabs_free; //free_objects表示slabs_partial和slabs_free的全部SLAB中空闲对象的总数 unsigned long free_objects; //free_limit指定了全部SLAB上允许未使用对象的最大数目 unsigned int free_limit; unsigned int colour_next; /* Per-node cache coloring 各结点缓存着色 */ spinlock_t list_lock; struct array_cache *shared; /* shared per node 结点内共享 */ struct array_cache **alien; /* on other nodes 在其余结点上 */ /* updated without locking 无需锁定便可更新 next_reap定义了内核在两次尝试收缩缓存之间,必须通过的时间间隔,其思想是防止因为频繁的缓存收缩和增加操做而下降系统性能 这种操做可能在某些系统共负荷下发生,该技术只能在NUMA系统上使用 */ unsigned long next_reap; /* updated without locking 无需锁定便可更新 free_touched表示缓存是不是活动的,在从缓存获取一个对象时,内核将该变量的值设置为1,在缓存收缩时,该值重置为0 但内核只有在free_touched预先设置为0时,才会收缩缓存,由于1表示内核的另外一部分刚从该缓存获取对象,此时收缩是不合适的 */ int free_touched; };
用于每一个对象的长度并不反映其确切的大小,相反,长度已经进行了舍入,以知足对齐方式的要求,有两种可用的备选对齐方案
1. SLAB建立时使用标志SLAB_HWCACHE_ALIGN,SLAB用户能够要求对象按硬件缓存行对齐,那么会按照cache_line_size的返回值进行对齐,该函数返回特定于处理器的L1缓存大小,若是对象小于缓存行长度的一半,那么将多个对象放入一个缓存行 2. 若是不要求按硬件缓存行对齐,那么内核保证对象按BYTES_PER_WORD对齐,该值是表示void指针所需字节的数目 /* 在32位机器上,void指针须要4个字节,所以对有6个字节的对象,则须要8 = 2 * 4个字节,15个字节的对象须要16 = 4 * 4个字节,多余的字节称为填充字节,填充字节能够加速对SLAB中对象的访问,若是使用对齐的地址,那么在几乎全部的体系结构上,内存访问都会更快,这弥补了使用填充字节必然致使须要更多内存的不利状况 */
大多数状况下,SLAB内存区的长度(减去了头部管理数据)是不能被(可能填补过的)对象长度整除的,所以,内核就有了一些多余的内存,能够用来以偏移量的形式给SLAB进行"着色",即缓存的各个SLAB成员会指定不一样的偏移量,以便将数据定位到不一样的缓存行,于是SLAB开始和结束处的空闲内存是不一样的,在计算偏移量时,内核必须考虑其余的对齐因素,例如L1高速缓存中数据的对齐
管理数据能够放置在SLAB自身,也能够放置到使用kmalloc分配的不一样内存区中,内核如何选择,取决于SLAB的长度和已用对象的数量,管理数据和SLAB内存之间的关联很容易创建,由于SLAB头包含一个指针,指向SLAB数据区的起始处(不管管理数据是否在SLAB上)
例以下图给出了管理数据不在SLAB自身,而位于另外一内存区的情形
最后,内核须要一种方法,经过对象自身便可识别SLAB(以及对象驻留的缓存)(即逆向查找),根据对象的物理内存地址,能够找到相关的页,所以能够在全局mem_map数组中找到对应的page实例
1. struct page -> lru.next: 指向页驻留的缓存的管理结构 2. struct page -> lru.prev: 指向保存该页的SLAB的管理结构
设置或读取SLAB信息、处理缓存信息的任务分别由下列函数完成
//处理缓存信息的设置和读取 1. static inline void page_set_cache(struct page *page, struct kmem_cache *cache) 2. static inline struct kmem_cache *page_get_cache(struct page *page) //设置或读取SLAB信息 1. static inline void page_set_slab(struct page *page, struct slab *slab) 2. static inline struct slab *page_get_slab(struct page *page)
此外,内核还对分配给SLAB分配器的每一个物理内存页都设置标志PG_SLAB
Relevant Link:
http://cxw06023273.iteye.com/blog/867312 http://guojing.me/linux-kernel-architecture/posts/slab-structure/
0x4: 实现
为了实现SLAB分配器,使用了各类数据结构,SLAB的KERNEL代码在内核中不多被修改,相关的代码并不老是很容易阅读或理解,这是由于许多内存区须要使用指针运算和类型转换进行操做,这不是C语言中以清晰简明著称的领域,因为SLAB系统带有大量调试选项,因此代码中遍及着预处理语句
1. 危险区(red zoning) 在每一个对象的开始和结束处增长一个额外的内存区,其中填充已知的字节模式,若是模式被修改,程序员在分析内核内存时就能够注意到,多是由于某些代码访问了不属于它们的内存区 2. 对象毒化(object poisoning) 在创建和释放SLAB时,将对象用预约义的的模式填充,若是在对象分配时注意到该模式已经改变,程序员知道已经发生了未受权的访问
1. 数据结构
每一个缓存由kmem_cache结构的一个实例表示
linux-2.6.32.63\mm\slab.c
/* internal cache of cache description objs 该结构在内核中其余地方是不可见的,缓存的用户无须详细了解缓存是如何实现的,将SLAB缓存视为经过一组标准函数来高效地建立和释放特定类型对象的机制,就足够了 */ static struct kmem_cache cache_cache = { .batchcount = 1, .limit = BOOT_CPUCACHE_ENTRIES, .shared = 1, .buffer_size = sizeof(struct kmem_cache), .name = "kmem_cache", };
2. 初始化
为初始化SLAB数据结构,内核须要若干远小于一整页的内存块,这些最适合由kmalloc分配,这里关键问题是: 只有在SLAB系统已经启用后,才能使用kmalloc(kmalloc是基于SLAB实现的),更确切地说,该问题涉及kmalloc和per-CPU缓存的初始化,在这些缓存可以初始化以前,kmalloc必须能够用来分配所需的内存空间,而kmalloc自身也处于初始化的过程当中,即kmalloc只能在kmalloc已经初始化以后初始化,这是觉得伪命题,所以内核必须借助一些技巧
kmem_cache_init函数用于初始化SLAB分配器,它在内核初始化阶段(start_kernel)、伙伴系统启用以后调用。但在多处理系统上,启动CPU此时正在运行,而其余CPU还没有初始化,kmem_cache_init采用了一个多步骤过程,逐步激活SLAB分配器
1. kmem_cache_init建立系统中的第一个SLAB缓存,以便为kmem_cache的实例提供内存,为此,内核使用的主要是在编译时建立的静态数据,实际上,一个静态的数据结构(initarray_cache)用做per-CPU数组,该缓存的名称是cache_cache 2. kmem_cache_init接下来初始化通常性的缓存,用做kmalloc内存的来源,为此,针对所需的各个缓存长度,分别调用kmem_cache_create 3. 在kmem_cache_init最后一步,把到目前为止一直使用的数据结构的全部静态实例化成员,用kmalloc动态分配的版本替代
3. 建立缓存
建立新的缓存必须调用kmem_cache_create
\linux-2.6.32.63\mm\slab.c
/* 1. name: 缓存名称,随后会出如今/proc/slabinfo中 2. size: 被管理对象以字节记的长度 3. align: 在对齐数据时使用的偏移量 4. flags: 一组标志 5. ctor: 构造函数 */ struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
建立新缓存是一个漫长的过程
struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *)) { size_t left_over, slab_size, ralign; struct kmem_cache *cachep = NULL, *pc; gfp_t gfp; /* Sanity checks... these are all serious usage bugs. 参数有效性检查 */ if (!name || in_interrupt() || (size < BYTES_PER_WORD) || size > KMALLOC_MAX_SIZE) { printk(KERN_ERR "%s: Early error in slab %s\n", __func__, name); BUG(); } /* * We use cache_chain_mutex to ensure a consistent view of * cpu_online_mask as well. Please see cpuup_callback */ if (slab_is_available()) { get_online_cpus(); mutex_lock(&cache_chain_mutex); } list_for_each_entry(pc, &cache_chain, next) { char tmp; int res; /* * This happens when the module gets unloaded and doesn't * destroy its slab cache and no-one else reuses the vmalloc * area of the module. Print a warning. */ res = probe_kernel_address(pc->name, tmp); if (res) { printk(KERN_ERR "SLAB: cache with size %d has lost its name\n", pc->buffer_size); continue; } if (!strcmp(pc->name, name)) { printk(KERN_ERR "kmem_cache_create: duplicate cache %s\n", name); dump_stack(); goto oops; } } #if DEBUG WARN_ON(strchr(name, ' ')); /* It confuses parsers */ #if FORCED_DEBUG /* * Enable redzoning and last user accounting, except for caches with * large objects, if the increased size would increase the object size * above the next power of two: caches with object sizes just above a * power of two have a significant amount of internal fragmentation. */ if (size < 4096 || fls(size - 1) == fls(size-1 + REDZONE_ALIGN + 2 * sizeof(unsigned long long))) flags |= SLAB_RED_ZONE | SLAB_STORE_USER; if (!(flags & SLAB_DESTROY_BY_RCU)) flags |= SLAB_POISON; #endif if (flags & SLAB_DESTROY_BY_RCU) BUG_ON(flags & SLAB_POISON); #endif /* * Always checks flags, a caller might be expecting debug support which * isn't available. */ BUG_ON(flags & ~CREATE_MASK); /* Check that size is in terms of words. This is needed to avoid unaligned accesses for some archs when redzoning is used, and makes sure any on-slab bufctl's are also correctly aligned. 对象对齐 */ if (size & (BYTES_PER_WORD - 1)) { size += (BYTES_PER_WORD - 1); size &= ~(BYTES_PER_WORD - 1); } /* calculate the final buffer alignment: */ /* 1) arch recommendation: can be overridden for debug 对象对齐一般也是基于基于处理器的字长,但若是设置了SLAB_HWCACHE_ALIGN标志,则内核按照特定于体系结构的函数cache_line_size给出的值,来对齐数据 内核还尝试将尽量多的对象填充到一个缓存行中,只要对象长度容许,则会一直尝试将对齐值除以2,所以,会有二、四、6..个对象放入一个缓存行,而不是只有一个对象 */ if (flags & SLAB_HWCACHE_ALIGN) { /* * Default alignment: as specified by the arch code. Except if * an object is really small, then squeeze multiple objects into * one cacheline. */ ralign = cache_line_size(); while (size <= ralign / 2) ralign /= 2; } else { ralign = BYTES_PER_WORD; } /* * Redzoning and user store require word alignment or possibly larger. * Note this will be overridden by architecture or caller mandated * alignment if either is greater than BYTES_PER_WORD. */ if (flags & SLAB_STORE_USER) ralign = BYTES_PER_WORD; if (flags & SLAB_RED_ZONE) { ralign = REDZONE_ALIGN; /* If redzoning, ensure that the second redzone is suitably * aligned, by adjusting the object size accordingly. */ size += REDZONE_ALIGN - 1; size &= ~(REDZONE_ALIGN - 1); } /* 2) arch mandated alignment 内核也考虑如下事实,某些体系结构须要一个最小值做为数据对齐的边界,由ARCH_SLAB_MINALIGN定义,用于所要求的对齐也能够接收 */ //体系结构强制的最小对齐值 if (ralign < ARCH_SLAB_MINALIGN) { ralign = ARCH_SLAB_MINALIGN; } /* 3) caller mandated alignment 使用者强制的对齐值 */ if (ralign < align) { ralign = align; } /* disable debug if necessary */ if (ralign > __alignof__(unsigned long long)) flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER); /* 4) Store it. 存储最后计算出的对齐值 */ align = ralign; if (slab_is_available()) gfp = GFP_KERNEL; else gfp = GFP_NOWAIT; /* Get cache's description obj. 在数据对齐值计算完毕以后,分配struct kmem_cache的一个新实例 */ cachep = kmem_cache_zalloc(&cache_cache, gfp); if (!cachep) goto oops; #if DEBUG cachep->obj_size = size; /* * Both debugging options require word-alignment which is calculated * into align above. */ if (flags & SLAB_RED_ZONE) { /* add space for red zone words */ cachep->obj_offset += sizeof(unsigned long long); size += 2 * sizeof(unsigned long long); } if (flags & SLAB_STORE_USER) { /* user store requires one word storage behind the end of * the real object. But if the second red zone needs to be * aligned to 64 bits, we must allow that much space. */ if (flags & SLAB_RED_ZONE) size += REDZONE_ALIGN; else size += BYTES_PER_WORD; } #if FORCED_DEBUG && defined(CONFIG_DEBUG_PAGEALLOC) if (size >= malloc_sizes[INDEX_L3 + 1].cs_size && cachep->obj_size > cache_line_size() && ALIGN(size, align) < PAGE_SIZE) { cachep->obj_offset += PAGE_SIZE - ALIGN(size, align); size = PAGE_SIZE; } #endif #endif /* Determine if the slab management is 'on' or 'off' slab. (bootstrapping cannot cope with offslab caches so don't do it too early on.) 肯定是否将SLAB头存储在SLAB之上相对比较简单,若是对象长度大于页帧的1/8,则将头部管理数据存储在SLAB以外,不然存储在SLAB之上 */ if ((size >= (PAGE_SIZE >> 3)) && !slab_early_init) /* * Size is large, assume best to place the slab management obj * off-slab (should allow better packing of objs). */ flags |= CFLGS_OFF_SLAB; //增长对象的长度size,直至对应到以前计算的对齐值 size = ALIGN(size, align); /* 尝试找到适当的页数用做SLAB长度,不过小,也不太大。SLAB中对象太少会增长管理开销,下降方法的效率,而过大的SLAB内存区则对伙伴系统不利 针对特定的页数,来计算对象数目、浪费的空间、着色所需的空间,循环往复,直至内核对结果满意为止 */ left_over = calculate_slab_order(cachep, size, align, flags); if (!cachep->num) { printk(KERN_ERR "kmem_cache_create: couldn't create cache %s.\n", name); kmem_cache_free(&cache_cache, cachep); cachep = NULL; goto oops; } slab_size = ALIGN(cachep->num * sizeof(kmem_bufctl_t) + sizeof(struct slab), align); /* * If the slab has been placed off-slab, and we have enough space then * move it on-slab. This is at the expense of any extra colouring. */ if (flags & CFLGS_OFF_SLAB && left_over >= slab_size) { flags &= ~CFLGS_OFF_SLAB; left_over -= slab_size; } if (flags & CFLGS_OFF_SLAB) { /* really off slab. No need for manual alignment */ slab_size = cachep->num * sizeof(kmem_bufctl_t) + sizeof(struct slab); #ifdef CONFIG_PAGE_POISONING /* If we're going to use the generic kernel_map_pages() * poisoning, then it's going to smash the contents of * the redzone and userword anyhow, so switch them off. */ if (size % PAGE_SIZE == 0 && flags & SLAB_POISON) flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER); #endif } cachep->colour_off = cache_line_size(); /* Offset must be a multiple of the alignment. */ if (cachep->colour_off < align) cachep->colour_off = align; cachep->colour = left_over / cachep->colour_off; cachep->slab_size = slab_size; cachep->flags = flags; cachep->gfpflags = 0; if (CONFIG_ZONE_DMA_FLAG && (flags & SLAB_CACHE_DMA)) cachep->gfpflags |= GFP_DMA; cachep->buffer_size = size; cachep->reciprocal_buffer_size = reciprocal_value(size); if (flags & CFLGS_OFF_SLAB) { cachep->slabp_cache = kmem_find_general_cachep(slab_size, 0u); /* * This is a possibility for one of the malloc_sizes caches. * But since we go off slab only for object size greater than * PAGE_SIZE/8, and malloc_sizes gets created in ascending order, * this should not happen at all. * But leave a BUG_ON for some lucky dude. */ BUG_ON(ZERO_OR_NULL_PTR(cachep->slabp_cache)); } cachep->ctor = ctor; cachep->name = name; if (setup_cpu_cache(cachep, gfp)) { __kmem_cache_destroy(cachep); cachep = NULL; goto oops; } /* cache setup completed, link it into the list 为完成初始化,将初始化过的kmem_cache实例添加到全局链表,表头为cache_chain */ list_add(&cachep->next, &cache_chain); oops: if (!cachep && (flags & SLAB_PANIC)) panic("kmem_cache_create(): failed to create slab `%s'\n", name); if (slab_is_available()) { mutex_unlock(&cache_chain_mutex); put_online_cpus(); } return cachep; } EXPORT_SYMBOL(kmem_cache_create);
4. 分配对象
kmem_cache_alloc用于从特定的缓存获取对象,相似于全部的malloc函数,其结果多是指向分配内存区的指针,或者分配失败返回NULL
/* 1. cachep: 用于获取对象的缓存 2. flags: 精确描述分配特征的标志变量 */ void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
5. 缓存的增加
若是扫描了全部的SLAB仍然没有找到空闲的对象,那么必须使用cache_grow扩大缓存,这是一个代价较高的操做,涉及到对伙伴系统的操做
6. 释放对象
若是一个分配的对象已经再也不须要,那么必须使用kmem_cache_free返回给SLAB分配器
7. 销毁缓存
若是要销毁只包含未使用对象的一个缓存,则必须调用kmem_cache_destroy函数,该函数主要在删除模块时调用,此时须要将分配的内存都释放
1. 依次扫描slabs_free链表上的slab,首先对每一个slab的每一个对象调用析构器函数,而后将slab的内存空间返回给伙伴系统 2. 释放用于per-CPU缓存的内存空间 3. 从cache_cache链表移除相关数据
0x5: 通用缓存
若是不涉及对象缓存,而是传统意义上的分配/释放内存,则必须调用kmalloc/kfree函数,这两个函数至关于用户空间C库malloc、free函数的内核等价物。咱们知道,kmalloc、kfree实现为SLAB分配器的前端,其语义尽量地模仿malloc/free
1. kmalloc的实现
kmalloc的基础是一个数组,其中是一些分别用于不一样内存长度的SLAB缓存,数组项是cache_sizes的实例
\linux-2.6.32.63\include\linux\slab_def.h
/* Size description struct for general caches. */ struct cache_sizes { //cs_size指定了该项负责的内存区的长度,每一个长度对应于两个SLAB缓存,其中之一提供适合DMA访问的内存 size_t cs_size; struct kmem_cache *cs_cachep; #ifdef CONFIG_ZONE_DMA struct kmem_cache *cs_dmacachep; #endif };
\linux-2.6.32.63\mm\slab.c
/* * These are the default caches for kmalloc. Custom caches can have other sizes. */ struct cache_sizes malloc_sizes[] = { #define CACHE(x) { .cs_size = (x) }, #include <linux/kmalloc_sizes.h> CACHE(ULONG_MAX) #undef CACHE }; EXPORT_SYMBOL(malloc_sizes);
2. kfree的实现
\linux-2.6.32.63\mm\slab.c
/** * kfree - free previously allocated memory * @objp: pointer returned by kmalloc. * * If @objp is NULL, no operation is performed. * * Don't free memory not originally allocated by kmalloc() * or you will run into trouble. */ void kfree(const void *objp) { struct kmem_cache *c; unsigned long flags; trace_kfree(_RET_IP_, objp); if (unlikely(ZERO_OR_NULL_PTR(objp))) return; local_irq_save(flags); kfree_debugcheck(objp); c = virt_to_cache(objp); debug_check_no_locks_freed(objp, obj_size(c)); debug_check_no_obj_freed(objp, obj_size(c)); __cache_free(c, (void *)objp); local_irq_restore(flags); } EXPORT_SYMBOL(kfree);
5. 处理器高速缓存和TLB控制
高速缓存对系统总性能十分关键,这也是内核尽量提供其利用效率的缘由,这主要是经过在内存中巧妙地对齐内核数据,谨慎地混合使用普通函数、内联定义、宏,也有助于从处理器汲取更高的性能
内核提供了一些命令,能够直接做用于处理器的高速缓存和TLB,但这些命令并不是用于提供系统的效率,而是用于维护缓存内容的一致性,确保不出现不正确和过期的缓存项,例如在从一个进程的地址空间移除一个映射时,内核负责从TLB中删除对应项,若是未能这么作,那么在先前被映射占据的虚拟内存地址添加新数据时,对该地址的读写操做将被重定向到物理内存中不正确的地址
不一样体系结构上,高速缓存和TLB的硬件实现千差万别,所以内核必须创建TLB和高速缓存的一个视图,在其中考虑到各类不一样的硬件实现方法,还不能忽略各个体系结构的特定性质
1. TLB的语义抽象是将虚拟地址转换为物理地址的一种机制 2. 内核将高速缓存视为经过虚拟地址快速访问数据的一种机制,该机制无需访问物理内存,数据和指令高速缓存并不老是明确区分,若是高速缓存区分数据和指令,那么特定于体系结构的代码负责对此进行处理
实际上没必要要为每种处理器类型都实现内核定义的每一个控制函数,若是不须要某个函数,其调用能够替换为空操做(do{}while(0)),然后由编译器优化掉,对于高速缓存相关的操做来讲,这种状况很是常见,由于咱们知道,内核假定寻址是基于虚拟地址,那么对于物理地址组织的高速缓存来讲,问题就不存在,一般也没必要要实现缓存控制函数
内核中各个特定于CPU的部分都必须提供下列函数(即便只是空操做),以便控制TLB和高速缓存
1. flush_tlb_all、flush_cache_all 刷出整个TLB/高速缓存,这只在操纵内核(而非用户空间进程的)页表时须要,由于此类修改不只影响全部进程,并且影响系统中的全部处理器 2. flush_tlb_mm(struct mm_struct * mm)、flush_cache_mm 刷出全部属于地址空间mm的TLB/高速缓存项 3. flush_tlb_range(struct vm_area_struct * vma, unsigned long start, unsigned long end)、flush_cache_range(vma, start, end) 刷出地址范围vma->vm_mm中虚拟地址start、end之间的全部TLB/高速缓存项 4. flush_tlb_page(struct vm_area_struct * vma, unsigned long page)、flush_cache_page(vma, page) 刷出虚拟地址在[page, page + PAGE_SIZE]范围内全部的TLB/高速缓存项 5. update_mmu_cache(struct vm_area_struct * vma, unsigned long address, pte_t pte) 在处理页失效以后调用,它在处理器的内存管理单元MMU中加入信息,使得虚拟地址address由页表项pte描述。仅当存在外部MMU时,才须要该函数,一般MMU集成在处理器内部,但有例外状况,例如MIPS处理器具备外部MMU
内核对数据和指令高速缓存并不做区分,若是须要区分,特定于处理器的代码可根据vm_area_struct->flags的VM_EXEC标志位是否设置,来肯定高速缓存包含的是指令仍是数据
flush_cache_、flush_tlb_函数常常成对出现,例如,在使用fork复制进程的地址空间时
\linux-2.6.32.63\kernel\fork.c
//1. 刷出高速缓存 flush_cache_mm(oldmm); ... //操做页表(操做内存) ... //3. 刷出TLB flush_tlb_mm(oldmm);
遵照先刷出高速缓存、操做内存、刷出TLB这个顺序很重要,有如下缘由
1. 若是顺序反过来,那么在TLB刷出以后,正确信息提供以前,多处理器系统中的另外一个CPU可能从进程的页表取得错误的信息 2. 在刷出高速缓存时,某些体系结构须要依赖TLB中的"虚拟->物理"转换规则(具备该性质的高速缓存称之为严格的)。flush_tlb_mm必须在flush_cache_mm以后执行,以确保这一点
有些控制函数明确地应用于数据高速缓存(flush_dcache_xxx)、或指令高速缓存(flush_icache_xxx)
1. 若是高速缓存包含几个虚拟地址不一样的项指向内存中的同一页,可能会发生所谓的alias问题,flush_dcache_page(struct page * page)有助于防止该问题。在内核向页缓存中的一页写入数据,或者从映射在用户空间中的一页读出数据时,老是会调用该函数,这个例程使得存在alias问题的各个体系结构有机会防止问题的发生 2. 在内核向内核内存范围(start、end之间)写入数据,而该数据将在此后做为代码执行,则此时须要调用flush_icache_range(unsigned long start, unsigned long end)。该场景的一个标准示例是向内核载入模块时,二进制数据首先复制到物理内存中,而后执行。 flush_icache_range确保在数据和指令高速缓存分别实现的状况下,两者彼此不发生干扰 3. flush_icache_user_range(*vma, *page, addr, len)是一个特殊的函数,用于ptrace机制 为了将修改传送到被调试进程的地址空间,须要使用该函数
小结
1. 在内核进入正常运做以后,内存管理分为两个层次处理。伙伴系统负责物理页帧的管理,而SLAB分配器则处理小块内存的分配,并提供了用户层malloc函数族的内核等价物 2. 伙伴系统围绕着由多页组成的连续内存块的拆分和再合并而展开,在连续内存区变为空闲时,内核会自动注意到这一点,并在相应的分配请求出现时使用它,因为该机制在系统长时间运行后,没法以使人满意的方式防止物理内存碎片发生,所以2.6以后的内核版本引入了反碎片技术,它一方面容许按页的可移动性将其分组,另外一方面增长了一个新的虚拟内存域。两者的实质都在于下降在大块内存中间分配内存的概率(这是致使碎片的根本缘由),以免碎片出现 3. SLAB分配器在伙伴系统之上实现,它不只容许分配任意用途的小块内存,还用于对常用的数据结构建立特定的缓存
6. 内存管理的概念
内存管理(Memory Management)是操做系统设计中最重要和最复杂的内容之一。虽然计算机硬件一直在飞速发展,内存容量也在不断增加,可是仍然不可能将全部用户进程和系统所须要的所有程序和数据放入主存中,因此操做系统必须将内存空间进行合理地划分和有效地动态分配。
操做系统对内存的划分和动态分配,就是内存管理的概念。有效的内存管理在多道程序设计中很是重要,不只方便用户使用存储器、提升内存利用率,还能够经过虚拟技术从逻辑上扩充存储器。
内存管理的功能有:
1. 内存空间的分配与回收: 由操做系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提升编程效率 2. 地址转换: 在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,所以存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址 3. 内存空间的扩充: 利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存 4. 存储保护: 保证各道做业在各自的存储空间内运行,互不干扰
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2608.html
7. 内存覆盖与内存交换
0x1: 内存覆盖
早期的计算机系统中,主存容量很小,虽然主存中仅存放一道用户程序,可是存储空间放不下用户进程的现象也常常发生,这一矛盾能够用覆盖技术来解决。
覆盖的基本思想是:因为程序运行时并不是任什么时候候都要访问程序及数据的各个部分(尤为是大程序),所以能够把用户空间分红一个固定区和若干个覆盖区。将常常活跃的部分放在固定区,其他部分按调用关系分段。首先将那些即将要访问的
段放入覆盖区,其余段放在外存中,在须要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。
覆盖技术的特色是打破了必须将一个进程的所有信息装入主存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行
将内存覆盖技术和cache更新技术进行类比,等效于对cache直接进行"简答替换(简单删除)",即不采用任何的排序和选择策略,粗暴地将cache中的某一段数据清空出去,这种方法不适合在高并发、大流量的场景下
0x2: 内存交换
交换(对换)的基本思想是: 1. 把处于等待状态(或在CPU调度原则下被剥夺运行权利)的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出 2. 把准备好竞争CPU运行的程序从辅存移到内存,这一过程又称为换入
有关交换须要注意如下几个问题:
1. 交换须要备份存储,一般是快速磁盘。它必须足够大,而且提供对这些内存映像的直接访问。 2. 为了有效使用CPU,须要每一个进程的执行时间比交换时间长,而影响交换时间的主要是转移时间。转移时间与所交换的内存空间成正比。 3. 若是换出进程,必须确保该进程是彻底处于空闲状态。 4. 交换空间一般做为磁盘的一整块,且独立于文件系统,所以使用就可能很快。 5. 交换一般在有许多进程运行且内存空间吃紧时开始启动,而系统负荷下降就暂停。 6. 普通的交换使用很少,但交换策略的某些变种在许多系统中(如UNIX系统)仍发挥做用。
须要注意的是,覆盖技术则已成为历史;而交换技术在现代操做系统中仍具备较强的生命力。咱们今天在操做系统原理相关书籍上学习的相关调度策略,都是针对"交换技术"的具体实现,而不一样策略之间的区别就在于2个问题:交换谁?什么时候交换?
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2609.html
8. 内存连续分配管理方式
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2610.html
9. 内存非连续分配管理方式
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2611.html
10. 虚拟内存的概念、特征及其实现
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2612.html
11. 请求分页管理方式实现虚拟内存
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2613.html
12. 页面置换算法
本小节学习页面置换算法,本质上就是在学习内存的"交换策略"
进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就须要从内存中调出一页程序或数据,送入磁盘的对换区。(这和cache的动态更新原理是同样的)
选择调出页面的算法就称为"页面置换算法"。好的页面置换算法应有较低的页面更换频率,也就是说,应将之后不会再访问或者之后较长时间内不会再访问的页面先调出
0x1: 最佳置换算法(OPT)
最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是之后永不使用的,或者是在最长时间内再也不被访问的页面,这样能够保证得到最低的缺页率。但因为人们目前没法预知进程在内存下的若千页面中哪一个是将来最长时间内再也不被访问的,于是该算法没法实现。这只是一种判断算法的最优标准
最佳置换算法能够用来评价其余算法。假定系统为某进程分配了三个物理块,并考虑有如下页面号引用串:
7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
进程运行时
1. 先将7, 0, 1三个页面依次装入内存 2. 进程要访问页面2时,产生缺页中断,根据最佳置换算法,选择第18次访问才需调入的页面7予以淘汰 3. 而后,访问页面0时,由于已在内存中因此没必要产生缺页中断 4. 访问页面3时又会根据最佳置换算法将页面1淘汰 5. ……依此类推
能够看到,发生缺页中断的次数为9,页面置换的次数为6
访问页面 | 7 | 0 | 1 | 2 | 0 | 3 | 0 | 4 | 2 | 3 | 0 | 3 | 2 | 1 | 2 | 0 | 1 | 7 | 0 | 1 |
物理块1 | 7 | 7 | 7 | 2 | 2 | 2 | 2 | 2 | 7 | |||||||||||
物理块2 | 0 | 0 | 0 | 0 | 4 | 0 | 0 | 0 | ||||||||||||
物理块3 | 1 | 1 | 3 | 3 | 3 | 1 | 1 | |||||||||||||
缺页否 | √ | √ | √ | √ | √ | √ | √ | √ | √ |
访问页面 | 7 | 0 | 1 | 2 | 0 | 3 | 0 | 4 | 2 | 3 | 0 | 3 | 2 | 1 | 2 | 0 | 1 | 7 | 0 | 1 |
物理块1 | 7 | 7 | 7 | 2 | 2 | 2 | 4 | 4 | 4 | 0 | 0 | 0 | 7 | 7 | 7 | |||||
物理块2 | 0 | 0 | 0 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 | 1 | 0 | 0 | ||||||
物理块3 | 1 | 1 | 1 | 0 | 0 | 0 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | |||||||
缺页否 | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ |
访问页面 | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
物理块1 | 1 | 1 | 1 | 4 | 4 | 4 | 5 | ,5' | 5 | |||
物理块2 | 2 | 2 | 2 | 1 | 1 | 1 | 3 | 3 | ||||
物理块3 | 3 | 3 | 3 | 2 | 2 | 2 | 4 | |||||
缺页否 | √ | √ | √ | √ | √ | √ | √ | √ | √ | |||
1 | 1 | 1 | 5 | 5 | 5 | 5 | 4 | 4 | ||||
物理块2* | 2 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 5 | |||
物理块3* | 3 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | ||||
物理块4* | 4 | 4 | 4 | 4 | 3 | 3 | 3 | |||||
缺页否 | √ | √ | √ | √ | √ | √ | √ | √ | √ |
0x3: 最近最久未使用(LRU)置换算法
LRU算法是一个被普遍使用和接收的cache调度算法,它的调度思想具备较好的合理性
LRU算法的思想是:选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的未来可能也不会被访问。
回到内存交换的总原则:交换谁?什么时候交换?对于LRU算法来讲,这里的评判标准就是"最近最长时间未使用过",如何计算这个值呢?
1. 用一个链表、或者具备链表特征的数据结构来保存数据,取数据时只从头部取,插入数据的时候只从尾部插入,这样,每次数据被使用后就会从头部取出,并插入尾部。这种策略隐含的思想就是越尾部的数据就是越最近被使用的,
头部的都是不常常被使用的 2. 为每一个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。
这2种算法都能知足LUR的要求,再对上面的实例釆用LRU算法进行页面置换
访问页面 | 7 | 0 | 1 | 2 | 0 | 3 | 0 | 4 | 2 | 3 | 0 | 3 | 2 | 1 | 2 | 0 | 1 | 7 | 0 | 1 |
物理块1 | 7 | 7 | 7 | 2 | 2 | 4 | 4 | 4 | 0 | 1 | 1 | 1 | ||||||||
物理块2 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 3 | 3 | 0 | 0 | |||||||||
物理块3 | 1 | 1 | 3 | 3 | 2 | 2 | 2 | 2 | 2 | 7 | ||||||||||
缺页否 | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ |
#include <iostream> #include "lru.hpp" using namespace std; struct stHashInfo { int hints; time_t tmodify; }; int main() { // Create a cache typedef plb::LRUCacheH4<int, struct stHashInfo> lru_cache; lru_cache cache(5000); struct stHashInfo a = {5, 19910726}; struct stHashInfo b = {8, 19910727}; struct stHashInfo c = {12, 19910728}; struct stHashInfo d = {22, 19910729}; /* cache[1] = a; cache[2] = b; cache[3] = c; cache[4] = d; */ cache.insert(1, a); cache.insert(2, b); cache.insert(3, c); cache.insert(4, d); cache.find(1); cache.find(2); cache.find(2); cache.find(2); cache.find(2); cache.find(2); cache.find(2); cache.find(1); cache.find(3); cache.itemerase(2); for (lru_cache::const_iterator it = cache.lru_begin(); it != cache.end(); it++) { cout << it.key() << " -> hints: " << it.value().hints << " tmodify: " << it.value().tmodify << endl; } return 0; }
我在google code的基础上进行了小幅度的改进,让这个cache类库支持增删改查(CRUD)操做,连接在下面给出
1. 运算量不能太大,自己cache存在的意义就是为了减小CPU运算负载,若是由于插入key致使更多的计算量,则失去了用cache的意义了 2. HASH函数要有足够的敏感性,对微笑的扰动要能做出较大的值变化,即须要有足够的抗碰撞性
code
#include <iostream> #include <string.h> #include "lru.hpp" using namespace std; struct stHashInfo { int hints; time_t tmodify; }; uint32_t Murmur2( const string &str) { const uint8_t *key = (const uint8_t*)str.c_str(); uint32_t len = str.length(); uint32_t seed = 65536; const uint32_t m = 0x5bd1e995; const uint8_t r = 24; uint32_t h = len + seed; const uint8_t * data = (const uint8_t *)key; for ( ; len >= 4; len -= 4, data += 4 ) { uint32_t k = *(uint32_t *)data * m; k ^= k >> r; k *= m; h = ( h * m ) ^ k; } switch ( len ) { case 3: h ^= data[2] << 16; case 2: h ^= data[1] << 8; case 1: h ^= data[0]; h *= m; default:; } h ^= h >> 13; h *= m; h ^= h >> 15; return h; } int main() { // Create a cache typedef plb::LRUCacheH4<int, struct stHashInfo> lru_cache; lru_cache cache(5000); struct stHashInfo a = {5, 19910726}; struct stHashInfo b = {8, 19910727}; struct stHashInfo c = {12, 19910728}; struct stHashInfo d = {22, 19910729}; /* cache[1] = a; cache[2] = b; cache[3] = c; cache[4] = d; */ cache.insert(Murmur2(string("111")), a); cache.insert(Murmur2(string("222")), b); cache.insert(Murmur2(string("333")), c); cache.insert(Murmur2(string("444")), d); cache.find(Murmur2(string("444"))); cache.find(Murmur2(string("111"))); cache.find(Murmur2(string("111"))); cache.find(Murmur2(string("222"))); cache.itemerase(2); for (lru_cache::const_iterator it = cache.lru_begin(); it != cache.end(); it++) { cout << it.key() << " -> hints: " << it.value().hints << " tmodify: " << it.value().tmodify << endl; } return 0; }
https://code.google.com/p/lru-cache-cpp/ http://files.cnblogs.com/LittleHann/lru.rar http://floodyberry.com/noncryptohashzoo/Murmur2.html
0x4: 时钟(CLOCK)置换算法
LRU算法的性能接近于OPT,可是实现起来比较困难,且开销大;FIFO算法实现简单,但性能差。为了得到一个平衡,操做系统的设计者试图用比较小的开销接近LRU的性能,这类算法都是CLOCK算法的变体
1. 简单的CLOCK算法是给每一帧关联一个附加位,称为使用位 2. 当某一页首次装入主存时,该帧的使用位设置为1 3. 当该页随后再被访问到时,它的使用位也被置为1 4. 对于页替换算法,用于替换的候选帧集合看作一个循环缓冲区,而且有一个指针与之相关联 5. 当某一页被替换时,该指针被设置成指向缓冲区中的下一帧 6. 当须要替换一页时,操做系统扫描缓冲区,以查找使用位被置为0的一帧。每当遇到一个使用位为1的帧时,操做系统就将该位从新置为0 7. 若是在这个过程开始时,缓冲区中全部帧的使用位均为0,则选择遇到的第一个帧替换 8. 若是全部帧的使用位均为1,则指针在缓冲区中完整地循环一周,把全部使用位都置为0,而且停留在最初的位置上,替换该帧中的页(交换是必需要作的)
因为该算法循环地检查各页面的状况,故称为CLOCK算法,又称为最近未用(Not Recently Used, NRU)算法
CLOCK算法的性能比较接近LRU,而经过增长使用的位数目,能够使得CLOCK算法更加高效。在使用位的基础上再增长一个"修改位",则获得改进型的CLOCK置换算法。这样,每一帧都处于如下四种状况之一:
1. 最近未被访问,也未被修改(u=0, m=0)。 2. 最近被访问,但未被修改(u=1, m=0)。 3. 最近未被访问,但被修改(u=0, m=1)。 4. 最近被访问,被修改(u=1, m=1)。
算法执行以下操做步骤:
1. 从指针的当前位置开始,扫描帧缓冲区。在此次扫描过程当中,对使用位不作任何修改。选择遇到的第一个帧(u=0, m=0)用于替换。 2. 若是第1)步失败,则从新扫描,查找(u=0, m=1)的帧。选择遇到的第一个这样的帧用于替换。在这个扫描过程当中,对每一个跳过的帧,把它的使用位设置成0。 3. 若是第2)步失败,指针将回到它的最初位置,而且集合中全部帧的使用位均为0。重复第1步,而且若是有必要,重复第2步。这样将能够找到供替换的帧。
改进型的CLOCK算法优于简单CLOCK算法之处在于替换时首选没有变化的页。因为修改过的页在被替换以前必须写回,于是这样作会节省时间
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2614.html http://blog.csdn.net/ojshilu/article/details/22955741
13. 页面分配策略
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2615.html
14. 页面抖动和工做集
Relevant Link:
http://see.xidian.edu.cn/cpp/html/2616.html
15. 缺页异常的处理
在实际须要某个虚拟内存区域的数据以前,虚拟和物理内存之间的关联不会创建。若是进程访问的虚拟地址空间部分还没有与页帧关联,处理器会自动触发一个"缺页异常",内核必须处理此异常
16. 堆与内存管理
相对于栈而言,对内存面临着更复杂的行为模式,在任意时刻,程序可能发出请求,要么申请一段内存,要么释放一段已经申请过的内存,并且申请的大小从几个字节到数GB都是有可能的,所以,堆的管理显得较为复杂
0x1: Linux堆简介
仅仅使用栈对现代程序设计来讲是远远不够的,由于栈上的数据在函数返回的时候就会被释放掉,因此没法将数据传递至函数外部。而全局变量没有办法动态地产生,只能在编译的时候定义,不少状况下缺少表现力,在这种状况下,堆(Heap)是惟一的选择
堆是一块巨大的内存空间,经常占据整个虚拟空间的绝大部分,在这片空间里,程序能够请求一块连续内存,并自由的使用,这块内存在程序主动放弃以前都会一直有效
0x2: 堆操做系统调用
堆内存是操做系统的一种资源,内核负责总管全部进程的地址空间。Linux提供了两种堆空间分配的方式,即两个系统调用
1. brk() #include <unistd.h> int brk(void *addr); brk的做用实际上就是设置进程数据段的结束地址,即它能够扩大或者缩小数据段(Linux下数据段和BSS合并在一块儿统称为数据段)。若是咱们将数据段的结束地址向高地址移动,那么扩大的那部分空间就能够被程序使用,把这块空间做为堆空间是最多见的作法之一 void *sbrk(intptr_t increment); Glibc中还有一个函数叫sbrk,它的功能和brk相似,只是参数和返回值不一样,sbrk以一个增量(increment)做为参数,即须要增长(负数为减少)的空间大小,返回值是增长(或减小)后数据段结束地址。sbrk其实是对brk系统调用的包装(wrapper function),它在内部是基于brk()实现的 2. mmap() #include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); /* 参数: 1. addr: 须要申请空间的起始地址,若是起始地址为0,则Linux系统会自动挑选合适的起始地址 2. prot: 申请空间的权限 1) 可读 2) 可写 3) 可执行 3. flags: 映射类型 1) 文件映射 2) 匿名空间 4. fd: 在进行文件映射时指定文件描述符 */ int munmap(void *addr, size_t length); mmap()的做用和windows系统习的VirtualAlloc()很相似,它的做用就是向操做系统申请一段"虚拟地址空间",固然这块虚拟地址空间能够映射到某个文件,当它不将地址映射到某个文件时,咱们称这块空间为匿名(Anonymous)空间,匿名空间就能够拿来做为堆空间
值得注意的是,mmap()和windows下的VirtualAlloc()相似,它们都是系统虚拟空间申请函数,它们申请的空间的起始地址(addr)和大小(length)都必须是系统页的大小的整数倍。对于字节数很小的请求若是也使用mmap的话,会形成大量的空间浪费的
0x3: Glibc对堆内存的分配和管理
对于操做系统来讲,全部进程的堆内存都是经过内核来统一管理的,可是对于Ring3的应用程序来讲,若是每次进行申请或者释放堆空间都要进行系统调用,会带来很大的性能开销,因此比较好的作法是由程序的运行库(Glibc库)向操做系统一次性申请一块"适当大小"的堆空间,而后由程序运行库本身管理这块空间
运行库至关于向操做系统"批发"了一块较大的堆空间,而后再"零售"分配给程序用。当所有分配完或程序有大量的内存需求时,再根据实际需求向操做系统再次申请分配。运行库在向程序"零售"分配堆空间时,必须对这块堆空间进行有效地管理,这个算法就是"堆的分配算法"
glibc的malloc函数的处理逻辑
\glibc-2.18\malloc\malloc.c
1. 对于小于128KB的请求,直接在现有的堆空间里,按照堆分配算法为它分配一块空间并返回 2. 对于大于128KB的请求,它会使用mmap()系统调用分配一块匿名空间,而后在这个匿名空间中为用户分配空间
code
/* malloc example: random string generator*/ #include <stdio.h> /* printf, scanf, NULL */ #include <stdlib.h> /* malloc, free, rand */ int main () { int i,n; char * buffer; printf ("How long do you want the string? "); scanf ("%d", &i); buffer = (char*) malloc (i+1); if (buffer==NULL) exit (1); for (n=0; n<i; n++) { buffer[n]=rand()%26+'a'; } buffer[i]='\0'; printf ("Random string: %s\n",buffer); free (buffer); return 0; }
0x4: C++的运算符对堆内存的分配和管理: new/delete
对于非内部数据类的对象而言,光用maloc/free没法知足动态对象的要求。对象在建立的同时要自动执行构造函数,对象消亡以前要自动执行析构函数。因为malloc/free是库函数而不是运算符,不在编译器控制权限以内,不可以把执行构造函数和析构函数的任务强加给malloc/free
int *p1 = (int *)malloc(sizeof(int) * length); == int *p2 = new int[length];
使用new/delete和使用malloc/free相比,有以下特性
1. new内置了sizeof、类型转换和类型安全检查功能 2. 对于非内部数据类型的对象而言,new 在建立动态对象的同时完成了初始化工做。若是对象有多个构造函数,那么new的语句也能够有多种形式 3. 若是用new建立对象数组,那么只能使用对象的无参数构造函数 /* OK: Obj *objects = new Obj[100]; // 建立100 个动态对象 NO: Obj *objects = new Obj[100](1); // 建立100 个动态对象的同时赋初值1 */ 4. 在用delete释放对象数组时,留意不要丢了符号'[]' /* OK: delete []objects; NO: delete objects; //至关于delete objects[0],漏掉了另外99个对象。 */ 5. new自动计算须要分配的空间,而malloc须要手工计算字节数 6. new是类型安全的,而malloc不是 new operator 由两步构成,分别是 operator new 和 construct /* OK: int* p = new float[2]; // 编译时指出错误 NO: int* p = malloc(2*sizeof(float)); // 编译时没法指出错误 */ 7. operator new对应于malloc,但operator new能够重载,能够自定义内存分配策略,甚至不作内存分配,甚至分配到非内存设备上。而malloc无能为力 8. new将调用constructor,而malloc不能;delete将调用destructor,而free不能 9. malloc/free要库文件支持,new/delete则不要
Relevant Link:
http://blog.csdn.net/hackbuteer1/article/details/6789164
0x5: 堆分配算法
在大多数状况下,应用程序使用Glibc库的malloc/free进行堆内存的申请和释放(固然,应用程序也能够使用原始的方法直接使用mmap、brk系统调用进行堆空间的申请),对于Glibc库来讲,如何管理一大块连续的内存空间,可以按照需求分配、释放其中的空间,这就是堆分配算法
1. 空闲列表
空闲链表(Free List)的方式实际上就是把堆中各个空闲的块按照链表的方式链接起来
1. 当用户请求一块内存空间时,能够遍历整个列表,直到找到合适大小的块而且将它拆分 2. 当用户释放空间时,将它合并到空闲链表中
空闲链表是这样一种结构,在堆里的每一个空闲空间的开头(或结尾)有一个头(header),头结构里记录了"上一个"(prev)和"下一个"(next)空闲的地址,也就是说,全部空闲的块造成了一个链表
基于这种结构,在请求分配空间时
1. 首先在空闲链表里查找足够容纳请求大小的一个空闲块 2. 而后将这个块分为2部分 1) 一部分为程序请求的空间 2) 另外一部分为剩余下来的空闲空间 3. 将链表里对应原来空闲块的结构更新为新的剩下的空闲块 4. 若是剩下的空闲块大小为0,则直接将这个结构从链表里删除
2. 位图
针对空闲链表的弊端,有一种更加稳健的分配方式,即位图(bitmap),核心思想是
1. 将整个堆内存划分为大量的块(block),每一个块的大小相同 2. 当用户请求内存的时候,老是分配数个块的空间给用户 1) 第一个块: 已分配区域的头(head) 2) 其他的块: 已分配区域的主体(body) 3. 咱们能够使用一个整数数组来记录块的使用状况,因为每一个块只有头/主体/空闲这3种状态,所以仅仅须要两位便可表示一个块,由于称为"位图" //位图(bitmap)不位于这全部内存块中,而是保存其余地方的一块独立的内存区域 4. 位图(bitmap)的目的是为实际的堆内存维护一个描述分配状态的元数据(经常是数组形式),经过对所有目标内存地址创建一一对应的关系,经过bit的方式进行状态描述
位图(bitmap)的优缺点以下
1. 速度快 整个堆的空闲信息存储在一个数组内,所以访问该数组时cache容易命中 2. 稳定性好 为了不用户越界读写破坏数据,咱们只须要对占用空间较小的位图进行备份便可 3. 块不须要额外信息,易于管理 4. 分配内存的时候容易产生碎片 5. 若是堆很大,块很小(这样可能减少碎片),那么位图会很大,这样可能会致使失去cache命中率高的优点,并且也会浪费必定的空间。针对这种状况,咱们能够使用多级的位图
3. 对象池
对象池的思路很简单,若是每一次分配的空间大小都同样,那么就能够按照这个每次请求分配的大小做为一个单位,把整个堆空间划分为大小的小块,每次请求的时候只须要找到一个小块便可
对象池的管理方法能够采用空闲链表,也能够采用位图,与它们的区别仅仅在于对象池假定了每次请求都是一个固定的大小
4. 总结
在真实场景中,堆的分配算法每每是采起多种算法复合而成的,例如glibc
1. 小于64byte的空间申请: 采用相似对象池的算法 2. 大于64byte、小于512byte的空间申请: 采起空闲链表或者位图 3. 大于512byte的空间申请: 最佳适配算法 4. 大于128KB的空间申请: 使用mmap机制直接向操做系统申请空间
Copyright (c) 2014 LittleHann All rights reserved