嵌入式linux学习记录十二,mmap

  1. mmap(Memory Map)是把文件或设备的物理内存 直接映射到进程的虚拟地址空间,让进程可以像访问普通内存一样访问文件或设备。

    普通 read/write 和 mmap 对比

    普通 read/write:

    复制代码
       硬件/文件
           │
           ▼
       内核缓冲区         ← 数据在这里
           │
           │  copy_to_user(数据拷贝)
           ▼
       用户缓冲区         ← 数据又拷贝一份
           │
           ▼
       用户程序使用

    mmap:

    复制代码
       硬件/文件的物理内存
           │
           │  直接映射,无需拷贝
           ▼
       用户虚拟地址空间
           │
           ▼
       用户程序直接访问

    mmap 省去了内核和用户空间之间的数据拷贝。
    2.

    原理

    复制代码
       进程虚拟地址空间
    
       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 函数。
    4.

    完整驱动示例

    复制代码
       #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: virt=0x%lx phys=0x%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;
       }

    优点

    1. 零拷贝,性能高

      复制代码
      普通 read/write:
      物理内存 ──拷贝──→ 内核缓冲区 ──拷贝──→ 用户缓冲区   (2次拷贝)
      
      mmap:
      物理内存 ──直接访问──→ 用户程序                       (0次拷贝)
    2. 大数据量传输效率极高

      复制代码
      传输 1GB 数据:
      read/write:需要拷贝 1GB 数据
      mmap:      只需建立页表映射,几乎无开销
    3. 多进程共享内存

      复制代码
      进程A  ──→ mmap 同一块物理内存 ←── 进程B
                    │
                    ▼
               直接共享,无需IPC拷贝
    4. 直接操作硬件寄存器

      复制代码
      嵌入式开发中:
      把硬件寄存器物理地址 mmap 到用户空间
      用户程序直接读写寄存器,无需驱动中转

    缺点

    1. 映射大小必须是页的整数倍

      复制代码
      PAGE_SIZE = 4096 字节
      映射 100 字节 ──→ 实际占用 4096 字节
      有内存浪费
      小数据量用 read/write 更合适
    2. 建立映射有开销

      复制代码
      mmap 需要:
          修改页表
          处理缺页异常
          
      小数据量时这些开销比 read/write 还大
      适合大数据量场景
    3. 安全性较低

      复制代码
      用户可以直接访问内核内存
      指针错误可能导致内核数据损坏
      需要驱动做好边界检查
    4. 不适合频繁小块读写

      复制代码
      每次访问可能触发缺页异常
      频繁小块访问开销反而更大

    使用场景总结

    复制代码
       适合 mmap:
           ✅ 大数据量传输(视频、音频)
           ✅ 多进程共享内存
           ✅ 直接操作硬件寄存器
           ✅ 数据库文件映射
           ✅ 帧缓冲(LCD显示)
    
       不适合 mmap:
           ❌ 小数据量频繁传输
           ❌ 需要严格数据校验
           ❌ 网络数据传输

    mmap 本质是通过修改页表让用户空间直接访问物理内存,省去数据拷贝,适合大数据量场景。驱动实现核心是 remap_pfn_range,把物理地址映射到用户请求的虚拟地址范围。

  2. 避免用户申请的内存容量大于驱动预留的内存容量:
    1.

    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);
    复制代码
      *** ** * ** ***
相关推荐
源码宝1 小时前
基于SpringCloud+UniApp的智慧工地云平台整体架构设计与实现
java·人工智能·spring cloud·源码·智慧工地·云平台
似水এ᭄往昔1 小时前
【Linux系统编程】--进程概念
linux·运维·服务器
共享家95271 小时前
OpenClaw的通道配置
人工智能·学习·openclaw
Dxy12393102162 小时前
Linux 如何关闭关不掉的进程
linux·运维·chrome
天文家2 小时前
深入理解装饰器与适配器:从设计模式到 Spring AOP 的工程实践
java·设计模式
贺国亚2 小时前
Spring-AI与LangChain4j
java·人工智能·spring
野生技术架构师2 小时前
2026 Java面试宝典(春招/社招/秋招通用):没有前言,只有答案,直接开背
java·开发语言·面试
小徐敲java2 小时前
Linux读取串口实时数据
linux·运维·服务器
mN9B2uk172 小时前
数据库的约束简介
java·数据库·sql