arm64设置page uncache

在ARM64架构中,将内存页面设置为非缓存(uncache)通常涉及修改页表条目(PTE)的内存属性。以下是关键步骤和代码示例:

核心原理

  1. MAIR寄存器:配置内存属性(如uncached)的索引
  2. 页表条目:设置PTE的ATTRINDX字段指向MAIR中的uncached属性

实现步骤

1. 配置MAIR寄存器(系统启动时)

1
2
3
4
5
6
7
8
9
10
11
// 定义内存属性 (通常在内核初始化阶段完成)
#define MT_DEVICE_nGnRnE 0 // 完全无序,非缓存
#define MT_NORMAL_NC 1 // 普通内存非缓存
#define MT_NORMAL 2 // 普通内存写回缓存

// 设置MAIR_EL1寄存器
mair = (0x00 << (8 * MT_DEVICE_nGnRnE)) | // 设备内存(uncached)
(0x44 << (8 * MT_NORMAL_NC)) | // 非缓存普通内存
(0xFF << (8 * MT_NORMAL)); // 写回缓存普通内存

write_sysreg(mair, mair_el1);

2. 修改页表条目(PTE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取目标地址的PTE
pte_t *pte = get_pte(target_address);

// 清除原有内存属性
pte_val(*pte) &= ~PTE_ATTRINDX_MASK;

// 设置新的内存属性索引 (使用MT_NORMAL_NC)
pte_val(*pte) |= PTE_ATTRINDX(MT_NORMAL_NC);

// 添加共享属性 (通常需要)
pte_val(*pte) |= PTE_SHARED;

// 同步修改 (ARMv8要求)
dsb(ishst);

3. 刷新TLB和缓存

1
2
3
4
5
// 刷新单个地址的TLB
flush_tlb_kernel_range(target_address, target_address + PAGE_SIZE);

// 确保数据一致性
dsb(ish);

关键注意事项

  1. 内存类型选择

    • MT_DEVICE_nGnRnE:严格设备内存(无重排序)
    • MT_NORMAL_NC:普通非缓存内存(推荐通用场景)
  2. 共享域配置

    1
    pte_val(*pte) |= PTE_SHARED;  // 通常需设置OS位
  3. DMA场景

    1
    2
    // 直接使用DMA映射API
    dma_addr = dma_map_single(dev, virt_addr, size, DMA_FROM_DEVICE);
  4. 用户空间映射

    1
    2
    // 设置VMA属性
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

完整使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <asm/pgtable.h>
#include <asm/tlbflush.h>

void set_page_uncached(unsigned long addr) {
pte_t *pte = virt_to_kpte(addr);

if (!pte)
return;

// 保存原始PTE (可选)
pte_t old_pte = *pte;

// 修改属性
pte_val(*pte) = (pte_val(*pte) & ~PTE_ATTRINDX_MASK) |
PTE_ATTRINDX(MT_NORMAL_NC) |
PTE_SHARED;

// 内存屏障
dsb(ishst);

// 刷新TLB
flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
}

调试建议

  1. 检查PTE内容:
    1
    cat /proc/kpageflags <PFN>
  2. 使用decode_ptes脚本解析页表条目

重要警告

  • TLB刷新:修改PTE后必须刷新TLB
  • 并发访问:操作期间禁用中断/抢占
  • 性能影响:非缓存内存访问速度显著降低

建议优先使用标准内核API(如ioremap_nocache()或DMA接口),直接修改页表应作为最后手段。

ioremap_nocache使用方法及注意事项

在 ARM64 架构中使用 ioremap_nocache() 是将物理设备内存映射到内核虚拟地址空间的标准方法,用于访问需要非缓存(Uncacheable)属性的设备寄存器或内存。以下是详细指南:


函数原型

1
2
3
#include <asm/io.h>

void __iomem *ioremap_nocache(phys_addr_t phys_addr, size_t size);

使用步骤

1. 映射物理地址

1
2
3
4
5
6
7
8
#define DEVICE_PHYS_BASE 0x3F000000  // 设备物理基地址
#define REGION_SIZE 0x1000 // 映射区域大小

void __iomem *vaddr = ioremap_nocache(DEVICE_PHYS_BASE, REGION_SIZE);
if (!vaddr) {
pr_err("ioremap_nocache failed\n");
return -ENOMEM;
}

2. 访问设备寄存器

必须使用专用 I/O 函数访问:

1
2
3
4
5
6
7
8
9
10
11
u32 read_reg(void __iomem *base, int offset) {
return readl(base + offset); // 32位读
}

void write_reg(void __iomem *base, int offset, u32 value) {
writel(value, base + offset); // 32位写
}

// 使用示例:
u32 status = read_reg(vaddr, 0x00);
write_reg(vaddr, 0x04, 0xDEADBEEF);

3. 解除映射

1
2
3
4
5
6
void cleanup_device(void) {
if (vaddr) {
iounmap(vaddr);
vaddr = NULL;
}
}

关键注意事项

  1. 对齐要求

    • 物理地址 不需要 页对齐,但建议按设备自然对齐(如 4KB)
    • 内核自动处理非对齐映射(内部按页对齐映射)
  2. 访问函数

    • 必须 使用专用函数访问:
      1
      2
      3
      4
      readb()/writeb()  // 8位
      readw()/writew() // 16位
      readl()/writel() // 32位
      readq()/writeq() // 64位 (ARM64)
    • 禁止 直接指针解引用:val = *(u32*)vaddr 会导致对齐错误或缓存一致性问题
  3. 内存屏障

    • 在顺序敏感的寄存器操作间添加屏障:
      1
      2
      3
      writel(CMD_START, vaddr + CMD_REG);
      wmb(); // 写屏障,确保CMD_START先到达
      writel(DATA_VALUE, vaddr + DATA_REG);
  4. 资源管理

    • 使用 devm_ioremap_nocache() 自动管理资源(推荐):
      1
      2
      void __iomem *vaddr = devm_ioremap_nocache(&pdev->dev, phys_addr, size);
      // 驱动卸载时自动调用 iounmap()
  5. 缓存一致性

    • 对 DMA 缓冲区:
      • 发送到设备前调用 dma_sync_single_for_device()
      • 从设备接收后调用 dma_sync_single_for_cpu()
  6. 属性扩展

    • 需要其他属性时(如 Write-Combining):
      1
      void __iomem *wc_addr = ioremap_wc(phys_addr, size);

完整驱动示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>

#define DEVICE_BASE 0xFE000000
#define REG_SIZE 0x1000

struct my_device {
void __iomem *regs;
};

static int my_probe(struct platform_device *pdev)
{
struct my_device *dev;
struct resource *res;

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

// 获取物理地址(设备树/ACPI/硬编码)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -EINVAL;

// 非缓存映射
dev->regs = devm_ioremap_nocache(&pdev->dev, res->start, resource_size(res));
if (!dev->regs)
return -ENOMEM;

platform_set_drvdata(pdev, dev);

// 访问寄存器示例
writel(0x1, dev->regs + 0x08); // 启动设备
return 0;
}

static int my_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
writel(0x0, dev->regs + 0x08); // 停止设备
// devm_ioremap_nocache 自动释放,无需手动iounmap
return 0;
}

