内存管理 : 05 内存换入-请求调页
操作系统内存换入 - 请求调页讲解
这一讲主要内容是内存的换入,下一讲要讲内存的换出(swap out),这两讲合在一起就是内存的换入换出。讲完内存的换入换出,操作系统关于内存管理这部分内容,也就是我们课程里内存管理部分就结束了。
内存换入换出的重要性及与虚拟内存的关系
讲到这儿,我们得明白为什么要讲内存的换入换出,其实这是从虚拟内存概念引申出来的,为了实现虚拟内存就需要换入换出。我们回顾一下,之前为了实现分段和分页,引入了虚拟内存的概念。我们在虚拟内存中划分出一部分作为分段,再将虚拟内存分成一页一页映射到物理内存上,所以虚拟内存是连接分段和分页的核心,是实现分段和分页的关键所在。而分段和分页是操作系统管理内存最重要的两个机制,没有内存的换入换出,虚拟内存就无法实现。
我们来梳理一下整个内存管理的知识脉络。一开始我们讲分段,是为了方便用户使用内存;后来发现光有分段会导致内存效率低、空间利用率低,所以引出了分页;接着我们说要同时实现分段和分页,而其核心就是虚拟内存,要实现虚拟内存就必须有换入和换出机制。所以整个操作系统关于内存管理的核心,就是基于虚拟内存的分段加上分页,并用换入换出来实现虚拟内存。
虚拟内存的概念与原理
虚拟内存对于应用程序和用户来说,是一个0 - 4g(32位机)的地址空间,在用户眼里,看不到物理内存,只看到这规整的0 - 4g内存空间,就像拥有4g内存一样可以随意使用,比如设置指针p等于3g就在相应位置使用,这大大方便了用户编写和使用程序。
但实际上这4g内存是虚拟的、假的,要完成真正的工作,需要将虚拟内存映射到实际物理内存上,只有这样程序发出的指令和数据才能真正在内存上读写。而这个映射过程对用户是透明的,用户不知道操作系统是如何映射的。操作系统通过映射给用户提供4g内存的假象,而内存换入和换出的核心就是在映射上做文章。
为什么要在映射上做文章呢?因为用户眼中有4g大且规整的内存,但实际物理内存可能没那么大,可能是1g、2g等,无论物理内存多大,操作系统都要给用户提供0 - 4g虚拟内存的感觉,隐藏硬件细节,方便用户使用。那么如何用较小的物理内存给用户提供4g内存的感觉呢?核心就是换入换出。
例如,当用户使用0 - 1g内存空间时,操作系统就把这部分从磁盘映射到物理内存上;当使用3 - 4g空间时,再把相应部分映射上去,也就是真正使用时才进行映射,这就引出了内存换入的概念。
内存换入(请求调页)的具体过程
换入的基本概念
假设用户程序的代码段和数据段在磁盘上,当程序要执行0 - 1g这一段代码时,操作系统就把这段代码从磁盘换入到物理内存,并建立映射,这样用户就能正常使用这部分内存了。当程序跳到3g开始的数据段,而这部分数据还没映射到内存时,操作系统就再从磁盘把数据读进来,建立映射,让用户感觉整个0 - 4g内存都可以正常使用,而这些换入和映射过程对用户完全透明,用户只是感觉执行指令有时会稍微慢一点。
我们用日常生活中的例子来理解,0 - 4g虚拟内存就像仓库,里面有各种货物;物理内存就像门店,空间有限。当顾客要A类物品,店员从仓库拿出来放到门店;当另一个顾客要C类物品,店员又从仓库取,由于门店空间有限,可能要把A类物品放回仓库,这样顾客想要的物品在门店都能看到,只是有时需要等待。在计算机中,就是通过换入换出来实现让用户感觉0 - 4g虚拟内存都能随意使用,只把用户需要使用的部分换入。
请求调页的实现原理
在计算机系统中,具体实现内存换入是通过请求调页。一个程序用逻辑地址在0 - 4g虚拟地址空间随意访问,算出虚拟地址后,当这个虚拟地址要映射物理页时,会通过页表查询。如果页表中没有该页的映射,也就是缺页,此时就需要操作系统进行处理。
当缺页发生时,硬件会协助处理。硬件设备MMU查页表发现缺页后,会产生一个中断信号,在CPU的寄存器上把相应位置为1。每执行完一条指令,CPU都会检查是否有中断产生,当检测到缺页中断后,就会执行中断处理程序。
中断处理程序要做的事情就是实现内存换入:首先要从磁盘找到缺的那一页,这需要数据结构与算法的支持;然后在内存中找一个空闲页(get free page);接着把磁盘上的页读入到空闲页中;最后建立虚拟页和物理页的映射关系,也就是填写页表。中断处理完成后,程序回到发生中断的指令处继续执行(硬件可做改动,让MMU发现缺页中断时,PC指针不动,不自动加一,保证执行原指令)。从用户角度看,就像什么都没发生,只是感觉指令执行稍慢了些。
请求调页的实际代码实现
在实际系统中,整个过程从缺页中断开始,因为MMU产生中断我们无法捕捉,但中断处理是我们可以控制的。缺页中断是14号中断,在系统初始化时,就要设置好14号中断由谁来处理,也就是在系统启动时进行相关设置,主要是初始化修改中断向量表(AIDT表)。
当发生缺页中断进入中断处理程序(page fault)时,首先要保存现场,因为要进入内核,相关寄存器如DS、ES等要修改为内核栈对应的值。重点是把页错误的线性地址(虚拟地址,在课程中虚拟地址常被称为线性地址)从寄存器CR2赋给EDX,然后将EDX入栈,这是为了给后续调用的C函数传递参数,让其知道哪里缺页。
接下来调用do_no_page函数,它要完成从磁盘读入缺页并建立映射的工作。具体过程是,先根据传入的虚拟地址得到虚拟页号(去掉虚拟地址的后12位,即业内偏移),然后调用get free page函数申请一个空闲的物理内存页;接着从磁盘读入缺页(b_read函数,因为磁盘是块设备,按块读写,所以从磁盘读入一页到申请的空闲内存中);最后调用put_page函数建立虚拟页和物理页的映射,也就是修改页表,根据虚拟地址找到页目录项,再找到页表项,把读入内存的物理页信息写入页表项,完成映射。
通过在中断处理程序中完成申请空闲页、从磁盘读页、建立映射这三件事,就能实现内存换入(请求调页),让用户感觉0 - 4g虚拟内存都能正常使用。至此,内存换入(请求调页)的内容就全部讲完了。