操作系统概述之虚拟内存

现代的应用程序都运行在一个内存空间里,具体到计算机硬件就是内存条。在 32 位系统中,这个内存空间拥有 4GB (2 的 32 次方)的寻址能力。大多数操作系统都会将 4GB 的内存空间一部分挪给内核使用,应用程序无法直接访问这一段内存,这一部分内存地址被称为内核空间。Windows 在默认的情况下会将高地址的 2GB 空间分配给内核(也可以配置 1GB)。Linux 默认情况下将高地址的 1GB 空间分配给内核。用户使用的剩下的 2GB 或 3GB 的内存空间称为用户空间。栈由高地址向低地址增长。堆由低地址向高地址增长。

图片

CPU 对内存的寻址最简单的方式就是直接使用物理内存地址,这种方式一般叫做物理寻址。早期的 PC 使用物理寻址。物理寻址的好处是简单,坏处是不安全,操作系统的地址直接暴露给用户程序,用户程序可以破坏操作系统。这种解决方案是采用特殊的硬件保护;还有同时运行多个程序比较困难:多个用户程序如果都直接引用物理地址,很容易互相干扰。

Linux 系统下,提供两种堆空间分配方式:brk() 统调用和 mmap() 系统调用。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准 C 库中,提供了malloc/free 函数分配释放内存,这两个函数底层是由 brk,mmap,unmap 这些系统调用实现的。malloc 和物理内存可以说没关系,malloc 申请的地址是线性地址,申请的时候并没有进行映射。访问到的时候触发缺页异常时才会进行物理地址映射。

虚拟内存的基本思想是,每个程序都有自己的地址空间,这个地址空间被划分为多个称为页面(page) 的块。每一页都是连续的地址范围。这些页通过内存管理单元(MMU)被映射到物理内存。当程序引用到一部分在物理内存中的地址空间时,硬件会立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。
引入虚拟内存可在较小的可用内存中执行较大的用户程序;提供给用户可用的虚拟内存空间通常大于物理内存,可在内存中容纳更多程序并发执行。

用户态到内核态的触发手段

系统调用:用户进程通过系统调用申请使用内核提供的服务程序来完成工作,调用系统调用后会向CPU发送中断信号。这时CPU会暂停执行下一条指令(用户态)转而执行与该中断信号对应的中断处理程序(内核态) 此处由系统调用查找系统调用表,然后找到对应的内核api 比如open对应的内核api是sys_open。通过sysenter指令进入内核空间,在内核空间有内核堆栈专门用来在内核空间执行函数调用。
异常:当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发通过sysenter指令进入内核空间,由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常及除以0的运算等,然后查找中断描述符表IDT。IDT把所有程序中断和异常发生以后要执行的后续操作整理成一个表,查询这个表就知道下面要进行什么操作。一般操作是给进程发信号,比如处除0对应中断描述附表对应触发的操作就是给进程发一个SIGFPE信号中断进程。 完成这些之后进程通过iret命令返回用户空间,不过回去之前要先处理刚才给进程本身发来的信号。

中断: 当CPU执行运行在用户态下的程序时,有重要的事情发生,比如鼠标点击或网络有数据包到来等等需要打断线程手头的工作,让出CPU去处理,通过sysenter指令进入内核空间。和异常相比最大的区别在于中断是异步,而异常是同步的。因为中断什么时候来是不知道的,是被迫被打断的,而异常是执行指令主动造成的。异常发生以后也要查询中断描述符表进行后续操作。

虚拟内存的实现

1.分页式

进程页表:完成虚拟地址到物理地址的映射。每个进程有一个页表,描述该进程占用的物理页面及逻辑排列顺序。页表存储在内存里,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的工作。

物理页面表:整个系统有一个物理页面表,描述物理内存空间的分配使用状况,其数据结构可采用位示图和空闲页链表。对于位示图法,如果该页面已被分配,则对应比特位置1,否置0.

请求表:整个系统有一个请求表,描述系统内各个进程页表的位置和大小,用于地址转换也可以结合到各进程的PCB(进程控制块)里。如图:
 

CPU将虚拟地址发送给MMU,MMU将虚拟地址翻译成物理地址,再寻址物理内存。虚拟内存的页和物理内存的页大小一样。在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址

