3. Linux内存管理入门

理解Linux内核缓存一致性与DMA性能优化,必须先建立一张“内存地图”:进程看到的地址、CPU访问的地址、物理内存条上的真实位置、DMA控制器读写的地址,以及内核分配器管理的对象,它们并不是同一个概念。很多DMA失败、缓存不一致、性能抖动问题,根源都在于混淆了这些层次。

一、核心原理:从虚拟地址到物理页帧

Linux为每个进程提供独立的虚拟地址空间。进程访问变量时使用的是虚拟地址,CPU的MMU通过页表将其翻译为物理地址。物理内存被划分为固定大小的页帧,典型为4KB,也有2MB、1GB的大页。页表是多级结构,x86_64常见为四级或五级页表,ARM64常见为四级页表。页表项中除了物理页帧号,还包含权限位、脏位、访问位、缓存属性等关键信息。

虚拟内存带来的好处是隔离、按需分配、共享和超额提交。但DMA设备通常只认物理地址或IOMMU映射后的IOVA地址,不认进程虚拟地址。因此,驱动必须把缓冲区转换为设备可访问的地址,并处理缓存一致性。

关键链路:用户虚拟地址 → 页表 → 物理页帧 → 页缓存/slab对象 → DMA映射 → 设备可见地址。

二、页缓存、mmap与slab分配器的位置

页缓存是内核用物理页缓存文件内容的结构。当进程read/write文件时,数据通常先进入页缓存,再由内核与磁盘交换。mmap把文件或匿名内存映射到进程虚拟地址空间,使进程可以像访问数组一样访问文件页。mmap本身不复制数据,它建立的是虚拟地址到页缓存页的映射关系。

slab分配器则面向内核小对象。内核频繁创建task_struct、inode、dentry、sk_buff等对象,如果每次都走页分配器,开销大且碎片多。slab/slub从伙伴系统获取整页,再切成固定大小对象缓存复用。slab对象位于内核虚拟地址空间,通常也对应真实物理页。

DMA内存区域是另一类特殊区域。设备要求缓冲区物理连续、对齐、位于特定地址范围,或不能跨越边界。常见API包括dma_alloc_coherent、dma_map_single、dma_map_sg。一致性DMA映射通常返回CPU和设备都能访问的地址,并保证缓存一致;流式DMA映射则要求驱动在设备访问前后正确执行sync操作。

Linux内存层次与DMA访问路径 用户进程虚拟地址空间 malloc / mmap / 栈 / 堆 地址如 0x7f2a... 进程隔离,不能直接给DMA 页表 MMU 多级页表:PGD → PUD → PMD → PTE 权限 / 脏位 / 访问位 / 缓存属性 虚拟地址 → 物理页帧 物理内存页帧 4KB / 2MB / 1GB 页 伙伴系统管理空闲页 真实内存条位置 页缓存 Page Cache 缓存文件内容,按页管理 read/write 与 mmap 共享 脏页回写,减少磁盘IO slab / slub 分配器 task_struct / inode / sk_buff 从伙伴系统取页,切分复用 内核小对象缓存 DMA 内存区域 dma_alloc_coherent dma_map_single / sg 物理连续 / 对齐 / IOMMU 设备 DMA 访问路径 驱动准备缓冲区 → DMA映射得到设备地址 → 设备读写 → sync/解除映射 → CPU可见 若缓存未同步,CPU可能读到旧数据;若地址不连续,设备可能传输错误

三、方法流程:驱动如何正确准备DMA缓冲区

以网卡接收为例,驱动通常先分配接收缓冲区,再建立DMA映射,把设备地址写入描述符。设备收到包后通过DMA写入内存,驱动在中断或轮询中处理。若使用流式DMA,必须在设备写入后执行dma_sync_single_for_cpu,让CPU看到最新数据;在CPU修改缓冲区后、设备读取前执行dma_sync_single_for_device。

/* 一致性DMA映射示例 */
void *cpu_addr;
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
    return -ENOMEM;

/* 设备使用 dma_handle 访问,CPU 使用 cpu_addr 访问 */
writel(dma_handle, dev->regs + RX_DESC_ADDR);

/* 释放 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);

对于流式映射,常见流程是:dma_map_single得到设备地址,启动DMA,等待完成,dma_unmap_single。若方向为DMA_FROM_DEVICE,unmap前通常需要invalidate缓存;若方向为DMA_TO_DEVICE,map后可能需要clean缓存。具体行为取决于架构和是否启用IOMMU。

四、案例与场景:mmap采集卡缓冲区

某数据采集卡驱动使用mmap把内核DMA缓冲区映射到用户态,用户程序直接读取采样数据。若驱动只做了dma_alloc_coherent,却没有正确设置vma的缓存属性,用户态可能通过缓存读到旧数据。正确做法是在mmap的fault或remap阶段使用dma_mmap_coherent,或根据架构设置pgprot_noncached。否则会出现“设备已写入新数据,用户程序仍读到上一帧”的典型缓存不一致现象。

误区:认为mmap返回的地址就是物理地址。mmap返回的是虚拟地址,DMA需要的是设备地址。两者之间必须经过页表和DMA映射转换。

五、误区复盘与对照清单

概念 管理者 典型单位 能否直接给DMA 常见问题
虚拟内存 进程/MMU 虚拟页 不能 地址翻译失败、缺页
物理内存 伙伴系统 页帧 可,但需映射 碎片、连续性不足
页表 MMU/内核 多级表项 不直接 权限、缓存属性错误
页缓存 VFS/内存管理 文件页 需映射后 脏页回写延迟
slab slab/slub 对象 通常不直接 对象跨页、对齐不足
DMA区域 DMA API/IOMMU 缓冲区 是 缓存未同步、地址越界

排查DMA问题时,可以按以下顺序检查:第一,确认缓冲区是否来自DMA API,而不是普通kmalloc;第二,确认映射方向与sync调用是否匹配;第三,确认设备地址是否经过IOMMU转换;第四,确认用户态mmap的缓存属性;第五,用dma-debug和tracepoint观察映射与同步事件。只有把虚拟内存、物理内存、页表、页缓存、slab和DMA区域放在同一张图中理解,才能在缓存一致性与共享内存优化中做出正确判断。