虚拟内存可以提高系统的稳定性和安全性,主要通过以下两点:

  • 1、控制物理内存的访问权限。

    例如,Text Segment被只读保护起来,防止被错误的指令意外改写,内核地址空间也被保护起来,防止在用户模式下执行错误的指令意外改写内核数据。这样,执行错误指令或恶意代码的破坏能力受到了限制,顶多使当前进程因段错误终止,而不会影响整个系统的稳定性。

  • 2、让每个进程有独立的地址空间。

    所谓独立的地址空间是指,不同进程中的同一个VA被MMU映射到不同的PA,并且在某一个进程中访问任何地址都不可能访问到另外一个进程的数据,这样使得任何一个进程由于执行错误指令或恶意代码导致的非法内存访问都不会意外改写其它进程的数据,不会影响其它进程的运行,从而保证整个系统的稳定性。

虚拟内存 + 共享库可以大大节省内存。

比如libc共享库,系统中几乎所有的进程都映射libc到自己的进程地址空间,而libc的只读部分在物理内存中只需要存在一份,就可以被所有进程共享,这就是“共享库”这个名称的由来了。

虚拟内存方便给分配和释放内存带来方便,物理地址不连续的几块内存可以映射成虚拟地址连续的一块内存。

虚拟内存带来了交换分区。这样就提高了系统中可分配的内存总量,可以运行更多进程。

比如要用malloc分配一块很大的内存空间,虽然有足够多的空闲物理内存,却没有足够大的连续空闲内存,这时就可以分配多个不连续的物理页面而映射到连续的虚拟地址范围。

What-什么是虚拟内存

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上物理内存通常被分隔成多个内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。此外,虚拟内存技术可以使多个进程共享同一个运行库,并通过分割不同进程的内存空间来提高系统的安全性。

注意:虚拟内存不只是“用磁盘空间来扩展物理内存”的意思——这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是使用虚拟内存技术的一个结果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。

那些需要快速存取或者相应时间非常稳定的嵌入式系统,以及其他的具有特殊应用的计算机系统,可能会为了避免让运算结果的可预测性降低,而选择不使用虚拟内存。

How

