《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 11 章 内存与 I/O 访问

参考:宋宝华 著,机械工业出版社,2015年版


11.1 CPU 与内存之间的关系

11.1.1 CPU 访问内存的基本方式

CPU 通过地址总线、数据总线和控制总线与内存进行通信:

CPU 与内存的连接:

┌─────────────────────────────────────────────────────────┐
│                        CPU                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────────┐  │
│  │  寄存器   │  │  Cache   │  │      MMU             │  │
│  │  (< 1ns) │  │ (1~10ns) │  │  (地址转换)          │  │
│  └──────────┘  └──────────┘  └──────────────────────┘  │
└────────────────────────┬────────────────────────────────┘
                         │ 地址总线 + 数据总线 + 控制总线
┌────────────────────────▼────────────────────────────────┐
│                      内存控制器                          │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                    DDR SDRAM                             │
│                    (50~100ns)                            │
└─────────────────────────────────────────────────────────┘

11.1.2 物理地址与虚拟地址

地址类型:

物理地址(Physical Address,PA):
  - 内存芯片上的实际地址
  - 由内存控制器直接使用
  - 范围:0 ~ 物理内存大小

虚拟地址(Virtual Address,VA):
  - CPU 在用户/内核代码中使用的地址
  - 由 MMU 转换为物理地址
  - 范围:0 ~ 2^32(32位)或 0 ~ 2^64(64位)

总线地址(Bus Address):
  - 设备(如 DMA 控制器)访问内存时使用的地址
  - 在没有 IOMMU 的系统中,总线地址 = 物理地址
  - 在有 IOMMU 的系统中,总线地址可能与物理地址不同

地址转换关系:
  虚拟地址 → MMU → 物理地址 → 内存控制器 → 内存芯片
  设备地址 → IOMMU → 物理地址 → 内存控制器 → 内存芯片

11.1.3 内存映射 I/O(MMIO)

在嵌入式系统中,外设寄存器通常映射到 CPU 的地址空间中,CPU 通过读写这些地址来控制外设:

内存映射 I/O(MMIO)示意:

CPU 地址空间(ARM 32位,4GB):
0xFFFFFFFF ┌─────────────────────────────┐
           │   内核空间(1GB)             │
0xC0000000 ├─────────────────────────────┤
           │   用户空间(3GB)             │
0x00000000 └─────────────────────────────┘

SoC 物理地址映射(以 i.MX6ULL 为例):
0xFFFFFFFF ┌─────────────────────────────┐
           │   DDR3 SDRAM(最大 2GB)     │ 0x80000000~
0x80000000 ├─────────────────────────────┤
           │   片内外设寄存器              │ 0x02000000~0x021FFFFF
           │   UART/SPI/I2C/GPIO 等       │
0x02000000 ├─────────────────────────────┤
           │   片内 SRAM(128KB)         │ 0x00900000~
0x00000000 └─────────────────────────────┘

11.2 内存管理单元(MMU)

11.2.1 MMU 的功能

MMU(Memory Management Unit)是处理器中负责虚拟地址到物理地址转换的硬件单元:

MMU 的三大功能:

1. 地址转换(Address Translation)
   虚拟地址 → 物理地址
   通过页表(Page Table)完成映射

2. 内存保护(Memory Protection)
   每个页面有访问权限(读/写/执行)
   用户空间不能访问内核空间
   防止进程间相互干扰

3. Cache 控制
   每个页面可以设置 Cache 属性
   设备寄存器通常设置为不可 Cache(防止读写被缓存)

11.2.2 页表与地址转换

ARM 32位 两级页表地址转换:

虚拟地址(32位):
┌──────────────┬──────────────┬──────────────┐
│  L1 索引     │  L2 索引     │  页内偏移    │
│  [31:20]     │  [19:12]     │  [11:0]      │
│  12位        │  8位         │  12位        │
└──────────────┴──────────────┴──────────────┘

转换过程:
1. 从 TTBR(Translation Table Base Register)获取 L1 页表基地址
2. 用 VA[31:20] 索引 L1 页表,获取 L2 页表基地址
3. 用 VA[19:12] 索引 L2 页表,获取物理页帧号(PFN)
4. 物理地址 = PFN × 4096 + VA[11:0]

TLB(Translation Lookaside Buffer):
  页表缓存,避免每次地址转换都访问内存
  TLB 命中:直接获得物理地址(< 1ns)
  TLB 缺失:访问内存中的页表(50~100ns)

11.2.3 MMU 对驱动开发的影响

/*
 * 驱动程序不能直接使用物理地址访问外设寄存器
 * 必须通过 ioremap() 将物理地址映射到内核虚拟地址
 */

/* 错误:直接使用物理地址 */
#define UART_BASE  0x44E09000
volatile u32 *uart_reg = (volatile u32 *)UART_BASE;  /* 危险!*/
*uart_reg = 0x01;  /* 可能导致内核崩溃 */

/* 正确:通过 ioremap 映射后使用 */
void __iomem *uart_base = ioremap(0x44E09000, 0x1000);
writel(0x01, uart_base + 0x0C);  /* 安全 */
iounmap(uart_base);

