linux驱动开发(3)-页面分配器
Linux系统中对物理内存进行分配的核心建立在页面级的伙伴系统之上。在系统初始化期间,伙伴系统负责对物理内存页面进行跟踪,记录哪些是已经被内核使用的页面,哪些是空闲页面。有了伙伴系统就可以让系统分配单个物理页面或者连续的几个物理页面。驱动程序在内存分配时如果需要分配比较大的地址空间,可以在这一层面利用页面分配器提供的接口函数。这些函数(或者是宏)只能分配2的整数次幂个连续的物理页。下图给出了mem_map、物理内存页面及系统虚拟地址之间关系的一个概略的示意图:
每个物理页面都有一个struct page对象与之对应。根据内存使用及内核虚拟地址空间限制等因素,内核将物理内存分为三个区:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。因为mem_map中每个struct page对象与物理页面之间严格的一一对应关系,这使得在mem_map所引导的struct page实例中,事实上也形成了三个区。Linux系统初始化期间,会将虚拟地址空间的物理页面直接映射区作线性地址映射到ZONE_DMA和ZONE_NORMAL,这意味着如果页面分配器所分配的页面落在这两个zone中,那么对应的内核虚拟地址到物理地址的映射的页目录表项已经建立,而且是所谓的线性映射,也就是虚拟地址和物理地址之间只有一个差值(PAGE_OFFSET,也即图中的0xC0000000)。而如果页面分配器所分配的页面落在ZONE_HIGHMEM中,那么内核此时尚没有对该页面进行地址映射,因此,页面分配器的调用者(比如设备驱动程序等内核模块)在这种情况下需要做的事是,在内核虚拟地址空间的动态映射区或者固定映射区分配一个虚拟地址,然后映射到该物理页面上。当然内核提供了实现这些步骤的接口函数,内核模块只要调用相应的函数就可以了。(修改页表,增加一个页表项)
以上是页面分配器的大致工作原理,接下来开始具体讨论页面分配器所提供的接口函数,无论是对UMA还是NUMA系统而言,这些函数的接口是完全一致的。页面分配器函数的核心成员其实只有两个,分别是alloc_pages和__get_free_pages,其他的一些函数则是在这二者的基础上通过调整某些参数而来。而alloc_pages和__get_free_pages最终都会调用到alloc_pages_node,所以两者背后的实现原理完全一样,只是__get_free_pages不能在高端内存区分配页面,此外两者返回值的形式也有所区别。
gfp_mask
gfp mask是页面分配函数中一个重要的参数,用于控制分配行为的掩码,并可以告诉内核应该到哪个zone中分配物理内存页面。一些常见的gfp mask掩码含义说明如下,内核模块中使用最多的是GFP_KERNEL和GFP_ATOMIC:
<include/linux/gfp.h>
#define__GFP_DMA ((__force gfp_t)0x01u)
#define__GFP_HIGHMEM ((__force gfp_t)0x02u)
#define__GFP_DMA32 ((__force gfp_t)0x04u)
#define__GFP_MOVABLE ((__force gfp_t)0x08u)
#define__GFP_WAIT ((__force gfp_t)0x10u)
#define__GFP_HIGH ((__force gfp_t)0x20u)
#define__GFP_IO ((__force gfp_t)0x40u)
#define__GFP_FS ((__force gfp_t)0x80u)
#define__GFP_COLD ((__force gfp_t)0x100u)
#define__GFP_NOWARN ((__force gfp_t)0x200u)
#define__GFP_REPEAT ((__force gfp_t)0x400u)
#define__GFP_NOFAIL ((__force gfp_t)0x800u)
#define__GFP_NORETRY ((__force gfp_t)0x1000u)
#define__GFP_COMP ((__force gfp_t)0x4000u)
#define__GFP_ZERO ((__force gfp_t)0x8000u)
#define__GFP_NOMEMALLOC ((__force gfp_t)0x10000u)
#define__GFP_HARDWALL ((__force gfp_t)0x20000u)
以“__”打头的GFP掩码只限于在内存管理组件内部的代码使用,对于提供给外部的接口,比如驱动程序中所使用的页面分配函数,gfp_mask掩码以“GFP_”的形式出现,而这些掩码基本上就是上面提到的掩码的组合,例如内核为外部模块提供的最常使用的几个掩码如下:
<include/linux/gfp.h>
#define GFP_ATOMIC (__GFP_HIGH)
#define GFP_NOIO (__GFP_WAIT)
#define GFP_NOFS (__GFP_WAIT|__GFP_IO)
#define GFP_KERNEL (__GFP_WAIT|__GFP_IO|__GFP_FS)
#define GFP_USER (__GFP_WAIT|__GFP_IO|__GFP_FS|__GFP_HARDWALL)
#define GFP_HIGHUSER (__GFP_WAIT|__GFP_IO|__GFP_FS|__GFP_HARDWALL|\__GFP_HIGHMEM)
#define GFP_DMA __GFP_DMA
GFP_ATOMIC内核模块中最常使用的掩码之一,用于原子分配,也是上面几个掩码中唯一不带__GFP_WAIT的。此掩码告诉页面分配器,在分配内存页时,绝对不能中断当前进程或者把当前进程移出调度器。必要的情况下可以使用仅限紧急情况使用的保留内存页。在驱动程序中,一般在中断处理例程或者非进程上下文的代码中使用GFP_ATOMIC掩码进行内存分配,因为这两种情况下分配都必须保证当前进程不能睡眠。GFP_KERNEL内核模块中最常使用的掩码之一,带有该掩码的内存分配可能导致当前进程进入睡眠状态。GFP_USER用于为用户空间分配内存页,可能引起进程的休眠。
alloc_pages
在源码中,alloc_pages以宏的形式出现,其定义为:
<include/linux/gfp.h>
#define alloc_pages(gfp_mask, order) \alloc_pages_node(numa_node_id(), gfp_mask, order)
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,unsigned int order)
{/*Unknown node is current node*/if(nid<0)nid = numa_node_id();return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}
__alloc_pages函数负责分配2 order个连续的物理页面并返回起始页的struct page实例。在调用这个函数时,如果gfp_mask中没有明确指定__GFP_HIGHMEM,那么分配的物理页面必然来自ZONE_NORMAL或者ZONE_DMA,由于这两个域中内核已经在初始化阶段就为之建立了映射关系,所以内核模块可以使用page_address来获得对应页面的内核虚拟地址KVA(Kernel Virtual Address)。因为是线性映射,所以此时获得KVA很简单,这里用伪代码将page_address的原理大致表述如下:
unsigned long pfn = (unsigned long)(page - mem_map); //获得页帧号
unsigned long pg_pa=pfn<<PAGE_SHIFT;//获得页面的物理地址
return (void*)__va(pg_pa); //返回物理页面对应的KVA,KVA=PAGE_OFFSET+pg_pa
如果在调用alloc_pages时在gfp_mask中指定了__GFP_HIGHMEM,那么页分配器将优先在ZONE_HIGHMEM域中分配物理页,但也不排除因为ZONE_HIGHMEM没有足够的空闲页导致页面来自ZONE_NORMAL与ZONE_DMA域的可能性。对于新分配出的高端物理页面,由于内核尚未在页表中为之建立映射关系,所以此时需要:1.在内核的动态映射区分配一个KVA;2.通过操作页表,将1中的KVA映射到该物理页面上。内核为此提供了一个函数kmap:
<arch/x86/mm/highmem_32.c>
void *kmap(struct page *page)
{might_sleep();if(!PageHighMem(page))return page_address(page);return kmap_high(page);
}
先,该函数在执行过程中可能睡眠,所以不能用在中断处理等上下文中。其次,可以看到它用PageHighMem(page)来判断页面是否真的来自高端内存,如果不是,则用page_address来返回页面所对应的KVA,否则将调用kmap_high在内核虚拟地址空间的动态映射区或者固定映射区分配一个新的KVA并将其映射到物理页面上,之后将该KVA返回给调用者。因为涉及页表的操作,所以从高端内存分配物理页对系统的开销是比较大的。与kmap行为相反的函数是kunmap,在x86平台上的定义如下:
<arch/x86/mm/highmem_32.c>
void kunmap(struct page *page)
{if (in_interrupt())BUG();if (!PageHighMem(page))return;kunmap_high(page);
}
函数将在页表项中拆除对page的映射,同时将来自动态映射区中的KVA释放出去,这样该KVA可以被再次映射到别的物理页面。内核针对kmap函数可能睡眠的情形提供了另一个备选的函数kmap_atomic,该函数的执行是原子的,而且比kmap要快。另一个页面分配函数是alloc_page,只用于分配一个物理页面。alloc_page(gfp_mask)是order=0时alloc_pages的简化形式,只分配单个页面。如果系统中没有足够的空闲页面来满足alloc_pages的分配,函数将返回NULL,内核模块需要仔细检查alloc_pages函数的返回值。
__get_free_pages
__get_free_pages函数的内核源码为:
<mm/page_alloc.c>
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);
}
函数分配2order个连续的物理页面,返回起始页面所在内核线性地址。函数内部调用alloc_pages负责实际的页面分配工作。__get_free_pages不能从高端内存中分配物理页,VM_BUG_ON宏在CONFIG_DEBUG_VM定义的情形下可以捕捉到这一错误,如果CONFIG_DEBUG_VM没有定义,且调用者在gfp_mask中设置了__GFP_HIGHMEM掩码,那么__get_free_pages返回0。在正常情况下,__get_free_pages从低端内存区中分配2order个连续物理页面,并通过page_address来返回这些页面中起始页面的内核线性地址。如果内核模块只想分配单个物理页面,那么可以使用__get_free_page(gfp_mask),它是order=0时__get_free_pages的简化形式。