引入这个kmalloc的目的,是因为前面的slab接口太过于复杂,因此需要一个全新的封装kmalloc接口,内存申请编程接口实现。kmalloc底层起始也是基于slab缓存实现的。
1.kmalloc 调用流程
- 参数解析: 解析
gfp_mask
参数,确定分配时是否可以睡眠、是否需要零初始化等。解析size
参数,确定要分配的内存大小。 - 查找缓存:(1)根据请求的大小
size
查找合适的缓存。(2)如果找不到合适的缓存,则可能需要创建一个新的缓存。 - 分配对象:(1)从找到的缓存中分配一个对象 (2)如果缓存中的对象不足,则可能需要创建一个新的 slab。(3)如果需要,初始化分配的内存为零。
- 返回结果: 返回指向分配的内存块的指针。
大概实现方式示例
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/kmalloc.h>
void *kmalloc(size_t size, gfp_t gfp_mask)
{struct kmem_cache *cache;void *ptr;// Find the appropriate cachecache = find_kmem_cache(size);// Allocate from the cacheptr = kmem_cache_alloc(cache, gfp_mask);// Initialize memory if requiredif (gfp_mask & __GFP_ZERO)memset(ptr, 0, size);return ptr;
}static struct kmem_cache *find_kmem_cache(size_t size)
{return kmem_cache_find(size);
}
2.函数的实践
3.实现源码解析
总结下来就是说,kmalloc函数最终调用的是kmem_cache和伙伴系统来实现的,它比kmem_cache那套函数接口实现要简单很多。
当申请的内存大于8KB的时候:
如果不是slob,并且大小于8KB,然后去获取索引:index = kmalloc_index(size)。接下来就通过kmem_alloc_trace申请内存。上节说到slab通过一些列的kmem_cache链表连接组成,获取索引(实际上就是order)之后需要找到对应的kmem_cache,kmem_cache_alloc_trace函数根据前面的索引,再到对应的slab上获取对应的内存。
kmem_cache_alloc_trace函数需要的参数就比较明显了:(1)需要知道是哪个kmem_cache,大小,和flags。然后内部又有slab_alloc进行封装。
slab_alloc最终的申请过程包括:(1)this_cpu_read函数到对应的CPU缓存区申请。这里其实也挺复杂的,没有想象中那么简单。
遗留问题:
kmalloc能申请的最大块的连续内存是多少?
- 由于kmalloc最终是通过slab或者伙伴系统中获取的内存,所以最大的内存就是pageblock的大小(4MB和max_order的配置有关)。
kmalloc返回的地址已对齐方式?
- slab的接口支持配置对齐方式,前面的kmem_cache_create函数有这个参数
kmalloc返回的是虚拟地址还是物理地址?
- 不管是slab或者伙伴系统内存申请,最终都会通过page_to_virt进行转换虚拟地址。在 Linux 内核中,
kmalloc
是用于分配内核空间中的小块内存的函数。它返回的指针是指向内核虚拟地址空间中的内存区域。这是因为现代计算机系统普遍使用虚拟内存机制,内核和用户空间的内存访问都是通过虚拟地址进行的。当你调用kmalloc
时,它会根据你请求的大小查找或创建一个合适的缓存,并从这个缓存中分配一个对象。这个对象的地址是内核虚拟地址空间中的地址。因此,当你使用kmalloc
分配的内存时,你是在操作虚拟地址。 kmem_cache_alloc
函数返回的是虚拟地址。- 在 Linux 内核中,
struct page
结构体代表一个物理页帧,并包含了关于该页帧的一些元数据。alloc_pages
函数用于分配物理页帧,并返回一个指向这些页帧的struct page
结构体数组的指针。但是所有的内存申请,最终会在某个阶段通过page_to_virt转换成虚拟地址