11.3 Linux 内存管理

11.3.1 Linux 内存布局

Linux 内存管理的层次结构:

物理内存
    ↓ 由内存管理子系统管理
┌─────────────────────────────────────────────────────────┐
│                  内存管理子系统                           │
│  ┌──────────────────────────────────────────────────┐   │
│  │              页分配器(Buddy System)              │   │
│  │  管理物理页(4KB/页),按 2^n 个页分配             │   │
│  └──────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────┐   │
│  │              Slab 分配器                          │   │
│  │  在页分配器基础上,管理小对象(< 页大小)          │   │
│  └──────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────┐   │
│  │              vmalloc                              │   │
│  │  分配虚拟连续但物理不连续的大块内存               │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

11.3.2 内核内存区域

Linux 内核内存区域(ARM 32位):

内核虚拟地址空间(0xC0000000 ~ 0xFFFFFFFF,1GB):

0xFFFFFFFF ┌─────────────────────────────┐
           │   固定映射区(fixmap)        │ ← ioremap 等使用
           │   vmalloc 区域               │ ← vmalloc 分配
0xF0000000 ├─────────────────────────────┤
           │   物理内存直接映射区          │ ← kmalloc 分配
           │   (线性映射,PA = VA - PAGE_OFFSET)
0xC0000000 └─────────────────────────────┘

内存区域(Zone):
  ZONE_DMA:物理地址 < 16MB(ISA DMA 使用)
  ZONE_NORMAL:16MB ~ 896MB(直接映射区)
  ZONE_HIGHMEM:> 896MB(高端内存,需要临时映射)

11.4 内存存取

11.4.1 用户空间内存动态申请

用户空间通过 C 标准库函数申请内存:

#include <stdlib.h>
#include <string.h>

/* malloc:申请内存(不初始化)*/
void *ptr = malloc(1024);
if (!ptr) {
    perror("malloc failed");
    return -1;
}

/* calloc:申请并清零内存 */
void *ptr = calloc(10, sizeof(int));  /* 10个int,清零 */

/* realloc:重新调整内存大小 */
ptr = realloc(ptr, 2048);

/* free:释放内存 */
free(ptr);
ptr = NULL;  /* 防止悬空指针 */

/* 内存对齐分配(用于 DMA 等需要对齐的场景)*/
void *aligned_ptr;
int ret = posix_memalign(&aligned_ptr, 4096, 4096);  /* 4KB 对齐,4KB 大小 */
free(aligned_ptr);

/* mmap:映射内存(可用于大块内存或文件映射)*/
#include <sys/mman.h>
void *mmap_ptr = mmap(NULL, 4096,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS,
                       -1, 0);
munmap(mmap_ptr, 4096);

11.4.2 内核空间内存动态申请

kmalloc

kmalloc 是内核中最常用的内存分配函数,分配物理连续的内存:

#include <linux/slab.h>

/*
 * kmalloc(size, flags)
 *
 * size:申请的字节数(通常 < 128KB)
 * flags:分配标志
 *
 * 返回:内核虚拟地址(物理连续),NULL(失败)
 */
void *ptr = kmalloc(1024, GFP_KERNEL);
if (!ptr) {
    pr_err("kmalloc 失败\n");
    return -ENOMEM;
}
kfree(ptr);

/*
 * GFP 标志(Get Free Pages flags):
 *
 * GFP_KERNEL:最常用,可睡眠等待内存,用于进程上下文
 * GFP_ATOMIC:不可睡眠,用于中断上下文、自旋锁持有期间
 * GFP_DMA:分配 DMA 可用内存(物理地址 < 16MB)
 * GFP_DMA32:分配 32 位 DMA 可用内存(物理地址 < 4GB)
 * GFP_NOWAIT:不等待,分配失败立即返回
 * GFP_NOIO:不触发 I/O 操作(避免死锁)
 * GFP_NOFS:不触发文件系统操作
 * __GFP_ZERO:分配后清零(等价于 kzalloc)
 */

/* kzalloc:分配并清零(推荐使用)*/
void *ptr = kzalloc(sizeof(struct my_dev), GFP_KERNEL);

/* kfree:释放 kmalloc/kzalloc 分配的内存 */
kfree(ptr);

/* krealloc:重新调整大小 */
ptr = krealloc(ptr, new_size, GFP_KERNEL);

/* kmalloc 的大小限制 */
/*
 * kmalloc 能分配的最大内存受 KMALLOC_MAX_SIZE 限制
 * 通常为 4MB(与 PAGE_SIZE 和 MAX_ORDER 相关)
 * 超过此限制应使用 vmalloc
 */

kmalloc 的使用案例

struct my_buffer {
    size_t   size;
    u8      *data;
};

static int alloc_buffer(struct my_buffer *buf, size_t size)
{
    buf->data = kmalloc(size, GFP_KERNEL);
    if (!buf->data)
        return -ENOMEM;

    buf->size = size;
    memset(buf->data, 0, size);
    return 0;
}

static void free_buffer(struct my_buffer *buf)
{
    kfree(buf->data);
    buf->data = NULL;
    buf->size = 0;
}

