众所周知,linux的理念是万物皆文件,自然少不了对文件的各种操作,常见的诸如open、read、write等,都是大家耳熟能详的操作。除了这些常规操作外,还有一个不常规的操作:mmap,其在file_operations结构体中的定义如下: 这个函数的作用是什么了?

      

      1、对于读写文件,传统经典的api都是这样的:先open文件,拿到文件的fd;再调用read或write读写文件。由于文件存放在磁盘,3环的app是没有权限直接操作磁盘的,所以需要通过系统调用进入操作系统的内核,再通过事先安装好的驱动读写磁盘数据。这样一来,磁盘的数据会分别存放在内核空间和用户空间,也就是同一份数据会在内存内部放在两个不同的地方,而且也需要拷贝2次,整个过程是“又费柴油又费马达”;流程示例如下:

    

    这样做既然浪费内存空间,也浪费拷贝的时间,该怎么优化了?

   2、上述做法的结症在于同一份数据拷贝2次,那么能不能只拷贝1次了?答案是可以的,mmap就是这么干的!

  (1)先看看mmap的用例,直观了解一下是怎么使用的,如下:

  用例是不是很简单了?还是先调用open函数得到文件的fd,再调用mmap建立文件在内存的映射,这时得到了文件在内存映射的地址p,最后通过p指针读写文件数据!整个逻辑非常简单,是个码农都能看懂!这么简单方便、效率还高(只复制一次)的mmap又是怎么实现的了?

  (2)先说一下mmap的原理:mmap只复制1次的原理也简单,就是在进程的虚拟内存开辟一块空间,映射到内核的物理内存,并建立和文件的关联,再把文件内容读到这块内存;后续3环的app读写文件都不走磁盘了,而是直接读写这块建立好映射的内存!等到进程退出或出意外奔溃,操作系统把映射内存的数据重新写回磁盘的文件!

      

       mmap的原理也不复杂,具体是到代码层面是怎么做的了?

    (3)从上面的demo可以看出,3环应用层直接调用的是mmap函数,但很明显这个功能因为涉及到磁盘读写,肯定是需要操作系统支持得,所以mmap肯定需要通过系统调用进入内核执行代码。操作系统提供的系统调用函数是do_mmap,在mm\mmap.c文件中,代码如下:

  代码有很多,但是核心功能其实并不复杂:找到空闲的虚拟内存地址,并根据不同的文件打开方式设置不同的vm标志位flag!在函数末尾处调用了mmap_region函数,核心功能是创建和初始化虚拟内存区域,并加入红黑树节点进行管理,代码如下:

  以上两个函数的核心功能是查找、分配、初始化空闲的vma,并加入链表和红黑树管理,同时设置vma的各种flags属性,便于后续管理!那么问题来了:数据最终都是要存放在物理内存的,截至目前所有的操作都是虚拟内存,这些vma都是在哪和物理内存建立映射的了?关键的函数是remap_pfn_range,在mm/memory.c文件中;

  最核心的就是remap_pud_range方法了,从这个方法开始,逐级构造页表的各个映射转换!阅读代码前,可以先熟悉一下4级页表转换原理如下:

     

 

      代码如下:3个方法的结构类似,层层深入,直到最后一级pte!pte内部调用set_pte_at方法最终完成物理地址和虚拟地址的映射!

 

 

注意事项&总结事项:

1、脱壳的时候如果遇到mmap就要注意了:有可能是要加载壳文件了!

2、页对齐的代码:也可以借鉴用来做其他数字的对齐,把PAGE_SIZE改成其他数字就好

3、核心原理:只分配1块物理内存,把进程的虚拟地址映射到这块物理内存,达到读写一次到位的目的! 

 

参考:

1、https://mp.weixin.qq.com/s/y4LT5rtLZXXSvk66w3tVcQ  三种实现mmap的方式

4、https://www.leviathan.vip/2019/01/13/mmap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/  mmap源码分析

5、https://www.cnblogs.com/pengdonglin137/p/8150981.html  remap_pfn_range源码分析

6、https://zhuanlan.zhihu.com/p/79607142  TLB缓存