操做系统思考 第三章 虚拟内存

第三章 虚拟内存

做者:Allen B. Downeyhtml

原文:Chapter 3 Virtual memorygit

译者:飞龙github

协议:CC BY-NC-SA 4.0数组

3.1 简明信息理论

比特是二进制的数字,也是信息的单位。一个比特有两种可能的状况,写为0或者1。若是是两个比特,那就有四种可能的组合,00、0一、10和11。一般,若是你有b个比特,你就能够表示2 ** b个值之一。一个字节是8个比特,因此它能够储存256个值之一。函数

从其它方面来说,假设你想要储存字母表中的字母。字母共有26个,因此你须要多少个比特呢?使用4个比特你能够表示16个值之一,这是不够的。使用5个比特你能够表示32个值,这对于全部字母是够用的,同时还有一点点浪费。布局

一般,若是你想要表示N个值之一,你就须要求出最小的b使2 ** b >= N。在两边计算以2为底的对数,就会获得b >= log(2, N)编码

假设我投掷一枚硬币而且告诉你结果,我就向你提供了1比特的信息。若是我投掷六个面的筛子并告诉你结果,我就向你提供了log(2, 6)比特的信息。而且一般,若是结果的几率是1/n,结果应该包含log(2, N)比特的信息。spa

一样,若是结果的几率为p,那么信息的内容为-log(2, p)。这个数量叫作“自信息”(self-information)。它度量告终果有多么使人意外,因此也叫做“惊异度”。若是你的赛马只有十六分之一的概率获胜,而且它获胜了,那么你就获得了4比特的信息(以及奖金)。可是若是它的获胜概率为75%,这条新闻只含有0.42个比特。操作系统

能够由直以为出,非预期的新闻会带有大量信息;与之相反,若是你对一件事情颇有自信,对它的验证只会获得少许的信息。翻译

对于书中的一些话题,咱们只须要熟练于在比特数量b和它们所编码的值的数量N = 2 ** b之间进行转换。

3.2 内存(Memory)和储存器(Storage)

当进程处于运行期间,它的多数数据都放在“主存”(内存)之中,它一般是一些随机储存器(RAM)。在当前的大多数电脑上,主存很是易失,也就是说,当电脑关闭时,主存的内容就没了。一个典型的台式电脑拥有2~8GiB的内存。GiB表明“gibibyte”,至关于2 ** 30个字节。

若是进程会读写文件,这些文件一般放在机械硬盘(HDD)或固态硬盘(SSD)里面。这些储存器都是非易失的,因此他们可用于长时间储存。当前,一个典型的台式电脑拥有500GB到2TB的HDD。GB表明“gigabyte”,至关于10 ** 9个字节。TB表明“terabyte”,至关于10 ** 12个字节。

你可能会注意到我使用二进制单位GiB来描述主存大小,并使用十进制单位GB和TB来描述HDD的大小。因为历史和技术因素,内存以二进制单位度量,而且硬盘以十进制单位度量。本书中我会当心区分二进制和十进制单位,可是你应该注意到“gigabyte”以及GB缩写一般在使用上很是模糊。

非正式的用法中,“内存”有时会用于HDD和SSD(特别是移动设备),以及RAM。然而,这些设备的属性截然不同,因此咱们须要区分它们。我会使用“储存器”来指代HDD和SSD。

3.3 地址空间

主存中的每一个字节都由一个“物理地址”整数所指定,物理地址的集合叫作物理“地址空间”。它的范围一般为0到N-1,其中N是主存的大小。在带有1GiB主存的的系统上,最高的有效地址是2 ** 30 - 1,十进制表示为1,073,741,823,16进制表示为0x03ff ffff(前缀0x表示十六进制)。

然而,许多操做系统提供“虚拟内存”,也就是说程序永远不须要处理物理地址,也不须要知道有多少物理内存是有效的。