上述过程通常由处理器的硬件直接完成,不需要软件参与。通常操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存:
第一次用来查找页表将操作数的逻辑地址变换为物理地址;
第二次完成真正的读写操作。
这样做时间上耗费严重。程序是有局部性的,在一段时间内,整个程序的执行仅限于程序中的某一部分。所访问的存储空间也局限于某个内存区域。把最常访问的几个页表项存储到CPU芯片中专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB (Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。

在 CPU 芯片内部封装了内存管理单元(Memory Management Unit),它用来完成地址转换和 TLB 的访问与交互。有了 TLB , CPU 在寻址时会先查 TLB,如果没找到才会继续查常规的页表。

过程总结

step1 创建
装载器创建虚拟地址空间,每个进程都有自己独立的4G(32位系统下)虚拟内存空间。

step2 映射
创建页表,建立虚拟地址空间到程序在磁盘中位置的映射。

step3 缺页
装载器找到程序的入口点,经过若干时间,操作系统调度该进程执行,找到程序入口地址。cpu找到程序入口地址,发现还没装载进内存,产生缺页中断,启动缺页处理程序,在硬盘中找到程序,然后用请求调入功能将硬盘中的程序部分装入内存。

step4 置换
如果内存已经满了就根据页面置换算法找一个页覆盖,如果被覆盖的页曾经被修改过则需要将此页写回磁盘。不同部分安插到物理内存的不同位置处, 同时修改页表。

step5 执行
现在cpu可以通过页表找到内存中的程序指令了。然后cpu通过和内存之间的交互即提取指令,解码指令和执行指令来执行。时间片到了进程挂起等会再执行。进程执行结束后内存中的数据会被清理覆盖。

页表存在的问题是页表项过多,解决办法: 

1.倒排页表。物理页号做索引,映射到多个虚拟地址。通过虚拟地址查找的时候就需要通过虚拟地址的中间几位来做索引了。
2.多级页表。以两级页表为例。一级页表中的每个PTE (page table entry)映射虚拟地址空间的一个4MB的片,每一片由1024个连续的二级页表组成。一级PTE指向二级页表的基址。这样32位地址空间使用1024个一级页表就可以表示。需要的二级页表总条目还是2^32 / 2^12 =2^20个。这里的关键在于如果一级页表中的页面都未被分配,一级页表就为空。多级页面的一个简单示意图如下。

多级页表减少内存占用的关键在于,如果某个一级页表的页表项没有被用到,就不需要创建这个页表项对应的二级页表了,这是一种巨大的潜在节约。只有一级页表才需要常驻内存。虚拟内存系统可以在需要时创建、页面调入或者调出二级页表,从而减轻内存的压力。

第二个问题是页表在内存中,而MMU位于CPU芯片中,这样每次地址翻译可能都需要先访问一次内存中的页表(CPU L1,L2,L3 Cache Miss的时候访问内存), 效率非常低下。对应的解决方案是引入页表的高速缓存: TLB ( Translation Lookaside Buffer)。

分页内存管理的缺点:

1.共享困难:通过共享页面来实现共享的问题在于,要保证页面上只包含可以共享的内容并不容易,因为进程空间是直接映射到页面上的。这样一个页面上很可能包含不能共享的内容(比如既包含代码又包含数据,代码可以共享,而数据不能共享)。
2.程序地址空间受限于虚拟地址:将程序全部映射到一个统一的虚拟地址的问题在于不好扩张。程序的地址按代码放在一起,然后把数据放在一起,然后再放XXX,这样其中某一部分的空间扩张起来都会影响到相邻的空间,非常不方便。

2.分段式

一个比较直观的解决方法是提供多个独立的地址空间,也就是段(segment)。每个段的长度视具体的段不同而不同,而且可以在运行期动态改变。因为每个段都构成了一个独立的地址空间,所以它们可以独立的增长或者减小而不会影响到其他的段。如果一个段比较大就不能把它整个保存到内存中。可以对段采用分页管理,只有需要的页面才会被调入内存。

分段的办法解决了程序本身不需要关⼼具体的物理内存地址的问题,但也有些不便之处: 第一是内存碎片的问题;第二是内存交换的效率低的问题。

段页式内存管理实现的方式: 先将程序划分为多个有逻辑意义的段; 接着把每个段划分为多个页(也就是对分段划分出来的连续空间,再划分固定大小的页)。这样,地址结构就由段号、段内页号和页偏移三部分组成。过程如下:
1.根据段号,在段表中找到页表地址。
2.检查该段的页表是否在内存中。如果在,则找到它的位置,如果不在,则产生段错误。.
3.检查所请求的虚拟页面的页表项,如果该页面不在内存中则产生缺页中断,如果在内存中就从页表项中取出这个页面在内存中的起始地址。
4.将页面起始地址和偏移量进行拼接得到物理地址,然后完成读写。

页式和段式系统有许多相似之处。比如两者都采用离散分配方式,且都通过地址映射机构来实现地址变换。但概念上两者也有很多区别,主要表现在:
1)、需求:页是信息的物理单位,分页是为了实现离散分配方式,以减少内存的碎片,提高内存的利用率。或者说分页仅仅是由于系统管理的需要,而不是用户的需要。段是信息的逻辑单位,它含有一组意义相对完整的信息。分段是为了更好地满足用户的需要。一条指令或一个操作数可能会跨越两个页的分界处,而不会跨越两个段的分界处。
2)、大小:页大小固定且由系统决定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的。段的长度不固定,且决定于用户所编写的程序,通常由编译系统在对源程序进行编译时根据信息的性质来划分。
3)、逻辑地址表示:页式系统地址空间是一维的, 即单一的线性地址空间,程序员只需利用一个标识符,即可表示一个地址。 分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。
4)、段比页大,因而段表比页表短,可以缩短查找时间,提高访问速度。

在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。 这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。

CPU阿甘之烦恼

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值