/* 在中断上下文中分配内存 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    /* 中断上下文必须使用 GFP_ATOMIC */
    u8 *buf = kmalloc(64, GFP_ATOMIC);
    if (!buf)
        return IRQ_HANDLED;  /* 分配失败,但不能睡眠等待 */

    /* 使用 buf */
    kfree(buf);
    return IRQ_HANDLED;
}

11.4.3 vmalloc

vmalloc 分配虚拟连续但物理不连续的内存,适合分配大块内存:

#include <linux/vmalloc.h>

/*
 * vmalloc(size):分配虚拟连续内存
 * 物理上不连续,不能用于 DMA
 * 适合分配大块内存(> 128KB)
 * 只能在进程上下文中使用(可能睡眠)
 */
void *ptr = vmalloc(1024 * 1024);  /* 分配 1MB */
if (!ptr) {
    pr_err("vmalloc 失败\n");
    return -ENOMEM;
}

/* 使用 vmalloc 分配的内存 */
memset(ptr, 0, 1024 * 1024);

/* vfree:释放 vmalloc 分配的内存 */
vfree(ptr);

/* vzalloc:分配并清零 */
void *ptr = vzalloc(1024 * 1024);
vfree(ptr);

/* vmalloc_32:分配 32 位地址范围内的虚拟内存 */
void *ptr = vmalloc_32(size);

kmalloc vs vmalloc 对比

kmalloc vs vmalloc:

特性              kmalloc              vmalloc
─────────────────────────────────────────────────────────────
物理连续性        是                   否
虚拟连续性        是                   是
最大分配大小      ~4MB                 受虚拟地址空间限制
可用于 DMA        是                   否
分配速度          快                   慢(需要建立页表)
适用场景          小块内存,DMA        大块内存,不需要物理连续
中断上下文        是(GFP_ATOMIC)     否(可能睡眠)

11.4.4 slab 与内存池

slab 分配器

slab 分配器是内核内存管理的核心,用于高效分配固定大小的小对象

#include <linux/slab.h>

/*
 * kmem_cache:slab 缓存
 * 为特定大小/类型的对象创建专用缓存
 * 避免频繁分配/释放导致的内存碎片
 */

/* 创建 slab 缓存 */
struct kmem_cache *my_cache;

my_cache = kmem_cache_create(
    "my_object_cache",          /* 缓存名称(显示在 /proc/slabinfo)*/
    sizeof(struct my_object),   /* 对象大小 */
    0,                          /* 对齐(0 = 默认)*/
    SLAB_HWCACHE_ALIGN,         /* 标志:按 Cache 行对齐 */
    NULL                        /* 构造函数(可选)*/
);

if (!my_cache) {
    pr_err("创建 slab 缓存失败\n");
    return -ENOMEM;
}

/* 从缓存分配对象 */
struct my_object *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
if (!obj) {
    pr_err("从 slab 缓存分配失败\n");
    return -ENOMEM;
}

/* 初始化对象 */
memset(obj, 0, sizeof(*obj));
obj->id = 1;

/* 释放对象回缓存 */
kmem_cache_free(my_cache, obj);

/* 销毁缓存(模块卸载时)*/
kmem_cache_destroy(my_cache);

/* 查看 slab 信息 */
/* cat /proc/slabinfo */
/* cat /sys/kernel/slab/my_object_cache/object_size */

slab 缓存的完整使用案例

/* 场景:网络驱动中频繁分配/释放数据包描述符 */
struct pkt_desc {
    struct list_head list;
    dma_addr_t       dma_addr;
    void            *data;
    size_t           len;
    u32              flags;
};

static struct kmem_cache *pkt_cache;

static int __init net_driver_init(void)
{
    /* 创建数据包描述符缓存 */
    pkt_cache = kmem_cache_create("pkt_desc",
                                   sizeof(struct pkt_desc),
                                   0,
                                   SLAB_HWCACHE_ALIGN | SLAB_PANIC,
                                   NULL);
    /* SLAB_PANIC:分配失败时内核 panic(用于关键资源)*/
    return 0;
}

/* 分配数据包描述符 */
static struct pkt_desc *alloc_pkt_desc(void)
{
    struct pkt_desc *desc = kmem_cache_alloc(pkt_cache, GFP_ATOMIC);
    if (desc)
        memset(desc, 0, sizeof(*desc));
    return desc;
}

/* 释放数据包描述符 */
static void free_pkt_desc(struct pkt_desc *desc)
{
    kmem_cache_free(pkt_cache, desc);
}
内存池(mempool)

内存池保证在内存紧张时仍能分配到内存:

#include <linux/mempool.h>

/*
 * mempool_t:内存池
 * 预先分配一定数量的对象,保证在内存紧张时仍能分配
 * 适用于关键路径(如中断处理、I/O 路径)
 */

mempool_t *my_pool;

/* 创建内存池(基于 slab 缓存)*/
my_pool = mempool_create_slab_pool(
    16,          /* 最小保留对象数量 */
    my_cache     /* 使用的 slab 缓存 */
);

/* 创建内存池(基于 kmalloc)*/
my_pool = mempool_create_kmalloc_pool(16, sizeof(struct my_object));