做为代替,程序处理虚拟地址,它被编码为从0到M-1,其中M是有效虚拟地址的大小。虚拟地址空间的大小取决于所处的操做系统和硬件。

你必定听过人们谈论32位和64位系统。这些术语代表了寄存器的尺寸,也一般是虚拟地址的大小。在32位系统上,虚拟地址是32位的,也就是说虚拟地址空间为从0到0xffff ffff。这一地址空间的大小是2 ** 32个字节,或者4GiB。

在64位系统上,虚拟地址空间大小为2 ** 64个字节,或者4 * 1024 ** 6个字节。这是16个EiB,大约比当前的物理内存大十亿倍。虚拟内存比物理内存大不少,这看上去有些奇怪,可是咱们很快就就会看到它如何工做。

当一个程序读写内存中的值时,它使用虚拟地址。硬件在操做系统的帮助下,在访问主存以前将物理地址翻译虚拟地址。翻译过程在进程层级上完成,因此即便两个进程访问相同的虚拟地址,它们所映射的物理地址可能不一样。

所以,虚拟内存是操做系统隔离进程的一种重要途径。一般,一个进程不能访问其余进程的数据,由于没有任何虚拟地址能映射到其余进程分配的物理内存。

3.4 内存段

一个运行中进程的数据组织为4个段:

  • text段包含程序文本,即程序所组成的机器语言指令、

  • static段包含由编译器所分配的变量,包括全局变量,和使用static声明的局部变量。

  • stack段包含运行时栈,它由栈帧组成。每一个栈帧包含函数参数、本地变量以及其它。

  • heap段包含运行时分配的内存块,一般经过调用C标准库函数malloc来分配。

这些段的组织方式部分取决于编译器,部分取决于操做系统。不一样的操做系统中细节可能不一样,可是下面这些是共同的:

  • text段靠近内存“底部”,即接近0的地址。

  • static段一般恰好在text段上面。

  • stack段靠近内存顶部,即接近虚拟地址空间的最大地址。在扩张过程当中,它向低地址的方向增加。

  • heap一般在static段的上面。在扩张过程当中,它向高地址的方向增加。

为了搞清楚这些段在你操做系统上的布局,能够尝试运行这个程序,它就是这本书的仓库中的aspace.c

#include <stdio.h>
#include <stdlib.h>

int global;

int main ()
{
    int local = 5;
    void *p = malloc(128);

    printf ("Address of main is %p\n", main);
    printf ("Address of global is %p\n", &global);
    printf ("Address of local is %p\n", &local);
    printf ("Address of p is %p\n", p);
}

main是函数的名称,当它用做变量时,它指向main中第一条机器语言指令的地址,咱们认为它在text段内。

global是一个全局变量,因此咱们认为它在static段内。local是一个局部变量,因此咱们认为它在栈上。

p持有malloc所返回的地址,它指向堆区所分配的空间。malloc表明“内存分配”(memory allocate)。

格式化占位符%p告诉printf把每一个地址格式化为“指针”,它是地址的另外一个名字。

当我运行这个程序时,输出就像下面这样(我添加了空格使它更加易读):

Address of main is   0x      40057c
Address of global is 0x      60104c
Address of local is  0x7fffd26139c4
Address of p is      0x     1c3b010

正如预期的那样,main的地址最低,随后是globalplocal的地址会更大,它是12个十六进制数字,每一个十六进制数字对应4比特,因此它是48位的地址。这代表虚拟内存的可用部分为2 ** 48个字节。

做为一个练习,你须要在你的电脑上运行这个程序,并将你的结果与个人结果比较。添加对malloc的第二个调用来检查你系统上的堆区是否向上增加(地址更高)。添加一个函数来打印出局部变量的地址,检查栈是否向下增加。

3.5 静态局部变量

栈上的局部变量有时称为“自动变量”,由于它们当函数建立时自动被分配,而且当函数返回时自动被释放。

C语言中又另外一种局部变量,叫作“静态变量”,它分配在在static段上。它在程序启动时初始化,而且在函数调用之间保存它的值。

