接前一篇文章:Linux内核与驱动面试经典“小”问题集锦(3)
问题5
问:Linux内核中内存分配都有哪些方式?它们之间的使用场景都是什么?
备注:这个问题是笔者近期参加蔚来面试时遇到的一个问题。这道题说是一道小题,其实应该是一道大题,它考察的是候选者对于Linux内存管理子系统中内存分配这一块的功力深浅。
答:
在Linux内核空间中,申请内存所涉及的函数主要包括kmalloc()、__get_free_pages()和vmalloc()等。其中,kmalloc()和__get_free_pages()(及其类似函数)申请的内存位于DMA和常规区域的映射区,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系;而vmalloc()在虚拟内存空间给出一块连续的内存区。实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc()申请的虚拟内存和物理内存之间也没有简单的换算关系。
1. kmalloc()
kmalloc函数在include/linux/slab.h中,代码如下:
static __always_inline __alloc_size(1) void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size) && size) {
unsigned int index;
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
index = kmalloc_index(size);
return kmalloc_trace(
kmalloc_caches[kmalloc_type(flags, _RET_IP_)][index],
flags, size);
}
return __kmalloc(size, flags);
}
kmalloc函数的第一个参数是要分配的块的大小;第二个参数为分配标志,用于控制kmalloc()的行为。
最常用的分配标志是GFP_KERNEL,其含义是在内核空间的进程中申请内存。kmalloc()的底层依赖于__get_free_pages()来实现,分配标志的前缀GFP正好是这个底层函数的缩写。使用GFP_KERNEL标志申请内存时,若暂时不能满足,则进程会休眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用GFP_KERNEL申请内存。
备注:这也是经常会被问到的一道经典面试题,即GFP_KERNEL能否用在中断中?或者中断中应该使用哪些标志?
由于在中断处理函数、tasklet和内核定时器等非进程上下文中不能阻塞,所以此时驱动应当使用GFP_ATOMIC标志来申请内存。当使用GFP_ATOMIC标志申请内存时,若不存在空闲页,则不等待,直接返回。
其它的申请标志还包括:
- GFP_USER:用来为用户空间页分配内存,可能阻塞。
- GFP_HIGHUSER:类似于GFP_USER,但它从高端内存分配。
- GFP_DMA:从DMA区域分配内存,
- GFP_NOIO:不允许任何I/O初始化。
- GFP_NOFS:不允许任何文件系统调用。
- __GFP_HIGHMEM:指示分配的内存可以位于高端内存。
- __GFP_COLD:请求一个较长时间不访问的页。
- __GFP_NOWARN:当一个分配无法满足时,阻止内核发出警告。
- __GFP_HIGH:高优先级请求,允许获得被内核保留给紧急情况使用的最后的内存页。
- __GFP_REPEAT:分配失败,则尽力重复尝试。
- __GFP_NOFAIL:只许申请成功,不许失败。不推荐使用此标志。
- __GFP_NORETRY:若申请不到,则立即放弃。
使用kmalloc()申请的内存应该使用kfree()释放,这个函数的用法和用户空间的free()类似。
2. __get_free_pages()
__get_free_pages()系列函数/宏本质上是Linux内核最底层用于获取空闲内存的方法,因为底层的buddy(伙伴)算法以2^n页为单位管理空闲内存,因此最底层的内存申请总是以2^n页为单位的。
__get_free_pages()系列函数/宏包括get_zeroed_page()、__get_free_page()和__get_free_pages()。
- get_zeroed_page()
该函数返回一个指向新页的指针,并且将该页清零。其在mm/page_alloc.c中,代码如下:
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
return __get_free_page(gfp_mask | __GFP_ZERO);
}
EXPORT_SYMBOL(get_zeroed_page);
- __get_free_page();
该宏返回一个指向新页的指针,但该页不清零。其定义在include/linux/gfp.h中,如下:是:
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask), 0)
它实际上就是调用了下边的__get_free_pages()申请一页。
- __get_free_pages()
__get_free_pages()也是在mm/page_alloc.c中,代码如下:
/*
* Common helper functions. Never use with __GFP_HIGHMEM because the returned
* address cannot represent highmem pages. Use alloc_pages and then kmap if
* you need to access high mem.
*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);
该函数可分配多个页,并返回所分配内存的首地址。分配的页数为2^order,分配的页不清零。oeder允许的最大值是10(1024页)或者11(2048页),这取决于具体的硬件平台。
__get_free_pages()和get_zeroed_page()在实现中调用了alloc_pages函数,alloc_pages()既可以在内核空间分配,也可以在用户空间分配。该函数也在mm/page_alloc.c中,其原型如下:
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
nodemask_t *nodemask);
其参数含义与__get_free_pages()相似,但它返回分配的第一个页的描述符而非首地址。
3. vmalloc
vmalloc()一般只为存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存。vmalloc()远大于__get_free_pages()的开销。为了完成vmalloc(),新的页表项需要被建立。因此,只是调用vmalloc()来分配少量的内存(如1页以内的内存)是不妥的。
vmalloc函数在mm/vmalloc.c中,代码如下:
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
*
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*
* Return: pointer to the allocated memory or %NULL on error
*/
void *vmalloc(unsigned long size)
{
return __vmalloc_node(size, 1, GFP_KERNEL, NUMA_NO_NODE,
__builtin_return_address(0));
}
EXPORT_SYMBOL(vmalloc);
vmalloc函数在申请内存时,会进行内存的映射,改变页表项,而不像kmalloc()实际用的是开机过程中就映射好了的DMA和常规区域的页表项。因此,vmalloc()的虚拟地址和物理地址不是一个简单的线性映射。
vmalloc函数不能用在原子上下文中,因为其内部实现使用了标志位GFP_KERNEL的kmalloc()。
这里多说一点。关于kmalloc与vmalloc的区别,参见笔者的这篇文章:中移(苏州)软件技术有限公司面试问题与解答(7)—— kmalloc与vmalloc的区别与联系及使用场景。
以上是从具体的内存分配函数的角度来说的。从更大的层面来讲,Linux内核物理内存分配的一般方式包括:
(1)伙伴系统(Buddy System)
伙伴系统将物理内存划分为不同大小的块,每个块大小都是2的幂次。这些块被组织成“伙伴”对,每对伙伴的大小是一样的。
(2)slab分配器
slab分配器用于管理小块内存分配,如内核数据结构的分配。slab分配器将内存划分为不同的对象缓存,以提高内存分配和释放的效率。
(3)CMA(Contiguous Memory Allocator,连续内存分配器)
对于需要连续大块内存的需求,Linux引入了CMA。它可以用于分配连续的物理内存区域,如视频缓冲等。
(4)页分配器
Linux内核将物理内存划分为固定大小的页,通常是4KB。当进程需要内存时,内核会使用页分配器来分配这些页面。
(5)内存回收
Linux内核还会定期执行内存回收,以回收未使用的内存。这包括清除不再使用的页面,并将其返回到内存池中。
可见,本题虽然看似是一道面试小题,但实际上其背后蕴含的知识点是非常丰富的,也是非常考验功力的。
参考资料:
《Linux设备驱动开发详解 —— 基于最新的Linux 4.0内核》 宋宝华 编著,机械工业出版社