$ ps
PID TTY TIME CMD
9612 pts/2 00:00:00 bash
32070 pts/2 00:00:00 ps
$ pmap 9612
9612: bash
08048000 668K r-x-- /bin/bash
080ef000 24K rw--- /bin/bash
080f5000 2056K rw--- [ anon ]
b7c6d000 36K r-x-- /lib/tls/i686/cmov/libnss_files-2.7.so
b7c76000 8K rw--- /lib/tls/i686/cmov/libnss_files-2.7.so
b7c78000 32K r-x-- /lib/tls/i686/cmov/libnss_nis-2.7.so
b7c80000 8K rw--- /lib/tls/i686/cmov/libnss_nis-2.7.so
b7c82000 80K r-x-- /lib/tls/i686/cmov/libnsl-2.7.so
b7c96000 8K rw--- /lib/tls/i686/cmov/libnsl-2.7.so
b7c98000 8K rw--- [ anon ]
b7c9a000 28K r-x-- /lib/tls/i686/cmov/libnss_compat-2.7.so
b7ca1000 8K rw--- /lib/tls/i686/cmov/libnss_compat-2.7.so
b7cb4000 252K r---- /usr/lib/locale/en_US.utf8/LC_CTYPE
b7cf3000 900K r---- /usr/lib/locale/en_US.utf8/LC_COLLATE
b7dd4000 4K rw--- [ anon ]
b7dd5000 1316K r-x-- /lib/tls/i686/cmov/libc-2.7.so
b7f1e000 4K r---- /lib/tls/i686/cmov/libc-2.7.so
b7f1f000 8K rw--- /lib/tls/i686/cmov/libc-2.7.so
b7f21000 16K rw--- [ anon ]
b7f25000 8K r-x-- /lib/tls/i686/cmov/libdl-2.7.so
b7f27000 8K rw--- /lib/tls/i686/cmov/libdl-2.7.so
b7f29000 180K r-x-- /lib/libncurses.so.5.6
b7f56000 12K rw--- /lib/libncurses.so.5.6
b7f59000 4K r---- /usr/lib/locale/en_US.utf8/LC_NUMERIC
b7f5a000 4K r---- /usr/lib/locale/en_US.utf8/LC_TIME
b7f5b000 4K r---- /usr/lib/locale/en_US.utf8/LC_MONETARY
b7f5c000 4K r---- /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES
b7f5d000 4K r---- /usr/lib/locale/en_US.utf8/LC_PAPER
b7f5e000 4K r---- /usr/lib/locale/en_US.utf8/LC_NAME
b7f5f000 4K r---- /usr/lib/locale/en_US.utf8/LC_ADDRESS
b7f60000 4K r---- /usr/lib/locale/en_US.utf8/LC_TELEPHONE
b7f61000 4K r---- /usr/lib/locale/en_US.utf8/LC_MEASUREMENT
b7f62000 28K r--s- /usr/lib/gconv/gconv-modules.cache
b7f69000 4K r---- /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION
b7f6a000 8K rw--- [ anon ]
b7f6c000 4K r-x-- [ anon ]
b7f6d000 104K r-x-- /lib/ld-2.7.so
b7f87000 8K rw--- /lib/ld-2.7.so
bfad4000 84K rw--- [ stack ]
total 5948K
例 1. 进程的地址空间
这是bash进程的虚拟地址空间,32位CPU的虚拟地址空间是4GB,也就是0x0000 0000-0xffff ffff,该进程占用的地址范围近似为0x0000 0000-0xbfff ffff,地址范围0xc000 0000-0xffff ffff由内核占用,用户进程不允许访问。在这个bash进程的地址空间中,从0x0804 8000开始的668K的权限为r-x--,表示代码段,从0x080e f000开始的24K的权限是rw---,表示数据段,从0x080f 5000开始的2056K的权限也是rw---,但是没有对应任何磁盘文件,而是用[ anon ](anonymous,匿名)来表示,这是堆所占的空间,从0xb7c6 d000开始是共享库和资源文件的映射空间,每个共享库也分为代码段和数据段,用不同的权限表示,可以看到,从堆空间到下面的共享库映射空间之间有很大的地址空洞,最末从0xbfad 4000开始的84K是栈空间。
为什么需要虚拟内存管理呢?可以从以下几个方面来理解。
第一,让每个进程有独立的地址空间是引入虚拟内存管理的最主要目的。所谓独立的地址空间是指,不同进程中的同一个VA被MMU映射到不同的PA,并且在某一个进程中访问任何地址都不可能访问到另外一个进程的数据,这样使得任何一个进程由于程序BUG或恶意代码所导致的非法内存访问都不会意外改写其它进程的数据,不会影响其它进程的运行,从而保证了整个系统的稳定性。另一方面,每个进程都认为自己独占4GB的地址空间,编写程序会比较方便,不必为每个进程分配一个地址范围,而是每个进程都可以使用一个完整的地址空间中的任何地址。
我们继续用上面的例子来理解,再打开一个shell窗口,用pmap命令看一下这个新的bash进程的地址空间,可以发现和刚才的地址空间布局差不多:
$ ps
PID TTY TIME CMD
32371 pts/1 00:00:00 bash
32387 pts/1 00:00:00 ps
$ pmap 32371
32371: bash
08048000 668K r-x-- /bin/bash
080ef000 24K rw--- /bin/bash
080f5000 2000K rw--- [ anon ]
b7c71000 36K r-x-- /lib/tls/i686/cmov/libnss_files-2.7.so
b7c7a000 8K rw--- /lib/tls/i686/cmov/libnss_files-2.7.so
......
该进程也占用了0x0000 0000-0xbfff ffff的地址空间,代码段也是从0x0804 8000开始的668K,数据段也是从0x080e f000开始的24K,共享库的内存布局也差不多。这个进程和刚才的例子是同一个系统中同时运行着的两个进程,它们都认为自己占有0x0000 0000-0xbfff ffff的地址空间,并且它们的数据段的地址范围是重合的,但是两个进程各自干各自的事情,显然数据段中的数据是不同的,正是因为不同进程中的同一个VA被映射到了不同的PA,所以两个进程的数据段其实是在不同的物理地址上,如下图所示。
图 4. 进程地址空间是独立的
从图中还可以看到,两个进程都是bash进程,代码段是一样的,并且代码段是只读的,不会被改写,因此操作系统会安排两个进程的代码段共享相同的物理内存。由于每个进程都有自己的一套VA到PA的映射表,整个地址空间中的任何VA都在每个进程自己的映射表中查找相应的物理地址,因此不可能访问到其它进程的地址,也就没有可能意外改写其它进程的数据。
第二,引入VA到PA的映射也会给分配和释放内存带来方便,物理上不连续的空间可以映射为逻辑上连续的虚拟地址空间。比如要malloc一块很大的内存空间,而物理内存虽然有足够的空闲内存,却没有足够大的连续空闲内存,这时就可以分配多个不连续的物理页面,而映射为连续的虚拟地址范围。如下图所示。
图 5. 不连续的PA可以映射为连续的VA
第三,一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行。因为各进程分配的只不过是虚拟内存的页,这个页的内容可以映射到物理内存的页框,也可以临时保存到磁盘上而不占用物理内存的页框,磁盘上这一部分称为交换设备(Swap Device),可能是一个磁盘分区,也可能是一个磁盘文件。当物理内存不够时将物理内存中不常用的页框临时保存到磁盘上,而当用到这些页框时再从磁盘加载回内存,这称为换页(Paging)因此:
系统中可分配的内存总量 = 物理内存的大小 + 交换设备的大小
如下图所示。第一张图是换出(Page out),将物理页面的内容保存到磁盘,并解除地址映射,释放物理页面。第二张图是换入(Page in),从空闲的物理页面中分配一个,将磁盘暂存的页面加载回内存,并建立地址映射。
图 6. 换页
第四,虚拟内存管理可以控制物理页面的访问权限。物理内存本身是不限制访问的,任何地址都可以读写,而操作系统要求实现各种不同的访问权限,在先前的例子中我们已经看到,代码段要求是rx的,数据段要求是rw的,用户进程不能访问属于内核的地址空间,这些都是操作系统和MMU配合实现的。
MMU中还实现了一种访问限制是关于Cache的。Cache(高速缓存)是CPU内的一小块高速RAM,用来缓存最近访问过的内存数据,CPU访问Cache的速度是访问内存速度的数十倍,所以有效地利用Cache可以大大提高计算机的整体性能。CPU核要访问数据时首先发出VA,Cache利用VA查找相应的数据有没有被缓存[2],如果有就通知CPU核,如果是读操作就直接将Cache中的数据传给CPU核中的寄存器,如果是写操作就直接改写Cache中的数据,而不需要访问物理内存。但是,有些VA所对应的PA并不是物理内存中的地址而是设备寄存器的地址,对这些寄存器进行读写并不是为了保存数据,而是对设备做特殊操作,这种VA通常是不允许缓存的,因为如果缓存了,对VA的读写将只在Cache中起作用,而不会传到设备寄存器对设备进行操作。以串口的收发寄存器为例,如果收发寄存器地址被缓存了会出现什么问题呢?如下图所示。
图 7. 串口收发寄存器如果被缓存会出什么问题
如果发送寄存器的地址被缓存起来,CPU核往发送寄存器的地址做写操作都写到Cache中去了,发送寄存器并没有及时得到数据,也就不能及时发送,此外,CPU核先后发出的1、2、3三个数据都会写到Cache中的同一个地址,最后Cache中只保存了第3个数据,如果这时Cache的数据写回到发送寄存器去,只能把第3个数据发送出去,前两个数据就丢失了。与此类似,如果接收寄存器的地址被缓存起来,CPU核在读第1个数据时,Cache会从接收寄存器读进来缓存,然而接收寄存器后面收到2、3两个数据Cache并不知道,因为Cache把接收寄存器当作内存,并且相信内存中的数据是不会自己变的,所以以后每次CPU核读接收寄存器时,Cache都提供给CPU核第1个数据。