/* 从内存池分配(保证成功,可能睡眠等待)*/
struct my_object *obj = mempool_alloc(my_pool, GFP_KERNEL);

/* 释放回内存池 */
mempool_free(obj, my_pool);

/* 销毁内存池 */
mempool_destroy(my_pool);

11.5 设备 I/O 端口和 I/O 内存的访问

11.5.1 I/O 端口与 I/O 内存

两种外设访问方式:

I/O 端口(I/O Port):
  - x86 架构特有
  - 使用专用的 I/O 地址空间(与内存地址空间分离)
  - 通过 in/out 指令访问
  - 地址范围:0 ~ 65535(16位)
  - ARM 等 RISC 架构不支持

I/O 内存(I/O Memory / MMIO):
  - 所有架构都支持
  - 外设寄存器映射到内存地址空间
  - 通过普通内存读写指令访问
  - ARM 嵌入式系统主要使用此方式

11.5.2 I/O 端口访问(x86)

#include <asm/io.h>

/* x86 I/O 端口访问函数 */

/* 读操作 */
u8  inb(unsigned long port);    /* 读 8 位 */
u16 inw(unsigned long port);    /* 读 16 位 */
u32 inl(unsigned long port);    /* 读 32 位 */

/* 写操作 */
void outb(u8  value, unsigned long port);   /* 写 8 位 */
void outw(u16 value, unsigned long port);   /* 写 16 位 */
void outl(u32 value, unsigned long port);   /* 写 32 位 */

/* 字符串 I/O(批量传输)*/
void insb(unsigned long port, void *addr, unsigned long count);
void outsb(unsigned long port, const void *addr, unsigned long count);

/* 示例:访问 x86 串口(COM1,I/O 端口 0x3F8)*/
#define COM1_BASE  0x3F8
#define COM1_DATA  (COM1_BASE + 0)  /* 数据寄存器 */
#define COM1_LSR   (COM1_BASE + 5)  /* 线路状态寄存器 */

void com1_send_byte(u8 data)
{
    /* 等待发送缓冲区为空 */
    while (!(inb(COM1_LSR) & 0x20));
    outb(data, COM1_DATA);
}

11.5.3 I/O 内存访问(MMIO)

#include <asm/io.h>

/*
 * I/O 内存访问函数(所有架构通用)
 * 这些函数内部包含必要的内存屏障,确保访问顺序正确
 *
 * 注意:必须先通过 ioremap() 将物理地址映射到虚拟地址
 *       然后才能使用这些函数访问
 */

void __iomem *base = ioremap(phys_addr, size);

/* 读操作 */
u8  readb(const volatile void __iomem *addr);   /* 读 8 位 */
u16 readw(const volatile void __iomem *addr);   /* 读 16 位 */
u32 readl(const volatile void __iomem *addr);   /* 读 32 位 */
u64 readq(const volatile void __iomem *addr);   /* 读 64 位 */

/* 写操作 */
void writeb(u8  value, volatile void __iomem *addr);  /* 写 8 位 */
void writew(u16 value, volatile void __iomem *addr);  /* 写 16 位 */
void writel(u32 value, volatile void __iomem *addr);  /* 写 32 位 */
void writeq(u64 value, volatile void __iomem *addr);  /* 写 64 位 */

/* 使用示例:访问 UART 寄存器 */
#define UART_THR   0x00   /* 发送保持寄存器 */
#define UART_LSR   0x14   /* 线路状态寄存器 */
#define UART_LSR_THRE  BIT(5)  /* 发送保持寄存器空 */

void uart_send_byte(void __iomem *base, u8 data)
{
    /* 等待发送缓冲区为空 */
    while (!(readl(base + UART_LSR) & UART_LSR_THRE))
        cpu_relax();  /* 告诉 CPU 正在忙等待,可以降低功耗 */

    writeb(data, base + UART_THR);
}

/*
 * 为什么不能直接用指针访问 I/O 内存?
 *
 * 错误方式:
 * volatile u32 *reg = (volatile u32 *)ioremap(addr, size);
 * *reg = value;  ← 可能被编译器优化,缺少内存屏障
 *
 * 正确方式:
 * void __iomem *base = ioremap(addr, size);
 * writel(value, base);  ← 包含内存屏障,保证访问顺序
 */

11.6 申请与释放设备的 I/O 端口和 I/O 内存

11.6.1 申请 I/O 端口

#include <linux/ioport.h>

/*
 * request_region:申请 I/O 端口范围
 * start:起始端口号
 * n:端口数量
 * name:设备名称(显示在 /proc/ioports)
 *
 * 返回:非 NULL(成功),NULL(失败,端口已被占用)
 */
struct resource *res = request_region(0x3F8, 8, "serial");
if (!res) {
    pr_err("I/O 端口 0x3F8 已被占用\n");
    return -EBUSY;
}

/* 释放 I/O 端口 */
release_region(0x3F8, 8);

/* 查看已申请的 I/O 端口 */
/* cat /proc/ioports */

11.6.2 申请 I/O 内存

#include <linux/ioport.h>
#include <linux/io.h>