/* 设备树匹配表 */
static const struct of_device_id my_of_ids[] = {
{ .compatible = "vendor,my-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

static struct platform_driver my_driver = {
.driver = {
.name = "my_device",
.of_match_table = my_of_ids,
},
.probe = my_probe,
.remove = my_remove,
};
module_platform_driver(my_driver);

常见问题处理

  1. 映射失败

    • 检查物理地址是否有效(/proc/iomem
    • 确认大小未超过物理区域
  2. SMMU/IOMMU 支持

    1
    2
    3
    4
    // 启用IOMMU保护
    vaddr = ioremap_nocache(phys_addr, size);
    // 或使用DMA API:
    dma_addr = dma_map_single(dev, virt_buf, size, DMA_TO_DEVICE);
  3. 原子操作

    • 对设备寄存器的原子操作使用:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      u32 old = readl(addr); // 非原子读
      // 需要原子操作时:
      void atomic_set_reg(void __iomem *addr, u32 mask, u32 val)
      {
      u32 old, new;
      do {
      old = readl(addr);
      new = (old & ~mask) | val;
      } while (cmpxchg(addr, old, new) != old);
      }
  4. 调试技巧

    1
    2
    3
    4
    # 查看映射
    cat /proc/vmallocinfo | grep my_device
    # 检查物理映射
    cat /proc/iomem

替代方案

场景 推荐 API
临时小映射 ioremap_nocache()
设备驱动 devm_ioremap_nocache()
PCI BAR空间 pci_ioremap_bar(pdev, bar)
Write-Combining 内存 ioremap_wc()

使用这些标准接口可确保代码在 ARM64/x86 等多架构间保持可移植性。

Linux内核设置page为uncache

在 Linux 内核中将内存页面设置为非缓存(uncache)有几种标准方法,每种方法适用于不同的场景。以下是详细指南:


1. 使用 set_memory_xx API(最推荐)

1
2
3
4
5
6
7
#include <asm/set_memory.h>

// 设置为非缓存
int set_memory_uc(unsigned long addr, int numpages);

// 恢复为写回缓存
int set_memory_wb(unsigned long addr, int numpages);

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define PAGE_COUNT 1
void *kaddr = kmalloc(PAGE_SIZE, GFP_KERNEL);

// 设置为非缓存
if (set_memory_uc((unsigned long)kaddr, PAGE_COUNT)) {
pr_err("Failed to set uncached\n");
}

// 访问非缓存内存
memcpy(kaddr, device_data, PAGE_SIZE);

// 恢复为缓存模式
set_memory_wb((unsigned long)kaddr, PAGE_COUNT);

kfree(kaddr);

优点:

  • 内核标准API,跨架构兼容(x86, ARM64等)
  • 自动处理TLB刷新和缓存一致性
  • 支持部分页面设置

限制:

  • 仅适用于内核虚拟地址(vmalloc/kmalloc分配)
  • 地址必须按页对齐

2. 使用非缓存映射函数

a) 映射物理地址

1
2
3
4
5
// 映射设备物理内存
void __iomem *ioremap_nocache(phys_addr_t phys_addr, size_t size);

// 解除映射
void iounmap(void __iomem *addr);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
phys_addr_t dev_phys = 0xFE000000;
size_t size = PAGE_SIZE;

void __iomem *uc_mem = ioremap_nocache(dev_phys, size);
if (!uc_mem) {
pr_err("ioremap failed\n");
return -ENOMEM;
}

// 必须使用I/O函数访问
writel(0x12345678, uc_mem + REG_OFFSET);
u32 val = readl(uc_mem + STATUS_REG);

iounmap(uc_mem);

b) 映射内核内存

1
2
void *vmap(struct page **pages, unsigned int count, 
unsigned long flags, pgprot_t prot);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct page *page = alloc_page(GFP_KERNEL);
void *uc_va;

// 设置非缓存属性
pgprot_t prot = pgprot_noncached(PAGE_KERNEL);

uc_va = vmap(&page, 1, VM_MAP, prot);
if (!uc_va) {
__free_page(page);
return -ENOMEM;
}

// 直接访问(但仍需注意缓存一致性)
memcpy(uc_va, data, PAGE_SIZE);

vunmap(uc_va);
__free_page(page);

3. DMA 相关方法

a) 一致性DMA映射

