众所周知, 在早期, 操作系统还没有分时的概念, 当时都是单进程执行, 只有一个进程结束了, 才能执行后一个进程. 但是这样的执行很容易想到的一个问题, 若进程在空闲状态, 则 CPU 就空下来了, 造成无谓的浪费. 后来为了解决这个问题, 于是进程可以主动申请轮换, 将当前时间交由其他进程. 但若是一个进程一直不出让控制权的话, 又退回到之前的情况了. 于是有了现在的分时系统, 即每个进程执行一小段时间, 然后强制切换到另一个进程执行, 由于切换的时间很短, 视觉上造成了很多进程同时执行的错觉.
但是, 当允许同时运行多个进程的时候, 就出现了新的问题, 内存.
假设你现在共有内存128m, 程序 A 运行需要100m, 程序 B 需要10m. 当分别运行的时候, 内存是足够的, 若同时运行程序 A 和 B, 将内存的0-100分给 A, 100-110 分给 B, 也够. 但是这种简单的内存分配有几个问题:
于是衍生成了虚拟内存的技术, 虚拟内存将内存存储在磁盘中, 待到需要的时候再读取到物理内存中.
计算机中的一切问题, 都可以通过增加一个中间层来解决.
为了解决内存空间的隔离问题, 通过在程序与内存中添加一个中间层来解决. 于是, 将每一个程序的内存, 分别和物理内存进行映射, 如下:
操作系统维护着这样一个虚拟内存到物理内存之间的映射关系, 进程访问的地址通过映射, 转换为实际的物理地址.
这样, 当操作系统检测到访问的内存超出范围时可以进行干预. 同时, 映射关系对于进程来说是透明的, 每个进程不需要关心实际物理内存的变化, 只需要认为地址从0开始即可.
如此, 同时解决了上面的问题1, 3, 4. 但并没有解决内存使用效率低的问题. 试想, 如果此时再运行一个需要50m 内存的进程 C, 因为剩余内存不足, 将进程 C 读入物理内存后, 势必会覆盖其他进程的内存空间. 这时, 要先将程序 A 的内存写回磁盘以便下次运行, 再将进程 C 从磁盘写入内存. 之后切换回进程 A 的时候, 又要重复刚才的整个操作.
试想一下, 现在运行两个进程, 每个占用内存100m, 那么每次切换进程时几乎都要重写整个内存. 而这个切换操作对于分时系统来说又十分频繁. 根据程序的局部性原理, 在一段时间内仅使用了一小部分数据. 也就是说, 如果分别给两个进程50m 的空间, 就可以满足一段时间内的运行需要的所有数据, 大大避免了内存的频繁置换.
于是人们想到, 如果在上面的基础上将虚拟内存再切割成一个一个小块, 用到哪块读哪块, 岂不是就解决这个问题了么. 于是有了这样的模型:
进程能够看到的仍然只有虚拟内存, 不过, 操作系统将虚拟内存按照4k(比如) 的大小分成了很多块, 每一块称为一页. 其维护了虚拟内存中每一页到物理内存的映射关系, 这样就可以做到, 只将目前需要的部分内容读取到内存中.
同时, 可以针对页设置读写权限, 仅特定的进程可以对页进行读或写的操作, 非法读写会被系统捕捉到.
另外这种虚拟内存到物理内存转换, 是可以通过硬件支持的, 及内存管理单元MMU
. CPU 将虚拟地址, 通过MMU
转换后, 得到物理地址进行访问. MMU
在进行地址翻译的时候, 会在物理内存中读取查询表来动态翻译地址, 而这张查询表是由操作系统进行维护的. 若读取时, 发现还没有读到物理内存内存中, 则交由操作系统将其读取到物理内存中, 并更新查询表.
因为有了虚拟内存的存在, 才可以在一个物理内存128m 的机器上, 运行需要内存200m 的进程, 虽然相比直接运行在物理内存上, 速度上要有一些牺牲. 在32位机器上, 虚拟内存最大为4G.
分页技术也是现在的操作系统使用的技术, 可以看到, 在进程看来连续的内存, 在物理内存中不一定连续. 也就是说你定义的数组, 可能分布在物理内存相隔很远的两个地方.