说在前面的话:
如果你对OS的一些基本概念在当初学习的时候都了然于胸,只是目前有点淡忘,你可以直接阅读本文;如果你对虚存管理不是很理解,请看我之前写的文章,涉及到了一些基本概念
OS内存管理
OS的中断、异常、系统调用
虚存管理
计算机内存越来越大,但是软件的内存开销也是随之增加的,计算机系统总是会出现内存不够的问题,于是出现了以下几种办法来解决内存空间不够的问题:
- 覆盖(overlay)
应用程序手动把需要的指令和数据保存在内存中,这项技术的关键代表就是MS-DOS操作系统。 - 交换(swapping)
OS自动把暂时不能执行的程序保存到外存中。 - 虚拟内存(virtual memory)
在有限容量的内存中,以页为单位自动状图更多更大的程序。
下面一一叙述.
覆盖技术 (overlay)
介绍
覆盖技术的方法是,依据程序的逻辑结构,将程序划分为若干的功能模块,将不会同时执行的模块放在一块共享内存区域内。覆盖技术中主要设计两大部分:
- 必选部分(常用部分)的代码和数据的常驻内存
- 可选部分(非常用功能)放在其他程序模块中,只在需要用到的时候装入内存
这里主要是考虑代码之间的调用逻辑,比如上图,有一个程序,一共有A~F几个模块,模块之间的调用信息如树状图所示。于是我们可以确地,B和C肯定不会在同一时刻调用,但总有一个会被调用,DEF同理,那么我们只需要开一个空间,空间的大小是多少呢?就是可能被调用的程序的最大内存空间。
不足
覆盖技术还是比较老的技术,其中增加了程序员的编程困难,需要去划分功能模块,确定他们之间复杂的调用关系,个人感觉,也不太符合当今OOP的思想
。此外,这种从外存装入覆盖模块的时间开销比较大。
交换技术 (swapping)
介绍
交换技术和覆盖技术有很大的相似之处,关键区别是,交换是针对某个进程来说的(process),而覆盖技术是对某个进程中的某些模块来说的(module),如下图:
每次swap in和swap out的对象都是某个进程(或者说程序),而且这一过程完全由OS来管理,不需要程序员控制。说明一点,换入和换出的物理内存可以是不一样的(也应该是不一样的),地址空间可以使用的基于分页式的动态地址映射来完成。
不足
交换技术的粒度可能太大,增加了CPU的开销。
虚存技术
虚存技术可以看做是上面两种技术的结合,首先,它也是类似swap的这种换入换出机制,不过粒度不是一个进程,而是我们之前提到的页
或者说帧
;此外,虚存也是由OS来管理的,我们程序员要做的,就是尽量写出满足局部性较好
的代码。
什么是代码局部性?
所谓代码的局部性,就是尽量在写代码时,让程序在未来执行的过程中的一个较短时间内,所执行的指令地址和指令的操作数地址尽量在一个小范围区域内,这样CPU可以在较短的时间内访问数据及执行指令。如果你不是很理解,请看下面这个例子:
我们都知道,C语言中的数组在内存空间中是按行优先的顺序进行存储的,即a[0]、a[1]、a[2]…a[n]的顺序,二维数组类似,可以看作是一维数组的数组,即a[0][0]、a[0][1]、a[0][2]…a[0][n],a[1][0]、a[1][1]、a[1][2]…a[1][n],a[3][0]、a[3][1]、a[3][2]…a[3][n]的顺序。
现在,假设我们的机器是32位的,所以int就是4字节长度了,并且每个进程只能被分配到一个物理帧,假设页面大小为4K(还记得页的概念吗? 他是虚拟内存中的一个概念,和物理内存帧的大小一致),也就是1024个int的长度,现在你在写程序的时候,开了一个int array[1024][1024]
,这就是1024个页的大小了,数组的每一行占一页。
现在,我们要遍历这个数组,你可以按照以下两种方式。
- 按行遍历
1
2
3
4
5
6
7fot(int i=0;i<1024;i++)
{
fot(int j=0;j<1024;j++)
{
cout<<array[i][j];
}
} - 按列遍历如果按列遍历,那么我们每得到下一个值,都会因为需要遍历的数据在下一个页中,总共会发生缺页故障1024*1024次,而按行遍历智慧产生1024次,按列遍历需要频繁的页换入换出,这样的代码就是
1
2
3
4
5
6
7fot(int j=0;j<1024;j++)
{
fot(int i=0;i<1024;i++)
{
cout<<array[i][j];
}
}局部性不好
的代码。这也是在虚拟内存管理方式下,我们程序员需要关注的地方,写出更好的代码。
虚拟内存的管理
虚存的管理是基于段或基于页式的,大多数教科书上都是以按页的模式来讲解的,在该基础上,增加请求调页和页面置换的思路如下:
- 当用户程序需要加载到内存时,只加载一部分页,就开始启动程序。
- 进程在运行的过程中,如果发现有数据或代码不存在内存时,则向系统发出缺页异常请求。(异常是由用户程序产生的)
- os处理缺页异常,将外存相应的页换入到内存帧中
所以,关键就是如何处理缺页异常?在讲解具体缺页异常如何处理之前,我们有必要对之前讲过的页表复习一下(还记得吗?页表就是实现虚拟地址到物理地址映射时的一个工具)
解释:
- 逻辑页号:虚拟内存内存中的概念,也就是用户进程看到的内存空间,实际运行时会将一个
(virtualPageNum,offset)
的二元组去查页表,得到实际内存地址。 - 驻留位:指的是当前该页号i对应的页是否真的在内存里面,如果在,驻留位为1,否则为0。这就是判断是否出现缺页异常的关键。
- 修改位:这个主要是用在页的swap-out过程。你想,假如一个页在运行的时候先从外存(硬盘)导入内存中,然后运行的时候只是简单的读了下这个页中的信息,而没作相应修改,这样,在这个页被swap-out的时候,为了减少磁盘I/O,我们就可以不用将这个页当前内存中的数据导出到导入时对应的磁盘空间中去了。运行时修改了页,修改位为1,否则为0.
- 访问位:这个是用来标记该页是否被访问,主要是用在页替换算法中的。
- 保护位:有些页可能是只读、可读写、可执行等。
- 物理页帧号:该页对应的物理内存地址。
缺页异常处理
- 若在内存中如果有空闲的物理帧(不需要换入换出,就是简单的导入),就分配一个物理页帧,记为f,转向第5步。
- (此时必定是需要换入换出页)依据页面置换算法选择即将被替换的物理页帧f,以及对应的逻辑页q
- 如果q被修改过(修改位==1),则把它写会外存
- 修改q的页表项驻留位为0(因为已经被换出了)
- 将新来的需要访问的页p装入到物理页帧f中
- 修改p的页表项驻留位为1(因为现在被换入了),并设置其对应的物理页帧为f
- 重新执行缺页异常前的指令。
其实图示已经很清楚了,没看明白的话,可以参考我上面写的解释,特别是括号里面的东西。
虚存管理的开销
我们都知道,内存的访问时间是快的(在ns级别),而硬盘的操作较慢(在ms级别),之前差了6个数量级,由此可知,虚拟内存的管理方式的开销在于出现缺页异常时带来的硬盘读写耗时,有如下图的总结公式:
观察EAT表达式可知,p越小,则EAT越小。p怎么减小?尽量让他少缺页。怎么少缺页?right, 代码局部性好!
参考附录
[1] 清华大学 操作系统(Operating Systems) (2019) 第八讲 虚拟存储概念 http://os.cs.tsinghua.edu.cn/oscourse/OS2019spring/lecture08