-
2016-06-18 14:57:14
1、伙伴系统
---- 固定分区和动态分区方式都有不足之处。
---- 固定分区方式限制了活动进程的数目,当进程的大小与空闲分区大小不匹配时,内存空间利用率很低。
---- 动态分区方式算法复杂,回收空闲分区时需要进行分区合并等,系统开销较大。
---- 伙伴系统方式是对以上两种内存方式的一种折衷方案。
伙伴系统规定:无论已分配分区或空闲分区,其大小均为2的k次幂,k为整数,1<=k<=m,k大于等于1,小于等于m。
其中:2的1次方表示分配的最小分区的大小,2的m次方表示分配的最大分区的大小,通常2的m次方是整个可分配内存的大小。
2、空间分配
---- 当需要为进程分配一个长度为n的存储空间时,首先计算一个i值,使n大于2^(i-1)次方,小于等于2^i次方,然后在空闲分区大小为2^i的空闲分区
链表中查找。若找到,即把该空闲分区分配给进程。否则,表明长度为2^i的空闲分区已经耗尽,则在分区大小为2^(i+1)的空闲分区链表中寻找。
---- 若存在一个2^(i+1)的空闲分区,则把该空闲分区分为相等的两个分区,这两个分区称为一对伙伴,其中的一个分区用于分配,把另一个加入分区
大小为2^i的空闲分区链表中。若大小为2^(i+1)的空闲分区也不存在,则需要查找大小为2^(i+2)的空闲分区。
---- 若找到一个2^(i+2)的空闲分区,则对其进行两次分割:第一次,将其分割为大小为2^(i+1)的两个分区,一个用于分配,一个加入到大小为2^(i+1)
的空闲分区链表中;第二次,将第一次用于分配的空闲区分割为2^i的两个分区,一个用于分配,一个加入到大小为2^i的空闲分区链表中。
若仍然找不到,则继续查找大小为2^(i+3)的空闲分区,以此类推。
---- 由此可见,在最坏的情况下,可能需要对2^k的空闲分区进行k次分割才能得到所需分区。
3、空间回收
---- 与一次分配可能要进行多次分割一样,一次回收也可能要进行多次合并,如回收大小为2^i的空闲分区,若事先已存在2^i的空闲分区,则应将其与
伙伴分区合并为大小为2^(i+1)的空闲分区,若事先已存在2^(i+1)的空闲分区时,又应继续与其伙伴分区合并为大小为2^(i+2)的空闲分区,以此类推。
---- 在伙伴系统中,其分配和回收的时间性能取决于查找空闲分区的位置和分割,合并空闲分区所花费的时间。
---- 与前几种动态分区分配算法相比较,由于该算法在回收空闲分区时,需要对空闲分区进行合并,所以其时间性能比前面所述的分类搜索法差,
但比顺序搜索法好,而其空间性能则远优于分类搜索法,比顺序搜索法略差。
==================================================
在当前的操作系统中,普遍采用的是基于分页和分段机制的虚拟内存机制,该机制较伙伴算法更为合理和高效。
但在多处理机系统中,伙伴系统仍不失为一种有效的内存分配和释放方法,得到了大量的应用。
更多相关内容 -
伙伴系统分配模拟
2016-12-25 12:26:39c语言实现模拟伙伴系统分配内存页面(buddy_allocte) -
伙伴系统 c++
2013-08-18 10:23:38设计一个程序模拟伙伴算法,功能包括: (1)根据用户申请内存的大小,判断是否符合分配条件,如果能根据伙伴算法分配 (2)能够按照伙伴算法回收内存 (3)能够实时输出内存的分配情况 -
内存分配算法(含伙伴系统)
2014-01-11 19:45:02内含伙伴系统、最坏、最佳三种分配方式的演示,有内存分配动态变化图。 -
linux物理内存管理-伙伴系统
2018-05-16 20:07:29linux使用伙伴系统来管理物理内存页。一、伙伴系统原理1. 伙伴关系定义:由一个母实体分成的两个各方面属性一致的两个子实体,这两个子实体就处于伙伴关系。在操作系统分配内存的过程中,一个内存块常常被分成两个...linux使用伙伴系统来管理物理内存页。
一、伙伴系统原理
1. 伙伴关系
定义:由一个母实体分成的两个各方面属性一致的两个子实体,这两个子实体就处于伙伴关系。在操作系统分配内存的过程中,一个内存块常常被分成两个大小相等的内存块,这两个大小相等的内存块就处于伙伴关系。它满足 3 个条件 :
- 两个块具有相同大小记为 2^K
- 它们的物理地址是连续的
- 从同一个大块中拆分出来
2. 伙伴算法的实现原理
为了便于页面的维护,将多个页面组成内存块,每个内存块都有 2 的方幂个页,方幂的指数被称为阶 order。order相同的内存块被组织到一个空闲链表中。伙伴系统基于2的方幂来申请释放内存页。
当申请内存页时,伙伴系统首先检查与申请大小相同的内存块链表中,检看是否有空闲页,如果有就将其分配出去,并将其从链表中删除,否则就检查上一级,即大小为申请大小的2倍的内存块空闲链表,如果该链表有空闲内存,就将其分配出去,同时将剩余的一部分(即未分配出去的一半)加入到下一级空闲链表中;如果这一级仍没有空闲内存;就检查它的上一级,依次类推,直到分配成功或者彻底失败,在成功时还要按照伙伴系统的要求,将未分配的内存块进行划分并加入到相应的空闲内存块链表
在释放内存页时,会检查其伙伴是否也是空闲的,如果是就将它和它的伙伴合并为更大的空闲内存块,该检查会递归进行,直到发现伙伴正在被使用或者已经合并成了最大的内存块。
二、linux中的伙伴系统相关的结构
系统中的每个物理内存页(页帧)都对应一个struct page数据结构,每个节点都包含了多个zone,每个zone都有struct zone表示,其中保存了用于伙伴系统的数据结构。zone中的- struct free_area free_area[MAX_ORDER];
struct free_area的定义如下
- struct free_area {
- structlist_head free_list[MIGRATE_TYPES];
- unsignedlong nr_free;
- };
- nr_free:其中nr_free表示内存页块的数目,对于0阶的表示以1页为单位计算,对于1阶的以2页为单位计算,n阶的以2的n次方为单位计算。
- free_list:用于将具有该大小的内存页块连接起来。由于内存页块表示的是连续的物理页,因而对于加入到链表中的每个内存页块来说,只需要将内存页块中的第一个页加入该链表即可。因此这些链表连接的是每个内存页块中第一个内存页,使用了struct page中的struct list_head成员lru。free_list数组元素的每一个对应一种属性的类型,可用于不同的目地,但是它们的大小和组织方式相同。
基于伙伴系统的内存管理方式专注于内存节点的某个内存域的管理,但是系统中的所有zone都会通过备用列表连接起来。伙伴系统和内存域/节点的关系如下图所示:
系统中伙伴系统的当前信息可以通过/proc/buddyinfo查看:
这是我的PC上的信息,这些信息描述了每个zone中对应于每个阶的空闲内存页块的数目,从左到右阶数依次升高。
三、避免碎片
1.碎片概念
伙伴系统也存在一些问题,在系统长时间运行后,物理内存会出现很多碎片,如图所示:
这是虽然可用内存页还有很多,但是最大的连续物理内存也只有一页,这对于用户程序不成问题,因为用户程序通过页表映射,应用程序看到的总是连续的虚拟内存。但是对于内核来说就不行了,因为内核有时候需要使用连续的物理内存。
2.linux解决方案
碎片问题也存在于文件系统,文件系统中的碎片可以通过工具来解决,即分析文件系统,然后重新组织文件的位置,但是这种方不适用于内核,因为有些物理页时不能随意移动。内核采用的方法是反碎片(anti-fragmentation)。为此内核根据页的可移动性将其划分为3种不同的类型:
- 不可移动的页:在内存中有固定位置,不能移动。分配给核心内核的页大多是此种类型
- 可回收的页:不能移动,但是可以删除,其内容可以从某些源重新生成。
- 可移动的页:可以随意移动。属于用户进程的页属于这种类型,因为它们是通过页表映射的,因而在移动后只需要更新用户进程页表即可。
需要注意的是按照可移动性对内存页进行分组时在运行中进行的,而不是在一开始就设置好的。
1.数据结构
内核定义了MIGRATE_TYPES中迁移类型,其定义如下:
- enum {
- MIGRATE_UNMOVABLE,
- MIGRATE_RECLAIMABLE,
- MIGRATE_MOVABLE,
- MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
- MIGRATE_RESERVE = MIGRATE_PCPTYPES,
- MIGRATE_ISOLATE, /* can't allocate from here */
- MIGRATE_TYPES
- };
- MIGRATE_PCPTYPES:是per_cpu_pageset,即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目
- MIGRATE_RESERVE:是在前三种的列表中都没用可满足分配的内存块时,就可以从MIGRATE_RESERVE分配
- MIGRATE_ISOLATE:用于跨越NUMA节点移动物理内存页,在大型系统上,它有益于将物理内存页移动到接近于是用该页最频繁地CPU
类似于从zone中的分配,如果无法从指定的迁移类型分配到页,则会按照fallbacks指定的次序从备用迁移类型中尝试分配,它定义在page_alloc.c中。
虽然该特性总是编译进去的,但是该特性只有在系统中有足够的内存可以分配到每种迁移类型对应的链表时才有意义,也就是说每个可以迁移性链表都要有“适量”的内存,内核需要对“适量”的判断是基于两个宏的:
- pageblock_order:内核认为够大的一个分配的阶。
- pageblock_nr_pages:内核认为启用该特性时每个迁移链表需要具有的最少的内存页数。它的定义是基于pageblock_order的。
内核定义了两个标志:__GFP_MOVABLE和 __GFP_RECLAIMABLE分别用来表示可移动迁移类型和可回收迁移类型,如果没有设置这两个标志,则表示是不可移动的。如果页面迁移特性被禁止了,则所有的页都是不可移动页。
struct zone中包含了一个字段pageblock_flags,它用于跟踪包含pageblock_nr_pages个页的内存区的属性。在初始化期间,内核自动保证对每个迁移类型,在pageblock_flags中都分配了足够存储NR_PAGEBLOCK_BITS个比特的空间。
set_pageblock_migratetype用于设置一个以指定的页为起始地址的内存区的迁移类型。
页的迁移类型是预先分配好的,对应的比特位总是可用,在页释放时,必须将其返还给正确的链表。get_pageblock_migratetype可用于从struct page中获取页的迁移类型。
通过/proc/pagetypeinfo可以获取系统当前的信息。
在内存初始化期间memmap_init_zone会将所有的内存页都初始化为可移动的。该函数在paging_init中会最终被调到(会经过一些中间函数,其中就有free_area_init_node)。
3.虚拟可移动内存
内核还提供了一种机制来解决碎片问题,即使用虚拟内存域ZONE_MOVABLE。其思想是:可用内存划分为两个部分,一部分用于可移动分配,一部分用于不可移动分配。这样就防止了不可移动页向可移动内存区域引入碎片。
该机制需要管理员来配置两部分内存的大小。
kernel参数kernelcore用于指定用于不可移动分配的内存数量,如果指定了该参数,其值会保存在required_kernelcore会基于它来计算。
kernel参数movablecore用于指定用于可移动分配的内存数量,如果指定了该参数,则其值会被保存在required_movablecore中,同时会基于它来计算required_kernelcore,代码如下(函数find_zone_movable_pfns_for_nodes):
- corepages = totalpages - required_movablecore;
- required_kernelcore = max(required_kernelcore, corepages);
该zone是一个虚拟zone,它不和任何物理内存相关联,该域中的内存可能来自高端内存或者普通内存。用于不可移动分配的内存会被均匀的分布到系统的各个内存节点中;同时用于可移动分配的内存只会取自最高内存域的内存,zone_movable_pfn记录了取自各个节点的用于可移动分配的内存的起始地址。
四、初始化内存域和节点数据结构
在内存管理的初始化中,架构相关的代码要完成系统中可用内存的检测,并要将相关信息提交给架构无关的代码。架构无关的代码free_area_init_nodes负责完成管理数据结构的创建。该函数需要一个参数max_zone_pfn,它由架构相关的代码提供,其中保存了每个内存域的最大可用页帧号。内核定义了两个数组:
- static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES];
- static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];
然后内核开始调用find_zone_movable_pfns_for_nodes对ZONE_MOVABLE域进行初始化。
然后内核开始为每一个节点调用free_area_init_node,这个函数将完成:
- 调用calculate_node_totalpages计算节点中页的总数
- 调用alloc_node_mem_map负责初始化struct pglist_data中的node_mem_map,为它分配的内存将用于存储本节点的所有物理内存的struct page结构。这片内存将对其到伙伴系统的最大分配阶上。而且如果当前节点是第0个节点,则该指针信息还将保存在全局变量mem_map中。
- 调用free_area_init_core完成初始化进一步的初始化
free_area_init_core将完成内存域数据结构的初始化,在这个函数中
- nr_kernel_pages记录直接映射的页面数目,而nr_all_pages则记录了包括高端内存中页数在内的页数
- 会调用zone_pcp_init初始化该内存域的每CPU缓存
- 会调用init_currently_empty_zone初始化该zone的wait_table,free_area列表
- 调用memmap_init初始化zone的页,所有页都被初始化为可移动的
五、分配器API
伙伴系统只能分配2的整数幂个页。因此申请时,需要指定请求分配的阶。
有很多分配和释放页的API,都定义在gfp.h中。最简单的是alloc_page(gfp_mask)用来申请一个页, free_page(addr)用来释放一个页。
这里更值得关注的获取页面时的参数gfp_mask,所有获取页面的API都需要指定该参数。它用来影响分配器的行为,其中有是分配器提供的标志,标志有两种:
zone修饰符:用于告诉分配器从哪个zone分配内存
行为修饰符:告诉分配器应该如何进行分配
其中zone修饰符定义为- #define __GFP_DMA ((__force gfp_t)___GFP_DMA)
- #define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
- #define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
- #define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* Page is movable */
- #define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
更详细的可以参考gfp.h,其中包含了所有的标志及其含义。
1.分配页
__alloc_pages会完成最终的内存分配,它是伙伴系统的核心代码(但是在内核代码中,这种命名方式的函数都是需要小心调用的,一般都是给实现该功能的代码自己调用,不作为API提供出去的,因而它的包装器才是对外提供的API,也就是alloc_pages_node)。
1.选择页
选择页中最重要的函数是get_page_from_freelist,它负责通过标志和分配阶来判断分配是否可以进行,如果可以就进行实际的分配。该函数还会调用zone_watermark_ok根据指定的标识判断是否可以从给定的zone中进行分配。该函数需要struct zonelist的指针指向备用zone,当当前zone不能满足分配需求时就依次遍历该列表尝试进行分配。整体的分配流程是:
- 调用get_page_from_freelist尝试进行分配,如果成功就返回分配到的页,否则
- 唤醒kswapd,然后再次调用get_page_from_freelist尝试进行分配,如果成功就返回分配的页,否则
- 如果分配的标志允许不检查阈值进行分配,则以ALLOC_NO_WATERMARKS为标志再次调用get_page_from_freelist尝试分配,如果成功则返回分配的页;如果不允许不检查阈值或者仍然失败,则
- 如果不允许等待,就分配失败,否则
- 如果支持压缩,则尝试先对内存进行一次压缩,然后再调用get_page_from_freelist,如果成功就返回,否则
- 进行内存回收,然后再调用get_page_from_freelist,如果成功就返回,否则
- 根据回收内存并尝试分配的结果以及分配标志,可能会调用OOM杀死一个进程然后再尝试分配,也可能不执行OOM这一步的操作,如果执行了,则在失败后可能就彻底失败,也可能重新回到第2步,也可能继续下一步
- 回到第2步中调用get_page_from_freelist的地方或者再尝试一次先压缩后分配,如果走了先压缩再分配这一步,这就是最后一次尝试了,要么成功要么失败,不会再继续尝试了
2.移出所选择的页
在函数get_page_from_freelist中,会首先在zonelist中找到一个具有足够的空闲页的zone,然后会调用buffered_rmqueue进行处理,在分配成功时,该函数会把所分配的内存页从zone的free_list中移出,并且保证剩余的空闲内存页满足伙伴系统的要求,该函数还会把内存页的迁移类型存放在page的private域中。
该函数的步骤如图所示:可以看出buffered_rmqueue的工作过程为:
- 如果申请的是单页,会做特殊处理,内核会利用每CPU的缓存加速这个过程。并且在必要的时候会首先填充每CPU的缓存。函数rmqueue_bulk用于从伙伴系统获取内存页,并添加到指定的链表,它会调用函数__rmqueue。
- 如果是分配多个页,则会首先调用__rmqueue从内存域的伙伴系统中选择合适的内存块,这一步可能失败,因为虽然内存域中有足够数目的空闲页,但是页不一定是连续的,如果是这样这一步就会返回NULL。在这一步中如果需要还会将大的内存块分解成小的内存块来进行分配,即按照伙伴系统的要求进行分配。
- 无论是分配单页还是多个页,如果分配成功,在返回分配的页之前都要调用prep_new_page,如果这一步的处理不成功就会重新进行分配(跳转到函数buffered_rmqueue的开始),否则返回分配的页。
- 首先调用__rmqueue_smallest尝试根据指定的zone,分配的阶,迁移类型进行分配,该函数根据指定的信息进行查找,在找到一个可用的空闲内存页块后会将该内存页块从空闲内存页块链表中删除,并且会调用expand使得剩余的内存页块满足伙伴系统的要求。如果在这一步成功就返回,否则执行下一步
- 调用__rmqueue_fallback尝试从备用zone分配。该函数用于根据前一类型的备用列表尝试从其它备用列表分配,但是需要注意的是这里会首先尝试最大的分配阶,依次降低分配的阶,直到指定的分配的阶,采用这个策略是为了避免碎片—如果要用其它迁移类型的内存,就拿一块大的过来,而不是在其它迁移类型的小区域中到处引入碎片。同时如果从其它迁移类型的空闲内存页块分配到的是一个较大的阶,则整块内存页块的迁移类型可能会发生改变,从原来的类型改变为申请分配时所请求的类型(即迁移类型发生了改变)。分配成功时的动作和__rmqueue_smallest类似,移出内存页,调用expand。
- 对页进行检查,以确保页确实是可用的,否则就返回一个非0值导致分配失败
- 设置页的标记以及引用计数等等。
- 如果设置而来__GFP_COMP标志,则调用prep_compound_page将页组织成复合页(hugetlb会用到这个)。
复合页具有如下特性:
- 复合页中第一个页称为首页,其它所拥有页都称为尾页
- 组成复合页的所有的private域都指向首页
- 第一个尾页的lru的next域指向释放复合页的函数指针
- 第一个尾页的lru的prev域用于指向复合页所对应的分配的阶,即多少个页
2.释放页
__free_pages是释放页的核心函数,伙伴系统提供出去的API都是它的包装器。其流程:
- 减小页的引用计数,如果计数不为0则直接返回,否则
- 如果释放的是单页,则调用free_hot_cold_page,否则
- 调用__free_pages_ok
函数__free_pages_ok最终页会调到__free_one_page来释放页,__free_one_page会将页面释放返还给伙伴系统,同时在必要时进行递归合并。
在__free_one_page进行合并时,需要找到释放的page的伙伴的页帧号,这是通过__find_buddy_index来完成的,其代码非常简单:
- __find_buddy_index(unsigned long page_idx,unsigned int order)
- {
- returnpage_idx ^ (1 << order);
- }
如果可以合并还需要取得合并后的页帧号,这个更简单,只需要让两个伙伴的页帧号相与即可。
__free_one_page调用page_is_buddy来对伙伴进行判断,以决定是否可以合并。
六、不连续内存页的分配
内核总是尝试使用物理上连续的内存区域,但是在分配内存时,可能无法找到大片的物理上连续的内存区域,这时候就需要使用不连续的内存,内核分配了其虚拟地址空间的一部分(vmalloc区)用于管理不连续内存页的分配。
每个vmalloc分配的子区域都自包含的,在内核的虚拟地址空间中vmalloc子区域之间都通过一个内存页隔离开来,这个间隔用来防止不正确的访问。
1. 用vmalloc分配内存
vmalloc用来分配在虚拟地址空间连续,但是在物理地址空间不一定连续的内存区域。它只需要一个以字节为单位的长度参数。为了节省宝贵的较低端的内存区域,vmalloc会使用高端内存进行分配。
内核使用struct vm_struct来管理vmalloc分配的每个子区域,其定义如下:
- struct vm_struct {
- struct vm_struct *next;
- void *addr;
- unsigned long size;
- unsigned long flags;
- struct page **pages;
- unsigned int nr_pages;
- phys_addr_t phys_addr;
- const void *caller;
- };
- next:指向下一个vmalloc子区域
- addr:vmalloc子区域在内核虚拟地址空间的起始地址
- size:vmalloc子区域的长度
- flags:与该区域相关标志
- pages:指针,指向映射到虚拟地址空间的物理内存页的struct page实例
- nr_pages:映射的物理页面数目
- phys_addr:仅当用ioremap映射了由物理地址描述的内存页时才需要改域,它保存物理地址
- caller:申请者
2. 创建vmalloc子区域
所有的vmalloc子区域都被连接保存在vmlist中,该链表按照addr排序,顺序是从小到大。当创建一个新的子区域时需要,需要找到一个合适的位置。查找合适的位置采用的是首次适用算法,即从vmalloc区域找到第一个可以满足需求的区域,查找这样的区域是通过函数__get_vm_area_node完成的。其分配过程以下几步:
- 调用__get_vm_area_node找到合适的区域
- 调用__vmalloc_area_node分配物理内存页
- 调用map_vm_area将物理内存页映射到内核的读你地址空间
- 将新的子区域插入vmlist链表
还有其它的方式来建立虚拟地址空间的连续映射:
- vmalloc_32:与vmallo工作方式相同,但是确保所使用的物理地址总可以用32位指针寻址
- vmap:将一组物理页面映射到连续的虚拟地址空间
- ioremap:特定于处理器的分配函数,用于将取自物理地址空间而、由系统总线用于I/O操作的一个内存块,映射到内核的虚拟地址空间
3. 释放内存
vfree用于释放vmalloc和vmalloc_32分配的内存空间,vunmap用于释放由vmap和ioremap分配的空间(iounmap会调到vunmap)。最终都会归结到函数__vunmap。
__vunmap的执行过程:
- 调用remove_vm_area从vmlist中找到一个子区域,然后将其从子区域删除,再解除物理页面的映射
- 如果设置了deallocate_pages,则将物理页面归还给伙伴系统
- 释放管理虚拟内存的数据结构struct vm_struct
七、内核映射
高端内存可通过vmalloc机制映射到内核的虚拟地址空间,但是高端内存往内核虚拟地址空间的映射并不依赖于vmalloc,而vmalloc是用于管理不连续内存的,它也并不依赖于高端内存。
1.持久内核映射
如果想要将高端内存长期映射到内核中,则必须使用kmap函数。该函数需要一个page指针用于指向需要映射的页面。如果没有启用高端内存,则该函数直接返回页的地址,因为所有页面都可以直接映射。如果启用了高端内存,则:
- 如果不是高端内存的页面,则直接返回页面地址,否则
- 调用kmap_high进行处理
1.使用的数据结构
vmalloc区域后的持久映射区域用于建立持久映射。pkmap_count是一个有LAST_PKMAP个元素的数组,每个元素对应一个持久映射。每个元素的值是被映射页的一个使用计数器:
- 0:相关的页么有被使用
- 1:该位置关联的页已经映射,但是由于CPU的TLB没有刷新而不能使用
- 大于1的其它值:表示该页的引用计数,n表示有n-1处在使用该页
- struct page_address_map {
- struct page *page;
- void *virtual;
- struct list_head list;
- };
- page:指向全局数据结构mem_map数组中的page实例的指针
- virtual:该页在虚拟地址空间中分配的位置
函数page_address用于根据page实例获取器对应的虚拟地址。其处理过程:
- 如果不是高端内存直接根据page获得虚拟地址(利用__va(paddr)),否则
- 在散列表中查找该page对应的struct page_address_map实例,获取其虚拟地址
2.创建映射
函数kmap_high完成映射的实际创建,其工作过程:
- 调用page_address获取对应的虚拟地址
- 如果没有获取到,则调用map_new_virtual获取虚拟地址
- pkmap_count数组中对应于该虚拟地址的元素的引用计数加1
- 执行一个无限循环:
- 更新last_pkmap_nr为last_pkmap_nr+1
- 同时如果last_pkmap_nr为0,调用flush_all_zero_pkmaps,flush CPU高速缓存
- 检查pkmap_count数组中索引last_pkmap_nr对应的元素的引用计数是否为0,如果是0就退出循环,否则
- 将自己加入到一个等待队列
- 调度其它任务
- 被唤醒时会首先检查是否有其它任务已经完成了新映射的创建,如果是就直接返回
- 回到循环头部重新执行
- 获取与该索引对应的虚拟地址
- 修改内核页表,将该页映射到获取到的虚拟地址
- 更新该索引对应的pkmap_count元素的引用计数为1
- 调用set_page_address将新的映射加入到page_address_htable中
- 调用flush_cache_kmaps执行高速缓存flush动作
- 遍历pkmap_count中的元素,如果某个元素的值为1就将其减小为0,并删除相关映射同时设置需要刷新标记
- 如果需要刷新,则调用flush_tlb_kernel_range刷新指定的区域对应的tlb。
3.解除映射
kunmap用于解除kmap创建的映射,如果不是高端内存,什么都不做,否则kunmap_high将完成实际的工作。kunmap_high的工作很简单,将对应的pkmap_count中的元素的引用计数的值减1,如果新值为1,则看是否有任务在pkmap_map_wait上等待,如果有就唤醒它。根据该机制的涉及原理,该函数不能将引用计数减小到小于1,否则就是一个BUG。
2.临时内核映射
kmap不能用于无法休眠的上线文,如果要在不可休眠的上下文调用,则需要调用kmap_atomic。它是原子的,特定于架构的。同样的只有是高端内存时才会做实际的映射。
kmap_atomic使用了固定映射机制。在固定映射区域,系统中每个CPU都有一个对应的“窗口”,每个窗口对应于KM_TYPE_NR中不同的类型都有一项。这个映射的核心代码如下(取自powerpc):
- type = kmap_atomic_idx_push();
- idx = type + KM_TYPE_NR*smp_processor_id();
- vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
- __set_pte_at(&init_mm, vaddr, kmap_pte-idx, mk_pte(page, prot), 1);
- local_flush_tlb_page(NULL, vaddr);
- enum fixed_addresses {
- FIX_HOLE,
- /* reserve the top 128K for early debugging purposes */
- FIX_EARLY_DEBUG_TOP = FIX_HOLE,
- FIX_EARLY_DEBUG_BASE = FIX_EARLY_DEBUG_TOP+((128*1024)/PAGE_SIZE)-1,
- <strong>#ifdef CONFIG_HIGHMEM
- FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
- FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
- #endif</strong>
- /* FIX_PCIE_MCFG, */
- __end_of_fixed_addresses
- };
转:https://blog.csdn.net/goodluckwhh/article/details/9989695
-
内存管理笔记十、buddy伙伴系统
2018-03-11 19:57:42内存管理笔记十、buddy伙伴系统 引言:上一篇笔记中,我们介绍了段页式的内存管理方式其不仅获得分段和分页的好处,又规避了单纯分段和分页的缺陷。这看似是一个完美的解决方案。但每次申请内存,均要完成虚拟地址...内存管理笔记十、buddy伙伴系统
引言:上一篇笔记中,我们介绍了段页式的内存管理方式其不仅获得分段和分页的好处,又规避了单纯分段和分页的缺陷。这看似是一个完美的解决方案。但每次申请内存,均要完成虚拟地址至物理地址的映射、要改写内核的页表项、刷新TLB,以页为单位降低了内存分配速度。因此linux在段页式内存管理基础上,增加伙伴系统分配机制,可以理解为以空间换取时间和性能的机制。
一、linux物理内存划分管理
为了有效的管理物理内存(分配、回收),Linux将整个物理内存划分为若干页,对每一个页,都有相关的数据结构来记录该页的状态和使用信息。在Linux中,每个页的大小是4KB。对于一个512MB的物理内存一共有(512 * 1024)/ 4 = 131072个页。对于每一个页,Linux都有一个struct page数据结构来记录该物理页的使用情况。所有页的struct page结构组成一个连续的数组存放在物理内存的某个地方。某页在物理内存中的物理地址除以4KB,就得到该页是第几个物理页索引,然后索引就可以查询struct page数组,得到该页的具体信息。
图1、linux物理内存分页及管理示意图除了使用struct page来记录某个4KB物理页的状态和使用信息外,Linux还将整个物理内存根据物理地址划分为不同的区。区的划分是与体系结构相关的(由于硬件的限制,内核不能对所有的页一视同仁)。对于X86,ZONE可以划分为DMA区[0,16MB]、NORMAL区[16MB, 896MB]和HIGHMEM区[896MB,memory length]。对于ARM,ZONE划分为NORMAL区和HIGHMEM区。其中NORMAL区对应线性映射的物理内存,HIGHMEM区对应非线性映射的物理内存。
图2、内存区的划分
二、Buddy伙伴系统
我们介绍分页系统时已经讲过,分页系统不会产生外部碎片,一个进程占用的内存空间可以是不连续的,并且一个进程的虚拟页面在不需要的时候可以存放在磁盘上。当进程需要较大运行内存,以页为单位分配物理内存,每页均要完成虚拟地址至物理地址的映射、改写内核页表项、刷新TLB,效率较低。因此linux在段页式内存管理基础上,增加伙伴系统分配机制,我理解其为以空间换取性能的一种方式。
2.1、伙伴系统的作用:
它要解决的问题是频繁地请求和释放不同大小的一组连续页框,必然导致在已分配的块内分散了许多小块的空闲页面,由此带来的问题是,即使有足够的空闲页面可以满足请求,但要分配一个大块的连续页框可能无法满足请求。其要完成的作用,即高效的分配和回收资源,降低外部碎片。
2.2、伙伴系统的介绍:
Buddy System是Linux Kernel 进行物理内存页管理的一个子系统。在Buddy System中,管理的一个基本单位是block,每一个block有若干个连续的物理页组成,物理页的个数为2n,这个n在buddy system中被称为order。相同order的block,挂载一条双向链表上。
图3、伙伴系统的双向链表当某个block空闲时,只要发现对应的伙伴也是空闲的,就和伙伴组成一个页数为2n+1的block,挂载在order为(n+1)的双向链表上,换句话说一个页数为2n的block,是由两个页数为2n-1的伙伴block组成的。因此,一个block的伙伴肯定是和这个block在物理地址上是连续的。在Linux中,order的默认的取值范围是[0,10],其单次分配的最大内存为4M,即210 个4K页面。
2.3、申请和释放过程:
申请物理内存过程:假设请求一个页框的块(即4KB),算法先在1个页框的链表中检查是否有空闲块。若没有,则查找下一个更大的块,即在2个页框的链表中找一个空闲块。如果存在这样的块,内核就把2的页框分成两等份,一半用作满足请求,另一半插入到1 个页框的链表中。
如果在2个页框的块链表中也没找到空闲块,就继续找更大的块 —— 4个页框的块。如果这样的块存在,内核把4个页框块的1 个页框用作请求,然后从剩余的3个页框中拿2个插入到2个页框的链表中,再把最后的1个插入到1个页框的链表中。如果最终至1024个页框的链表还是空的,算法就放弃并发出错信号。
图4、加入要申请order为1的页,但buddy sustem中只有order为5的页释放物理内存过程:以上过程的逆过程就是页框块的释放过程,也是该算法名字的由来。内核试图把大小为b的一对空闲伙伴(两个块具有相同大小,且它们物理地址连续)合并为一个大小为2b的单独块。这个算法是迭代的,如果它成功合并所释放的块,它会试图合并2b 的块,以再次试图形成更大的块。
2.4、伙伴系统实现 ——相关数据结构:
我们介绍了区的概念,每个管理区都有自己的struct zone, 而struct zone中的struct free_area则是用来描述该管理区伙伴系统的空闲内存块的。其部分代码如下:
struct zone { ... ... struct free_area free_area[MAX_ORDER]; ... ... }
struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; };
free_area共有MAX_ORDER个元素,,这些空闲块在free_list中以双向链表的形式组织起来,对于同等大小的空闲块,其类型不同,将组织在不同的free_list中,nr_free记录了该free_area中总共的空闲内存块的数量。MAX_ORDER的默认值为11,这意味着最大内存块的大小为2^10=1024个页框。
关于伙伴系统的内核实现及相关细节,可以参考伙伴系统概述、伙伴系统初始化、伙伴系统分配页、伙伴系统释放页、通过迁移类型实现反碎片等。
2.5、伙伴算法的优缺点分析:
优点:
1)、较好的解决外部碎片问题
2)、当需要分配若干个内存页面时,用于DMA的内存页面必须连续,伙伴算法很好的满足了这个要求
3)、只要请求的块不超过1024个页面(4M),内核就尽量分配连续的页面。
4)、针对大内存分配设计缺点:
1)、合并的要求太过严格,只能是满足伙伴关系的块才能合并
2)、 碎片问题:一个连续的内存中仅仅一个页面被占用,导致整块内存区都不具备合并的条件
3)、浪费问题:伙伴算法只能分配2的幂次方内存区,当需要8K(2页)时,好说,当需要9K时,那就需要分配16K(4页)的内存空间,但是实际只用到9K空间,多余的7K空间就被浪费掉。
4)、算法的效率问题: 伙伴算法涉及了比较多的计算还有链表和位图的操作,开销还是比较大的,如果每次2^n大小的伙伴块就会合并到2^(n+1)的链表队列中,那么2^n大小链表中的块就会因为合并操作而减少,但系统随后立即有可能又有对该大小块的需求,为此必须再从2^(n+1)大小的链表中拆分,这样的合并又立即拆分的过程是无效率的。总结: Linux针对大内存的物理地址分配,采用伙伴算法,如果是针对小于一个page的内存,频繁的分配和释放,有更加适宜的解决方案,如slab,那是后面笔记的内容
参考内容:
认识Linux物理内存管理系统 - Buddy System
Linux伙伴算法介绍
linux伙伴系统概述纠错与建议
邮箱:db_hebut@163.com
-
伙伴系统的设计与实现
2013-04-23 16:04:44伙伴系统的简单实现,是操作系统的课程设计,看看吧。 代码+文档+心得。 -
内存管理:连续内存分配(固定分区、动态分区、伙伴系统)
2020-10-12 15:46:21文章目录内存保护覆盖(Overlay)碎片整理:分区对换(Swapping)固定分区分配动态分区分配伙伴系统(Buddy system) 内存保护 需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。两种...内存保护
需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。两种方法
- CPU中设置上、下限寄存器。
存放用户作业在主存中的下限和上限地址,每当CPU要访问一个地址时,分别和两个寄存器的值对比,判断有无越界 - 采用 重定位寄存器(或基址寄存器) 和 界地址寄存器(限长寄存器) 来实现保护。
重定位寄存器中含最小的物理地址值,界地址寄存器含逻辑地址的最大值。
每个逻辑地址值必须小于界地址寄存器;若比较后未越界则加上重定位寄存器的值后映射成物理地址,再送内存单元。
覆盖(Overlay)
早期用的。
基本思想:由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可把用户空间分成一个固定区和若干覆盖区。将经常活跃的部分放在固定区,其他部分按调用关系分段。首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段。
实现:
- 将程序的必要部分代码和数据常驻内存;
- 可选部分在其他程序模块中实现,平时存放在外存中(覆盖文件),需
要用到时才装入到内存; - 不存在调用关系的模块不必同时装入到内存,可相互覆盖。
特点:打破了必须将一个进程全部信息装入主存后才能运行的限制;但当同时运行代码大于主存时仍不能运行;内存中能更新的地方只有覆盖区的段,不在覆盖区的段会常驻内存。
碎片整理:分区对换(Swapping)
早期使用,基本思想:
- 把内存中处于等待状态(或在CPU调度原则下被剥夺运行权力的)程序换出到外存上【换出】,以腾出足够的内存空间;
- 把准备好竞争CPU运行的程序从辅存移到内存【换入】。
优点:不要求用户给出程序段之间的逻辑覆盖结构。
注意:
- 交换需要备份存储,通常是快速磁盘;
- 进程执行时间要比交换时间长;
- 若换出经常,必须确保该进程完全处于空闲状态;
- 交换空间常作为磁盘的一整块,且独立于文件系统,因此使用很快;
- 交换在内存吃紧时启动,系统负荷降低就暂停。
固定分区分配
把内存分为大小相等或不等的固定分区,在每个分区中只装入一道程序。
分区分配算法:为进程分配某个空闲分区。
- 分区相等时,任选一个空闲分区;
- 分区不等时,根据空闲分区选择最佳任务或根据任务选择最佳空闲分区。
存储保护:界限寄存器
优点: 比单一连续分配方法,内存利用率提高了;可以支持多道程序,无外部碎片;实现简单。
缺点:
- 分区数目确定,限制了系统中活跃进程的数目
- 小作业的内部碎片可能比较大;
- 作业必须预先能估计要占用多大的内存空间。
动态分区分配
优点:- 支持多道程序
- 管理方案相对简单,不需要更多的软、硬件开销
- 实现存储保护的手段比较简单
缺点:
- 主存利用不够充分,存在外部碎片; ➡ 通过紧凑技术解决
- 无法实现多进程共享存储器的信息;
- 无法实现主存的逻辑扩充,进程的地址空间受实际物理内存的限制。
伙伴系统(Buddy system)
综合静态划分技术和动态划分技术的优点。
分配: 进程申请大小为k的空间,系统分配一 2u 的空闲分区,其中,2u-1<k ≤ 2u
- 查找大小为 2i 的空闲分区,若找到则分配;
- 若未找到大小为 2i 的空闲分区,则查找大小为 2i+1 的空闲分区;若找到,则将该空闲分区划分为相等的两个分区(一对伙伴),其中的一个用于分配,另一个分区加入大小为 2i 的空闲分区链中;
- 若未找到 2i+1 的空闲分区,则需要查找大小为 2i+2 的空闲分区,若找到则需要进行两次划分。第一次将其分割为大小为 2i+1 的两个分区,一个用于分配,另一个加入到大小为 2i+1 的空闲分区链中;第二次将第一次用于分配的空闲分区分割为 2i 的两个分区,一个用于分配,另一个加入到大小为 2i 的空闲分区链中。
- 若仍未找到 2i+2 的空闲分区,则继续查找 2i+3 的空闲分区,以此类推。
回收: 释放一个大小为 2i 的分区时,算法:
- 若事先不存在 2i 的空闲分区,则保留该分区为一个独立的空闲分区;
- 若事先已存在 2i 的空闲分区时,则将其与伙伴分区合并为大小为为 2i+1 的空闲分区;
- 若事先已存在 2i+1 的空闲分区时,继续合并为 2i+2 的空闲分区,以此类推。
缺点:速度快,但内存利用率不高。
- CPU中设置上、下限寄存器。
-
深入浅出内存管理-- 伙伴系统(buddy system)
2018-12-17 18:57:39伙伴系统是内核中用来管理物理内存的一种算法,我们知道内存中有一些是被内核代码占用,还有一些是被特殊用途所保留,那么剩余的空闲内存都会交给内核内存管理系统来进行统一管理和分配, 内核中会把内存按照页来... -
操作系统学习笔记(九):连续内存分配——伙伴系统
2019-05-15 23:02:34伙伴系统是连续存储分配的一种办法。它比较好地折中了分配和回收过程中分配块的位置碎片和合并的问题。伙伴系统地概念如下图:整个可分配分区大小为2的幂次方,当需要的内存空间大于当前块的一半的时候就将整个分区... -
伙伴系统、slab、malloc辨析
2019-04-25 18:00:04伙伴系统 伙伴系统用于管理物理页,主要目的在于维护可用的连续物理空间,避免外部碎片。所有关于内存分配的操作都会与其打交道,buddy是物理内存的管理的门户。 Slab slab的目的在于避免内部碎片。从buddy系统... -
伙伴系统之伙伴系统概述--Linux内存管理(十五)
2016-09-03 00:13:15内核如何记住哪些内存块是空闲的 分配空闲页面的方法 影响分配器行为的众多标识位 内存碎片的问题和分配器如何处理碎片 2 伙伴系统的结构 2.1 伙伴系统数据结构 系统内存中的每个物理内存页(页帧),都对应于一个... -
linux 环境下.伙伴系统.实现代码
2009-11-17 11:18:13在动态内存管理方式中,伙伴系统具有管理方式简单,分配与释放速度快等优点,但伙伴系统的缺点是对内存空间的利用率比较低,在嵌入式系统中,内存管理除了应该具备管理方式简单和处理速度快等特点外,内存的利用率也... -
Linux系统内存管理之伙伴系统分析
2017-04-03 18:07:021.伙伴系统概念 伙伴系统是一种经典的内存管理方法。Linux伙伴系统的引入为内核提供了一种用于分配一组连续的页而建立的一种高效的分配策略,并有效的解决了外碎片问题。 2.伙伴系统的组织结构 Linux中的... -
伙伴系统的内存分配浅析
2013-09-22 15:10:43最近在网上看到一篇关于伙伴系统的内存分配问题比较好的文章。这里分享过来供大家参考。 原文地址:http://blog.csdn.net/vanbreaker/article/details/7605367 伙伴系统的概述 Linux内核内存管理的一项重要工作... -
Linux伙伴系统(一)--伙伴系统的概述
2012-05-28 16:18:02水平有限,描述不当之处还请指出,转载请注明出处...Linux采用伙伴系统解决外部碎片的问题,采用slab解决内部碎片的问题,在这里我们先讨论外部碎片问题。避免外部 -
全面解析Linux 内核 3.10.x - 内存管理 - 伙伴系统算法(Buddy System)
2016-05-21 10:28:32基于以上的种种需求,内接提供了一种解决碎片问题的方法,称之为伙伴系统算法(buddy system),伙伴系统算法将所有空闲的页框分为11个块链表,每个块链表分别包含大小1,2,4,8,16,32,64,128,256,512,1024个连续的页框。... -
采用伙伴系统算法编写内存分配和回收模拟程序
2014-06-14 10:07:10一、需求分析 动态分区和固定分区的方案都有缺陷,固定分区方案限定了活动进程的数目,并且如果可用分区的大小与进程大小非常不匹配则内存空间的...基于伙伴系统的优秀特点,本次综合实验将实现伙伴系统的典型功能。 -
伙伴系统和slab缓存详解
2017-03-28 16:39:51本节,我将介绍linux系统物理内存分配时所用到的技术——伙伴系统和slab缓存。 知识背景1. DMA/HIGH_MEM/NROMAL 分区 在x86结构中,Linux内核虚拟地址空间划分0~3G为用户空间,3~4G为内核空间(注意,内核可以... -
看数据结构写代码(50)伙伴系统
2015-04-16 21:08:06伙伴系统 是一种 只 可以 分配 2的 幂次方 个 空间的 ,回收 内存 时 只 合并 “伙伴空间” 的一种 动态内存管理方式。 例如 一个 空间 大小 为 64 的 内存,伙伴 系统 为 这 64 的内存 建立 一组 双向循环 链表,... -
Linux伙伴系统(二)--伙伴系统的初始化
2012-05-29 13:06:39伙伴系统的初始化主要是初始化之前介绍的伙伴系统涉及到的数据结构,并且把系统初始化时由bootmem allocator管理的低端内存以及系统的高端内存释放到伙伴系统中去。其中有些和zone相关的域在前面>中已经有所介绍。 -
内存管理之伙伴系统算法(The Buddy System Algorithm)
2013-10-14 09:06:47Linux内核中对于内存分配采用的是伙伴系统算法,该算法主要用于解决外部碎片问题(external fragmentation)。该算法大体原理如下所示: -
Linux内存管理之伙伴系统(内存释放)
2012-01-09 10:06:49Linux内核伙伴系统中页面释放,主函数为free_pages() 一、上层操作 /*用虚拟地址进行释放*/ void free_pages(unsigned long addr, unsigned int order) { if (addr != 0) { VM_BUG_ON(!virt_addr_valid((void *... -
Linux内核——伙伴系统和slab缓存
2015-08-03 21:01:02本节,我将介绍linux系统物理内存分配时所用到的技术——伙伴系统和slab缓存。 伙伴系统 使用场景:内核中很多时候要求分配连续页,为快速检测内存中的连续区域,内核采用了一种技术:伙伴系统。 原理:系统中... -
Buddy System,伙伴系统
2012-09-06 18:28:37Buddy System,伙伴系统,linux中用来分配、释放内存页块的经典算法。数据结构structzone中有个free_area数组。存放着本zone的空闲页块链表,注意这是一组链表,而不是一个链表。我们常常需要按“块”分配连续多个... -
伙伴系统算法详解
2016-10-06 11:18:22参考 http://www.voidcn.com/blog/EmbedStudio/article/p-3848869.html 看一下这篇blog下面的碎片解决问题的链接 -
伙伴系统和slab机制
2015-09-06 12:15:29伙伴系统 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示。四级页表分别为: 页全局目录(Page Global ... -
简述Linux内存分配--伙伴系统 原理
2016-08-15 09:31:16Linux内存分配——伙伴系统 目的:最大限度的降低内存的碎片化。 原理: 1.将内存块分为了11个连续的页框块(1,2,4,8....512,1024),其中每一个页框块中用链表将内存块对应内存大小的块进行链接。 2.若... -
操作系统学习之用C语言模拟伙伴(Buddy)算法
2021-05-27 23:00:49C语言 Buddy算法 FIFO算法 LRU算法 Clock算法 操作系统 -
伙伴系统之避免碎片--Linux内存管理(十六)
2016-09-28 21:57:25日期 内核版本 架构 作者 GitHub ...1 前景提要1.1 碎片化问题分页与分段页是信息的物理单位, 分页是为了实现非连续分配, 以便解决内存碎片问题, 或者说分页是由于系统管理的需要. 段是信息的逻辑单位