接下来我将讨论基于page fault和page table可以做的一些其他酷的事情。另一个简单但是使用的非常频繁的功能是zero-fill-on-demand。
当你查看一个用户程序的地址空间时,存在text区域,data区域,同时还有一个BSS区域(注,BSS区域包含了未被初始化或者初始化为0的全局或者静态变量)。当编译器在生成二进制文件时,编译器会填入这三个区域。text区域是程序的指令,data区域存放的是初始化了的全局变量,BSS包含了未被初始化或者初始化为0的全局变量。
之所以这些变量要单独列出来,是因为例如你在C语言中定义了一个大的矩阵作为全局变量,它的元素初始值都是0,为什么要为这个矩阵分配内存呢?其实只需要记住这个矩阵的内容是0就行。
在一个正常的操作系统中,如果执行exec,exec会申请地址空间,里面会存放text和data。因为BSS里面保存了未被初始化的全局变量,这里或许有许多许多个page,但是所有的page内容都为0。
通常可以调优的地方是,我有如此多的内容全是0的page,在物理内存中,我只需要分配一个page,这个page的内容全是0。然后将所有虚拟地址空间的全0的page都map到这一个物理page上。这样至少在程序启动的时候能节省大量的物理内存分配。
当然这里的mapping需要非常的小心,我们不能允许对于这个page执行写操作,因为所有的虚拟地址空间page都期望page的内容是全0,所以这里的PTE都是只读的。之后在某个时间点,应用程序尝试写BSS中的一个page时,比如说需要更改一两个变量的值,我们会得到page fault。那么,对于这个特定场景中的page fault我们该做什么呢?
学生回答:我认为我们应该创建一个新的page,将其内容设置为0,并重新执行指令。
是的,完全正确。假设store指令发生在BSS最顶端的page中。我们想要做的是,在物理内存中申请一个新的内存page,将其内容设置为0,因为我们预期这个内存的内容为0。之后我们需要更新这个page的mapping关系,首先PTE要设置成可读可写,然后将其指向新的物理page。这里相当于更新了PTE,之后我们可以重新执行指令。
为什么这是一个好的优化?或者说为什么操作系统要这么做?
学生回答:这样节省一部分内存。你可以在需要的时候才申请内存。
是的,这里类似于lazy allocation。假设程序申请了一个大的数组,来保存可能的最大的输入,并且这个数组是全局变量且初始为0。但是最后或许只有一小部分内容会被使用。
第二个好处是在exec中需要做的工作变少了。程序可以启动的更快,这样你可以获得更好的交互体验,因为你只需要分配一个内容全是0的物理page。所有的虚拟page都可以映射到这一个物理page上。
学生提问:但是因为每次都会触发一个page fault,update和write会变得更慢吧?
Frans教授:是的,这是个很好的观点,所以这里是实际上我们将一些操作推迟到了page fault再去执行。并且我们期望并不是所有的page都被使用了。如果一个page是4096字节,我们只需要对每4096个字节消耗一次page fault即可。但是这里是个好的观点,我们的确增加了一些由page fault带来的代价。
page fault的代价是多少呢?我们该如何看待它?这是一个与store指令相当的代价,还是说代价要高的多?
学生回答:代价要高的多。store指令可能需要消耗一些时间来访问RAM,但是page fault需要走到内核。
是的,在lec06中你们已经看到了,仅仅是在trap处理代码中,就有至少有100个store指令用来存储当前的寄存器。除此之外,还有从用户空间转到内核空间的额外开销。所以,page fault并不是没有代价的,之前问的那个问题是一个非常好的问题。