/*
 * request_mem_region:申请 I/O 内存区域
 * start:物理地址起始
 * n:大小(字节)
 * name:设备名称(显示在 /proc/iomem)
 */
struct resource *res = request_mem_region(0x44E09000, 0x1000, "uart0");
if (!res) {
    pr_err("I/O 内存 0x44E09000 已被占用\n");
    return -EBUSY;
}

/* 映射到内核虚拟地址 */
void __iomem *base = ioremap(0x44E09000, 0x1000);
if (!base) {
    release_mem_region(0x44E09000, 0x1000);
    return -ENOMEM;
}

/* 使用 I/O 内存 */
writel(0x83, base + 0x0C);

/* 释放(逆序)*/
iounmap(base);
release_mem_region(0x44E09000, 0x1000);

/* 查看已申请的 I/O 内存 */
/* cat /proc/iomem */

11.6.3 使用 platform 资源的推荐方式

在现代 Linux 驱动中,推荐通过设备树和 platform 资源 API 获取 I/O 内存:

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    /* 方法一:分步获取资源和映射 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "获取内存资源失败\n");
        return -ENODEV;
    }

    /* request_mem_region + ioremap 的组合 */
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 方法二:直接获取并映射(更简洁,推荐)*/
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 使用 base 访问寄存器 */
    writel(0x01, base + CTRL_REG);

    return 0;
}

/* 对应的设备树节点 */
/*
my_device: my-device@44E09000 {
    compatible = "myvendor,my-device";
    reg = <0x44E09000 0x1000>;   ← 物理地址和大小
    interrupts = <0 72 4>;
};
*/

11.7 设备内存映射与 mmap

11.7.1 mmap 的概念

mmap 允许将设备内存或文件直接映射到用户进程的地址空间,用户程序可以像访问普通内存一样访问设备:

mmap 的工作原理:

用户进程地址空间:
┌─────────────────────────────────────────────────────────┐
│  ...                                                    │
│  ┌─────────────────────────────────────────────────┐   │
│  │  mmap 映射区域(用户虚拟地址)                   │   │
│  │  用户程序直接读写此区域                          │   │
│  └─────────────────────────────────────────────────┘   │
│  ...                                                    │
└─────────────────────────────────────────────────────────┘
                    ↕ 页表映射(MMU)
┌─────────────────────────────────────────────────────────┐
│  设备内存(物理地址)或内核内存                          │
│  用户程序的读写直接作用于此                              │
└─────────────────────────────────────────────────────────┘

优点:
  ✓ 零拷贝:避免内核↔用户空间的数据复制
  ✓ 高性能:适合大量数据传输(如视频帧缓冲)
  ✓ 直接访问:用户程序可以直接操作设备寄存器

11.7.2 驱动中实现 mmap

#include <linux/mm.h>

/*
 * mmap 函数的实现
 *
 * filp:文件结构体
 * vma:虚拟内存区域描述符
 *   vma->vm_start:映射的起始虚拟地址
 *   vma->vm_end:映射的结束虚拟地址
 *   vma->vm_pgoff:文件偏移(页为单位)
 *   vma->vm_flags:映射标志
 *   vma->vm_page_prot:页面保护属性
 */
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;
    unsigned long pfn;

    /* 检查映射大小 */
    if (size > dev->mem_size)
        return -EINVAL;

    /* 获取物理页帧号(PFN)*/
    pfn = virt_to_phys(dev->mem_buf) >> PAGE_SHIFT;

    /*
     * remap_pfn_range:将物理内存映射到用户虚拟地址空间
     *
     * vma:虚拟内存区域
     * vma->vm_start:用户空间起始虚拟地址
     * pfn:物理页帧号
     * size:映射大小
     * vma->vm_page_prot:页面保护属性
     */
    if (remap_pfn_range(vma, vma->vm_start, pfn, size,
                         vma->vm_page_prot)) {
        return -EAGAIN;
    }

    return 0;
}

/* 映射设备 I/O 内存(寄存器)到用户空间 */
static int device_reg_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long phys_addr = 0x44E09000;  /* UART 物理地址 */
    unsigned long size = vma->vm_end - vma->vm_start;

    /* 设置为不可 Cache(设备寄存器不能被缓存)*/
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    /* 映射设备物理地址 */
    if (io_remap_pfn_range(vma, vma->vm_start,
                            phys_addr >> PAGE_SHIFT,
                            size, vma->vm_page_prot)) {
        return -EAGAIN;
    }

    return 0;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .mmap  = my_mmap,
    /* ... */
};

11.7.3 用户空间使用 mmap

/* 用户空间使用 mmap 访问设备内存 */
#include <sys/mman.h>
#include <fcntl.h>

int fd = open("/dev/mydevice", O_RDWR);

/* 映射设备内存到用户空间 */
void *mapped = mmap(NULL,
                    4096,                    /* 映射大小 */
                    PROT_READ | PROT_WRITE,  /* 读写权限 */
                    MAP_SHARED,              /* 共享映射 */
                    fd,
                    0);                      /* 文件偏移 */

if (mapped == MAP_FAILED) {
    perror("mmap failed");
    close(fd);
    return -1;
}

