5.5.2 页错误 【 文摘园地】 假设上面那个例子中页表入口的第11位为0,这代表了需要的页并没有装入内存中,这一事件就被称为页错误。如果它发生了,CPU会产生一个内部中断,会导致到OS的强行跳转。OS首先会决定将内存中的哪个已置位页写回到硬盘以腾出空间,然后它会将请求的页从硬盘中写入相应位置,此后OS会更新页表中的两个入口参数:(a)它会修改被覆盖的页的入口参数,将第11位置0,并重置31到12位;(b)它会更新新载入的页的入口参数,指明该页已经装载入内存了,并且说明它的位置。 因为读写硬盘的速度远远慢于读写内存的速度,因此,如果页错误出现的太多,程序的运行速度就会非常迟缓。举个例子,假如你家里的PC机内存不够,那么你会发现装载大型软件的时候需要等待很长时间,并且硬盘指示灯会闪得很厉害,就是因为OS要将许多当前置位的页写回硬盘、并从硬盘中装载入新的页。 【 文摘园地】 5.5.3 访问破坏 【 文摘园地】 另一方面,如果发生一个访问破坏,那么OS会声明一个错误-—-在UNIX系统中,把这个错误称为段错误-—-并会导致进程被杀死,也就是从进程表中移除。 举个例子,参看下面的代码: 【 文摘园地】 int q[200]; 【 文摘园地】 main() { int i; for (i = 0; i < 2000; i++)~ { q[i] = i; } } 【 文摘园地】 注意到编程者很显然在循环结构中犯了个错误,他将叠代数200替换成了2000。C编译器在编译过程中会忽略该错误,并且编译出的机器代码在执行过程中也不会检查到数组索引超界了。 如果该程序在非VM平台上运行,那么它可以畅快的没有任何明显错误的运行。它会简单的在q数组后写入1800个字,这能否造成破坏就取决于多余的字数据的目的了。 不过在VM平台上,这里就是UNIX系统下,实际上会报告一个错误,和一个“段错误”的信息。不过,在我们深入理解该错误之前,它出现的时间或许会另你吃惊。这个错误不像是在i=200时出现的,而像是在此之后很长一段时间才出现的。 为了说明它,我选择在gdb下运行此程序以便我能够查看q[199]的地址。在运行该程序之后,我发现段错误不是出现在i=200处,而是出现在i=728的时候。让我们看看这是为什么。 通过查询gdb我发现数组q是在地址0x080497bf处结束的,也就是说,q[199]这个最后的元素处于该地址空间中。对于Intel的机器,页大小为4096字节,所以一个虚拟地址被分成20位的页号和12位的偏移量,正如在5.5.1节中所述的那样。在我们目前这个例子中,q结束于虚拟页号为0x8049、十进制为32841,偏移量为0x7bf、十进制为1983的位置处。所以,在q[199]之后,还有4096-1984=2112个字节空间在同一个页中可供使用。这写空间如果用于存储int型变量,可以存储2112/4=528个,也就是说,可以储存从q[200]到q[727]这528个元素。当然,实际上它们并不是q数组的元素,不过正如前面讲的,编译器并不会对此提出异议。同样的,硬件也不会,由于我们拥有对页的写权限,因此我们可以把相应的数据写入这些空间中。不过当i自增到728时,会使程序使用一个新页,该页不会付予我们写权限(或者其他任何权限),这会被硬件发现,并且硬件会触发段错误。 不仅试图读写超过范围的数据项会导致段错误,同样,试图执行超过范围的指令也会导致段错误。举例说明,考虑下面的代码: 【 文摘园地】 1 int f(int x) 2 { 3 return x*x; 4 } 5 6 int (*p)(int); 7 8 main() 9 { 10 p = f; 11 u = (*p)(5); 12 printf("%d\n",u); 13 } 如果我们忘了写如下一行: 【 文摘园地】 u=(*p)(5); 【 文摘园地】 那么变量p不会指向任何函数,也就是说,我们会企图执行超出我们程序范围的空间中的代码,这就会导致段错误。
|