例如,下面的函数跟踪了它所调用的次数:

int times_called()
{
  static int counter = 0;
  counter++;
  return counter;
}

static关键字表示counter是静态局部变量。它的初始化只发生一次,就是程序启动的时候。

若是你将这个函数添加到aspace.c,你能够肯定counter和全局变量一块儿分配在static段上,而不是在栈上。

3.6 地址翻译

虚拟地址(VA)如何翻译成物理地址(PA)?基本的机制十分简单,可是简单的实现方式十分耗时,而且占据大量空间。因此实际的实现会复杂一点。

大多数处理器提供了内存管理单元(MMU),位于CPU和主存之间。MMU在VA和PA之间执行快速的翻译。

  1. 当程序读写变量时,CPU会获得VA。

  2. MMU将VA分红两部分,称为页码和偏移。“页”是一个内存块,页的大小取决于操做系统和硬件,一般为1~4KiB。

  3. MMU在“页表”里查找页码,而后获取相应的物理页码。以后它将物理页码和偏移组合获得PA。

  4. PA传递给主存,用于读写指定地址。

做为一个例子,假设VA为32位,物理内存为1GiB,划分为1KiB的页面。

  • 因为1GiB为2 ** 30个字节,物理页的数量为2 ** 20个,它们也称为“帧”。

  • 虚拟地址空间的大小为2 ** 32字节,这个例子中,页的大小为2 ** 10字节,因此共有2 ** 22个虚拟页。

  • 偏移的大小取决于页的大小。这个例子中页的大小为2 ** 10字节,因此须要10位来指定页中的一个字节。

  • 若是VA是32位,而偏移是10位,剩余的22位构成了虚拟页码。

  • 因为共有2 ** 20个物理页,每一个物理页码是20位。加上10位的偏移,PA的结果为30位。

到目前为止,看上去是是可行的。可是让咱们考虑一下页表应该占多大。页表最简单的实现是一个数组,每一个虚拟页面是一个条目。每一个条目都包含一个物理页码,在例子中它是20位,加上每帧的一些额外的数据,因此咱们认为每一个条目占用3~4个字节。因为共有2 ** 22个虚拟页,页面共须要2 ** 24个字节,或16MiB。

因为咱们须要为每一个进程建立一个页表,一个运行256个进程的系统就须要2 ** 32个字节,或者4GiB,这还只是页面的空间!这些就占用了所有32位虚拟地址。而在48或64位的虚拟地址上,这个数量更加荒谬。

幸运的是,并不须要这么大的空间,由于大多数进程不使用虚拟地址空间的每一个小片断。并且,若是一个进程不使用某个虚拟页面,咱们也不须要在页表中为其分配条目。

也就是说,页表是“稀疏”的,这暗示了最简单的实现,即页表条目的数组是个糟糕的想法。幸运的是,稀疏数组有一些不错的实现方式。

一种选择是多级页表,它被多数操做系统例如Linux所采用。另外一种选择是关联表,其中每一个条目包含虚拟页码和物理页码。在软件上搜索关联表会很是慢,可是硬件上咱们能够并行搜索整个表,因此关联数组常常用于在MMU中表示页表。

你能够在页表的维基百科页面阅读更多关于这些实现的信息。你也可能会找到有趣的细节。可是基本的想法就是页表应作成稀疏的,因此咱们须要为稀疏数组选择一个好的实现方式。

我以前提到了操做系统能够中断一个运行中的进程,保存它的状态,以后运行其它进程。这个机制叫作“上下文切换”。因为每一个进程都有本身的页表,操做系统须要和MMU配合来保证每一个进程拿到了正确的页表。在旧机器上,MMU中的页表信息在每次上下文切换时会被替换掉,开销很是大。在新的系统中,MMU的每一个页表条目包含进程ID,因此多个进程的页表能够同时储存在MMU中。

相关文章
相关标签/搜索