本部分不仅仅是LDD的介绍部分, 还包括了对linux的内存模型的总结.
一句话总结
伙伴系统是基石, slab基于伙伴系统, kmalloc基于slab.
要点
?伙伴系统是对连续大内存而言, 得到的内存的单位从1个page到211 page, 解决外部碎片问题.
?Slab分配器是针对小内存而言, 从32B到128KB, 解决的是内部碎片问题, kmalloc是基于slab分配器的.
?如果物理内存加上需要映射的IO空间内存的大小加起来超过896M, 则有必要开启highmen的功能.
Agenda
内存模型(zone, 伙伴系统, slab)
获得页(Request Page Frame)
释放页(Release Page Frame)
高端内存访问
?permanent kernel mapping
?temporary kernel mapping
?noncontiguous memory allocation
? linux将内存划分为一个个Node, 每个Node分三个zone
–zone_DMA, 通常对应16M以下空间, ISA总线的限制
–zone_NORMAL
–zone_HIGHMEM, 通常对应896M以上的空间, 这部分空间不会直接映射到内核的第4GB的范围, 应此内核无法直接访问
Buddy System Algorithm
?外部碎片: 频繁的分配/释放导致的, 会造成原本是一整块连续的内存, 变成有很多断断续续的碎片, 导致后续分配连续内存的时候失败, 即使总的剩余空间还是够的.
?解决方法:
–重新将不连续的物理地址映射为连续的线性地址
–开发内存管理技术, 记录空闲连续内存的情况
–内核采用第二种方法
?内核将空闲页框分组为11个块链表, 分别对应1,2,4,8,16,32,64,128,256,512,1024个连续页框, 即最大对应4M的分配大小.
且每个块的起始地址是该块里面的连续页框大小的整数倍.
?内核的做法
–假如要申请256个页框, 内核先检查对应的链表中是否有这样的空闲块.
–如果没有, 就检查512个页框对应的链表, 如果有空闲块, 就把512的页框等分, 一份用来满足请求, 另外一份就插入到256页框的那个链表中.
–如果512页框的那个链表没有空闲块, 就查找1024个页框的链表, 如果找到, 则256个页框用来满足要求. 剩下的786个页框再分成两份, 其中512个页框插入到512那个链表中,
剩下256个插入到256那个链表中.
–如果1024页框的那个链表也没有空闲块, 就返回错误.
–以上的逆过程就是对应的释放过程
?大小为b的页框的伙伴的要求
–也有同样的大小
–物理地址是连续的
–第一个块的第一个页框的物理地址是2 * b * 4K的倍数
Slab
?前面讲到, 伙伴算法采用页框作为基本的内存单元分配, 这适合于大块内存的分配, 对小内存要怎么处理呢?
?内核将小内存的分配再放入到同一个页框中, 但是这样又带来这新的问题.
?内部碎片: 内存请求的大小与分配给他的大小不匹配. 比如说, 需要25 byte, 但是分配了32 byte. 因为内核对小内存的分配也是以2的幂次方来分配的, 默认是13级, 从32 byte到 128K byte.
?怎么办
–频繁请求/释放: slab分配器
–不频繁请求/释放: kmalloc/kfree
?内核函数倾向于反复请求同一类型的内存区, 如果没有slab分配器, 内核需要反复的分配和回收包含同一内存区的页框, 影响效率.
而slab分配器把页框保存在高速缓存中, 可以很快的重复使用.
?现有的普通slab高速缓存
–cache_cache中包含的用于分配kmem_cache_t的缓存
–32B – 128KB的分配类型, 一种用于DMA, 一种用于常规分配(kmalloc)
–在kmem_cache_init中初始化
?内核用到的特殊slab高速缓存
–用kmem_cache_create来创建
–用kmem_cache_destroy来销毁(适用于module形式), 或kmem_cache_shrink
?Slab需要的页框怎么来
–kmem_getpages
?Slab的页框怎么释放
–kmem_freepages
?分配slab对象
–kmem_cache_alloc
?释放slab对象
–kmem_cache_free
Request page frame
?alloc_pages(gfp_mask, order)
得到2^order 连续的page frame, 返回满足条件的第一个page的struct page*, 或者NULL
?alloc_page(gfp_mask)
alloc_pages(gfp_mask, 0)
?_ _get_free_pages(gfp_mask, order)
类似alloc_pages, 但是返回第一个page的线性地址
?_ _get_free_page(gfp_mask)
_ _get_free_pages(gfp_mask, 0)
?get_zeroed_page(gfp_mask)
alloc_pages(gfp_mask | _ _GFP_ZERO, 0), 返回线性地址, page填充为0
?_ _get_dma_pages(gfp_mask, order)
_ _get_free_pages(gfp_mask | _ _GFP_DMA, order), 返回给DMA传输用的线性地址
?由于内核经常请求和释放单个页框, 为了提升性能, 每个zone都定义了一个per cpu的页高速缓存, 这个高速缓存包含了预先分配的页框.
?实际上, 这个高速缓存包含两个部分, 一个是hot高速缓存(会存放在cache中), 一个是cold高速(往往用做DMA, 不需要CPU参与)
struct per_cpu_pages { int count; /* number of pages in the list */ int low; /* low watermark, refill needed */ int high; /* high watermark, emptying needed */ int batch; /* chunk size for buddy add/remove */ struct list_head list; /* the list of pages */ };
?如果页框的个数超过high, 则从高速缓存中释放batch个页框回伙伴系统, 如果低于low, 则从伙伴系统中分配batch个页框到高速缓存中.
?API
–buffered_rmqueue 通过per cpu页框高速缓存分配
–free_hot_page/free_cold_page 释放页框到per cpu页框高速缓存
常用的GFP MASK的介绍
?Group name Corresponding flags
?GFP_ATOMIC _ _GFP_HIGH
?GFP_NOIO _ _GFP_WAIT
?GFP_NOFS _ _GFP_WAIT | _ _GFP_IO
?GFP_KERNEL _ _GFP_WAIT | _ _GFP_IO | _ _GFP_FS
?GFP_USER _ _GFP_WAIT | _ _GFP_IO | _ _GFP_FS
?GFP_HIGHUSER _ _GFP_WAIT | _ _GFP_IO | _ _GFP_FS | _ _GFP_HIGHMEM
__GFP_DMA和__GFP_HIGHMEM被称作管理区修饰符, 表示寻找空闲page frame的时候搜索的zone, 但实际上:
?如果__GFP_DMA被设置, 则只能从DMA zone去获取page frame
?如果__GFP_HIGHMEM没有被设置, 则按优先顺序从normal zone和DMA zone去获取
?如果__GFP_HIGHMEM被设置, 则按优先顺序从highmem zone, normal zone和DMA zone去获取
Release page frame
?_ _free_pages(page, order)
–检查page frame是否是reserved的, 如果不是, 减少counter, 如果counter为0, 这认为连续的2order的page frame都不再使用
?_ _free_page(page)
–_ _free_pages(page, 0)
?free_pages(addr, order)
–和__free_pages类似, 只是接受的是线性地址
?free_page(addr)
–free_pages(addr, 0)
高端内存访问
?由于896M以上的page frame并不映射在内核线性地址空间的第4G的范围, 因此内核不能直接访问.
?也就是说, 返回线性地址的分配函数(__get_free_pages)不能用于高端内存.
?能使用的是alloc_pages, 返回的是page*
?因此,内核线性地址空间的最后128M的一部分专门用来做高端内存的映射, 从而达到访问高端内存的目的.
高端内存映射方法
?permanent kernel mapping
?会阻塞当前进程, 因此不能用在中断和tasklet中
?存放在pkmap_page_table中, 总共有LAST_PKMAP(512/1024)个table, 也就是映射了2M/4M的地址.
?page_address( )函数, 根据page descriptor返回线性地址
–如果是非highmem的, 那么这个地址一定存在, __va(page_to_pfn(page) << PAGE_SHIFT)
–如果是在highmem中, 则到page_address_htable中查找, 如果没有就返回NULL
?Kmap用来建立永久映射(其实调用的是kmap_high)
void * kmap_high(struct page * page) { unsigned long vaddr; spin_lock(&kmap_lock); vaddr = (unsigned long)page_address(page); if (!vaddr) vaddr = map_new_virtual(page); pkmap_count[(vaddr-PKMAP_BASE) >> PAGE_SHIFT]++; spin_unlock(&kmap_lock); return (void *) vaddr; }
?Kunmap用来去除永久映射(其实调用的是kunmap_high)
void kunmap_high(struct page * page) { spin_lock(&kmap_lock); if ((--pkmap_count[((unsigned long) page_address(page) - PKMAP_BASE)>>PAGE_SHIFT]) == 1) if (waitqueue_active(&pkmap_map_wait)) wake_up(&pkmap_map_wait); spin_unlock(&kmap_lock); }
?temporary kernel mapping
– 不会阻塞, 但是要避免在使用相同的映射
– 数目很有限, 一般是每个CPU才13个映射的window
enum km_type { D(0) KM_BOUNCE_READ, D(1) KM_SKB_SUNRPC_DATA, D(2) KM_SKB_DATA_SOFTIRQ, D(3) KM_USER0, D(4) KM_USER1, D(5) KM_BIO_SRC_IRQ, D(6) KM_BIO_DST_IRQ, D(7) KM_PTE0, D(8) KM_PTE1, D(9) KM_IRQ0, D(10) KM_IRQ1, D(11) KM_SOFTIRQ0, D(12) KM_SOFTIRQ1, D(13) KM_TYPE_NR };
– 使用接口kmap_atomic 和 kunmap_atomic
void * kmap_atomic(struct page * page, enum km_type type) { enum fixed_addresses idx; unsigned long vaddr; current_thread_info( )->preempt_count++; if (!PageHighMem(page)) return page_address(page); idx = type + KM_TYPE_NR * smp_processor_id( ); vaddr = fix_to_virt(FIX_KMAP_BEGIN + idx); set_pte(kmap_pte-idx, mk_pte(page, 0x063)); _ _flush_tlb_single(vaddr); return (void *) vaddr; }
void kunmap_atomic(void *kvaddr, enum km_type type) { #ifdef CONFIG_DEBUG_HIGHMEM unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK; enum fixed_addresses idx = type + KM_TYPE_NR*smp_processor_id(); if (vaddr < FIXADDR_START) { // FIXME dec_preempt_count(); preempt_check_resched(); return; } if (vaddr != __fix_to_virt(FIX_KMAP_BEGIN+idx)) BUG(); pte_clear(kmap_pte-idx); __flush_tlb_one(vaddr); #endif dec_preempt_count(); preempt_check_resched(); }
?noncontiguous memory allocation
?vmalloc/vmalloc_32(只能从normal/DMA zone分配)
?vmap(前提是已经调用get_vm_area得到vm_struct描述符了)
?ioremap
?vfree
?vunmap
?iounmap