《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 11 章 内存与 I/O 访问
·
《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年
更多推荐



所有评论(0)