[Linux外设驱动详解]7. 内存管理与 I/O 访问

7. 内存管理与 I/O 访问

基于 Linux 内核源码分析 | 硬件平台:瑞芯微 RK3588 (ARM64)


目录


7.1 ioremap / iounmap

7.1.1 概述

在 Linux 驱动开发中,硬件设备的寄存器通常位于物理地址空间。内核代码使用的是虚拟地址,因此需要一种机制将物理地址映射到内核虚拟地址空间。ioremap 就是为此而设计的接口。

核心源码位置:

7.1.2 ioremap 映射流程

地址无效

有效

是 RAM

非 RAM

失败

成功

驱动调用 ioremap

ioremap phys_addr size

__ioremap_caller
设置 PROT_DEVICE_nGnRE 属性

参数检查

返回 NULL

页对齐处理

是否为 RAM 区域?

返回 NULL
禁止映射 RAM

get_vm_area_caller
分配虚拟地址空间

ioremap_page_range
建立页表映射

映射成功?

vunmap 释放资源

返回虚拟地址

7.1.3 核心实现分析

__ioremap_caller 函数

源码位置: kernel/arch/arm64/mm/ioremap.c:21-63

static void __iomem *__ioremap_caller(phys_addr_t phys_addr, size_t size,
                      pgprot_t prot, void *caller)
{
   
   
    unsigned long last_addr;
    unsigned long offset = phys_addr & ~PAGE_MASK;
    int err;
    unsigned long addr;
    struct vm_struct *area;

    /* 1. 页对齐处理 */
    phys_addr &= PAGE_MASK;
    size = PAGE_ALIGN(size + offset);

    /* 2. 检查地址范围有效性 */
    last_addr = phys_addr + size - 1;
    if (!size || last_addr < phys_addr || (last_addr & ~PHYS_MASK))
        return NULL;

    /* 3. 禁止映射 RAM 区域 */
    if (WARN_ON(pfn_valid(__phys_to_pfn(phys_addr))))
        return NULL;

    /* 4. 分配内核虚拟地址空间 */
    area = get_vm_area_caller(size, VM_IOREMAP, caller);
    if (!area)
        return NULL;
    addr = (unsigned long)area->addr;
    area->phys_addr = phys_addr;

    /* 5. 建立页表映射 */
    err = ioremap_page_range(addr, addr + size, phys_addr, prot);
    if (err) {
   
   
        vunmap((void *)addr);
        return NULL;
    }

    /* 6. 返回带偏移的虚拟地址 */
    return (void __iomem *)(offset + addr);
}
ioremap_page_range 页表建立

源码位置: kernel/mm/ioremap.c:222-250

页表层级遍历:

PGD (Page Global Directory)
  ↓
P4D (Page 4th Directory)
  ↓
PUD (Page Upper Directory)
  ↓
PMD (Page Middle Directory)
  ↓
PTE (Page Table Entry) → 物理页面

支持巨页映射优化:

页表级别 映射粒度 条件
P4D 级别 512GB 地址和大小都要 512GB 对齐
PUD 级别 1GB 地址和大小都要 1GB 对齐
PMD 级别 2MB 地址和大小都要 2MB 对齐
PTE 级别 4KB 常规页面映射

7.1.4 内存类型属性

源码位置: kernel/arch/arm64/include/asm/pgtable-prot.h:55-60

ARM64 定义了多种设备内存属性:

/* 设备内存属性 - 不可缓存 */
#define PROT_DEVICE_nGnRnE  /* non-Gathering, non-Reordering, No Early acknowledge */
#define PROT_DEVICE_nGnRE   /* non-Gathering, non-Reordering, Early acknowledge */

/* 正常内存属性 */
#define PROT_NORMAL_NC      /* Normal Non-Cacheable */
#define PROT_NORMAL         /* Normal Cacheable */
属性缩写 含义 说明
nG non-Gathering 读写操作不会合并
nR non-Reordering 读写操作不会重排序
RnE Read No Early 读操作不提前返回
RE Early 写操作可提前返回确认
NC Non-Cacheable 不使用缓存

常用 ioremap 变体:

/* 标准 ioremap - 设备内存,不可缓存 */
void __iomem *ioremap(phys_addr_t addr, size_t size);

/* 写合并模式 - 正常非缓存内存 */
void __iomem *ioremap_wc(phys_addr_t addr, size_t size);

/* 可缓存模式 - 用于正常 RAM 区域 */
void __iomem *ioremap_cache(phys_addr_t addr, size_t size);

/* PCI 配置空间 - nGnRnE 属性 */
void __iomem *pci_remap_cfgspace(phys_addr_t addr, size_t size);

7.1.5 iounmap 解除映射

源码位置: kernel/arch/arm64/mm/ioremap.c:72-83

void iounmap(volatile void __iomem *io_addr)
{
   
   
    unsigned long addr = (unsigned long)io_addr & PAGE_MASK;

    /*
     * 某些情况下(如 ioremap_cache 复用 RAM 映射),
     * 地址可能不在 vmalloc 范围内,需要检查。
     */
    if (is_vmalloc_addr((void *)addr))
        vunmap((void *)addr);
}

7.1.6 使用示例

/* 假设 RK3588 GPIO 控制器物理基址为 0xFD820000 */
#define GPIO_BASE_PHYS  0xFD820000

void __iomem *gpio_base;

/* 1. 映射设备寄存器 */
gpio_base = ioremap(GPIO_BASE_PHYS, 0x1000);
if (!gpio_base) {
   
   
    pr_err("Failed to ioremap GPIO registers\n");
    return -ENOMEM;
}

/* 2. 访问寄存器 */
u32 value = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JustaUncle

一杯咖啡,换我肝到天亮!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值