/* 直接读写设备内存(零拷贝)*/
volatile u32 *reg = (volatile u32 *)mapped;
*reg = 0x01;                    /* 写寄存器 */
u32 val = *reg;                 /* 读寄存器 */

/* 解除映射 */
munmap(mapped, 4096);
close(fd);

11.7.4 帧缓冲 mmap 案例

/*
 * 帧缓冲驱动的 mmap 实现
 * 将显存映射到用户空间,实现零拷贝绘图
 */
struct fb_dev {
    void        *fb_mem;        /* 帧缓冲内存(内核虚拟地址)*/
    dma_addr_t   fb_dma;        /* 帧缓冲 DMA 地址 */
    size_t       fb_size;       /* 帧缓冲大小 */
};

static int fb_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct fb_dev *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;

    if (size > dev->fb_size)
        return -EINVAL;

    /* 帧缓冲通常是 DMA 一致性内存,不需要 Cache */
    vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

    return remap_pfn_range(vma,
                            vma->vm_start,
                            virt_to_phys(dev->fb_mem) >> PAGE_SHIFT,
                            size,
                            vma->vm_page_prot);
}

/* 用户空间绘图(零拷贝)*/
/*
void *fb = mmap(NULL, fb_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
uint32_t *pixels = (uint32_t *)fb;
for (int i = 0; i < width * height; i++)
    pixels[i] = 0xFF0000FF;  // 红色像素(ARGB)
munmap(fb, fb_size);
*/

11.8 I/O 内存静态映射

11.8.1 静态映射的概念

在系统启动时,将常用的外设寄存器地址永久映射到内核虚拟地址,避免每次使用时动态 ioremap:

/*
 * 静态映射(Static Mapping):
 * 在内核启动时,通过 map_desc 结构体定义固定的物理→虚拟地址映射
 * 这些映射在系统运行期间始终有效
 *
 * 适用场景:
 * - 系统启动早期就需要访问的外设(如串口调试)
 * - 频繁访问的外设(避免 ioremap 的开销)
 * - 中断控制器、定时器等核心外设
 */

/* ARM 平台静态映射定义(arch/arm/mach-xxx/xxx.c)*/
#include <asm/mach/map.h>

static struct map_desc my_io_desc[] __initdata = {
    {
        .virtual  = (unsigned long)MY_UART_VIRT_BASE,  /* 内核虚拟地址 */
        .pfn      = __phys_to_pfn(MY_UART_PHYS_BASE),  /* 物理页帧号 */
        .length   = SZ_4K,                              /* 映射大小 */
        .type     = MT_DEVICE,                          /* 设备内存类型 */
    },
    {
        .virtual  = (unsigned long)MY_GPIO_VIRT_BASE,
        .pfn      = __phys_to_pfn(MY_GPIO_PHYS_BASE),
        .length   = SZ_4K,
        .type     = MT_DEVICE,
    },
};

/* 在 map_io 函数中注册静态映射 */
static void __init my_board_map_io(void)
{
    iotable_init(my_io_desc, ARRAY_SIZE(my_io_desc));
}

11.8.2 静态映射的内存类型

/* 内存类型(MT_xxx)定义了 Cache 和缓冲属性 */

MT_DEVICE          /* 设备内存:不可 Cache,不可缓冲(最严格)*/
MT_DEVICE_CACHED   /* 设备内存:可 Cache */
MT_DEVICE_WC       /* 设备内存:写合并(Write Combining)*/
MT_MEMORY_RWX      /* 普通内存:可读写执行 */
MT_MEMORY_RW       /* 普通内存:可读写 */
MT_ROM             /* ROM:只读 */

/*
 * 选择原则:
 * 外设寄存器 → MT_DEVICE(不可 Cache,保证读写顺序)
 * 帧缓冲     → MT_DEVICE_WC(写合并,提高写性能)
 * 普通内存   → MT_MEMORY_RWX
 */

11.9 DMA

11.9.1 DMA 的概念

**DMA(Direct Memory Access,直接内存访问)**允许外设在不经过 CPU 的情况下,直接与内存进行数据传输:

DMA 传输 vs CPU 传输:

CPU 传输(PIO,Programmed I/O):
  CPU → 读取外设数据 → 写入内存
  CPU 全程参与,效率低,CPU 无法做其他事

DMA 传输:
  CPU 配置 DMA 控制器(源地址、目标地址、长度)
  DMA 控制器独立完成数据传输
  传输完成后,DMA 控制器发送中断通知 CPU
  CPU 可以在 DMA 传输期间做其他事

DMA 传输示意:
┌──────────┐    DMA 请求    ┌──────────────┐
│  外设     │ ─────────────→ │  DMA 控制器  │
│ (UART/   │               │              │
│  SPI 等) │               │  自动传输数据 │
└──────────┘               └──────┬───────┘
                                  │ 直接访问内存
                           ┌──────▼───────┐
                           │   DDR 内存   │
                           └──────────────┘
                                  │ 传输完成中断
                           ┌──────▼───────┐
                           │     CPU      │
                           └──────────────┘

11.9.2 DMA 内存的要求

DMA 内存的特殊要求:

1. 物理连续
   DMA 控制器通常只能访问物理连续的内存
   (有 IOMMU 的系统可以例外)

