本文分析了Linux虚拟内存管理的作用并基于x86系统从宏观角度上分析了Linux实现虚拟内存管理的原理,用作个人备忘,如有错误,敬请指出。在介绍虚拟内存管理原理之前,先介绍一些概念。
一、相关概念介绍
1.虚拟内存管理作用
(1)使每个进程都拥有自己独立的内存地址空间;对于32位Linux而言,每个任务可寻址的内存地址空间都为0x00000000-0xFFFFFFFF(0-4GB)。换句话说,虚拟内存管理使得程序员在编程时无需考虑逻辑地址与物理地址之间的差异,只需认为自己程序可以操作的地址空间为0-4GB即可。
(2)当物理内存不够4GB时,虚拟内存管理模块会用外存空间模拟内存空间,并且该模拟过程对于应用程序来说是透明的。
2.逻辑地址与物理地址
(1)逻辑地址:程序在运行过程中用来访问存储器的地址。程序员在编程时,只需知道逻辑地址,不需考虑该地址与实际物理硬件上的存储单元如何对应。编译器在编译源程序时,也只需考虑逻辑地址。
(2)物理地址:表示物理存储器中一个存储单元的实际位置,地址总线上产生的就是物理地址。(总线地址)
(3)在实地址模式下,逻辑地址等于物理地址。在虚拟地址模式下,逻辑地址不等于物理地址,必须经过查表才能转换为物理地址,因此也叫虚拟地址。
3.用户地址空间与内核地址空间
(1)Linux将每个进程的4GB的独立地址空间又划分为用户地址空间(低3G 0x00000000-0xBFFFFFFF)和内核地址空间(高1G 0xC0000000-0xFFFFFFFF)两部分。
(2)操作系统内核代码和数据存放在内核地址空间;每个进程自己私有的代码和数据存放在用户地址空间。
(3)虽然Linux的内核代码和数据被映射到了每个进程的地址空间中(所有进程看到的内容是相同的),但在实际的物理内存中,只有内核代码和数据的一份拷贝。
(4)内核地址空间逻辑地址从3GB开始的896MB与物理地址空间从零开始的896MB做了线性平移映射;即内核逻辑地址空间中存储的数据的地址减去3G即为其在物理内存空间中的实际存储位置。
4.用户态与内核态
(1)区分内核态与用户态的目的之一是在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清理内存、设置时钟等。
(2)一般现代CPU都有几种不同的指令执行级别,在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。用户态指相应的低级别执行状态,代码的掌控范围会受到限制,只能执行CPU指令集的一个子集。举例来说,intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级分别来表示内核态和用户态。
(3)0xc0000000以上的内核地址空间只能在内核态下访问,0x00000000-0xbfffffff的用户地址空间在两种状态下都可以访问。
(4)从用户空间到内核空间有以下触发手段:
- 系统调用:用户进程通过系统调用申请使用操作系统提供的服务程序来完成工作,比如read()、fork()等。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现的。
- 中断:当外围设备完成用户请求的操作后,会想CPU发送中断信号。这时CPU会暂停执行下一条指令(用户态)转而执行与该中断信号对应的中断处理程序(内核态)
- 异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
二、Linux虚拟内存管理实现方式
Linux虚拟内存管理由CPU与操作系统共同完成。CPU中的内存管理单元MMU负责自动查表,将当前指令中的逻辑地址转化为物理地址,并在物理页未分配时触发缺页异常。操作系统负责初始化各个表,填充各个表的内容,并提供缺页中断的中断服务器程序。
进程页表:由于每个进程都认为自己有4GB的逻辑地址空间,要想达到此目标,每个进程需要有自己的一个进程页表,负责记录当前进程的虚拟页与物理内存的物理页的对应关系。各个进程间页表互不干预。
内页表:整个操作系统中有一个。由于要记录将哪个物理页面被分配给了哪个进程的哪个虚拟页面,所以操作系统中必然存在一个页表存放了这个记录关系,我们称之为内页表。
外页表:整个操作系统中有一个。由于在物理空间不足时需要使用外存空间来模拟内存空间,在将内存中已分配给某个进程的物理页面调到外存时必然需要一个表记录调到外存后的物理页面原来是属于哪个进程的哪个虚拟页的内容。
下面就基于这三种页表来从宏观上介绍Linux虚拟内存管理实现方式:
如上图所示,初始情况下假设进程1获得时间片,正在运行。当访问到1号虚拟页所对应的物理地址时发现其对应的物理页操作系统还没有分配,此时会由CPU触发缺页异常,进入由操作系统提供的缺页中断服务程序。操作系统检查外页表,发现进程1的1号虚拟页并没有被调页到外存,之后在内页表中查找空闲物理页,找到了0号物理页,将0号物理页分配给进程1的1号虚拟页,分配后三个表内容如图2:
接下来进程1时间片用完,由运行状态变为就绪状态,进程2获得时间片开始运行。当访问到进程2的1号虚拟页对应的物理页时发现其对应的物理页操作系统还没有分配,触发缺页异常。操作系统扫描外页表没有找到进程2的1号虚拟页对应的物理页面的调出记录,之后扫描内页表,期待找到一个空闲的物理页分配给进程2,但是扫描后发现所有物理页已经全部用光,此时就需要采用某种页面淘汰算法(比如LRU)将内存中已经分配出去的某个物理页置换到外存。比如此时选择将3号物理页复制到外存,之后将3号物理页分配给进程2的1号虚拟页。操作后三页表内容如下:
接下来进程2时间片用完,由运行状态变为就绪状态,进程3获得时间片开始运行。当访问到进程3的0号虚拟页对应的物理页时发现其对应的物理页操作系统还没有分配,触发缺页异常。操作系统扫描外页表没有找到进程3的0号虚拟页对应的物理页面的调出记录,之后扫描内页表,期待找到一个空闲的物理页分配给进程3,但是扫描后发现所有物理页已经全部用光,此时就需要采用某种页面淘汰算法(比如LRU)将内存中已经分配出去的某个物理页置换到外存。比如此时选择将2号物理页复制到外存,之后将2号物理页分配给进程3的0号虚拟页。操作后三页表内容如下:
接下来进程3时间片用完,由运行状态变为就绪状态,进程1获得时间片开始运行。当访问到进程1的0号虚拟页对应的物理页时发现其对应的物理页操作系统还没有分配,触发缺页异常。操作系统扫描外页表找到了进程1的0号虚拟页对应的物理页面的调出记录,之后扫描内页表,期待找到一个空闲的物理页将外存中进程1的0号虚拟页原来的内容调入后分配给进程1,但是扫描后发现所有物理页已经全部用光,此时就需要采用某种页面淘汰算法(比如LRU)将内存中已经分配出去的某个物理页置换到外存。比如此时选择将0号物理页复制到外存,之后将外存中内容调入0号物理页并分配给进程1的0号虚拟页。操作后三页表内容如下:
之后情况依次类推,总而言之,操作系统通过拆东墙补西墙战术来实现使用外存空间来模拟内存空间,通过拖延战术(不到必要时候不真正分配物理内存)来加快内存分配速度,减少内存占用。
上述的几张图中内页表中1号物理页被分配给操作系统占用,用来存储内核数据。前面提到过 虽然Linux的内核代码和数据被映射到了每个进程的地址空间中(所有进程看到的内容是相同的),但在实际的物理内存中,只有内核代码和数据的一份拷贝。 就是通过在各个进程页表中共享内核空间占用的物理页来实现的。
(完)