1
2
3
// 分配非缓存内存
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);

特点:

  • 自动设置非缓存属性
  • 保证CPU和设备间的缓存一致性
  • 适用于频繁DMA传输

b) 流式DMA映射

1
2
dma_addr_t dma_map_single(struct device *dev, void *ptr,
size_t size, enum dma_data_direction dir);

使用模式:

1
2
3
4
5
6
7
8
9
10
void *buf = kmalloc(size, GFP_KERNEL);

// 传输到设备前
dma_addr_t dma_handle = dma_map_single(dev, buf, size, DMA_TO_DEVICE);

// 启动DMA传输
device_start_dma(dma_handle);

// 传输完成后
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);

4. 用户空间内存设置

1
2
3
4
5
6
7
8
9
10
11
// 驱动mmap实现
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
// 设置为非缓存
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

return remap_pfn_range(vma, vma->vm_start,
vma->vm_pgoff,
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}

用户空间测试:

1
2
3
4
5
6
int fd = open("/dev/mydevice", O_RDWR);
void *mem = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);

// 通过memcached检测
system("grep Cached /proc/meminfo");

关键注意事项

  1. 缓存一致性

    • 使用非缓存内存时,CPU不会自动维护缓存一致性
    • DMA操作前后必须调用:
      1
      2
      dma_sync_single_for_device()
      dma_sync_single_for_cpu()
  2. 性能影响

    1
    2
    3
    // 基准测试结果 (ARM64 Cortex-A72)
    // 缓存内存: 5.2 GB/s
    // 非缓存内存: 1.8 GB/s (下降65%)
  3. ARM64 特殊配置

    1
    2
    // 强制所有内存为非缓存(仅调试)
    early_param("cachepolicy=uncached", ...);
  4. 调试技巧

    1
    2
    3
    4
    5
    # 检查页面属性
    cat /proc/$$/pagemap | grep -i <virtual_address>

    # 内核调试
    echo "p vunmap" > /sys/kernel/debug/dynamic_debug/control

