11.1 CPU 与内存、I/O11.1.1 内存空间与 I/O 空间架构I/O 访问方式典型场景x86 / x86_64独立的 I/O 端口空间in/out指令 MMIOPCI 设备 BAR、串口ARM / ARM64 / RISC-V只有 MMIO内存映射 I/O所有外设寄存器虚拟化端口 I/O 与 MMIO 都会被 trapQEMU 设备模型内核中的两种地址地址类型类型限定符访问函数I/O 端口无unsigned long portinb()/outb()/inl()/outl()I/O 内存void __iomem *readb()/readl()/writeb()/writel()普通内存普通指针直接解引用__iomem是 sparse 检查用的注解它能捕获把 MMIO 指针当普通指针解引用的错误。必须用readl()/writel()访问。11.1.2 内存管理单元MMUMMU 负责虚拟地址到物理地址的转换、权限检查、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_ISOLATIONKPTI。11.2 Linux 内存管理分配器适用大小物理连续可睡眠头文件kmalloc/kzalloc≤ 几 MBKMALLOC_MAX_SIZE是取决于 flaginclude/linux/slab.hvmalloc/vzalloc大块否是include/linux/vmalloc.hkmem_cache_*固定大小对象是取决于 flaginclude/linux/slab.halloc_pages/__get_free_pages整页是取决于 flaginclude/linux/gfp.hdma_alloc_coherentDMA 缓冲是对设备可见是include/linux/dma-mapping.hkvmalloc大块优先 kmalloc不保证是include/linux/slab.hdevm_*变体同上同上同上自动释放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(...)支持内存分配 profilingCONFIG_MEM_ALLOC_PROFILING。kvmalloc()成为不确定大小场景的推荐 API。11.3 内存存取11.3.1 用户空间内存动态申请/* 用户空间不属于驱动但驱动开发必须理解 */ 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 usercopyCONFIG_HARDENED_USERCOPY会检查拷贝是否越过 slab 对象边界是重要的安全机制。11.3.2 内核空间内存动态申请/* 基本分配 */ 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不打印失败警告探测式分配内存分配的选择流程必须避免的错误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 专属/* 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 内存所有架构/* 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 内存/* 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()卸载时自动回滚。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()寄存器访问的三个常见模式/* 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#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 寄存器到用户空间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 推荐用于稀疏映射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/* 在 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 DMA11.6.1 DMA 与 Cache 一致性DMA 的核心问题是CPU 通过 cache 访问内存设备直接访问物理内存二者可能不一致。场景问题解决方案设备读 CPU 刚写的数据CPU 数据还在 cache 里设备读到旧值dma_map_single(..., DMA_TO_DEVICE)会 flush cacheCPU 读设备刚写的数据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 都要频繁访问的小缓冲#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用于大块、方向明确的传输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-gatherDMA#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 掩码/* 设备支持 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返回 NULLdma_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_DEBUGy 故意写错方向观察dmesg中的 DMA API debug 报告。virtio 设备virtio-blk/virtio-net内部使用 DMA可用-trace观察。自定义 platform 驱动 dma_alloc_coherent在 QEMU 中验证分配与释放配对。CONFIG_DMA_CMA在menuconfig中配置 CMA 区域观察/proc/meminfo中的CmaTotal。# 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 dma11.7 常见错误错误现象修正直接解引用物理地址oops 或访问错误地址ioremap()后访问用普通指针访问__iomemsparse 警告可能优化掉用readl()/writel()MMIO 映射忘了pgprot_noncached()读到陈旧数据加 noncached 属性kmalloc大小溢出堆破坏用kcalloc()或先检查中断上下文GFP_KERNEL睡眠警告用GFP_ATOMICdma_map_single未检查dma_mapping_error静默失败必须检查DMA 方向写反数据损坏对照设备文档vmalloc内存用于 DMADMA 失败/数据错乱用dma_alloc_coherent忘记dma_set_mask_and_coherent大地址分配失败probe 中显式设置mmap直接映射普通 RAM 做 DMAcache 不一致用dma_mmap_coherentkfree后继续使用UAF用 KASAN 检测11.8 本章实验清单实验目的关键观察devm_platform_ioremap_resource映射学习 MMIO 映射流程readl能读到寄存器值自定义 mmap 驱动学习用户空间映射用户程序可直接读写映射区MMIO mmapnoncached学习缓存属性用户读写寄存器实时生效dma_alloc_coherentDMA_API_DEBUG学习一致性 DMAdebugfs 中无错误报告故意写错 DMA 方向观察调试机制dmesg报 DMA API 错误sg_table 映射学习分散聚集 DMAfor_each_sgtable_dma_sg遍历正确内存泄漏检测学习kmemleak/sys/kernel/debug/kmemleak为空11.9 总结I/O 端口用inb/outbI/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_DEBUGCONFIG_DMA_CMA可以完整复现原书的 DMA 实验无需真实硬件。
阅读完成 · 觉得有帮助?