mmap与remap_pfn_range
参考资料:
remap_pfn_range :
remap_pfn_range 是 Linux 内核中的一个函数,用于将物理页面框号(PFN)映射到用户空间的虚拟地址范围中。PFN 是物理页面在内存中的索引,而不是直接的物理地址。这个函数在内核中的 mm/memory.c 文件中定义。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot); // 参数和返回值说明 vma: 虚拟内存区域结构体指针,描述了要进行映射的虚拟内存区域。 virt_addr: 用户空间中要映射的虚拟地址的起始地址。 pfn: 物理页面框号的起始地址,即要映射的物理页面在内存中的索引。 size: 要映射的内存区域大小。 prot: 要应用于映射区域的页面保护标志,通常使用 vm_page_prot 定义。 映射成功返回0,失败返回错误码
mmap:
mmap() 是一个 Unix 和类 Unix 操作系统中的系统调用,用于在进程的地址空间中创建一个新的内存映射区域。它允许进程将文件或设备映射到其地址空间中,从而可以通过内存访问来读取和写入文件,或者与设备进行通信:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); // 参数和返回值说明 addr:欲映射的内存起始地址,通常设置为 NULL,表示由系统选择合适的地址。 length:映射区域的长度,以字节为单位。 prot:映射区域的保护方式,可以是 PROT_READ、PROT_WRITE、PROT_EXEC 和 PROT_NONE 的组合。 flags:控制映射区域的属性,可以是 MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS 和其他标志的组合。 fd:要映射的文件描述符,如果不是映射文件,可以设置为 -1,对应的flags要带MAP_ANONYMOUS。 offset:要映射的文件的偏移量,通常设置为 0。 成功时,返回映射区域的起始地址。 失败时,返回 MAP_FAILED,并设置 errno 表示错误原因。
当flags设置为MAP_ANONYMOUS时,这个标志告诉操作系统不关联任何文件,而是直接映射到系统内核管理的一块内存区域,也成为匿名内存区域,适合的使用场景:
1、共享内存:多个进程可以通过映射同一个匿名内存区域来实现进程间通信,从而共享数据。
2、动态内存分配:malloc() 和 free() 等内存管理函数可能会使用匿名内存区域来分配和释放内存。
3、临时缓冲区:某些临时数据或缓冲区可以放置在匿名内存区域中,从而避免了频繁的文件 I/O 操作。
mmap驱动和应用测试程序举例
mmap_demo.c
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/slab.h> #define DEVICE_NAME "mmap_demo" #define BUF_SIZE 4096 MODULE_LICENSE("GPL"); static int major; static char *buffer; static int mmap_demo_open(struct inode *inode, struct file *file) { return 0; } static int mmap_demo_release(struct inode *inode, struct file *file) { return 0; } static int mmap_demo_mmap(struct file *file, struct vm_area_struct *vma) { unsigned long size = vma->vm_end - vma->vm_start; // 检查请求的内存大小是否合法 if (size > BUF_SIZE) return -EINVAL; // 将设备内存映射到用户空间 if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(buffer) >> PAGE_SHIFT + vm->vm_pgoff, size, vma->vm_page_prot) < 0) return -EAGAIN; return 0; } static struct file_operations mmap_demo_fops = { .open = mmap_demo_open, .release = mmap_demo_release, .mmap = mmap_demo_mmap, }; static int __init mmap_demo_init(void) { major = register_chrdev(0, DEVICE_NAME, &mmap_demo_fops); if (major < 0) { printk(KERN_ALERT "Failed to register a major number\n"); return major; } buffer = kmalloc(BUF_SIZE, GFP_KERNEL); if (!buffer) { unregister_chrdev(major, DEVICE_NAME); printk(KERN_ALERT "Failed to allocate memory for the device\n"); return -ENOMEM; } printk(KERN_INFO "mmap_demo module loaded\n"); return 0; } static void __exit mmap_demo_exit(void) { kfree(buffer); unregister_chrdev(major, DEVICE_NAME); printk(KERN_INFO "mmap_demo module unloaded\n"); } module_init(mmap_demo_init); module_exit(mmap_demo_exit);
PAGE_SHIFT可以理解为一个页面的大小,在linux中一般为4K,也就是1 << 12。vma->vm_start表示用户空间要映射的虚拟地址的起始地址,virt_to_phys(buffer) >> PAGE_SHIFT + vm->vm_pgoff表示页帧号
应用层测试程序:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #define DEVICE_FILE "/dev/mmap_demo" #define BUF_SIZE 4096 int main() { int fd; char *mapped_mem; // 打开设备文件 fd = open(DEVICE_FILE, O_RDWR); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); } // 将设备内存映射到用户空间 mapped_mem = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mapped_mem == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); } // 写入数据到设备内存 const char *data = "Hello, mmap from user space!"; strncpy(mapped_mem, data, strlen(data)); // 从设备内存读取数据 printf("Data from device memory: %s\n", mapped_mem); // 解除内存映射 if (munmap(mapped_mem, BUF_SIZE) == -1) { perror("munmap"); } // 关闭设备文件 if (close(fd) == -1) { perror("close"); exit(EXIT_FAILURE); } return 0; }