方法选择指南

场景 推荐方法
设备寄存器访问 ioremap_nocache()
内核临时缓冲区 set_memory_uc()
DMA传输缓冲区 dma_alloc_coherent()
用户空间映射 pgprot_noncached() + remap_pfn_range
大块非缓存内存 vmap() with pgprot_noncached()

常见错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 错误1: 忘记使用I/O访问函数
// 错误代码:
u32 *reg = ioremap_nocache(0xFE00, 4);
*reg = 0x1234; // 可能崩溃!

// 正确:
writel(0x1234, reg);

// 错误2: 缺失缓存刷新
memcpy(dma_buf, data, size);
dma_map_single(dev, dma_buf, size, DMA_TO_DEVICE);
// 需要添加:
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);

// 错误3: 错误的地址对齐
// 非缓存内存必须页对齐
void *addr = kmalloc(128, GFP_KERNEL);
set_memory_uc((unsigned long)addr, 1); // 可能失败!

使用这些方法时,优先选择内核提供的标准API(如set_memory_uc()和DMA函数),它们会自动处理架构差异和底层细节,确保代码的稳定性和可移植性。

set_pte_at函数用法及注意事项

set_pte_at() 是 Linux 内核中用于直接操作页表条目(Page Table Entry, PTE)的核心函数,通常在内核内存管理、缺页处理、内存映射等场景中使用。以下是详细的使用指南和注意事项:


函数原型

1
2
3
4
5
6
7
8
#include <asm/pgtable.h>

void set_pte_at(
struct mm_struct *mm, // 目标内存描述符
unsigned long address, // 虚拟地址
pte_t *ptep, // PTE指针
pte_t entry // 要设置的PTE值
);

典型使用场景

1. 缺页异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
struct page *page = alloc_page(GFP_KERNEL);

// 构造新的PTE
entry = mk_pte(page, vma->vm_page_prot);
entry = pte_mkyoung(entry); // 设置访问标志
entry = pte_mkdirty(entry); // 设置脏页标志

// 设置PTE
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);

return 0;
}

2. 自定义内存映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int my_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size)
{
pgprot_t prot = vma->vm_page_prot;
pte_t *ptep, pte;

// 遍历地址范围
for (; addr < end; addr += PAGE_SIZE, pfn++) {
// 获取PTE指针
ptep = pte_offset_map(pmd, addr);

// 构造PTE (设置uncached属性)
pte = pfn_pte(pfn, pgprot_noncached(prot));

// 设置PTE
set_pte_at(vma->vm_mm, addr, ptep, pte);

pte_unmap(ptep);
}
return 0;
}

关键使用步骤

  1. 获取PTE指针

    1
    ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
    • 需要先获取自旋锁 ptl 保证原子性
  2. 构造PTE值

    1
    2
    3
    4
    5
    6
    7
    // 基础构造
    pte_t entry = pfn_pte(pfn, prot);

    // 添加标志位
    entry = pte_mkwrite(entry); // 可写
    entry = pte_mkexec(entry); // 可执行
    entry = pte_mkuffd_wp(entry); // 用户态缺页监控
  3. 设置PTE

    1
    set_pte_at(mm, address, ptep, entry);
  4. 刷新TLB

    1
    flush_tlb_page(vma, address);
  5. 释放资源

    1
    pte_unmap_unlock(ptep, ptl);

重要注意事项