虚拟内存技术是现代计算机系统结构中不可分割的一部分。现代所有用于一般应用的操作系统都对普通的应用程序使用虚拟内存技术,例如文字处理软件,电子制表软件,多媒体播放器等等。大部分架构通过CPU中独立的硬件内存管理单元(英语:memory management unit,缩写为MMU),有时称作分页内存管理单元(英语:paged memory management unit,缩写为PMMU)来辅助实现这一功能。

  • 操作系统利用体系结构提供的VA到PA的转换机制实现虚拟内存管理。

    ps命令查看当前终端下的进程,得知bash进程的id是27613,然后用cat /proc/27613/maps命令查看它的虚拟地址空间。/proc目录中的文件并不是真正的磁盘文件,而是由内核虚拟出来的文件系统,当前系统中运行的每个进程在/proc下都有一个子目录,目录名就是进程的id,查看目录下的文件可以得到该进程的相关信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    root@DESKTOP-KD33OT8:/home/demo# cat /proc/27613/maps
    5589e9d2b000-5589e9d42000 r--p 00000000 08:10 225588 /usr/bin/zsh
    5589e9d42000-5589e9dd7000 r-xp 00017000 08:10 225588 /usr/bin/zsh
    5589e9dd7000-5589e9df9000 r--p 000ac000 08:10 225588 /usr/bin/zsh
    5589e9dfa000-5589e9dfc000 r--p 000ce000 08:10 225588 /usr/bin/zsh
    5589e9dfc000-5589e9e02000 rw-p 000d0000 08:10 225588 /usr/bin/zsh
    5589e9e02000-5589e9e16000 rw-p 00000000 00:00 0
    5589eac55000-5589eb1af000 rw-p 00000000 00:00 0 [heap]
    7f90adfc6000-7f90adfc9000 r--p 00000000 08:10 225599 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/computil.so
    7f90adfc9000-7f90adfd6000 r-xp 00003000 08:10 225599 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/computil.so
    7f90adfd6000-7f90adfd8000 r--p 00010000 08:10 225599 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/computil.so
    7f90adfd8000-7f90adfd9000 r--p 00011000 08:10 225599 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/computil.so
    7f90adfd9000-7f90adfda000 rw-p 00012000 08:10 225599 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/computil.so
    7f90adfda000-7f90ae25a000 r--s 00000000 08:10 225058 /usr/share/zsh/functions/Completion/Unix.zwc
    7f90ae265000-7f90ae28a000 r--s 00000000 08:10 225225 /usr/share/zsh/functions/Completion/Zsh.zwc
    7f90ae299000-7f90ae2b2000 r--s 00000000 08:10 225469 /usr/share/zsh/functions/Zle.zwc
    7f90ae2b6000-7f90ae2da000 rw-p 00000000 00:00 0
    7f90ae325000-7f90ae331000 rw-p 00000000 00:00 0
    7f90ae331000-7f90ae37d000 rw-p 00000000 00:00 0
    7f90ae37d000-7f90ae381000 r--p 00000000 08:10 225596 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/compctl.so
    7f90ae381000-7f90ae38d000 r-xp 00004000 08:10 225596 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/compctl.so
    7f90ae38d000-7f90ae38f000 r--p 00010000 08:10 225596 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/compctl.so
    7f90ae38f000-7f90ae390000 r--p 00011000 08:10 225596 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/compctl.so
    7f90ae390000-7f90ae391000 rw-p 00012000 08:10 225596 /usr/lib/x86_64-linux-gnu/zsh/5.8/zsh/compctl.so
    7f90aecf3000-7f90aecf5000 rw-p 00000000 00:00 0
    7f90aecf5000-7f90aecf6000 r--p 00000000 08:10 2985 /usr/lib/locale/C.UTF-8/LC_TELEPHONE
    7f90aecf6000-7f90aecf7000 r--p 00000000 08:10 2978 /usr/lib/locale/C.UTF-8/LC_MEASUREMENT
    7f90aecf7000-7f90aecfe000 r--s 00000000 08:10 264854 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
    7f90aecfe000-7f90aecff000 r--p 00000000 08:10 38549 /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f90aecff000-7f90aed22000 r-xp 00001000 08:10 38549 /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f90aed22000-7f90aed2a000 r--p 00024000 08:10 38549 /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f90aed2a000-7f90aed2b000 r--p 00000000 08:10 2977 /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
    7f90aed2b000-7f90aed2c000 r--p 0002c000 08:10 38549 /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f90aed2c000-7f90aed2d000 rw-p 0002d000 08:10 38549 /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f90aed2d000-7f90aed2e000 rw-p 00000000 00:00 0
    7ffeb1e2d000-7ffeb1e7c000 rw-p 00000000 00:00 0 [stack]
    7ffeb1f0c000-7ffeb1f10000 r--p 00000000 00:00 0 [vvar]
    7ffeb1f10000-7ffeb1f11000 r-xp 00000000 00:00 0 [vdso]

Why-为什么需要虚拟内存

第一,虚拟内存管理可以控制物理内存的访问权限。

物理内存本身是不限制访问的,任何地址都可以读写,而操作系统要求不同的页面具有不同的访问权限,这是利用CPU模式和MMU的内存保护机制实现的。例如,Text Segment被只读保护起来,防止被错误的指令意外改写,内核地址空间也被保护起来,防止在用户模式下执行错误的指令意外改写内核数据。这样,执行错误指令或恶意代码的破坏能力受到了限制,顶多使当前进程因段错误终止,而不会影响整个系统的稳定性。

第二,虚拟内存管理最主要的作用是让每个进程有独立的地址空间。

所谓独立的地址空间是指,不同进程中的同一个VA被MMU映射到不同的PA,并且在某一个进程中访问任何地址都不可能访问到另外一个进程的数据,这样使得任何一个进程由于执行错误指令或恶意代码导致的非法内存访问都不会意外改写其它进程的数据,不会影响其它进程的运行,从而保证整个系统的稳定性。另一方面,每个进程都认为自己独占整个虚拟地址空间,这样链接器和加载器的实现会比较容易,不必考虑各进程的地址范围是否冲突。

