一文说清mmap内存映射底层原理
【目录】
一、宏观解释内存映射
二、进程虚拟地址空间
三、虚拟内存区域描述符
四、内存映射的实现
五、mmap在Framebuffer中的应用
前几天的一场面试中,面试官问:为什么可以通过mmap直接操作LCD?
当时回答的不大好,刚学习的时候没有在意底层的逻辑,所以这几天对于mmap内存映射机制进行了较为深入的学习,这里分享出来一些个人的学习记录,如果有不恰当的地方还请各位帮忙指出
一、宏观解释内存映射
- 虚拟地址空间
每个进程都有虚拟地址空间,且进程和进程之间的地址是独立的
进程看到的都是操作系统虚拟出来的地址空间,但是虚拟地址最终还是要映射在实际的物理内存地址上进行操作的
- 内存映射
通过mmap将文件或设备使用到的物理地址映射到进程的虚拟地址空间,通过返回的指针即可直接操作到物理地址上的数据
底层是通过页表来实现虚拟地址 --> 物理地址的映射,每个进程都有自己的页表,来管理地址的映射
谁来使用页表呢?MMU(内存管理单元),它来做地址的转换
二、进程虚拟地址空间
- 每个进程都有4G的虚拟地址空间,其中3G是用户空间,1G是内核空间
- 内核空间是每个进程共享的,用户空间是独立的
PS:用户态切换到内核态,就是指访问的进程地址从用户空间切换到了的内核空间,系统调用相关数据信息存储在内核空间中
咱们借助大模型来看一看,进程的虚拟地址中的 “内核空间” 映射到了哪里
-
所以总结出每个进程的虚拟地址空间的内核空间都被映射到了同一块物理内存上,并且通过MMU内存管理单元来管理
换句话说就是所有进程的虚拟地址空间中的内核空间都是共享的
-
更详细的进程虚拟地址空间示意图(如有侵权,联系删)
三、虚拟内存区域描述符
-
task_struct(进程描述符)
首先,我们的系统是如何管理和调度进程的呢,其实是通过一个结构体:task_struct (进程描述符)
每个进程都有一个 task_struct 结构体,结构体中包含了:进程状态、进程ID、进程组ID、内存指针、上下文数据等等
PS:了解更多关于task_struct的信息,可以参考Linux内核源码中的sched.h文件
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ ... pid_t pid; ... struct mm_struct *mm; //描述一个进程的整个虚拟地址空间 ... }
-
mm_struct (内存描述符)
该结构体描述了整个进程的虚拟地址空间,通过 vm_area_struct 结构体来管理,以双链表方式
且保存了当前进程虚拟地址空间的一些信息,比如
- pdg:当前进程页表基地址
- mmap_base:映射区域基地址
- start_code、end_code:代码段的起止地址
- start_data、end_data:数据段的起止地址
- 等等
struct mm_struct{ struct vm_area_struct *mmap; //list of VMAs: 每个VMA表示一个虚拟地址区域 pgd_t *pgd; // 当前进程的页表基址 unsigned long mmap_base; /* base of mmap area */ unsigned long start_code, end_code, start_data, end_data; ... }
-
vm_area_struct (虚拟内存区域描述符)
该结构体描述了进程某一段虚拟地址空间,其中包括 vm_start(起始虚拟地址)、vm_end(终止虚拟地址)、vm_next(下一段虚拟地址空间)、vm_prev(上一段虚拟地址空间)
struct vm_area_struct { /* The first cache line has the info for VMA tree walking. */ unsigned long vm_start; /* Our start address within vm_mm:vm_mm内的起始地址 */ unsigned long vm_end; /* The first byte after our end addresswithin vm_mm:在vm_mm内结束地址之后的第一个字节的地址*/ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; ... }
-
示意图(如有侵权,联系删)
四、内存映射的实现
在进程创建的时候,要申请16K的空间,把用户页表和内核页表创建好,然后通过MMU(内存管理单元) 来将进程对虚拟地址的操作落实到真实的物理地址上
- 通过用户页表来管理我们进程的虚拟地址空间中的用户空间
操作mmap返回指针的流程示意图:
- 使用mmap映射物理内存到进程的虚拟内存
- 会自动更新页表,添加新的虚拟内存到物理内存的映射页表项
- 当操作mmap返回的指针时,CPU能看到的是进程的虚拟地址,CPU获取到虚拟地址
- MMU通过该进程的用户空间页表查询到该虚拟地址对应的真实物理地址,然后告诉给CPU
- CPU无需切换到内核态,直接操作对应的物理地址上的数据
五、mmap在Framebuffer中的应用
内存映射的实现就不多赘述,mmap在Framebuffer中的应用主要是将LCD驱动申Framebuffer物理内存映射到进程的虚拟内存中,
对mmap返回指针赋值,就会直接将数据写到Framebuffer的物理内存中(看完上面的内容,应该就理解为什么可以直接修改到物理内存了)
另外对于ARM处理来说,内部已经集成了一个LCD控制器,且控制器内部有DMA,可以无需CPU干涉,直接将Framebuffer数据读取,并写入到LCD屏幕上,这样就可以实时显示画面了。
当然,在初始化LCD控制器时需要指定Framebuffer的物理地址和其他读取时的其他属性(比如像素格式是RGB888还是RGB565等,屏幕的尺寸信息等等)
不然LCD控制器也不知道该去哪里读取数据,也不知道该如何使用这些数据。
参考资料
【Linux驱动mmap内存映射】https://www.cnblogs.com/wanghuaijun/p/7624564.html
【用户进程的页表】https://www.bilibili.com/video/BV1CK411Z7pA
【如何理解虚拟地址空间】https://www.zhihu.com/question/290504400
【进程虚拟地址空间的分布详解 https://blog.csdn.net/Arlingtonroad/article/details/107148527