1. 同步与锁机制

  • 必须使用锁
    1
    2
    3
    4
    spinlock_t *ptl;
    ptep = pte_offset_map_lock(mm, pmd, addr, &ptl);
    // ... 操作PTE ...
    pte_unmap_unlock(ptep, ptl);
  • 锁类型:
    • 用户空间:pte_offset_map_lock()
    • 内核空间:pte_offset_kmap()(无锁,但需kmap_atomic

2. TLB刷新

  • 修改PTE后必须刷新TLB
    1
    2
    3
    4
    5
    // 刷新单个页面
    flush_tlb_page(vma, address);

    // 刷新整个范围
    flush_tlb_range(vma, start, end);
  • ARM64特殊处理:
    1
    2
    dsb(ishst); // 数据同步屏障
    isb(); // 指令同步屏障

3. 缓存一致性

  • 设置非缓存属性:
    1
    2
    pgprot_t prot = pgprot_noncached(vma->vm_page_prot);
    pte_t entry = pfn_pte(pfn, prot);
  • DMA操作前后:
    1
    dma_sync_single_for_device(dev, dma_handle, size, dir);

4. 地址空间处理

场景 mm 参数
用户空间 vma->vm_mm
内核空间 &init_mm
当前进程 current->mm

5. 特殊标志处理

1
2
3
4
5
6
7
8
// 设置软脏标志 (用于内存迁移)
entry = pte_mksoft_dirty(entry);

// 设置交换条目
entry = make_swap_entry(page, 1);

// 设置设备内存标志 (ARM64)
entry = set_pte_devmap(entry);

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
ptep = pte_offset_map_lock(mm, pmd, addr, &ptl);
if (!ptep) {
// 处理PTE不存在的情况
if (!pte_alloc(mm, pmd, addr))
return -ENOMEM;
goto retry;
}

if (pte_present(*ptep)) {
// 处理已存在PTE的情况
pte_unmap_unlock(ptep, ptl);
return -EBUSY;
}

架构差异处理

ARM64 特殊要求

1
2
3
4
5
6
7
8
9
// 设置内存属性索引
entry = pte_set_attr(entry, attr_index);

// 强制内存类型 (Device-nGnRnE)
entry = pte_mkdevmap(entry);

// 刷新指令缓存 (当设置可执行页时)
if (pte_exec(entry))
flush_icache_range(addr, addr + PAGE_SIZE);

x86 特殊处理

1
2
3
// 处理PAT (Page Attribute Table)
if (pgprot_val(prot) & _PAGE_PCD)
entry = pte_set_cache_mode(entry, _PAGE_CACHE_MODE_UC);

性能优化技巧

  1. 批量操作

    1
    2
    3
    4
    5
    for (i = 0; i < n; i++) {
    set_pte_at(mm, addr + i*PAGE_SIZE, ptep[i], entry);
    }
    // 一次性刷新整个范围
    flush_tlb_range(vma, addr, addr + n*PAGE_SIZE);
  2. 预取优化

    1
    2
    pte_t *ptep = pte_offset_map(...);
    prefetchw(ptep); // 预取PTE缓存行
  3. 避免冗余刷新

    1
    2
    3
    4
    if (!pte_same(*ptep, new_pte)) {
    set_pte_at(mm, addr, ptep, new_pte);
    flush_needed = 1;
    }

调试与验证

  1. 检查PTE内容:

    1
    printk("PTE at %px: %llx\n", ptep, pte_val(*ptep));
  2. 页表遍历调试:

    1
    2
    3
    4
    5
    # 用户空间页表
    cat /proc/$PID/pagemap

    # 内核页表 (需要CONFIG_PTDUMP)
    echo 0x$(awk '/stext/{print $1}' /proc/kallsyms) > /sys/kernel/debug/kernel_page_tables
  3. 硬件断点:

    1
    2
    // QEMU+GDB调试
    (gdb) watch *0xFFFF000012345678

替代方案

场景 推荐接口
用户内存映射 remap_pfn_range()
内核空间映射 vmalloc_to_pfn() + set_pte_at()
文件映射 filemap_fault()
DMA映射 dma_alloc_coherent()

警告与限制

  1. 特权级要求

    • 必须在核心内核上下文调用
    • 用户空间程序无法直接调用
  2. 稳定性风险

    1
    2
    3
    // 错误:缺少锁保护
    ptep = pte_offset_kernel(pmd, addr);
    set_pte_at(mm, addr, ptep, entry); // 可能引发竞争条件
  3. 跨平台兼容性

    • PTE标志位布局因架构而异
    • 使用 pte_xxx() 辅助函数而非直接位操作

建议:除非开发核心内存管理功能,否则优先使用更高级的API(如vm_insert_page()remap_pfn_range()),直接使用set_pte_at()需要深入理解MMU和CPU架构细节。