2. 对齐
   DMA 传输通常要求内存按特定边界对齐
   (如 4 字节、16 字节、Cache 行大小等)

3. Cache 一致性
   CPU 通过 Cache 访问内存,DMA 直接访问物理内存
   如果 Cache 中有脏数据,DMA 读到的是旧数据
   如果 DMA 写入了新数据,CPU 可能读到 Cache 中的旧数据
   
   解决方案:
   a. 使用一致性 DMA 内存(不可 Cache)
   b. 使用流式 DMA 映射(手动刷新 Cache)

11.9.3 一致性 DMA 内存

#include <linux/dma-mapping.h>

/*
 * dma_alloc_coherent:分配一致性 DMA 内存
 * 分配的内存不可 Cache,CPU 和 DMA 都能看到最新数据
 * 适合控制结构、描述符等频繁读写的小块内存
 *
 * dev:设备指针
 * size:分配大小
 * dma_handle:返回 DMA 地址(设备使用)
 * gfp:分配标志
 *
 * 返回:CPU 虚拟地址
 */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, 4096, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(dev, "DMA 内存分配失败\n");
    return -ENOMEM;
}

/* CPU 使用 cpu_addr 访问内存 */
memset(cpu_addr, 0, 4096);

/* DMA 控制器使用 dma_handle 访问内存 */
writel(dma_handle, dev_base + DMA_ADDR_REG);
writel(4096, dev_base + DMA_LEN_REG);
writel(DMA_START, dev_base + DMA_CTRL_REG);

/* 释放一致性 DMA 内存 */
dma_free_coherent(dev, 4096, cpu_addr, dma_handle);

11.9.4 流式 DMA 映射

/*
 * 流式 DMA 映射(Streaming DMA Mapping)
 * 将已有的内存映射为 DMA 可访问的地址
 * 性能比一致性 DMA 内存好(可以使用 Cache)
 * 但需要手动同步 Cache
 */

/* 分配普通内存 */
void *buf = kmalloc(4096, GFP_KERNEL);

/* 映射为 DMA 地址(同时刷新 Cache,确保 DMA 读到最新数据)*/
dma_addr_t dma_addr = dma_map_single(dev,
                                      buf,
                                      4096,
                                      DMA_TO_DEVICE);  /* CPU→设备 */

if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "DMA 映射失败\n");
    kfree(buf);
    return -ENOMEM;
}

/* 启动 DMA 传输 */
writel(dma_addr, dev_base + DMA_ADDR_REG);
writel(4096, dev_base + DMA_LEN_REG);
writel(DMA_START, dev_base + DMA_CTRL_REG);

/* 等待 DMA 完成(通过中断或轮询)*/
wait_for_completion(&dma_done);

/* 解除 DMA 映射(同时使 Cache 无效,确保 CPU 读到最新数据)*/
dma_unmap_single(dev, dma_addr, 4096, DMA_TO_DEVICE);

/* 现在可以安全地访问 buf 中的数据 */
kfree(buf);

/*
 * DMA 方向:
 * DMA_TO_DEVICE:CPU 写数据,DMA 读数据(发送)
 * DMA_FROM_DEVICE:DMA 写数据,CPU 读数据(接收)
 * DMA_BIDIRECTIONAL:双向传输
 * DMA_NONE:不传输(仅用于调试)
 */

11.9.5 DMA 引擎 API

Linux 4.0 提供了统一的 DMA 引擎 API,简化 DMA 驱动开发:

#include <linux/dmaengine.h>

/* 申请 DMA 通道 */
struct dma_chan *chan = dma_request_channel(mask, filter_fn, filter_param);
if (!chan) {
    dev_err(dev, "申请 DMA 通道失败\n");
    return -ENODEV;
}

/* 准备 DMA 传输描述符(内存到内存)*/
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_dma_memcpy(chan,
                                  dst_dma_addr,  /* 目标 DMA 地址 */
                                  src_dma_addr,  /* 源 DMA 地址 */
                                  len,           /* 传输长度 */
                                  DMA_PREP_INTERRUPT); /* 完成时产生中断 */

/* 设置完成回调 */
desc->callback = my_dma_callback;
desc->callback_param = dev;

/* 提交 DMA 传输 */
dma_cookie_t cookie = dmaengine_submit(desc);

/* 启动 DMA 传输 */
dma_async_issue_pending(chan);

/* 等待完成(或在回调中处理)*/
dma_sync_wait(chan, cookie);

/* 释放 DMA 通道 */
dma_release_channel(chan);

11.9.6 完整的 DMA 驱动案例

/*
 * DMA 数据传输驱动示例
 * 使用 DMA 将数据从设备 FIFO 传输到内存
 */
struct dma_dev {
    void __iomem        *base;
    int                  irq;
    struct dma_chan     *rx_chan;
    void                *rx_buf;
    dma_addr_t           rx_dma;
    size_t               rx_size;
    struct completion    rx_done;
};

/* DMA 完成回调 */
static void rx_dma_callback(void *data)
{
    struct dma_dev *dev = data;
    complete(&dev->rx_done);
}

