嵌入式linux学习记录十二,mmap
mmapMemory Map是把文件或设备的物理内存直接映射到进程的虚拟地址空间让进程可以像访问普通内存一样访问文件或设备。普通 read/write 和 mmap 对比普通 read/write硬件/文件 │ ▼ 内核缓冲区 ← 数据在这里 │ │ copy_to_user数据拷贝 ▼ 用户缓冲区 ← 数据又拷贝一份 │ ▼ 用户程序使用mmap硬件/文件的物理内存 │ │ 直接映射无需拷贝 ▼ 用户虚拟地址空间 │ ▼ 用户程序直接访问mmap 省去了内核和用户空间之间的数据拷贝。原理进程虚拟地址空间 0xFFFFFFFF ┌─────────────────┐ │ 内核空间 │ 0xC0000000 ├─────────────────┤ │ 用户栈 │ │ │ │ mmap映射区域 │ ← mmap 在这里建立映射 │ │ │ 堆 │ │ 数据段 │ 0x00000000 │ 代码段 │ └─────────────────┘ mmap 的本质 修改进程页表把虚拟地址直接指向物理内存 用户读写虚拟地址 ──→ CPU 通过页表 ──→ 直接访问物理内存缺页异常机制mmap 调用时并不立刻建立映射而是用到时才真正映射mmap() 调用 │ ▼ 只在页表里做个标记不实际分配物理内存 │ ▼ 用户访问该地址 │ ▼ 触发缺页异常Page Fault │ ▼ 内核处理缺页异常 建立虚拟地址到物理地址的映射 │ ▼ 用户程序继续执行透明无感知驱动中实现 mmap需要实现 file_operations 里的 mmap 函数#include linux/mm.h static int my_mmap(struct file *file, struct vm_area_struct *vma) { /* vma 描述了用户请求的虚拟地址范围 */ unsigned long size vma-vm_end - vma-vm_start; /* 把物理地址映射到用户虚拟地址 */ /* remap_pfn_range建立页表映射 */ if (remap_pfn_range( vma, vma-vm_start, // 用户虚拟地址起始 vma-vm_pgoff, // 物理地址页帧号 size, // 映射大小 vma-vm_page_prot)) // 保护标志 return -EAGAIN; return 0; } static struct file_operations my_fops { .mmap my_mmap, };vm_area_struct 是什么struct vm_area_struct { unsigned long vm_start; // 虚拟地址起始 unsigned long vm_end; // 虚拟地址结束 unsigned long vm_pgoff; // 物理地址页帧号偏移 pgprot_t vm_page_prot; // 页保护属性 ... };用户调用 mmap 时内核创建这个结构体描述映射区域传给驱动的 mmap 函数。完整驱动示例#include linux/module.h #include linux/fs.h #include linux/mm.h #include linux/device.h #include linux/slab.h #include linux/uaccess.h #define MEM_SIZE 4096 // 映射大小必须是页的整数倍 static char *kernel_buf; // 内核分配的内存 static int major; static struct class *my_class; static struct device *my_device; static int my_open(struct inode *inode, struct file *file) { return 0; } static int my_mmap(struct file *file, struct vm_area_struct *vma) { unsigned long size vma-vm_end - vma-vm_start; unsigned long pfn; /* 检查大小 */ if (size MEM_SIZE) return -EINVAL; /* 获取物理地址的页帧号 */ pfn virt_to_phys(kernel_buf) PAGE_SHIFT; /* 设置写合并属性适合显存、帧缓冲等顺序写入场景*/ vma-vm_page_prot pgprot_writecombine(vma-vm_page_prot); /* 建立映射 */ if (remap_pfn_range(vma, vma-vm_start, pfn, size, vma-vm_page_prot)) { printk(remap_pfn_range failed\n); return -EAGAIN; } printk(mmap: virt0x%lx phys0x%lx size%lu\n, vma-vm_start, pfn PAGE_SHIFT, size); return 0; } static struct file_operations my_fops { .owner THIS_MODULE, .open my_open, .mmap my_mmap, }; static int __init my_init(void) { /* 分配内核内存 */ kernel_buf kmalloc(MEM_SIZE, GFP_KERNEL); if (!kernel_buf) return -ENOMEM; /* 预置一些数据 */ strcpy(kernel_buf, hello from kernel); /* 注册字符设备 */ major register_chrdev(0, my_mmap, my_fops); my_class class_create(THIS_MODULE, my_mmap_class); my_device device_create(my_class, NULL, MKDEV(major, 0), NULL, my_mmap); printk(my_mmap driver loaded\n); return 0; } static void __exit my_exit(void) { device_destroy(my_class, MKDEV(major, 0)); class_destroy(my_class); unregister_chrdev(major, my_mmap); kfree(kernel_buf); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL);用户空间使用#include stdio.h #include fcntl.h #include sys/mman.h #include string.h int main() { int fd open(/dev/my_mmap, O_RDWR); if (fd 0) { perror(open); return -1; } /* mmap 映射 */ char *p mmap(NULL, // 让内核选择虚拟地址 4096, // 映射大小 PROT_READ | PROT_WRITE, // 可读可写 MAP_SHARED, // 共享映射 fd, 0); // 偏移 if (p MAP_FAILED) { perror(mmap); return -1; } /* 直接读取内核数据无需 read() */ printf(读到%s\n, p); /* 直接写入无需 write() */ strcpy(p, hello from user); /* 解除映射 */ munmap(p, 4096); close(fd); return 0; }优点零拷贝性能高普通 read/write 物理内存 ──拷贝──→ 内核缓冲区 ──拷贝──→ 用户缓冲区 2次拷贝 mmap 物理内存 ──直接访问──→ 用户程序 0次拷贝大数据量传输效率极高传输 1GB 数据 read/write需要拷贝 1GB 数据 mmap 只需建立页表映射几乎无开销多进程共享内存进程A ──→ mmap 同一块物理内存 ←── 进程B │ ▼ 直接共享无需IPC拷贝直接操作硬件寄存器嵌入式开发中 把硬件寄存器物理地址 mmap 到用户空间 用户程序直接读写寄存器无需驱动中转缺点映射大小必须是页的整数倍PAGE_SIZE 4096 字节 映射 100 字节 ──→ 实际占用 4096 字节 有内存浪费 小数据量用 read/write 更合适建立映射有开销mmap 需要 修改页表 处理缺页异常 小数据量时这些开销比 read/write 还大 适合大数据量场景安全性较低用户可以直接访问内核内存 指针错误可能导致内核数据损坏 需要驱动做好边界检查不适合频繁小块读写每次访问可能触发缺页异常 频繁小块访问开销反而更大使用场景总结适合 mmap ✅ 大数据量传输视频、音频 ✅ 多进程共享内存 ✅ 直接操作硬件寄存器 ✅ 数据库文件映射 ✅ 帧缓冲LCD显示 不适合 mmap ❌ 小数据量频繁传输 ❌ 需要严格数据校验 ❌ 网络数据传输mmap 本质是通过修改页表让用户空间直接访问物理内存省去数据拷贝适合大数据量场景。驱动实现核心是 remap_pfn_range把物理地址映射到用户请求的虚拟地址范围。避免用户申请的内存容量大于驱动预留的内存容量ioctl 查询大小推荐用户 mmap 之前先用 ioctl 问驱动要多大/* 驱动 */ #define MEM_SIZE (1024 * 1024) #define IOCTL_GET_SIZE _IOR(M, 1, int) static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { int size MEM_SIZE; switch (cmd) { case IOCTL_GET_SIZE: /* 把大小告诉用户 */ if (copy_to_user((int __user *)arg, size, sizeof(size))) return -EFAULT; break; default: return -EINVAL; } return 0; } static struct file_operations my_fops { .mmap my_mmap, .unlocked_ioctl my_ioctl, };/* 用户程序 */ int size; /* 先问驱动要多大 */ ioctl(fd, IOCTL_GET_SIZE, size); /* 再按实际大小 mmap */ char *p mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);