11.1 CPU 与内存、I/O
11.1.1 内存空间与 I/O 空间
| 架构 | I/O 访问方式 | 典型场景 |
|---|---|---|
| x86 / x86_64 | 独立的 I/O 端口空间(in/out 指令)+ MMIO |
PCI 设备 BAR、串口 |
| ARM / ARM64 / RISC-V | 只有 MMIO(内存映射 I/O) | 所有外设寄存器 |
| 虚拟化 | 端口 I/O 与 MMIO 都会被 trap | QEMU 设备模型 |
内核中的两种地址:
| 地址类型 | 类型限定符 | 访问函数 |
|---|---|---|
| I/O 端口 | 无(unsigned long port) |
inb()/outb()/inl()/outl() |
| I/O 内存 | void __iomem * |
readb()/readl()/writeb()/writel() |
| 普通内存 | 普通指针 | 直接解引用 |
__iomem 是 sparse 检查用的注解 ,它能捕获"把 MMIO 指针当普通指针解引用"的错误。必须用 readl()/writel() 访问。
11.1.2 内存管理单元(MMU)
- MMU 负责虚拟地址到物理地址的转换、权限检查、cache 属性。
- 内核空间映射在
PAGE_OFFSET之上,用户空间在低地址。 - 驱动不能直接把物理地址当指针用 ,必须
ioremap()或devm_ioremap_resource()。 - 页大小:x86_64 默认 4K,可选
CONFIG_X86_64_16K_PAGES/CONFIG_PAGE_SIZE_64KB(其他架构)。 - 7.2.5 支持 5 级页表(
CONFIG_X86_5LEVEL)和PAGE_TABLE_ISOLATION(KPTI)。
11.2 Linux 内存管理
| 分配器 | 适用大小 | 物理连续 | 可睡眠 | 头文件 |
|---|---|---|---|---|
kmalloc/kzalloc |
≤ 几 MB(KMALLOC_MAX_SIZE) |
是 | 取决于 flag | include/linux/slab.h |
vmalloc/vzalloc |
大块 | 否 | 是 | include/linux/vmalloc.h |
kmem_cache_* |
固定大小对象 | 是 | 取决于 flag | include/linux/slab.h |
alloc_pages/__get_free_pages |
整页 | 是 | 取决于 flag | include/linux/gfp.h |
dma_alloc_coherent |
DMA 缓冲 | 是(对设备可见) | 是 | include/linux/dma-mapping.h |
kvmalloc |
大块,优先 kmalloc | 不保证 | 是 | include/linux/slab.h |
devm_* 变体 |
同上 | 同上 | 同上 | 自动释放 |
7.2.5 的重要变化:
kmem_cache_create()新增 4 参数变体:kmem_cache_create(name, object_size, struct kmem_cache_args *args, flags),通过_Generic()同时兼容旧的 5 参数形式(name, size, align, flags, ctor)。新代码应使用struct kmem_cache_args。kmalloc/vmalloc等现在都包装为alloc_hooks(...),支持内存分配 profiling(CONFIG_MEM_ALLOC_PROFILING)。kvmalloc()成为"不确定大小"场景的推荐 API。
11.3 内存存取
11.3.1 用户空间内存动态申请
cpp
/* 用户空间(不属于驱动,但驱动开发必须理解) */
void *malloc(size_t size);
void *calloc(size_t n, size_t size);
void *realloc(void *p, size_t size);
void free(void *p);
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off);
int munmap(void *addr, size_t len);
/* 内核侧对应的 mmap 实现,见 11.4.4 */
驱动与用户空间的数据交换规则:
| 函数 | 用途 |
|---|---|
copy_to_user(dst, src, n) |
内核 → 用户,失败返回未拷贝字节数 |
copy_from_user(dst, src, n) |
用户 → 内核 |
get_user(x, ptr) / put_user(x, ptr) |
单个简单变量 |
strncpy_from_user() |
拷贝字符串 |
clear_user() |
清零用户缓冲 |
access_ok(ptr, n) |
检查用户指针范围(不能替代 copy_*_user 的失败检查) |
7.2.5 中 hardened usercopy(CONFIG_HARDENED_USERCOPY)会检查拷贝是否越过 slab 对象边界,是重要的安全机制。
11.3.2 内核空间内存动态申请
cpp
/* 基本分配 */
void *kmalloc(size_t size, gfp_t flags);
void *kzalloc(size_t size, gfp_t flags); /* 清零 */
void *kcalloc(size_t n, size_t size, gfp_t flags);
void *krealloc(const void *p, size_t new_size, gfp_t flags);
void kfree(const void *p);
/* 7.2.5 推荐:资源管理版本 */
void *devm_kmalloc(struct device *dev, size_t size, gfp_t flags);
void *devm_kzalloc(struct device *dev, size_t size, gfp_t flags);
void *devm_kcalloc(struct device *dev, size_t n, size_t size, gfp_t flags);
void *devm_krealloc(struct device *dev, void *p, size_t size, gfp_t flags);
/* 大块非连续内存 */
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size);
void vfree(const void *addr);
void *devm_kvmalloc(struct device *dev, size_t size, gfp_t flags);
/* 固定大小对象缓存 */
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
struct kmem_cache_args *args, slab_flags_t flags);
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags);
void kmem_cache_free(struct kmem_cache *s, void *obj);
void kmem_cache_destroy(struct kmem_cache *s);
gfp 标志表(include/linux/gfp_types.h):
| 标志 | 含义 | 可用上下文 |
|---|---|---|
GFP_KERNEL |
常规内核分配,可睡眠/回收 | 进程上下文 |
GFP_ATOMIC |
不睡眠,可从中断分配 | 任何上下文 |
GFP_NOWAIT |
不等待,快速返回 | 任何上下文 |
GFP_NOIO / GFP_NOFS |
禁止触发 I/O / 文件系统回收 | 存储栈 |
__GFP_ZERO |
清零(kzalloc 隐含) |
组合使用 |
GFP_DMA / GFP_DMA32 |
限制在 DMA 可达区域 | 老式 DMA |
__GFP_HIGHMEM |
允许高端内存(32 位) | 架构相关 |
__GFP_RETRY_MAYFAIL |
失败前多试几次 | 可失败的大分配 |
__GFP_NOWARN |
不打印失败警告 | 探测式分配 |
内存分配的选择流程:
需要 DMA 且要求设备可见的连续物理内存?
是 -> dma_alloc_coherent()
否 -> 大小 <= KMALLOC_MAX_SIZE 且需要物理连续?
是 -> kmalloc/kzalloc(优先 devm_kzalloc)
否 -> vmalloc/kvmalloc
必须避免的错误:
kmalloc(GFP_KERNEL)在中断上下文 → 睡眠警告。- 忘记检查返回值 → NULL 解引用。
kfree()后继续使用(UAF)→ 用 KASAN 检测。vmalloc返回的内存不能用于 DMA(不保证物理连续)。- 分配大小要防止整数溢出:
kmalloc(n * size, ...)必须先检查n > SIZE_MAX / size,或直接用kcalloc()(内部有溢出检查)。
11.4 设备 I/O 端口和 I/O 内存的访问
11.4.1 Linux I/O 端口和 I/O 内存访问接口
I/O 端口(x86 专属):
cpp
/* include/asm-generic/io.h, arch/x86/include/asm/io.h */
u8 inb(unsigned long port);
u16 inw(unsigned long port);
u32 inl(unsigned long port);
void outb(u8 val, unsigned long port);
void outw(u16 val, unsigned long port);
void outl(u32 val, unsigned long port);
/* 批量与字符串 I/O */
insb/insw/insl(port, addr, count);
outsb/outsw/outsl(port, addr, count);
I/O 内存(所有架构):
cpp
/* 8/16/32/64 位读写 */
u8 readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);
u64 readq(const volatile void __iomem *addr);
void writeb(u8 val, volatile void __iomem *addr);
void writew(u16 val, volatile void __iomem *addr);
void writel(u32 val, volatile void __iomem *addr);
void writeq(u64 val, volatile void __iomem *addr);
/* 带内存屏障的版本 */
readl_relaxed() / writel_relaxed() /* 无屏障,性能更高 */
ioread32() / iowrite32() /* 通用别名 */
/* 批量拷贝 */
memcpy_fromio(dst, src, count);
memcpy_toio(dst, src, count);
memset_io(addr, val, count);
/* 位操作 */
setbits32(addr, mask) / clrbits32(addr, mask) / setset32(...)
关键区别:
readl()自带__iomb()屏障 ,保证读写顺序;readl_relaxed()不带。- 驱动必须在"需要顺序保证"处使用带屏障的版本,在"已知安全"处用 relaxed 版本提升性能。
readl()/writel()必须在__iomem指针上使用,否则 sparse 会报警。
11.4.2 申请与释放设备的 I/O 端口和 I/O 内存
cpp
/* include/linux/ioport.h */
struct resource *request_mem_region(resource_size_t start, resource_size_t len,
const char *name);
void release_mem_region(resource_size_t start, resource_size_t len);
struct resource *request_region(unsigned long start, unsigned long len,
const char *name); /* I/O 端口 */
void release_region(unsigned long start, unsigned long len);
/* 7.2.5 推荐:从 platform_device 取资源 */
struct resource *platform_get_resource(struct platform_device *pdev,
unsigned int type, unsigned int num);
void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev,
unsigned int index);
void __iomem *devm_platform_ioremap_resource_byname(struct platform_device *pdev,
const char *name);
void __iomem *devm_platform_get_and_ioremap_resource(struct platform_device *pdev,
unsigned int index,
struct resource **res);
devm_platform_ioremap_resource() 是最推荐的写法 :它一次性完成 platform_get_resource() + request_mem_region() + ioremap(),卸载时自动回滚。
cpp
static int my_probe(struct platform_device *pdev)
{
struct my_dev *dev;
void __iomem *base;
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->base = base;
platform_set_drvdata(pdev, dev);
return 0;
}
11.4.3 设备 I/O 端口和 I/O 内存访问流程
完整流程(MMIO):
设备树/ACPI 描述 reg = <0x10000000 0x1000>
-> platform_device 得到 struct resource
-> platform_get_resource() / devm_platform_ioremap_resource()
-> request_mem_region() (防止冲突)
-> ioremap() (建立虚拟映射)
-> readl()/writel() (访问寄存器)
-> 卸载时 iounmap()/release_mem_region()(devm 自动完成)
完整流程(I/O 端口):
PCI 设备的 BAR 为 IORESOURCE_IO
-> request_region() (占用端口范围)
-> inb()/outb() (直接访问,无需 ioremap)
-> release_region()
寄存器访问的三个常见模式:
cpp
/* 1. 读-改-写:注意竞态 */
u32 val = readl(dev->base + REG_CTRL);
val |= CTRL_ENABLE;
writel(val, dev->base + REG_CTRL);
/* 2. 读-清-写(W1C 寄存器) */
writel(status, dev->base + REG_IRQ_CLEAR);
/* 3. 轮询等待硬件就绪 */
for (i = 0; i < 1000; i++) {
if (readl(dev->base + REG_STATUS) & STATUS_READY)
break;
udelay(10);
}
if (i == 1000)
return -ETIMEDOUT;
注意 :读-改-写必须加锁(如果多个上下文会访问同一寄存器),否则会丢失更新。
11.4.4 将设备地址映射到用户空间(mmap)
驱动实现 fops->mmap:
cpp
#include <linux/mm.h>
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct my_dev *dev = filp->private_data;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > dev->buffer_size)
return -EINVAL;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); /* MMIO 需非缓存 */
vma->vm_ops = &my_vm_ops; /* 可选:管理 VMA */
if (remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dev->buffer) >> PAGE_SHIFT,
size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
映射 MMIO 寄存器到用户空间:
cpp
static int regs_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct my_dev *dev = filp->private_data;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > dev->reg_size)
return -EINVAL;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
return remap_pfn_range(vma, vma->vm_start,
(virt_to_phys((void __force *)dev->base)) >> PAGE_SHIFT,
size, vma->vm_page_prot);
}
7.2.5 要点:
- 不要映射普通 RAM 给用户空间做 DMA :应该用
dma_mmap_coherent()或dma_mmap_attrs(),否则 cache 一致性无法保证。 - MMIO 映射必须
pgprot_noncached()或pgprot_writecombine(),否则 CPU 缓存与设备状态不一致。 remap_pfn_range()会检查vma->vm_flags & VM_IO等约束。- 7.2.5 的
fops->mmap之外还有fops->mmap_prepare(用于在 mmap 前做校验),以及FOP_MMAP_SYNC标志。 - 用
VM_DONTEXPAND | VM_DONTDUMP标记 VMA,避免被核心转储。
使用 vmf_insert_pfn() 的按需映射(7.2.5 推荐用于稀疏映射):
cpp
static vm_fault_t my_fault(struct vm_fault *vmf)
{
struct my_dev *dev = vmf->vma->vm_private_data;
unsigned long pfn = ...;
return vmf_insert_pfn(vmf->vma, vmf->address, pfn);
}
static const struct vm_operations_struct my_vm_ops = {
.fault = my_fault,
};
11.5 I/O 内存静态映射
4.0 时代的做法 :在板级文件里用 iotable_init()(ARM)或 map_desc 表静态建立映射。
7.2.5 的做法 :静态映射基本消失。原因是:
- ARM32 板级文件(
arch/arm/mach-*/)大部分已删除; - 设备树 +
ioremap()成为唯一主路径; - 少数 SoC 用
CONFIG_ARCH_MMAP类的早期映射仅用于 debug(见 11.5.1)。
早期映射(early ioremap):
cpp
/* 在 early_initcall 之前就能访问的映射 */
void __iomem *early_ioremap(resource_size_t phys_addr, unsigned long size);
void early_iounmap(void __iomem *addr, unsigned long size);
用于 earlycon、earlyprintk 等启动早期调试(见第 20 章 DEBUG_LL/EARLY_PRINTK)。
给驱动开发者的结论 :不要试图复现静态映射 ,用设备树 + devm_platform_ioremap_resource() 即可。
11.6 DMA
11.6.1 DMA 与 Cache 一致性
DMA 的核心问题是:CPU 通过 cache 访问内存,设备直接访问物理内存,二者可能不一致。
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 设备读 CPU 刚写的数据 | CPU 数据还在 cache 里,设备读到旧值 | dma_map_single(..., DMA_TO_DEVICE) 会 flush cache |
| CPU 读设备刚写的数据 | CPU cache 命中旧值 | dma_unmap_single(..., DMA_FROM_DEVICE) 会 invalidate cache |
| 双向访问 | 两个方向都可能不一致 | 用 DMA_BIDIRECTIONAL |
| 一致性要求高、数据量小 | 频繁 flush 代价大 | dma_alloc_coherent() 分配非缓存内存 |
DMA 方向枚举(include/linux/dma-direction.h):
| 方向 | 含义 |
|---|---|
DMA_BIDIRECTIONAL |
双向 |
DMA_TO_DEVICE |
内存 → 设备 |
DMA_FROM_DEVICE |
设备 → 内存 |
DMA_NONE |
仅用于调试 |
为什么现代系统更简单:
CONFIG_IOMMU_SUPPORT+ IOMMU 提供地址翻译与隔离,DMA 地址与物理地址解耦。CONFIG_DMA_API_DEBUG可在运行时检查映射/解映射配对、方向错误、双重释放。CONFIG_DMA_CMA提供连续内存区。
11.6.2 Linux 下的 DMA 编程
一致性 DMA(推荐用于设备与 CPU 都要频繁访问的小缓冲)
cpp
#include <linux/dma-mapping.h>
struct my_dev *dev;
/* 分配:返回 CPU 可见虚拟地址 + 设备可见 DMA 地址 */
dma_addr_t dma_handle;
void *cpu_addr;
size_t size = 4096;
cpu_addr = dma_alloc_coherent(dev->dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
return -ENOMEM;
/* 资源管理版本 */
cpu_addr = dmam_alloc_coherent(dev->dev, size, &dma_handle, GFP_KERNEL);
/* 使用:CPU 直接读写 cpu_addr,设备用 dma_handle */
memset(cpu_addr, 0, size);
writel(dma_handle, dev->base + REG_DMA_ADDR);
/* 释放 */
dma_free_coherent(dev->dev, size, cpu_addr, dma_handle);
流式 DMA(用于大块、方向明确的传输)
cpp
dma_addr_t dma_addr;
/* 映射 */
dma_addr = dma_map_single(dev->dev, buf, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev->dev, dma_addr))
return -ENOMEM;
/* 启动设备传输 */
writel(dma_addr, dev->base + REG_SRC);
writel(size, dev->base + REG_LEN);
writel(DMA_START, dev->base + REG_CTRL);
/* 传输完成后解除映射(会同步 cache) */
dma_unmap_single(dev->dev, dma_addr, size, DMA_TO_DEVICE);
分散/聚集(scatter-gather)DMA
cpp
#include <linux/scatterlist.h>
struct sg_table sgt;
struct scatterlist *sg;
int nents, i;
if (sg_alloc_table(&sgt, nr_pages, GFP_KERNEL))
return -ENOMEM;
sg_set_buf(sgt.sgl, buf, len); /* 或 sg_set_page() */
/* 7.2.5 推荐:dma_map_sgtable() 会自动设置 nents */
if (dma_map_sgtable(dev->dev, &sgt, DMA_TO_DEVICE, 0)) {
sg_free_table(&sgt);
return -ENOMEM;
}
/* 遍历映射后的段 */
for_each_sgtable_dma_sg(&sgt, sg, i) {
writel(sg_dma_address(sg), dev->base + REG_DESC_ADDR);
writel(sg_dma_len(sg), dev->base + REG_DESC_LEN);
}
dma_unmap_sgtable(dev->dev, &sgt, DMA_TO_DEVICE, 0);
sg_free_table(&sgt);
7.2.5 的关键变化:
dma_map_sg()已不推荐直接使用,改用dma_map_sgtable():后者会返回错误码并自动设置nents,避免了旧接口"返回 0 表示失败"的歧义。dma_map_sgtable()的返回值为 0 表示成功,负数表示失败。- 新增
for_each_sgtable_dma_sg()/for_each_sgtable_sg()等迭代器。 dma_set_mask_and_coherent(dev, mask)是设置 DMA 掩码的推荐方式。dma_mapping_error()必须检查,否则在 IOMMU 开启时可能拿到错误地址。
设置 DMA 掩码
cpp
/* 设备支持 64 位寻址 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
/* 回退到 32 位 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)))
return -EIO;
}
必须显式设置 :默认掩码可能过小,导致 DMA 分配失败或静默降级。7.2.5 中不调用 dma_set_mask*() 而使用 64 位 DMA 会触发警告。
DMA 编程检查清单
| 检查项 | 说明 |
|---|---|
| 分配时是否检查返回值 | dma_alloc_coherent 返回 NULL,dma_map_single 要 dma_mapping_error() |
| 方向是否正确 | 写反会导致 cache 不同步 |
| 映射/解映射是否配对 | CONFIG_DMA_API_DEBUG 会报错 |
是否在原子上下文用 GFP_KERNEL |
会睡眠警告 |
| 是否设置了 DMA 掩码 | 大地址设备会失败 |
vmalloc 内存是否用于 DMA |
不可以(非物理连续) |
是否在映射期间 kfree() |
严重 UAF |
| 是否处理了 IOMMU 映射失败 | 必须检查错误 |
11.6.3 QEMU 中的 DMA 实验
QEMU 中可用的 DMA 实验路径:
CONFIG_DMA_API_DEBUG=y+ 故意写错方向 :观察dmesg中的 DMA API debug 报告。- virtio 设备 :
virtio-blk/virtio-net内部使用 DMA,可用-trace观察。 - 自定义 platform 驱动 +
dma_alloc_coherent:在 QEMU 中验证分配与释放配对。 CONFIG_DMA_CMA:在menuconfig中配置 CMA 区域,观察/proc/meminfo中的CmaTotal。
bash
# QEMU 启动参数:给 DMA 实验留内存
qemu-system-x86_64 -m 2G ...
# guest 中
cat /proc/meminfo | grep -i cma
cat /sys/kernel/debug/dma-api/ # 需要 CONFIG_DMA_API_DEBUG
dmesg | grep -i dma
11.7 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
| 直接解引用物理地址 | oops 或访问错误地址 | ioremap() 后访问 |
用普通指针访问 __iomem |
sparse 警告,可能优化掉 | 用 readl()/writel() |
MMIO 映射忘了 pgprot_noncached() |
读到陈旧数据 | 加 noncached 属性 |
kmalloc 大小溢出 |
堆破坏 | 用 kcalloc() 或先检查 |
中断上下文 GFP_KERNEL |
睡眠警告 | 用 GFP_ATOMIC |
dma_map_single 未检查 dma_mapping_error |
静默失败 | 必须检查 |
| DMA 方向写反 | 数据损坏 | 对照设备文档 |
vmalloc 内存用于 DMA |
DMA 失败/数据错乱 | 用 dma_alloc_coherent |
忘记 dma_set_mask_and_coherent |
大地址分配失败 | probe 中显式设置 |
mmap 直接映射普通 RAM 做 DMA |
cache 不一致 | 用 dma_mmap_coherent |
kfree 后继续使用 |
UAF | 用 KASAN 检测 |
11.8 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
devm_platform_ioremap_resource 映射 |
学习 MMIO 映射流程 | readl 能读到寄存器值 |
| 自定义 mmap 驱动 | 学习用户空间映射 | 用户程序可直接读写映射区 |
| MMIO mmap(noncached) | 学习缓存属性 | 用户读写寄存器实时生效 |
dma_alloc_coherent + DMA_API_DEBUG |
学习一致性 DMA | debugfs 中无错误报告 |
| 故意写错 DMA 方向 | 观察调试机制 | dmesg 报 DMA API 错误 |
| sg_table 映射 | 学习分散聚集 DMA | for_each_sgtable_dma_sg 遍历正确 |
| 内存泄漏检测 | 学习 kmemleak |
/sys/kernel/debug/kmemleak 为空 |
11.9 总结
- I/O 端口用
inb/outb,I/O 内存用readl/writel且必须带__iomem。 - 现代驱动用
devm_platform_ioremap_resource()一站式完成资源申请与映射。 - 内核内存分配按"是否 DMA、是否需物理连续、是否可睡眠"三维度选择 API。
kmem_cache_create()在 7.2.5 中新增了struct kmem_cache_args形式的 4 参数接口。- DMA 的核心是 cache 一致性;一致性 DMA 用
dma_alloc_coherent,流式 DMA 用dma_map_single/dma_map_sgtable。 dma_map_sgtable()取代了dma_map_sg(),dma_mapping_error()必须检查。- QEMU +
CONFIG_DMA_API_DEBUG+CONFIG_DMA_CMA可以完整复现原书的 DMA 实验,无需真实硬件。