目录

虚拟内存 和 mmap

虚拟内存

  • 虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

  • 与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。

为什么要有虚拟内存

  • 内存使用效率

    • 在没有虚拟内存的时候,程序完全被装载进物理内存中,那么1G的存储空间永远只能执行固定个数的应用程序,一旦内存占满,就要触发 swap 和磁盘交换数据。而且每个应用程序大小不一,比如程序A占4M空间,程序B需要6M空间,即使A被释放了,B也无法使用这4M的空间。

    • 而在虚拟内存中,首先并不是将程序所需所有数据一次性装载进内存,而是需要什么数据才会装载什么数据。其次,虚拟内存以页为单位(常规4KB)同物理内存最小单位一致,那么就可以减少碎片化的问题,可以给程序分配4KB为单位的非连续的物理空间,但是虚拟内存中却看起来是连续的。

  • 直接操纵物理内存也很不安全,一旦修改了其他物理内存的数据,可能造成其他程序的崩溃。而虚拟内存可以有验证等权限控制。

  • 其他,有待思考

如何分配

常规32位机器中,可以访问的内存最大为 2^32 = 4G,通常 linux 规定其中3G是用户空间,1G是内核空间。

  • 1G内核空间如何映射全部4G物理内存

    • 通常,内核空间中有128M的特殊空间称为高端内存,其作用就是去对其他内存空间进行映射。
    • 剩余的896M空间是线性映射到内核空间去的,也就是说内核中所在的虚拟地址和真实的物理内存地址只是差了一个 Offset,所以内核访问其物理地址速度很快,不需要访问页表。
    • 那么意思是一次最多映射128M?超过了会怎么样???还是说这128M是页表空间??
  • 虚拟地址4G,真实物理内存只有2G怎么办?

    没看到什么文章详细介绍,如果我使用的内存已经超出2G,猜测是使用 swap 分区来和磁盘进行数据交换,那如果超出了真实内存和 swap 分区的总和呢?又会发生什么?

虚拟内存和物理内存如何转换

内核空间的一部分区域是和真实物理内存进行线性映射的,而用户空间就不一定了,虚拟地址转换为物理地址一般是 MMU 实现,是一个硬件。
但是,它也要不断访问进程页表才能转义出真实的物理地址。所以出现了 TLB,用来根据程序访问内存的局部性机制来缓存已经转换过的虚拟页与实际页的对应关系,加快速度。

  • 什么是页表

    • 页表用于存放逻辑页与物理页帧的对应关系
    • 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
    • 当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存中,则缺页异常,缺页异常的处理过程,就是把进程需要的数据从磁盘上拷贝到物理内存中,如果内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘。
  • 每个进程有自己的页表,页表占用空间过大

    一个页表项大小为4个字节,如果内存有4G,一个页大小为4K,那么就有2^20个页,那么一个进程要占用4MB空间存放自己的页表,显然浪费空间。所以还有多级页表的概念出现。
    因为页表项有一部分记录了此页是否在物理页上,如果不存在,其下一级页表也不会存在,不开辟空间,从而节省了很大空间。

esp和ebp

  • 为什么会说到这里?
    主要因为我们讨论虚拟内存的时候说到了64位机器,只用了48位寻址。为什么?
    讨论了一下 感觉是48位虚拟地址空间已经足够大,而且随着虚拟地址空间增大维护的页表也会增大,而且tlb的命中率也会降低所以折中选择了48位吧。

  • 为什么会牵扯到 esp 和 ebp
    比如说一个地址存着一个int32的值,那么这个值只有32位,但是存它的地址却是一个比他本身还大的空间,这就是一种浪费。组长是iOS的,它说在iOS里面会直接使用这个值,存上这个32位的值。
    当时,我就觉得很奇怪,我们都知道访问所有的东西都是需要一个地址的。那这个int值没有了自己的地址,怎么找到它呢?

    组长就说了 esp 和 ebp,因为这些变量都是在栈上创建的,其实在我们代码运行时,可以认为有两个指针一直会指向栈空间。当我们定义了一些基本变量的时候,它的大小是定值,那么在代码编译的时候,可以认为每个变量都有相对于栈空间的一个偏移量,通过指针偏移量来访问栈上的所有内容。

  • 变量创建在栈上一定要初始化
    之后我们就引c出了另一个问题,类C的语言,比如基本变量开辟在栈上,一定要初始化。为什么?
    因为栈空间没有人会帮你重置,那么当你在栈上定义了变量没有初始化,这个地址上也许已经存在内容了,那么此时变量的值就是错误的。

    /img/in-post/esp和ebp.png

      #include<stdio.h>  
    
      void test1(){
          int a = 0x12345678;
      }
    
      void test2(){
          int a;
          printf("%2x\n",a);
      }
    
      int main(){
          test1();
          test2();
          return 0;
      }
    

    这段代码打印出来的是什么?答案是0x12345678
    因为 test1 执行的时候将一片栈空间修改了,test2 执行的时候重新从之前的位置开始执行,同时也是个 int 类型,读出来的值就是 test1 中的值。

mmap

mmap 官方解释 map files or devices into memory

mmap 会在虚拟内存中开辟一段新的空间,并不会真的分配物理空间,当访问到这段虚拟地址时,根据前面的虚拟内存知识,页表中表示该虚拟地址没有对应的物理地址,则会发生缺页中断,此时才会进行物理内存分配和读取相应内容进物理内存。

总之,Linux 的一切都是一种懒加载的模式,只有真正用到,才会真的开辟真实空间。

那么 mmap 有什么作用?

  • 读取文件

    mmap 跟常规的 read 有区别,只有一次拷贝过程,效率更高(这里 read 的内容如果在页缓存中应该也差不多吧?),很多文章分析,并且有详细的函数调用过程。

  • 跨进程通信

    mmap 有一种 MAP_SHARED 的 flag,两个进程可以 mmap 同一个文件到自己的进程中,这样就可以通过这一个文件进行通信了。但是一个完善的跨进程通信有很多细节,比如这两个进程如何互相找到对方,数据传输等各种细节。

binder_mmap

android 中的基石 binder,就是用于跨进程通信,是一套完善的IPC机制,并且好像也合并入了新的 Linux 中。

具体的过程,未完待定~~

借鉴

https://github.com/Durant35/durant35.github.io/issues/24 https://blog.csdn.net/xungjhj/article/details/70946057