1. 内存管理概述:Linux内核内存管理层次结构、物理内存与虚拟内存、内存分配器简介

Linux内核作为现代操作系统的核心,其内存管理子系统是驱动开发中最关键、最复杂的部分之一。对于编写设备驱动的开发者而言,理解内核如何组织、分配和回收内存,是正确使用kmalloc、dma_alloc_coherent等API的前提。本章从宏观视角梳理内核内存管理的层次结构、物理与虚拟内存的映射关系,以及主要内存分配器的定位。

1.1 内核内存管理层次结构

Linux内核的内存管理并非单一模块,而是由多个层次协同工作的子系统。从硬件到上层接口,大致可分为四个层次:

层次 核心组件 职责描述
硬件层 MMU、TLB、DMA控制器 提供物理地址空间、页表转换、缓存一致性协议
内核核心层 页分配器 (Buddy)、slab分配器、vmalloc 管理物理页帧、小对象缓存、非连续虚拟地址映射
驱动接口层 kmalloc/kfree、dma_alloc_coherent、ioremap 为驱动提供标准化的内存申请与释放API
用户空间层 mmap、brk、页缓存 通过系统调用与内核交互,驱动通常不直接涉及
驱动开发视角: 驱动主要工作在内核核心层与驱动接口层。理解Buddy分配器如何管理连续物理页,以及slab分配器如何高效分配小对象,是掌握kmalloc行为的关键。
Linux内核内存管理层次结构 Linux内核内存管理层次结构 用户空间层 mmap / brk / 页缓存 / 缺页异常 驱动接口层 kmalloc / kfree / dma_alloc_coherent / ioremap 内核核心层 Buddy分配器 / slab分配器 / vmalloc / 页表管理 硬件层 MMU / TLB / DMA控制器 / 物理内存条 驱动通常不直接 操作用户空间内存 驱动开发主要 交互的接口层 理解Buddy和slab 是掌握kmalloc的关键

1.2 物理内存与虚拟内存

在驱动开发中,区分物理内存与虚拟内存至关重要。CPU通过MMU将虚拟地址转换为物理地址,这种机制为每个进程提供了独立的地址空间,同时也带来了内存管理的复杂性。

1.2.1 物理内存

  • 定义: 硬件RAM芯片上真实的存储单元,以页帧(Page Frame)为单位管理,通常大小为4KB。
  • 特点: 资源有限,多个进程/驱动共享;DMA操作通常需要物理连续的内存块。
  • 内核视角: 内核维护struct page数组描述每个物理页帧的状态(空闲、使用、保留等)。

1.2.2 虚拟内存

  • 定义: CPU通过页表映射给进程或内核线程的地址空间,每个进程拥有独立的虚拟地址空间(例如32位下4GB)。
  • 特点: 虚拟地址可以大于物理内存;通过页表实现按需分配、共享内存、写时复制等高级功能。
  • 内核空间: 内核自身也运行在虚拟地址空间中,但通常使用直接映射区(线性映射)访问物理内存。
对比维度 物理内存 虚拟内存
地址空间大小 受限于硬件容量(如8GB) 受限于CPU位数(如64位下16EB)
连续性 物理上可能不连续(外部碎片) 虚拟地址可以连续,映射到分散的物理页
DMA适用性 需要物理连续(除非使用SMMU/IOMMU) 通常不能直接用于DMA,需转换
驱动常用API __get_free_pages、dma_alloc_coherent kmalloc、vmalloc、ioremap
常见误区: 很多驱动新手误以为kmalloc返回的地址就是物理地址。实际上,kmalloc返回的是内核虚拟地址(直接映射区),需要通过virt_to_phys()转换为物理地址才能用于DMA。在启用IOMMU的平台上,情况更为复杂。
物理内存与虚拟内存映射关系 物理内存与虚拟内存映射关系 虚拟地址空间 页表项 (PTE) 页表项 (PTE) 页表项 (PTE) 页表项 (PTE) 虚拟页号 → 物理页帧号 MMU 地址转换 物理地址空间 物理页帧 0x1000 物理页帧 0x2000 物理页帧 0x3000 物理页帧 0x4000 物理地址不连续 虚拟地址连续 物理地址可分散 通过页表映射

1.3 内存分配器简介

内核提供了多种内存分配器,每种分配器针对不同的使用场景进行了优化。驱动开发者需要根据内存大小、连续性要求、分配频率和上下文(进程上下文 vs 中断上下文)选择合适的分配器。

1.3.1 主要分配器对比

分配器 分配粒度 物理连续性 适用场景 典型API
Buddy分配器 2^order 个物理页(4KB ~ 4MB) 物理连续 大块内存、DMA缓冲区、页表分配 __get_free_pages(), alloc_pages()
slab分配器 字节级(8B ~ 8KB) 物理连续(基于Buddy) 频繁分配/释放的小对象(如struct inode) kmem_cache_alloc(), kmalloc()
vmalloc 页级(物理页不连续) 虚拟连续,物理不连续 需要大块虚拟连续内存,但物理可分散(如模块加载) vmalloc(), vfree()
DMA分配器 页级或更大 物理连续(且满足DMA掩码) DMA缓冲区、设备内存映射 dma_alloc_coherent(), dma_pool_alloc()

1.3.2 分配器选择决策流程

  1. 是否需要物理连续?
    • 是 → 进入步骤2
    • 否 → 考虑vmalloc(大块)或kmalloc(小块,通常物理连续但非保证)
  2. 是否用于DMA?
    • 是 → 使用dma_alloc_coherent或dma_pool_alloc
    • 否 → 进入步骤3
  3. 分配大小?
    • < 8KB → 使用kmalloc(基于slab)
    • ≥ 8KB → 使用__get_free_pages(基于Buddy)
  4. 分配频率高?
    • 是 → 考虑创建专用slab缓存(kmem_cache_create)
    • 否 → 使用通用分配器即可
关键提示: kmalloc底层基于slab分配器,而slab分配器又基于Buddy分配器获取物理页。因此,kmalloc分配的内存始终是物理连续的(对于小于一个页面的分配),但分配大块内存时可能失败。对于驱动而言,kmalloc是使用最频繁的分配函数,后续章节将深入剖析其实现细节。
内核内存分配器架构图 内核内存分配器架构图 驱动内存分配API层 slab分配器 kmalloc / kmem_cache 小对象 < 8KB Buddy分配器 __get_free_pages 物理页帧管理 vmalloc 非连续物理页 大块虚拟连续 DMA分配器 dma_alloc_coherent DMA缓冲区 物理内存硬件层 (RAM / DMA引擎) 说明:slab分配器从Buddy分配器获取物理页,然后细分为小对象缓存。 DMA分配器通常直接调用Buddy分配器,并保证物理连续且满足DMA掩码。 vmalloc通过修改内核页表将不连续的物理页映射到连续的虚拟地址空间。

1.4 本章小结

本章建立了内核内存管理的整体视图:

  • 层次结构 从硬件MMU到驱动API,每一层都有明确的职责和抽象边界。
  • 物理 vs 虚拟内存 是驱动开发的核心概念,理解页表映射机制有助于避免地址转换错误。
  • 分配器选择 需要根据物理连续性、大小、DMA需求和分配频率综合判断。