漫谈操作系统5 -- 基础知识(进程隔离)
前一篇博客介绍了操作系统中进程和线程的概念,下面接着介绍操作系统内核关于进程隔离的基本内容。
进程隔离是操作系统内核对于资源管理和安全增强的特性,其最终的目的是对于操作系统内核能够更好的控制程序对资源的申请和使用,并且控制此程序可访问资源的范围并限定此程序异常之后能够影响的范围。 现有的小型嵌入式系统内核比如UC/OS 2, LittleKernel如果没有而外的库的帮助(例如LK的uthread库),都是不支持进程隔离的,其所有的任务运行在一个大的对每个任务都可见的内存空间之上,这么做的优点是其可以运行在没有MMU的处理器之上比如ARM的Cortex M处理器甚至是51单片机之上,但是其缺点是每个被操作系统调度的任务是没有机制隔离其资源的,而系统的安全性由于无法将单个任务崩溃造成的影响控制在此任务之内,所以整个系统的安全性和稳定性都非常的低。
1. 进程隔离的硬件要求
进程隔离对硬件有一些基本的要求,其中最主要的硬件是MMU (Memory Management Unit 内存管理单元),有时候ARM Cortex M之上的MPU也能达到类似的功能,但是其功能很弱,无法做到地之间的翻译,而只能在物理内存地址之上划定线性的范围。
下面重点介绍一下MMU的功能, 对于X86处理器来说其32位兼容模式运行还会有分段式内存访问的模式(其也可以达到隔离内存和翻译的作用),本文不会介绍,本文主要介绍内存分页访问的相关内容。
MMU的作用简单用一句话概括就是将线性地址翻译为物理地址, 对于理解操作系统内核来说我们并不需要了解太多MMU的细节,但是如果我们要实现一个内核,这部分知识是根据处理器体系结构的不同而不同的。
2. 为什么需要线性地址(虚拟地址) 到 物理地址的翻译
我们先看如果没有线性地址的概念只有物理地址会出现的问题:
a. 整个操作系统能够访问的地址空间和实际插入的物理内存大小相关。
b. 每个进程能够访问的地址空间是从物理内存空间上的一部分。
c. 编译生成的程序需要事先知道运行在物理地址空间的范围,才能够生成相应的执行代码。
而对于上面的三个问题是一般的操作系统都无法忍受的,对于通用的操作系统来说,其能访问的内存空间是根据地址线的范围来决定的,例如32位系统就是2^32, 而对于64位操作系统是2^48 不会因为实际插入的物理内存大小的变化而变化, 而同样的为操作系统编译生成的可执行程序需要具有通用性,而不能针对不同的硬件都重新生成可执行程序来适配不同的物理内存大小和范围。
引入了线性地址到物理地址的翻译就能够解决以上的问题, 例如对于32位处理器上运行的Linux操作系统,其每个进程的内存空间都如下图所示
从上图我们可以看到对于其上运行的每一个进程,它的内存线性地址空间地址都是从0 -- > 4GB的范围,其中Linux内核的地址空间为 3-->4GB的线性地址高位空间(同一个内核的地址空间映射在每个进程的3-->4GB的内存范围内), 而对于0 -- > 3GB来说为每个进程自己的用户态地址空间范围。 所以每个不同的应用程序在运行时其进程的0 -- > 3GB为它们的可访问地址范围,虽然它们的代码逻辑完全不同,但是内存的可访问范围却完全相同。
3. 分页内存访问的操作系统如何做到进程的线性地址空间隔离
操作系统内核上电之后会初始化MMU并将自己映射到3-- > 4GB的线性地址空间, 而当我们通过系统调用如fork创建进程时,其会在进程描述结构体内集成内存虚拟地址空间的结构体,其内容包含的是当前进程的地址空间页表,当操作系统进行任务切换时会改写CPU的页表基地址寄存器为当前被运行进程的页表基地址,从而达成切换地址空间范围到相应的进程内存范围的目的。
4. 地址翻译的过程
MMU将一个线性地址翻译为一个物理地址的过程如下图所示
对于32位处理器,其按照10, 10, 12的规则,头10位线性地址对应页目录的index, 通过此10位的值在页目录中查询到一个页表项的地址,然后再根据中间10位定位实际对应的页的物理地址, 最后根据最后12位4KB的偏移范围在一个物理页中寻址出实际需要访问的内存位置。
所以一个线性地址分为3部分,头10位是页目录中的索引值,中间10位是页表中的索引值,最后12位是页内便宜地址。
而不同的线性地址可以映射到相同的物理地址,此处可以极大的节省实际的内存开销,例如同一个共享库如glibc.so其实例在物理内存中只存在一份,而被不同的进程映射到了自己的线性地址空间中,它们共享的访问glibc的代码段,而其数据段则每个进程有不同的glibc数据段副本。