ARM64内存非缓存设置方法
arm64设置page uncache
在ARM64架构中,将内存页面设置为非缓存(uncache)通常涉及修改页表条目(PTE)的内存属性。以下是关键步骤和代码示例:
核心原理
- MAIR寄存器:配置内存属性(如uncached)的索引
- 页表条目:设置PTE的
ATTRINDX
字段指向MAIR中的uncached属性
实现步骤
1. 配置MAIR寄存器(系统启动时)
1 | // 定义内存属性 (通常在内核初始化阶段完成) |
2. 修改页表条目(PTE)
1 | // 获取目标地址的PTE |
3. 刷新TLB和缓存
1 | // 刷新单个地址的TLB |
关键注意事项
内存类型选择:
MT_DEVICE_nGnRnE
:严格设备内存(无重排序)MT_NORMAL_NC
:普通非缓存内存(推荐通用场景)
共享域配置:
1
pte_val(*pte) |= PTE_SHARED; // 通常需设置OS位
DMA场景:
1
2// 直接使用DMA映射API
dma_addr = dma_map_single(dev, virt_addr, size, DMA_FROM_DEVICE);用户空间映射:
1
2// 设置VMA属性
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
完整使用示例
1 |
|
调试建议
- 检查PTE内容:
1
cat /proc/kpageflags <PFN>
- 使用
decode_ptes
脚本解析页表条目
重要警告
- TLB刷新:修改PTE后必须刷新TLB
- 并发访问:操作期间禁用中断/抢占
- 性能影响:非缓存内存访问速度显著降低
建议优先使用标准内核API(如ioremap_nocache()
或DMA接口),直接修改页表应作为最后手段。
ioremap_nocache使用方法及注意事项
在 ARM64 架构中使用 ioremap_nocache()
是将物理设备内存映射到内核虚拟地址空间的标准方法,用于访问需要非缓存(Uncacheable)属性的设备寄存器或内存。以下是详细指南:
函数原型
1 |
|
使用步骤
1. 映射物理地址
1 |
|
2. 访问设备寄存器
必须使用专用 I/O 函数访问:
1 | u32 read_reg(void __iomem *base, int offset) { |
3. 解除映射
1 | void cleanup_device(void) { |
关键注意事项
对齐要求
- 物理地址 不需要 页对齐,但建议按设备自然对齐(如 4KB)
- 内核自动处理非对齐映射(内部按页对齐映射)
访问函数
- 必须 使用专用函数访问:
1
2
3
4readb()/writeb() // 8位
readw()/writew() // 16位
readl()/writel() // 32位
readq()/writeq() // 64位 (ARM64) - 禁止 直接指针解引用:
val = *(u32*)vaddr
会导致对齐错误或缓存一致性问题
- 必须 使用专用函数访问:
内存屏障
- 在顺序敏感的寄存器操作间添加屏障:
1
2
3writel(CMD_START, vaddr + CMD_REG);
wmb(); // 写屏障,确保CMD_START先到达
writel(DATA_VALUE, vaddr + DATA_REG);
- 在顺序敏感的寄存器操作间添加屏障:
资源管理
- 使用
devm_ioremap_nocache()
自动管理资源(推荐):1
2void __iomem *vaddr = devm_ioremap_nocache(&pdev->dev, phys_addr, size);
// 驱动卸载时自动调用 iounmap()
- 使用
缓存一致性
- 对 DMA 缓冲区:
- 发送到设备前调用
dma_sync_single_for_device()
- 从设备接收后调用
dma_sync_single_for_cpu()
- 发送到设备前调用
- 对 DMA 缓冲区:
属性扩展
- 需要其他属性时(如 Write-Combining):
1
void __iomem *wc_addr = ioremap_wc(phys_addr, size);
- 需要其他属性时(如 Write-Combining):
完整驱动示例
1 |
|
常见问题处理
映射失败:
- 检查物理地址是否有效(
/proc/iomem
) - 确认大小未超过物理区域
- 检查物理地址是否有效(
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);原子操作:
- 对设备寄存器的原子操作使用:
1
2
3
4
5
6
7
8
9
10u32 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);
}
- 对设备寄存器的原子操作使用:
调试技巧:
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 |
|
使用示例:
1 |
|
优点:
- 内核标准API,跨架构兼容(x86, ARM64等)
- 自动处理TLB刷新和缓存一致性
- 支持部分页面设置
限制:
- 仅适用于内核虚拟地址(vmalloc/kmalloc分配)
- 地址必须按页对齐
2. 使用非缓存映射函数
a) 映射物理地址
1 | // 映射设备物理内存 |
示例:
1 | phys_addr_t dev_phys = 0xFE000000; |
b) 映射内核内存
1 | void *vmap(struct page **pages, unsigned int count, |
示例:
1 | struct page *page = alloc_page(GFP_KERNEL); |
3. DMA 相关方法
a) 一致性DMA映射
1 | // 分配非缓存内存 |
特点:
- 自动设置非缓存属性
- 保证CPU和设备间的缓存一致性
- 适用于频繁DMA传输
b) 流式DMA映射
1 | dma_addr_t dma_map_single(struct device *dev, void *ptr, |
使用模式:
1 | void *buf = kmalloc(size, GFP_KERNEL); |
4. 用户空间内存设置
1 | // 驱动mmap实现 |
用户空间测试:
1 | int fd = open("/dev/mydevice", O_RDWR); |
关键注意事项
缓存一致性
- 使用非缓存内存时,CPU不会自动维护缓存一致性
- DMA操作前后必须调用:
1
2dma_sync_single_for_device()
dma_sync_single_for_cpu()
性能影响
1
2
3// 基准测试结果 (ARM64 Cortex-A72)
// 缓存内存: 5.2 GB/s
// 非缓存内存: 1.8 GB/s (下降65%)ARM64 特殊配置
1
2// 强制所有内存为非缓存(仅调试)
early_param("cachepolicy=uncached", ...);调试技巧
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 | // 错误1: 忘记使用I/O访问函数 |
使用这些方法时,优先选择内核提供的标准API(如set_memory_uc()
和DMA函数),它们会自动处理架构差异和底层细节,确保代码的稳定性和可移植性。
set_pte_at函数用法及注意事项
set_pte_at()
是 Linux 内核中用于直接操作页表条目(Page Table Entry, PTE)的核心函数,通常在内核内存管理、缺页处理、内存映射等场景中使用。以下是详细的使用指南和注意事项:
函数原型
1 |
|
典型使用场景
1. 缺页异常处理
1 | static int handle_pte_fault(struct vm_fault *vmf) |
2. 自定义内存映射
1 | int my_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, |
关键使用步骤
获取PTE指针
1
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
- 需要先获取自旋锁
ptl
保证原子性
- 需要先获取自旋锁
构造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); // 用户态缺页监控设置PTE
1
set_pte_at(mm, address, ptep, entry);
刷新TLB
1
flush_tlb_page(vma, address);
释放资源
1
pte_unmap_unlock(ptep, ptl);
重要注意事项
1. 同步与锁机制
- 必须使用锁:
1
2
3
4spinlock_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
2dsb(ishst); // 数据同步屏障
isb(); // 指令同步屏障
3. 缓存一致性
- 设置非缓存属性:
1
2pgprot_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 | // 设置软脏标志 (用于内存迁移) |
错误处理
1 | ptep = pte_offset_map_lock(mm, pmd, addr, &ptl); |
架构差异处理
ARM64 特殊要求
1 | // 设置内存属性索引 |
x86 特殊处理
1 | // 处理PAT (Page Attribute Table) |
性能优化技巧
批量操作:
1
2
3
4
5for (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);预取优化:
1
2pte_t *ptep = pte_offset_map(...);
prefetchw(ptep); // 预取PTE缓存行避免冗余刷新:
1
2
3
4if (!pte_same(*ptep, new_pte)) {
set_pte_at(mm, addr, ptep, new_pte);
flush_needed = 1;
}
调试与验证
检查PTE内容:
1
printk("PTE at %px: %llx\n", ptep, pte_val(*ptep));
页表遍历调试:
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硬件断点:
1
2// QEMU+GDB调试
(gdb) watch *0xFFFF000012345678
替代方案
场景 | 推荐接口 |
---|---|
用户内存映射 | remap_pfn_range() |
内核空间映射 | vmalloc_to_pfn() + set_pte_at() |
文件映射 | filemap_fault() |
DMA映射 | dma_alloc_coherent() |
警告与限制
特权级要求:
- 必须在核心内核上下文调用
- 用户空间程序无法直接调用
稳定性风险:
1
2
3// 错误:缺少锁保护
ptep = pte_offset_kernel(pmd, addr);
set_pte_at(mm, addr, ptep, entry); // 可能引发竞争条件跨平台兼容性:
- PTE标志位布局因架构而异
- 使用
pte_xxx()
辅助函数而非直接位操作
建议:除非开发核心内存管理功能,否则优先使用更高级的API(如vm_insert_page()
或remap_pfn_range()
),直接使用set_pte_at()
需要深入理解MMU和CPU架构细节。