/* 启动 DMA 接收 */
static int start_dma_rx(struct dma_dev *dev, size_t len)
{
    struct dma_async_tx_descriptor *desc;
    dma_addr_t fifo_dma;

    /* 设备 FIFO 的 DMA 地址(物理地址)*/
    fifo_dma = dev->base_phys + FIFO_REG;

    /* 准备设备到内存的 DMA 传输 */
    desc = dmaengine_prep_slave_single(dev->rx_chan,
                                        dev->rx_dma,  /* 目标内存 DMA 地址 */
                                        len,
                                        DMA_DEV_TO_MEM,
                                        DMA_PREP_INTERRUPT);
    if (!desc)
        return -ENOMEM;

    desc->callback       = rx_dma_callback;
    desc->callback_param = dev;

    reinit_completion(&dev->rx_done);
    dmaengine_submit(desc);
    dma_async_issue_pending(dev->rx_chan);

    /* 等待 DMA 完成(最多 5 秒)*/
    if (!wait_for_completion_timeout(&dev->rx_done,
                                      msecs_to_jiffies(5000))) {
        dmaengine_terminate_all(dev->rx_chan);
        return -ETIMEDOUT;
    }

    return 0;
}

static int dma_dev_probe(struct platform_device *pdev)
{
    struct dma_dev *dev;
    dma_cap_mask_t mask;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) return -ENOMEM;

    init_completion(&dev->rx_done);

    /* 申请 DMA 通道 */
    dma_cap_zero(mask);
    dma_cap_set(DMA_SLAVE, mask);
    dev->rx_chan = dma_request_channel(mask, NULL, NULL);
    if (!dev->rx_chan) {
        dev_err(&pdev->dev, "申请 DMA 通道失败\n");
        return -ENODEV;
    }

    /* 分配一致性 DMA 接收缓冲区 */
    dev->rx_size = 4096;
    dev->rx_buf  = dma_alloc_coherent(&pdev->dev, dev->rx_size,
                                       &dev->rx_dma, GFP_KERNEL);
    if (!dev->rx_buf) {
        ret = -ENOMEM;
        goto err_dma_chan;
    }

    platform_set_drvdata(pdev, dev);
    dev_info(&pdev->dev, "DMA 驱动加载成功\n");
    return 0;

err_dma_chan:
    dma_release_channel(dev->rx_chan);
    return ret;
}

static int dma_dev_remove(struct platform_device *pdev)
{
    struct dma_dev *dev = platform_get_drvdata(pdev);

    dma_free_coherent(&pdev->dev, dev->rx_size, dev->rx_buf, dev->rx_dma);
    dma_release_channel(dev->rx_chan);
    return 0;
}

本章小结

章节 核心知识点 关键 API
11.1 CPU与内存关系 物理/虚拟/总线地址;MMIO概念;SoC地址映射 概念理解
11.2 MMU 地址转换;页表;TLB;内存保护;Cache控制 ioremap()必要性
11.3 Linux内存管理 内存区域(ZONE_DMA/NORMAL/HIGHMEM);Buddy系统;Slab /proc/meminfo
11.4.1 用户空间内存 malloc/calloc/realloc/free;posix_memalign;mmap malloc()posix_memalign()
11.4.2 内核kmalloc GFP标志详解;kzalloc;中断上下文用GFP_ATOMIC kmalloc()kzalloc()kfree()
11.4.3 vmalloc 虚拟连续物理不连续;大块内存;不能用于DMA vmalloc()vfree()
11.4.4 slab与内存池 kmem_cache创建/分配/释放;mempool保证分配成功 kmem_cache_create()mempool_create_slab_pool()
11.5 I/O端口和I/O内存 x86 I/O端口(in/out);MMIO(readl/writel);为何不能直接用指针 readl()writel()inb()outb()
11.6 申请与释放 request_region/mem_region;devm_ioremap_resource;/proc/iomem request_mem_region()devm_platform_ioremap_resource()
11.7 mmap 零拷贝原理;remap_pfn_range;帧缓冲案例;pgprot_noncached remap_pfn_range()io_remap_pfn_range()
11.8 I/O内存静态映射 map_desc结构体;iotable_init;MT_DEVICE类型 iotable_init()map_desc
11.9 DMA DMA vs CPU传输;Cache一致性问题;一致性DMA内存;流式DMA映射;DMA引擎API;完整DMA驱动案例 dma_alloc_coherent()dma_map_single()dmaengine_prep_slave_single()

内存分配函数选择指南

内核内存分配选择:

需要物理连续?
  是 → 需要 DMA?
        是 → dma_alloc_coherent()(一致性)
             或 kmalloc() + dma_map_single()(流式)
        否 → kmalloc() / kzalloc()(< 4MB)
             或 alloc_pages()(直接页分配)
  否 → vmalloc()(大块内存,不需要物理连续)

在中断上下文?
  是 → kmalloc(size, GFP_ATOMIC)
  否 → kmalloc(size, GFP_KERNEL)

频繁分配/释放固定大小对象?
  是 → kmem_cache_create() + kmem_cache_alloc()
  
需要保证分配成功(关键路径)?
  是 → mempool_create() + mempool_alloc()

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