以下论述,皆以32位机器举例子.64位道理相同,类比即可 内存管理有两种状态的管理方式,分别是内核态和用户态.用户态比较随意一点,而内核态就必须严格把控内存(不这样,系统就会崩溃啊,不是吗?)
页学过计算机的都知道,OS是通过基本单位-页来管理内存的,那么具体主要有什么呐?
structpage{page_flags_tflags;页标志符atomic_t_count;页引用计数atomic_t_mapcount;页映射计数unsignedlongprivate;私有数据指针structaddress_space*mapping;该页所在地址空间描述结构指针,用于内容为文件的页帧pgoff_tindex;该页描述结构在地址空间radix树page_tree中的对象索引号即页号structlist_headlru;最近最久未使用structslab结构指针链表头变量void*virtual;页虚拟地址};
flags:页标志包含是不是脏的,是否被锁定等等,每一位单独表示一种状态,可同时表示出32种不同状态,定义在linux/page-flags.h
_count:计数值为-1表示未被使用。
virtual:页在虚拟内存中的地址,对于不能永久映射到内核空间的内存(比如高端内存),该值为NULL;需要事必须动态映射这些内存。
内核用structpage结构体表示每个物理页,假定structpage结构体占40个字节,系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。
需要注意的是:page结构体描述的是物理页而非逻辑页,描述的是内存页的信息而不是页中数据。,其实就和i-node 对象一样
区在X86架构中,Linux虚拟地址空间划分为0~3G为用户空间,3~4G为内核空间(为什么这样?为了减少系统调用的成本,通过系统调用的方式直接去3~4G的表中去查找内核态的页表(所有程序都一样的)就行了,而不用去切换到内核然后再去查页表,执行系统调用,所以这也就解释了为什么现代操作系统执行系统调用的时候几乎不用切换到内核态了)
内核空间的内存中所有的页分为三类,分别放在三个区
区描述物理内存(MB)ZONE_DMADMA使用的页16ZONE_NORMAL可正常寻址的页16~ZONE_HIGHMEM动态映射的页由于内核的虚拟和物理地址只差一个偏移量:物理地址=逻辑地址–0xC,其实这就是说内核的对应是一对一的对应.所以如果1G内核空间完全用来线性映射,显然物理内存也只能访问到1G区间,这显然是不合理的。HIGHMEM就是为了解决这个问题,专门开辟的一块不必线性映,可以灵活定制映射的区域,以便访问1G以上物理内存的区域。从网上扣来一图
具体对应structzone结构
structzone{/*Read-mostlyfields*//*zonewatermarks,accesswith*_wmark_pages(zone)macros*/unsignedlongwatermark[NR_WMARK];unsignedlongnr_reserved_highatomic;/**Wedontknowifthememorythatweregoingtoallocatewillbe*freeableor/anditwillbereleasedeventually,sotoavoidtotally*wastingseveralGBoframwemustreservesomeofthelowerzone*memory(otherwisewerisktorunOOMonthelowerzonesdespite*therebeingtonsoffreeableramonthehigherzones).Thisarrayis*recalculatedatruntimeifthesysctl_lowmem_reserve_ratiosysctl*changes.*/longlowmem_reserve[MAX_NR_ZONES];#ifdefCONFIG_NUMAintnode;#endifstructpglist_data*zone_pgdat;structper_cpu_pageset__percpu*pageset;#ifndefCONFIG_SPARSEMEM/**Flagsforapageblock_nr_pagesblock.Seepageblock-flags.h.*InSPARSEMEM,thismapisstoredinstructmem_section*/unsignedlong*pageblock_flags;#endif/*CONFIG_SPARSEMEM*//*zone_start_pfn==zone_start_paddrPAGE_SHIFT*/unsignedlongzone_start_pfn;unsignedlongmanaged_pages;unsignedlongspanned_pages;unsignedlongpresent_pages;constchar*name;#ifdefCONFIG_MEMORY_ISOLATION/**Numberofisolatedpageblock.Itisusedtosolveincorrect*freepagecountingproblemduetoracyretrievingmigratetype*ofpageblock.Protectedbyzone-lock.*/unsignedlongnr_isolate_pageblock;#endif#ifdefCONFIG_MEMORY_HOTPLUG/*seespanned/present_pagesformoredescription*/seqlock_tspan_seqlock;#endifintinitialized;/*Write-intensivefieldsusedfromthepageallocator*/ZONE_PADDING(_pad1_)/*freeareasofdifferentsizes*/structfree_areafree_area[MAX_ORDER];...}
需要注意的是:并不是所有的体系结构都定义了全部区,有些64位的体系结构,比如Intel的x86-64体系结构可以映射和处理64位的内存空间,所以其没有ZONE_HIGHMEM区。而有些体系结构中的所有地址都可用于DMA,所以这些体系结构就没有ZONE_DMA区。
内存页分配接口页的分配先来看alloc_pages(gfp_mask,order)函数
...staticinlinestructpage*alloc_pages_node(intnid,gfp_tgfp_mask,unsignedintorder){if(nid==NUMA_NO_NODE)nid=numa_mem_id();return__alloc_pages_node(nid,gfp_mask,order);}#ifdefCONFIG_NUMAexternstructpage*alloc_pages_current(gfp_tgfp_mask,unsignedorder);staticinlinestructpage*alloc_pages(gfp_tgfp_mask,unsignedintorder){returnalloc_pages_current(gfp_mask,order);}externstructpage*alloc_pages_vma(gfp_tgfp_mask,intorder,structvm_area_struct*vma,unsignedlongaddr,intnode,boolhugepage);#definealloc_hugepage_vma(gfp_mask,vma,addr,order)\alloc_pages_vma(gfp_mask,order,vma,addr,numa_node_id(),true)#else#definealloc_pages(gfp_mask,order)\...
再来看__get_free_pages(gfp_tgfp_mask,unsignedintorder)函数
unsignedlong__get_free_pages(gfp_tgfp_mask,unsignedintorder){structpage*page;//使用了上一个函数page=alloc_pages(gfp_mask~__GFP_HIGHMEM,order);if(!page)return0;//使用 page_address,转为了虚拟地址return(unsignedlong)page_address(page);}
最后来看get_zeroed_page(gfp_mask)函数
我们在使用这些接口获取页的时会面对一个问题,我们获得的这些页若是给用户态用,虽然这些页中的数据都是随机产生的垃圾数据,不过,虽然概率很低,但是也有可能会包含某些敏感信息。所以,更谨慎些,我们可以将获得的页都填充为0。这会用到get_zeroed_page函数
unsignedlongget_zeroed_page(gfp_tgfp_mask){//可以看到,这里只是加了一种 __GFP_ZERO的gfp_mask方式,其余的都一样return__get_free_pages(gfp_mask
__GFP_ZERO,0);}页的释放内存字节分配接口
前面讲的那些接口都是以页为单位进行内存分配与释放的。而在实际中内核需要的内存不一定是整个页,可能只是以字节为单位的一片区域。下面两个函数就是实现这样的目的。
kmalloc
static__always_inlinevoid*kmalloc(size_tsize,gfp_tflags){if(__builtin_constant_p(size)){#ifndefCONFIG_SLOBunsignedintindex;#endifif(sizeKMALLOC_MAX_CACHE_SIZE)returnkmalloc_large(size,flags);#ifndefCONFIG_SLOBindex=kmalloc_index(size);if(!index)returnZERO_SIZE_PTR;returnkmem_cache_alloc_trace(kmalloc_caches[kmalloc_type(flags)][index],flags,size);#endif}return__kmalloc(size,flags);}//kmalloc-__kmalloc-__do_kmalloc-....static__always_inlinevoid*__do_kmalloc(size_tsize,gfp_tflags,unsignedlongcaller){structkmem_cache*cachep;void*ret;if(unlikely(sizeKMALLOC_MAX_CACHE_SIZE))returnNULL;cachep=kmalloc_slab(size,flags);if(unlikely(ZERO_OR_NULL_PTR(cachep)))returncachep;ret=slab_alloc(cachep,flags,caller);kasan_kmalloc(cachep,ret,size,flags);trace_kmalloc(caller,ret,size,cachep-size,flags);returnret;}
kmalloc()函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存在物理内存中连续且保持原有的数据(不清零)
其中部分flags取值说明:
GFP_USER:用于用户空间的分配内存,可能休眠;
GFP_KERNEL:用于内核空间的内存分配,可能休眠;
GFP_ATOMIC:用于原子性的内存分配,不会休眠;典型原子性场景有中断处理程序,软中断,tasklet等
kmalloc内存分配最终总是调用__get_free_pages来进行实际的分配,故前缀都是GFP_开头。
kmalloc分最多只能分配32个page大小的内存,每个page=4k,也就是K大小,其中16个字节用来记录页描述结构。kmalloc分配的是常驻内存,不会被交换到文件中。最小分配单位是32或64字节。
kzalloc函数等于分配并置零
staticinlinevoid*kzalloc(size_tsize,gfp_tflags){returnkmalloc(size,flags
__GFP_ZERO);}vmalloc
void*vmalloc(unsignedlongsize){return__vmalloc_node_flags(size,NUMA_NO_NODE,GFP_KERNEL);}staticinlinevoid*__vmalloc_node_flags(unsignedlongsize,intnode,gfp_tflags){return__vmalloc_node(size,1,flags,PAGE_KERNEL,node,__builtin_return_address(0));}staticvoid*__vmalloc_node(unsignedlongsize,unsignedlongalign,gfp_tgfp_mask,pgprot_tprot,intnode,constvoid*caller){return__vmalloc_node_range(size,align,VMALLOC_START,VMALLOC_END,gfp_mask,prot,0,node,caller);}void*__vmalloc_node_range(unsignedlongsize,unsignedlongalign,unsignedlongstart,unsignedlongend,gfp_tgfp_mask,pgprot_tprot,unsignedlongvm_flags,intnode,constvoid*caller){structvm_struct*area;//vm_struct虚拟内存结构void*addr;unsignedlongreal_size=size;size=PAGE_ALIGN(size);if(!size
(sizePAGE_SHIFT)totalram_pages)gotofail;area=__get_vm_area_node(size,align,VM_ALLOC
VM_UNINITIALIZED
vm_flags,start,end,node,gfp_mask,caller);if(!area)gotofail;addr=__vmalloc_area_node(area,gfp_mask,prot,node);if(!addr)returnNULL;/**Inthisfunction,newlyallocatedvm_structhasVM_UNINITIALIZED*flag.Itmeansthatvm_structisnotfullyinitialized.*Now,itisfullyinitialized,soremovethisflaghere.*/clear_vm_uninitialized_flag(area);kmemleak_vmalloc(area,size,gfp_mask);returnaddr;fail:warn_alloc(gfp_mask,NULL,"vmalloc:allocationfailure:%lubytes",real_size);returnNULL;}
该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的。
不同之处在于,kmalloc分配的是虚拟地址连续,物理地址也连续的一片区域,vmalloc分配的是虚拟地址连续,物理地址不一定连续的一片区域。管理page的算法-Buddy由来:假设这是一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的个页框。这个时候,在这段内存上不能找到连续的个空闲的页框,就会去另一段内存上去寻找个连续的页框,这样子,久而久之就形成了页框的浪费。
Buddy算法是什么?为了避免出现这种情况,Linux内核中引入了伙伴系统算法(Buddysystem)。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,,26,12和个连续页框的页框块。最大可以申请个连续页框,对应4MB大小的连续内存。如图:
假设要申请一个26个页框的块,先从26个页框的链表中查找空闲块,如果没有,就去12个页框的链表中找,找到了则将页框块分为2个26个页框的块,一个分配给应用,另外一个移到26个页框的链表中。如果12个页框的链表中仍没有空闲块,继续向个页框的链表查找,如果仍然没有,则返回错误。页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。
从上面可以知道Buddy算法一直在对页框做拆开合并拆开合并的动作。Buddy算法牛逼就牛逼在运用了世界上任何正整数都可以由2^n的和组成。这也是Buddy算法管理空闲页表的本质。
查看自己系统的页面情况从左向右,依次是1,2,4,8…页框大小的剩余个数
这里我在想一个问题:就是这样的一个算法,他最后不也是会形成许许多多的小的碎片的吗?那么Linux是怎样解决的呐?目前正在探索.
Slab分配器由来:在Linux中,伙伴系统(buddysystem)是以页为单位管理和分配内存。但是现实的需求却以字节为单位,假如我们需要申请20Bytes,总不能分配一页吧!那岂不是严重浪费内存。那么该如何分配呢?slab分配器就应运而生了,专为小内存分配而生。
Slab分配器的作用?频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
解决小内存分配的问题
对存放的对象进行着色(color),以防止多个对象映射到相同的高速缓存行(cacheline)。
Slab分配器是什么?slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统。我们先来看一张图:
这里讲到的高速缓存结构就是下文中显示的kmem_cache物理内存中有多个高速缓存,每个高速缓存都是一个结构体类型,一个高速缓存中会有一个或多个slab,slab通常为一页(连续的内存块),其中存放着数据结构类型的实例化对象。
分配高速缓存的接口是structkmem_cachekmem_cache_create(constchar*name,size_tsize,size_talign,unsignedlongflags,void(*ctor)(void))
这个接口函数为一个结构体分配了高速缓存,那么高速缓存有了,是不是就要为缓存中分配实例化的对象呢?这个接口是
void*kmem_cache_alloc(structkmem_cache*cachep,gfp_tflags)参数是kmem_cache结构体,也就是分配好的高速缓存,flags是标志位。
该函数从给定的高速缓存cachep中返回-一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,fags的值传递给_getfree_pages()。
具体的slab还分为三种,这里不再详述,下文给出参考链接
这里举个例子来说明,用structkmem_cache结构描述的一段内存就称作一个slab缓存池。一个slab缓存池就像是一箱牛奶,一箱牛奶中有很多瓶牛奶,每瓶牛奶就是一个object。分配内存的时候,就相当于从牛奶箱中拿一瓶。总有拿完的一天。当箱子空的时候,你就需要去超市再买一箱回来。超市就相当于partial链表,超市存储着很多箱牛奶。如果超市也卖完了,自然就要从厂家进货,然后出售给你。厂家就相当于伙伴系统。
实例分析:内核初始化期间,/kernel/fork.c的fork_init()中会创建一个名叫task_struct的高速缓存;每当进程调用fork()时,会通过dup_task_struct()创建一个新的进程描述符,并调用do_fork(),完成从高速缓存中获取对象。
Maybe,这种缓存方式的实现,就是一种写时复制.我还不清楚哦
总结:预览时标签不可点收录于话题#个上一篇下一篇最近更新
推荐文章