Linux内核接口remap_pfn_range的用法及示例

Linux内核中的remap_pfn_range函数用于将物理内存映射到用户空间,常用于驱动程序的mmap实现。以下是其用法及示例的详细说明:


函数原型

1
2
3
4
5
6
7
int remap_pfn_range(
struct vm_area_struct *vma, // 用户空间的虚拟内存区域描述
unsigned long addr, // 映射的起始虚拟地址(通常为vma->vm_start)
unsigned long pfn, // 物理页帧号(Physical Frame Number)
unsigned long size, // 映射区域的大小
pgprot_t prot // 保护标志(如读写权限)
);

核心步骤

  1. 实现驱动的mmap方法
    在字符设备驱动的file_operations结构中定义mmap回调函数。

  2. 计算物理页帧号(PFN)
    将物理地址转换为PFN:pfn = (phys_addr >> PAGE_SHIFT)

  3. 调整内存保护标志
    根据硬件需求设置prot,如禁用缓存(pgprot_noncached)。

  4. 调用remap_pfn_range
    建立用户空间虚拟地址到物理内存的映射。


示例代码

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
63
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/module.h>

#define DEVICE_SIZE (4 * 1024) // 设备内存大小(4KB)

static phys_addr_t my_dev_phys_addr; // 设备物理地址
static void *my_dev_virt_addr; // 设备虚拟地址(内核空间)

// mmap实现
static int my_mmap(struct file *filp, struct vm_area_struct *vma) {
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; // 字节偏移量
unsigned long size = vma->vm_end - vma->vm_start;

// 检查偏移量是否越界
if (offset + size > DEVICE_SIZE) {
return -EINVAL;
}

// 计算PFN
unsigned long pfn = (my_dev_phys_addr + offset) >> PAGE_SHIFT;

// 调整保护标志(禁用缓存)
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

// 建立映射
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) {
return -EAGAIN;
}

return 0;
}

// 文件操作结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.mmap = my_mmap,
// 其他操作(如open/release等)
};

// 模块初始化
static int __init my_init(void) {
// 分配物理连续内存(示例使用dma_alloc_coherent)
my_dev_virt_addr = dma_alloc_coherent(NULL, DEVICE_SIZE, &my_dev_phys_addr, GFP_KERNEL);
if (!my_dev_virt_addr) {
return -ENOMEM;
}

// 注册字符设备(具体步骤略)
// ...
return 0;
}

// 模块退出
static void __exit my_exit(void) {
dma_free_coherent(NULL, DEVICE_SIZE, my_dev_virt_addr, my_dev_phys_addr);
// 注销设备
// ...
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

关键说明

  1. 物理地址获取

    • 示例中使用dma_alloc_coherent分配物理连续内存,适用于DMA操作。
    • 实际硬件驱动可能通过设备树或PCI BAR获取物理地址。
  2. 偏移量处理
    用户空间的offset参数对应vma->vm_pgoff(页帧偏移),需转换为字节偏移量后再计算PFN。

  3. 保护标志调整

    • 使用pgprot_noncached禁用缓存,确保直接访问硬件内存。
    • 写合并(pgprot_writecombine)可优化某些设备的写入性能。
  4. 错误处理

    • 检查映射范围是否越界(DEVICE_SIZE)。
    • remap_pfn_range失败,返回-EAGAIN-EFAULT

用户空间调用示例

1
2
3
4
5
6
7
8
9
10
// 用户程序通过mmap访问设备内存
int fd = open("/dev/mydev", O_RDWR);
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 读写操作
*(uint32_t*)addr = 0x1234; // 写入设备内存
uint32_t val = *(uint32_t*)addr; // 读取设备内存

munmap(addr, 4096);
close(fd);

注意事项

  • 对齐要求addrsize必须是页对齐的(PAGE_SIZE的整数倍)。
  • 安全性:仅映射允许用户空间访问的物理内存,避免泄露内核数据。
  • 缓存一致性:若设备内存被CPU和外部硬件共享,需正确配置缓存策略。

通过合理使用remap_pfn_range,驱动程序可高效实现用户空间与内核/硬件内存的交互。

上述示例不适用dma的接口

上述示例不使用dma的接口


不依赖DMA接口的remap_pfn_range示例

以下是使用内核通用接口分配物理连续内存的示例,替代dma_alloc_coherent,适用于不需要DMA的场景:


修改后的代码

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/gfp.h>

#define DEVICE_ORDER 0 // 分配2^0 = 1页(4KB)
#define DEVICE_SIZE (PAGE_SIZE * (1 << DEVICE_ORDER)) // 4KB

static struct page *my_dev_page = NULL; // 分配的物理页
static phys_addr_t my_dev_phys_addr; // 物理地址
static void *my_dev_virt_addr; // 内核虚拟地址

// mmap实现
static int my_mmap(struct file *filp, struct vm_area_struct *vma) {
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; // 字节偏移量
unsigned long size = vma->vm_end - vma->vm_start;

// 检查映射范围
if (offset + size > DEVICE_SIZE) {
return -EINVAL;
}

// 计算PFN(物理页帧号 = 物理地址 / PAGE_SIZE)
unsigned long pfn = (my_dev_phys_addr + offset) >> PAGE_SHIFT;

// 调整保护标志(示例禁用缓存)
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

// 建立映射
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) {
return -EAGAIN;
}

return 0;
}

