Linux内核中的内存分配
在用户空间,当我们谈论内存分配时,
malloc()几乎是唯一的答案。但当我们潜入Linux内核的深海,会发现内存管理的世界远比想象中复杂和精妙。内核代码对性能、稳定性和硬件交互有着极致的要求,因此它提供了一套多样化的工具箱来应对不同的内存需求。今天,我们将聚焦于内核中最核心的四种内存分配机制:kmalloc、alloc_pages、vmalloc和dma_alloc_coherent。理解它们的区别和底层原理,是每一位内核开发者和系统工程师的必修课。
内核内存分配的基石
在深入探讨每个函数之前,我们必须先了解支撑它们的三个核心组件:伙伴系统(Buddy System)、Slab分配器(Slab Allocator) 和 虚拟内存管理系统。
1. 伙伴系统 (Buddy System)
伙伴系统是Linux内核物理内存管理的核心。它以 页(Page) 为基本单位(在x86-64架构下通常是4KB),其核心思想是将所有空闲的物理页框分组为 $2^0, 2^1, 2^2, …, 2^n$ 个连续页块的链表。
- 分配:当内核需要分配 $2^{order}$ 个连续的物理页时,它会先在对应大小的链表中查找。如果找不到,它会去更大的链表(例如 $2^{order+1}$)中取出一块,将其一分为二,一块用于满足当前请求,另一块(它的“伙伴”)则放入 $2^{order}$ 的链表中备用。这个过程会递归进行,直到找到合适的内存块。
- 释放:当一块内存被释放时,系统会检查它的“伙伴”是否也处于空闲状态。如果是,两者会合并成一个更大的块,并被移动到更高阶的链表中。这个合并过程同样会递归进行。
伙伴系统的主要优点是能有效地分配和释放大块的、物理上连续的内存,并能很好地解决外部碎片问题。它的直接用户是 alloc_pages。
2. Slab 分配器 (Slab Allocator)
伙伴系统虽然强大,但它以页为单位进行管理。如果内核频繁需要分配大量的小对象(比如几十个字节的inode或dentry结构体),直接使用伙伴系统会造成巨大的内部碎片(例如,为一个32字节的对象分配一个4096字节的页)。
Slab分配器正是为了解决这个问题而生。它构建在伙伴系统之上,扮演着一个“零售商”的角色。
- 缓存(Cache):Slab为每种常用的小对象类型创建一个专用的“缓存”(Cache)。
- 大块(Slab):当某个缓存需要内存时,它会向伙伴系统申请一个或多个连续的物理页,这个大块被称为“Slab”。
- 对象(Object):然后,Slab分配器会将这个Slab预先格式化,切割成许多个固定大小的小对象。
当内核需要分配一个小对象时,Slab分配器可以直接从对应的缓存中快速取出一个空闲对象。释放时,对象被简单地标记为空闲,放回Slab中,而不是立即归还给伙伴系统。这极大地提高了小对象分配的效率,避免了内部碎片,并能更好地利用CPU缓存。kmalloc 就是构建在Slab分配器之上的。
3. 虚拟内存管理系统
当我们需要一块非常大的内存,但又不要求它在物理上是连续的时,虚拟内存管理系统就派上了用场。内核可以独立地从伙伴系统中申请多个不连续的物理页面,然后在内核的虚拟地址空间中,找到一段连续的虚拟地址,通过修改页表(Page Tables),将这段虚拟地址映射到那些离散的物理页面上。
对于CPU来说,它看到的是一段连续的内存,但底层的物理RAM却是碎片化的。这种机制的优势在于能够分配比物理连续内存更大的空间,但缺点是需要维护页表映射,并且可能导致TLB(Translation Lookaside Buffer)的开销,性能略低于直接分配物理连续内存的方式。vmalloc 使用的就是这套机制。
四大分配函数详解
了解了底层机制后,我们再来看这四个函数就豁然开朗了。下面是每个分配器家族的详细介绍。
1. kmalloc: 内核的日常小能手
kmalloc 是内核中最常用、最通用的内存分配方式,用于申请字节大小、物理连续的内存。
特征: 分配的内存物理上是连续的。适用于小块内存的分配。
分配单位: 字节。
底层依赖: Slab 分配器。
kmalloc本质上是从预先定义好的一系列通用Slab缓存(如size-32,size-64,size-128…)中获取内存。常用场景: 驱动程序中设备描述符、内核数据结构(如
inode,task_struct的部分字段)、小型缓冲区等。可观测性:
- 文件:
/proc/slabinfo - 命令:
slabtop(可以实时查看各个Slab缓存的活动情况)
- 文件:
常用函数详解:
void *kmalloc(size_t size, gfp_t flags)- 功能: 这是最核心的分配函数。它尝试分配一块大小至少为
size字节的、物理上连续的内存区域。flags参数(Get Free Pages flags)是关键,它控制了分配器的行为,例如:GFP_KERNEL: 表示在内核空间分配,可能会引起睡眠(阻塞),因此不能在中断上下文或持有自旋锁时使用。GFP_ATOMIC: 表示原子分配,不会睡眠。这是在中断处理程序等不能阻塞的代码中使用的唯一选择。它能使用的内存储备较少。
- 返回值: 成功时,返回一个指向所分配内存块的指针 (
void *)。如果无法满足分配请求,则返回NULL。
- 功能: 这是最核心的分配函数。它尝试分配一块大小至少为
void *kzalloc(size_t size, gfp_t flags)- 功能: 功能与
kmalloc完全相同,但额外的好处是它会将分配的内存全部初始化为零。它本质上等同于kmalloc之后立即调用memset进行清零操作,是一种便捷且安全的选择。 - 返回值: 与
kmalloc相同,成功时返回指向已清零内存的指针,失败时返回NULL。
- 功能: 功能与
void *kcalloc(size_t n, size_t size, gfp_t flags)- 功能: 用于分配一个包含
n个元素的数组,每个元素的大小为size字节。它同样会将整个分配区域(n * size字节)初始化为零。 - 返回值: 成功时返回指向数组首地址的指针,失败时返回
NULL。
- 功能: 用于分配一个包含
void *krealloc(const void *p, size_t new_size, gfp_t flags)- 功能: 重新调整由
kmalloc家族分配的内存块p的大小为new_size。如果new_size更大,可能会在原地扩展(如果空间允许),或者重新分配一块新内存并将旧内存的内容拷贝过去,然后释放旧内存。 - 返回值: 成功时返回指向新内存块的指针(地址可能与旧的不同),失败时返回
NULL(此时旧内存块p仍保持有效)。
- 功能: 重新调整由
void kfree(const void *ptr)- 功能: 释放之前通过
kmalloc、kzalloc、kcalloc或krealloc分配的内存块。 - 返回值: 无 (
void)。
- 功能: 释放之前通过
2. alloc_pages: 物理页的直接操纵者
alloc_pages 是直接与伙伴系统打交道的接口,用于分配大块的、以页为单位的物理内存。
特征: 分配的内存物理上是连续的。适用于大块内存的分配。
分配单位: 页 (Page),请求的单位是 $2^{order}$ 个页。
底层依赖: 伙伴系统 (Buddy System)。
常用场景: 当需要大量连续物理内存时,如页缓存、DMA缓冲区,或作为Slab和vmalloc的后端。
可观测性:
- 文件:
/proc/kpageflags(页标志),/proc/kpagecount(页引用计数)
- 文件:
常用函数详解:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)- 功能: 分配 $2^{order}$ 个连续的物理页面。这是一个底层函数。
- 返回值: 成功时,返回一个指向
struct page结构体的指针,该结构体是内核中描述第一个被分配物理页的元数据。失败时返回NULL。注意,返回的不是可以直接访问的虚拟地址。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)- 功能: 与
alloc_pages功能相同,也是分配 $2^{order}$ 个连续的物理页面。但它经过一层封装,更方便使用。 - 返回值: 成功时,返回所分配内存块的起始内核虚拟地址 (
unsigned long),可以直接在代码中使用。失败时返回0。
- 功能: 与
void free_pages(unsigned long addr, unsigned int order)- 功能: 释放之前通过
__get_free_pages或类似函数分配的 $2^{order}$ 个页面。addr参数必须是当初分配时返回的起始虚拟地址。 - 返回值: 无 (
void)。
- 功能: 释放之前通过
3. vmalloc: 大内存的虚拟艺术家
当需要一块巨大的内存,但物理上是否连续不重要时,vmalloc 是最佳选择。
特征: 分配的内存虚拟上连续,物理上不一定连续。适用于非常大的内存块。
分配单位: 字节。
底层依赖: 虚拟内存管理系统。它会从伙伴系统获取离散的物理页,并通过页表将它们映射成一段连续的内核虚拟地址。
常用场景: 内核模块的加载、大型软件驱动的缓冲区(如显卡驱动)、IOCTL数据交换等。
可观测性:
- 文件:
/proc/vmallocinfo(显示当前已分配的vmalloc区域)
- 文件:
常用函数详解:
void *vmalloc(size_t size)- 功能: 分配一块大小为
size字节的、在虚拟地址空间内连续的内存区域。这块内存的底层物理页面很可能是非连续的。 - 返回值: 成功时,返回指向这块虚拟内存的指针 (
void *)。失败时返回NULL。
- 功能: 分配一块大小为
void *vzalloc(size_t size)- 功能: 功能与
vmalloc相同,但会将分配的内存区域初始化为零。 - 返回值: 成功时返回指向已清零内存的指针,失败时返回
NULL。
- 功能: 功能与
void vfree(const void *addr)- 功能: 释放由
vmalloc或vzalloc分配的内存区域。重要提示:vmalloc分配的内存必须用vfree释放,kfree和vfree不能混用,否则会导致内核崩溃。 - 返回值: 无 (
void)。
- 功能: 释放由
4. dma_alloc_coherent: 硬件通信的专属通道
DMA (Direct Memory Access) 内存有特殊要求:不仅要物理连续,还要保证CPU缓存与主存之间的数据一致性。
特征: 分配物理连续且DMA安全(缓存一致)的内存。
分配单位: 字节。
底层依赖: 专门的DMA内存管理系统,它通常也基于伙伴系统来获取连续页。
常用场景: 任何需要DMA操作的设备驱动,如网络设备的收发包缓冲区、存储设备的数据缓冲区等。
可观测性:
- 文件:
/proc/dma_alloc(特定于某些旧架构或配置)
- 文件:
常用函数详解:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)- 功能: 为指定的设备
dev分配一块大小为size字节的DMA内存。它会同时返回两个地址:一个是CPU可以访问的虚拟地址(作为函数返回值),另一个是设备可以访问的总线地址(通过dma_handle指针返回)。该函数确保了CPU Cache和内存的一致性。 - 返回值: 成功时,返回CPU可用的内核虚拟地址 (
void *)。失败时返回NULL。同时,*dma_handle会被设置为设备可用的总线地址。
- 功能: 为指定的设备
void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle)- 功能: 释放之前通过
dma_alloc_coherent分配的DMA内存。释放时需要提供所有相关信息:设备dev、大小size、CPU虚拟地址vaddr以及设备总线地址dma_handle。 - 返回值: 无 (
void)。
- 功能: 释放之前通过
核心对比与地址空间问题
| 特性 | kmalloc | alloc_pages / __get_free_pages | vmalloc |
|---|---|---|---|
| 物理连续性 | 是 | 是 | 否 |
| 虚拟连续性 | 是 | 是 | 是 |
| 分配单位 | 字节 | 页 ($2^{order}$) | 字节 |
| 性能 | 高,通常无TLB Miss | 高 | 较低,有页表和TLB开销 |
| 大小限制 | 较小 (通常最大128KB或几MB) | 中等 (可达数MB) | 非常大 (可达GB级别) |
| 底层依赖 | Slab 分配器 | 伙伴系统 | 虚拟内存管理 |
分配的内存地址是否有重叠?
这是一个非常好的问题,答案是:不会。
Linux内核的虚拟地址空间被划分为不同的区域。其中,有两大主要区域与我们讨论的分配方式相关:
- 直接映射区 (Direct Mapping Zone): 这部分虚拟地址空间与物理地址有一个固定的、简单的偏移量。例如,虚拟地址
0xffff8800_00000000可能就对应物理地址0x0。因为映射关系简单,访问效率极高。kmalloc和__get_free_pages分配的内存都位于这个区域。 - Vmalloc区 (Vmalloc Zone): 这是内核虚拟地址空间中专门预留的一块区域,用于
vmalloc和内核模块加载。这个区域的地址与物理地址没有直接的线性关系,需要通过复杂的页表来查询。
因此,kmalloc 和 vmalloc 返回的地址指针来自于内核虚拟地址空间中两个完全不同、互不重叠的区间。你永远不会遇到一个 kmalloc 返回的地址与一个 vmalloc 返回的地址相同。
总结
选择正确的内存分配函数是编写高效、稳定内核代码的关键。
- 当你需要小块、物理连续的内存时,毫不犹豫地使用
kmalloc。 - 当你需要以页为单位的、物理连续的大块内存时,
alloc_pages或__get_free_pages是你的不二之选。 - 当你需要巨大的内存,并且不关心其物理布局时,
vmalloc可以满足你的需求。 - 当你编写的驱动需要与硬件进行DMA通信时,必须使用
dma_alloc_coherent来确保内存的连续性和一致性。
掌握这四大金刚,你就能在Linux内核的内存海洋中游刃有余。