Linux中内存的分配和回收.docx
- 文档编号:7067958
- 上传时间:2023-01-16
- 格式:DOCX
- 页数:14
- 大小:39.04KB
Linux中内存的分配和回收.docx
《Linux中内存的分配和回收.docx》由会员分享,可在线阅读,更多相关《Linux中内存的分配和回收.docx(14页珍藏版)》请在冰豆网上搜索。
Linux中内存的分配和回收
一。
系统启动时的内存操作
二。
伙伴算法
三。
slab分配器
四。
slob分配器
五。
slub分配器
六。
kmalloc和kfree
七。
vmalloc和vfree
八。
glibc中的malloc和free
九。
参考资料
一。
[[Anchor(NBE1)]]系统启动时的内存操作
1。
pg0的位置和尺寸
当系统刚刚启动时,在分页功能未打开前,线性地址和物理地址是一一对应的。
刚开启分页功能时,pg0的内存地址是在编译内核时定义好的,见arch\i386\kernel\vmlinux.lsd.S,大小为4096字节,启始地址紧跟内核在内存中物理地址。
由于内核保护模式代码启始位置为0x100000,所以pg0地址=0x100000+内核保护模式代码尺寸。
对pg0的操作主要是开启分页机制时填写页面描述表信息,上一节在第一次页寻址设置中已经详细介绍。
2。
内存位图的内存操作
内存位图是系统设置区域和页面管理前的内存使用状态表。
2.1内存位图的位置和尺寸
位图contig_page_data.bdata->node_bootmem_map的起始地址跟在init_pg_tables_end的后面。
大小等于所有物理页面数除以32,即每一位代表一个页面。
首先我们看文件arch/i386/kernel/setup.c中的setup_memory函数
min_low_pfn=PFN_UP(init_pg_tables_end);
然后看同样文件中的函数setup_bootmem_allocator
bootmap_size=init_bootmem(min_low_pfn,max_low_pfn);
在init_bootmem中min_low_pfn是init_pg_tables_end的页框号
最终在函数init_bootmem_core中我们看到
bdata->node_bootmem_map=phys_to_virt(PFN_PHYS(mapstart));
其中mapstart就是min_low_pfn;bdata就是NODE_DATA(0),也就是contig_page_data.bdata
所以contig_page_data.bdata->node_bootmem_map就是init_pg_tables_end指向地址
我们前面已经介绍init_pg_tables_end的实际位置和内核大小有关
init_pg_tables_end=内核保护模式代码启始地址(0x100000)+内核保护模式代码尺寸+pg0的1024个4字节页面描述符号+保证第一次分页设置的页表尺寸(一般还需要若干1024个4字节的页面描述符号,由内核尺寸决定)+描述1G内存的位图尺寸128K字节+描述1G内存的页表空间(1024*4096字节)+间隔空间(4*4096字节)
可以看出如果内核尺寸在4M左右,描述1G内存也需要大量页表空间(4M),这样第一次分页设置实际上就至少需要3个页表的页描述符。
2.2内存位图的释放
在设置了区域和页面管理以后,内存位图就不需要了。
释放内存位图的内存时,使用了函数free_pages_bootmem。
首先见启动过程中的mem_init()函数,这是在设置了区域和页面管理后执行的。
我们看其中的free_all_bootmem()调用,在free_all_bootmem_core中将bdata->node_bootmem_map的对应页面结构逐个调用free_pages_bootmem。
在free_pages_bootmem中首先清除了页面结构的PG_reserved标记,然后设置页面使用记数count为1,最后调用free_page(下面会详细描述)。
3。
pte的内存分配
设置页表时,pte的内存分配使用了函数alloc_bootmem_low_pages,由于此时区域页面管理还未开始使用,使用的都是内存位图搜索。
在one_page_table_init中使用alloc_bootmem_low_pages为页表分配内存,大小为一个页面4096字节。
见mm/bootmem.c中的alloc_bootmem_low函数,最后还是使用了alloc_bootmem_core,其中分配大小为PAGE_SIZE(实际上就是一个页面),对齐为PAGE_SIZE,启始搜寻地址为0,界限为ARCH_LOW_ADDRESS_LIMIT。
首先如果设置了启始搜寻地址,则从启始搜寻地址开始向上找。
使用find_next_zero_bit查找bdata->node_bootmem_map从搜寻地址开始的第一个未分配页面(test_bit(i,bdata->node_bootmem_map),i为页面位),然后检查接下来的连续申请页面是否都为空余页面。
在one_page_table_init中只需要申请一个页面。
找到申请页面并调整对齐以后,将页面转换为虚拟地址,即加上0xC0000000。
并使用test_and_set_bit(i,bdata->node_bootmem_map)在内存位图中标记内存已经使用。
最后返回虚拟地址。
4。
页面信息结构page的内存分配
在区域管理中设置页面信息page时,由函数alloc_node_mem_map使用了调用alloc_bootmem_node实现,由于此时区域页面管理还未开始使用,使用的也是内存位图搜索。
见mm/bootmem.c中的alloc_bootmem_node函数,最后也是使用了alloc_bootmem_core,其中对齐使用SMP_CACHE_BYTES,启始搜寻地址为MAX_DMA_ADDRESS,界限为0。
和pte页表分配不同的是,这里的尺寸不再是一个页面。
start=pgdat->node_start_pfn&~(MAX_ORDER_NR_PAGES-1);这里的pgdat->node_start_pfn就是early_node_map[i].start_pfn,在add_active_range(0,0,max_low_pfn)里面定义,实际上就是0。
end=pgdat->node_start_pfn+pgdat->node_spanned_pages;pgdat->node_spanned_pages是系统中总共的页框数目
end=ALIGN(end,MAX_ORDER_NR_PAGES);
size=(end-start)*sizeof(structpage);需要的空间就是系统中页框数和页面结构的乘积,也就是为系统中存在的每一个页表建立一个页表结构
页表结构的内存搜索从MAX_DMA_ADDRESS以上寻找。
0xC0000000+0x1000000=0xC1000000,即16M物理地址以上找空余内存。
其它操作和分配pte页面内存一样。
分配后获得的页面结构内存指针放在pgdat->node_mem_map和mem_map中。
5。
free_pages详解
free_pages
∙如果只释放一页就调用free_hot_page,否则调用free_pages_ok,这里只介绍后者
∙free_one_page(page,zone,order);要释放的页面指针page、页面所在区域指针zone和连续页面大小order
∙page_idx=page_to_pfn(page)&((1< ∙while(order ∙buddy=page_find_buddy(page,page_idx,order);使用buddy算法合并页面,但合并的连续页面不能大于MAX_ORDER-1,下面会详细介绍伙伴算法 ∙if(! page_is_buddy(page,buddy,order)) ∙break; ∙list_del(&buddy->lru);将伙伴页面从空余空间列表中删除 ∙area=zone->free_area+order;找到当前order的空余页面信息指针 ∙area->nr_free--;空余页面信息减1 ∙rmv_page_order(buddy);设置伙伴页面属性 ∙combined_idx=find_combined_index(page_idx,order);合并后索引 ∙page=page+(combined_idx-page_idx);指向合并后的页面指针 ∙page_idx=combined_idx;page_idx是给page_find_buddy寻找伙伴的参数 ∙order++;幂数加一 ∙} ∙set_page_order(page,order);设置页面伙伴信息 ∙list_add(&page->lru,&zone->free_area[order].free_list);将页面加入到空余空间列表 ∙zone->free_area[order].nr_free++;对应空余空间记数加一 二。 [[Anchor(NBE2)]]伙伴算法 1。 算法原理 BuddySystem是一种经典的内存管理算法。 在Unix和Linux操作系统中都有用到。 其作用是减少存储空间中的空洞、减少碎片、增加利用率。 避免外碎片的方法有两种: a.利用分页单元把一组非连续的空闲页框映射到非连续的线性地址区间。 b.开发适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而把大块的空闲块进行分割。 基于下面三种原因,内核选择第二种避免方法: a.在某些情况下,连续的页框确实必要。 b.即使连续页框的分配不是很必要,它在保持内核页表不变方面所起的作用也是不容忽视的。 假如修改页表,则导致平均访存次数增加,从而频繁刷新TLB。 c.通过4M的页可以访问大块连续的物理内存,相对于4K页的使用,TLB未命中率降低,加快平均访存速度。 buddy算法将所有空闲页框分组为10个块链表,每个块链表分别包含1,2,4,8,16,32,64,128,256,512个连续的页框,每个块的第一个页框的物理地址是该块大小的整数倍。 如,大小为16个页框的块,其起始地址是16*2^12的倍数。 例,假设要请求一个128个页框的块,算法先检查128个页框的链表是否有空闲块,如果没有则查256个页框的链表,有则将256个页框的块分裂两份,一份使用,一份插入128个页框的链表。 如果还没有,就查512个页框的链表,有的话就分裂为128,128,256,一个128使用,剩余两个插入对应链表。 如果在512还没查到,则返回出错信号。 回收过程相反,内核试图把大小为b的空闲伙伴合并为一个大小为2b的单独块,满足以下条件的两个块称为伙伴: a.两个块具有相同的大小,记做b。 b.它们的物理地址是连续的。 c.第一个块的第一个页框的物理地址是2*b*2^12的倍数。 该算法迭代,如果成功合并所释放的块,会试图合并2b的块来形成更大的块。 2。 buddy算法在linux中的应用 Linux内核对各个zone都有一个buddysystem。 structzone中的unsignedlongfree_pages保存的是空闲块的大小。 特别强调的是structfree_areafree_area[MAX_ORDER],保存着zone中的空闲块。 数组中的每一个元素都有个双链表结构。 structfree_area{ ∙structlist_headfree_list; ∙unsignedlongnr_free; }; 比如说free_area中第K个元素保存着大小为2的k次方大小的块的链表结构。 数组中保存的是表头结构,即指向第一个2的k次方大小块的第一个页面。 那块的剩余的页面怎么办? 不用管,因为都是按块来操作的,只需要知道块的第一个页面即可,最后一个页面就是第一个页面加上2的k次方。 同属于一个链表的块与块之间由每一个块的第一个页面的structpage中的list_headlru来相互链接。 分配一个大小为2的m次方的页面块,首先看freearea的第m个元素,如果其nr_free大于0,则从这个链表中取出来一个块来满足要求,如果不大于0,则看数组中m+1个元素,那要下去。 如果找到能够分配的,那么就将块的第一部分大小为2的m次方的块分出去,剩下的继续保存在buddysystem中。 而每一个zone结构里都有一个structpglist_data*zone_pgdat域,这个域的成员structpage*node_mem_map指向这个zone的第一个page在mem_map的位置,mem_map是一个structpage指针,对应系统中所有的物理内存页。 页面分配代码使用free_area数组来寻找和释放页面,此机制负责整个缓冲管理。 另外此代码与处理器使用的页面大小和物理分页机制无关。 free_area中的每个元素都包含页面块的信息。 数组中第一个元素描叙1个页面,第二个表示2个页面大小的块而接下来表示4个页面大小的块,总之都是2的次幂倍大小。 list域表示一个队列头,它包含指向mem_map指针中page数据结构的指针。 所有的空闲页面都在此队列中。 map域是指向某个特定页面尺寸的页面组分配情况位图的指针。 当页面的第N块空闲时,位图的第N位被置位。 2.1页面分配 Linux使用Buddy算法来有效的分配与回收页面块。 页面分配代码每次分配包含一个或者多个物理页面的内存块。 页面以2的次幂的内存块来分配。 这意味着它可以分配1个、2个和4个页面的块。 只要系统中有足够的空闲页面来满足这个要求(nr_free_pages>min_free_page),内存分配代码将在free_area中寻找一个与请求大小相同的空闲块。 free_area中的每个元素保存着一个反映这样大小的已分配与空闲页面的位图。 例如,free_area数组中第二个元素指向一个反映大小为四个页面的内存块分配情况的内存映象。 分配算法首先搜寻满足请求大小的页面。 它从free_area数据结构的list域着手沿链来搜索空闲页面。 如果没有这样请求大小的空闲页面,则它搜索两倍于请求大小的内存块。 这个过程一直将持续到free_area被搜索完或找到满足要求的内存块为止。 如果找到的页面块大于请求的块则对其进行分割以使其大小与请求块匹配。 由于块大小都是2的次幂所以分割过程十分简单。 空闲块被连进相应的队列而这个页面块被分配给调用者。 2.2页面回收 将大的页面块打碎进行分配将增加系统中零碎空闲页面块的数目。 页面回收代码在适当时机下要将这些页面结合起来形成单一大页面块。 事实上页面块大小决定了页面重新组合的难易程度。 当页面块被释放时,代码将检查是否有相同大小的相邻或者buddy内存块存在。 如果有,则将它们结合起来形成一个大小为原来两倍的新空闲块。 每次结合完之后,代码还要检查是否可以继续合并成更大的页面。 最佳情况是系统的空闲页面块将和允许分配的最大内存一样大。 在上面图中,如果释放页面框号1,它将和空闲页面框号0结合作为大小为2个页面的空闲块排入free_area的第一个元素中。 三。 [[Anchor(NBE3)]]slab分配器 所谓尺有所长,寸有所短。 以页为最小单位分配内存对于内核管理系统物理内存来说的确比较方便,但内核自身最常使用的内存却往往是很小(远远小于一页)的内存块——比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页。 这些用来存放描述符的内存相比页面而言,就好比是面包屑与面包。 一个整页中可以聚集多个这种这些小块内存;而且这些小块内存块也和面包屑一样频繁地生成/销毁。 为了满足内核对这种小内存块的需要,Linux系统采用了一种被称为slab分配器的技术。 Slab分配器的实现相当复杂,但原理不难,其核心思想就是“存储池”的运用。 内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到“存储池”里,留做下次使用,这无疑避免了频繁创建与销毁对象所带来的额外负载。 Linuxslab分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配器。 1。 slab的特点 下图给出了slab结构的高层组织结构。 在最高层是cache_chain,这是一个slab缓存的链接列表。 这对于best-fit算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。 cache_chain的每个元素都是一个kmem_cache结构的引用(称为一个cache)。 它定义了一个要管理的给定大小的对象池。 每个缓存都包含了一个slabs列表,这是一段连续的内存块(通常都是页面)。 存在3种类型的slab: slabs_full: 完全分配的slab slabs_partial: 部分分配的slab slabs_empty: 空slab,或者没有对象被分配 注意,slabs_empty列表中的slab是进行回收(reaping)的主要备选对象。 正是通过此过程,slab所使用的内存被返回给操作系统供其他用户使用。 slab列表中的每个slab都是一个连续的内存块(一个或多个连续页),它们被划分成一个个对象。 这些对象是从特定缓存中进行分配和释放的基本元素。 slab是slab分配器进行操作的最小分配单位,因此如果需要对slab进行扩展,这也就是所扩展的最小值。 通常来说,每个slab被分配为多个对象。 由于对象是从slab中进行分配和释放的,因此单个slab可以在slab列表之间进行移动。 例如,当一个slab中的所有对象都被使用完时,就从slabs_partial列表中移动到slabs_full列表中。 当一个slab中有对象被释放后,就从slabs_full列表中移动到slabs_partial列表中。 当所有对象都被释放之后,就从slabs_partial列表移动到slabs_empty列表中。 与传统的内存管理模式相比,slab缓存分配器提供了很多优点。 首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。 slab缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。 slab分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化。 最后,slab分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。 2。 slab在Linux中的实现 下面是在Linux中创建新slab缓存、向缓存中增加内存、销毁缓存的应用程序接口(API),以及slab中对对象进行分配和释放操作的函数实现介绍。 2.1slab缓存结构 structstructkmem_cache*my_cachep; kmem_cache结构包含了每个中央处理器单元的数据、一组可调整的(可以通过proc文件系统访问)参数、统计信息和管理slab缓存所必须的元素。 2.2kmem_cache_create 内核函数kmem_cache_create用来创建一个新缓存。 这通常是在内核初始化时执行的,或者在首次加载内核模块时执行 structkmem_cache*kmem_cache_create(constchar*name,size_tsize,size_talign,unsignedlongflags, void(*ctor)(void*,structkmem_cache*,unsignedlong), void(*dtor)(void*,structkmem_cache*,unsignedlong)); name参数定义了缓存名称,proc文件系统(在/proc/slabinfo中)使用它标识这个缓存。 size参数指定了为这个缓存创建的对象的大小,align参数定义了每个对象必需的对齐。 flags参数指定了为缓存启用的选项。 SLAB_RED_ZONE在对象头、尾插入标志,用来支持对缓冲区溢出的检查。 SLAB_POISON使用一种己知模式填充slab,允许对缓存中的对象进行监视(对象属对象所有,不过可以在外部进行修改)。 SLAB_HWCACHE_ALIGN指定缓存对象必须与硬件缓存行对齐。 ctor和dtor参数定义了一个可选的对象构造器和析构器。 构造器和析构器是用户提供的回调函数。 当从缓存中分配新对象时,可以通过构造器进行初始化。 在创建缓存之后,kmem_cache_create函数会返回对它的引用。 注意这个函数并没有向缓存分配任何内存。 相反,在试图从缓存(最初为空)分配对象时,refill操作将内存分配给它。 当所有对象都被使用掉时,也可以通过相同的操作向缓存添加内存。 2.3kmem_cache_destroy 内核函数kmem_cache_destroy用来销毁缓存。 这个调用是由内核模块在被卸载时执行的。 在调用这个函数时,缓存必须为空。 voidkmem_cache_destroy(structkmem_cache*cachep); 2.4kmem_cache_alloc 要从一个命名的缓存中分配一个对象,可以使用kmem_cache_alloc函数。 调用者提供了从中分配对象的缓存以及一组标志: voidkmem_cache_alloc(structkmem_cache*cachep,gfp_tflags); 这个函数从缓存中返回一个对象。 注意如果缓存目前为空,那么这个函数就会调用cache_alloc_refill向缓存中增加内存。 kmem_cache_alloc的flags选项与kmalloc的flags选项相同。 表2给出了标志选项的部分列表。 GFP_USER为用户分配内存(这个调用可能会睡眠)。 GFP_KERNEL从内核RAM中分配内存(这个调用可能会睡眠)。 GFP_ATOMIC使该调用强制处于非睡眠状态(对中断处理程序非常有用)。 GFP_HIGHUSER从高端内存中分配内存。 2.5kmem_cache_zalloc 内核函数kmem_cache_zalloc与kmem_cache_alloc类似,只不过它对对象执行memset操作,用来在将对象返回调用者之前对其进行清除操作。 2.6kmem_cache_free 要将一个对象释放回slab,可以使用kmem_cache_free。 调用者提供了缓存引用和要释放的对象。 voidkmem_cache_free(structkmem_cache*cachep,void*objp); 四。 [[Anchor(NBE4)]]slob分配器 slob是一个相对简单一些的分配器,主要使用在小型的嵌入式系统。 在选择了CONFIG_EMBEDDED后,就可以选用CONFIG_SLOB选项,使用SLOB分配器中。 slob是一个经典的K&R/UNIX堆分配器,其具有一个slab模拟层,和被slab替代的linux原来的kmalloc分配器比较相似,比slab更有空间效率,尺寸更小,但是依然存在碎片和难于扩展(对所有操作都简单地上锁)的
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux 内存 分配 回收