继续前面的实验,再打开一个终端窗口,看一下这个新的bash进程的地址空间,可以发现和先前的bash进程地址空间的布局差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ps
PID TTY TIME CMD
30697 pts/1 00:00:00 bash
30749 pts/1 00:00:00 ps
$ cat /proc/30697/maps
08048000-080f4000 r-xp 00000000 08:15 688142 /bin/bash
080f4000-080f9000 rw-p 000ac000 08:15 688142 /bin/bash
080f9000-080fe000 rw-p 080f9000 00:00 0
082d7000-084f9000 rw-p 082d7000 00:00 0 [heap]
b7cf1000-b7cfb000 r-xp 00000000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
b7cfb000-b7cfc000 r--p 00009000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
b7cfc000-b7cfd000 rw-p 0000a000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
...
b7e5e000-b7fb6000 r-xp 00000000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
b7fb6000-b7fb8000 r--p 00158000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
b7fb8000-b7fb9000 rw-p 0015a000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
...
b8006000-b8020000 r-xp 00000000 08:15 565466 /lib/ld-2.8.90.so
b8020000-b8021000 r-xp b8020000 00:00 0 [vdso]
b8021000-b8022000 r--p 0001a000 08:15 565466 /lib/ld-2.8.90.so
b8022000-b8023000 rw-p 0001b000 08:15 565466 /lib/ld-2.8.90.so
bff0e000-bff23000 rw-p bffeb000 00:00 0 [stack]

该进程也占用了0x0000 0000-0xbfff ffff的地址空间,Text Segment也是0x0804 8000-0x080f 4000,Data Segment也是0x080f 4000-0x080f 9000,和先前的进程一模一样,因为这些地址是在编译链接时写进/bin/bash这个可执行文件的,两个进程都加载它。这两个进程在同一个系统中同时运行着,它们的Data Segment占用相同的VA,但是两个进程各自干各自的事情,显然Data Segment中的数据应该是不同的,相同的VA怎么会有不同的数据呢?因为它们被映射到不同的PA。如下图所示。

图 20.5. 进程地址空间是独立的

img

从图中还可以看到,两个进程都是bash进程,Text Segment是一样的,并且Text Segment是只读的,不会被改写,因此操作系统会安排两个进程的Text Segment共享相同的物理页面。由于每个进程都有自己的一套VA到PA的映射表,整个地址空间中的任何VA都在每个进程自己的映射表中查找相应的PA,因此不可能访问到其它进程的地址,也就没有可能意外改写其它进程的数据。

另外,注意到两个进程的共享库加载地址并不相同,共享库的加载地址是在运行时决定的,而不是写在/bin/bash这个可执行文件中。但即使如此,也不影响两个进程共享相同物理页面中的共享库,当然,只有只读的部分是共享的,可读可写的部分不共享。

使用共享库可以大大节省内存。比如libc,系统中几乎所有的进程都映射libc到自己的进程地址空间,而libc的只读部分在物理内存中只需要存在一份,就可以被所有进程共享,这就是“共享库”这个名称的由来了。

现在我们也可以理解为什么共享库必须是位置无关代码了。比如libc,不同的进程虽然共享libc所在的物理页面,但这些物理页面被映射到各进程的虚拟地址空间时却位于不同的地址,所以要求libc的代码不管加载到什么地址都能正确执行。

第三,VA到PA的映射会给分配和释放内存带来方便,物理地址不连续的几块内存可以映射成虚拟地址连续的一块内存。

比如要用malloc分配一块很大的内存空间,虽然有足够多的空闲物理内存,却没有足够大的连续空闲内存,这时就可以分配多个不连续的物理页面而映射到连续的虚拟地址范围。如下图所示。

图 20.6. 不连续的PA可以映射为连续的VA

img

四,一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行。

因为各进程分配的只不过是虚拟内存的页面,这些页面的数据可以映射到物理页面,也可以临时保存到磁盘上而不占用物理页面,在磁盘上临时保存虚拟内存页面的可能是一个磁盘分区,也可能是一个磁盘文件,称为交换设备(Swap Device)。当物理内存不够用时,将一些不常用的物理页面中的数据临时保存到交换设备,然后这个物理页面就认为是空闲的了,可以重新分配给进程使用,这个过程称为换出(Page out)。如果进程要用到被换出的页面,就从交换设备再加载回物理内存,这称为换入(Page in)。换出和换入操作统称为换页(Paging),因此:

系统中可分配的内存总量 = 物理内存的大小 + 交换设备的大小

如下图所示。第一张图是换出,将物理页面中的数据保存到磁盘,并解除地址映射,释放物理页面。第二张图是换入,从空闲的物理页面中分配一个,将磁盘暂存的页面加载回内存,并建立地址映射。

图 20.7. 换页

image.png