// 文件操作结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.mmap = my_mmap,
// 其他操作(如open/release需自行实现)
};

// 模块初始化
static int __init my_init(void) {
// 分配物理连续的内存页(1页,4KB)
my_dev_page = alloc_pages(GFP_KERNEL, DEVICE_ORDER);
if (!my_dev_page) {
return -ENOMEM;
}

// 获取物理地址和内核虚拟地址
my_dev_phys_addr = page_to_phys(my_dev_page); // 物理地址
my_dev_virt_addr = page_address(my_dev_page); // 内核虚拟地址

// 初始化内存(可选)
memset(my_dev_virt_addr, 0, DEVICE_SIZE);

// 注册字符设备(需自行实现)
// ...

return 0;
}

// 模块退出
static void __exit my_exit(void) {
if (my_dev_page) {
__free_pages(my_dev_page, DEVICE_ORDER); // 释放内存
}
// 注销设备
// ...
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

关键修改说明

  1. 物理内存分配方式

    • 使用alloc_pages代替dma_alloc_coherent,直接分配物理连续的页。
    • DEVICE_ORDER指定分配页数为2^ORDER,此处为ORDER=0(即1页,4KB)。
  2. 物理地址获取

    • 通过page_to_phys(my_dev_page)struct page转换为物理地址。
    • 内核虚拟地址通过page_address(my_dev_page)获取。
  3. 内存释放

    • 使用__free_pages释放分配的物理页,需匹配alloc_pagesorder参数。
  4. 缓存策略

    • 通过pgprot_noncached禁用缓存,确保直接访问物理内存(适用于硬件寄存器或非缓存内存)。

用户空间调用(不变)

1
2
3
4
5
6
7
8
9
10
// 用户程序通过mmap访问设备内存
int fd = open("/dev/mydev", O_RDWR);
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 读写操作
*(uint32_t*)addr = 0x1234; // 写入设备内存
uint32_t val = *(uint32_t*)addr; // 读取设备内存

munmap(addr, 4096);
close(fd);

注意事项

  1. 物理连续性保证

    • alloc_pages分配的内存是物理连续的,但若系统内存碎片化,可能分配失败(需处理-ENOMEM)。
  2. 页对齐要求

    • remap_pfn_range要求addrsize按页对齐(PAGE_SIZE的整数倍)。
  3. 安全性与权限

    • 确保用户空间程序只能访问允许的内存范围(通过offset + size > DEVICE_SIZE检查)。
    • 根据硬件需求设置vm_page_prot(如PROT_READ/PROT_WRITE权限)。
  4. 替代接口选择

    • 若需要分配多个非连续物理页,可改用vmalloc(虚拟连续但物理不连续),但需配合vmalloc_to_pfn转换物理地址。

适用场景

  • 硬件寄存器映射:直接访问设备寄存器。
  • 自定义共享内存:用户空间与内核共享物理连续缓冲区。
  • 性能敏感操作:通过禁用缓存提升访问速度。

通过此方案,开发者无需依赖DMA接口即可实现物理内存到用户空间的映射,适用于大多数非DMA场景。