【程序是怎样跑起来的】第5章:内存和磁盘的亲密关系

内存,磁盘,磁盘缓存,虚拟内存

Posted by x-jeff on October 26, 2023

博客为参考《程序是怎样跑起来的》一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.不读入内存就无法运行

👉第5章热身问答:

  1. 存储程序方式指的是什么?
    • 在存储装置中保存程序,并逐一运行的方式。现在计算机采用的是存储程序方式。
  2. 通过使用内存来提高磁盘访问速度的机制称为什么?
    • Disk Cache(磁盘缓存)。磁盘缓存是指,把从磁盘中读出的数据存储在内存中,当该数据再次被读取时,不是从磁盘而是从内存中高速读出。
  3. 把磁盘的一部分作为假想内存来使用的机制称为什么?
    • 虚拟内存(virtual memory)。借助虚拟内存,哪怕是内存容量不足的计算机,也可以运行很大的程序。
  4. Windows中,在程序运行时,存储着可以动态加载调用的函数和数据的文件称为什么?
    • DLL(Dynamic Link Library)文件。
  5. 在EXE程序文件中,静态加载函数的方式称为什么?
    • 静态链接。函数的加载方式有静态链接和动态链接两种。
  6. 在Windows计算机中,一般磁盘的1个扇区是多少字节?
    • 512字节。扇区是磁盘保存数据的物理单位。

磁盘是计算机系统中的一种硬件设备。磁盘通常用于存储数据,它们可以是机械硬盘驱动器(HDD)或固态硬盘驱动器(SSD)。机械硬盘使用旋转的磁盘和移动的读写头来存储和检索数据,而固态硬盘则使用闪存存储数据,没有机械部件。这些磁盘驱动器作为计算机的主要存储设备之一,用于保存操作系统、应用程序和用户文件。

HDD的优点:较大的存储容量、低成本、适合大容量数据存储。SSD的优点:更快的读写速度、更低的能耗、更高的耐用性。

内存也是计算机系统中的一种硬件设备。它通常指的是随机存取存储器(RAM),用于临时存储数据和指令,以供中央处理器(CPU)快速访问和处理。RAM允许计算机在运行程序时进行数据交换和临时存储,而不是持久存储数据。 RAM以芯片的形式集成在内存模块上,这些模块插入到计算机的主板上。

从都具有存储程序命令和数据这点来看,内存和磁盘的功能是相同的。在计算机的5大部件$^1$中,内存和磁盘也都被归类为存储部件。不过,利用电流来实现存储的内存,同利用磁效应来实现储存的磁盘,还是有差异的。而从存储容量来看,内存是高速高价,而磁盘则是低速廉价。

  1. 一般把输入装置、输出装置、存储器、运算器和控制器这5种部件设备称为计算机的5大部件。

我们平时使用的计算机,至少都配备了512M大小的内存和80GB大小的磁盘。在计算机这个系统中,高速小容量的内存与低速高容量的磁盘进行协同作业。在本文中,内存主要是指主内存(负责存储CPU中运行的程序指令和数据的内存),磁盘主要是指硬盘。

考虑内存和磁盘的关系之前,我们首先来看一个前提性的问题。

程序保存在存储设备中,通过有序地被读出来实现运行,这一点大家都很清楚。这一机制称为存储程序方式(程序内置方式),现在看来这是理所当然的,但在当时它的提出可以说是一个里程碑。为什么这么说呢?因为在此以前的程序都是通过改变计算机的布线等来变更程序的。

计算机中主要的存储部件是内存和磁盘。磁盘中存储的程序,必须要加载到内存后才能运行。在磁盘中保存的原始程序是无法直接运行的。这是因为,负责解析和运行程序内容的CPU,需要通过内部程序计算器来指定内存地址,然后才能读出程序。即使CPU可以直接读出并运行磁盘中保存的程序,由于磁盘读取速度慢,程序的运行速度还是会降低。总之,存储在磁盘中的程序需要读入到内存后才能运行(图5-1)。

2.磁盘缓存加快了磁盘访问速度

磁盘缓存指的是把从磁盘中读出的数据存储到内存空间中的方式。这样一来,当接下来需要读取同一数据时,就不用通过实际的磁盘,而是从磁盘缓存中把内容读出。使用磁盘缓存可以大大改善磁盘数据的访问速度(图5-2)。

Windows提供了磁盘缓存机制作为操作系统。现在,随着硬盘访问速度的大幅改善,磁盘缓存的效果也没有之前那么明显了。

把低速设备的数据保存在高速设备中,需要时可以直接将其从高速设备中读出,这种缓存的方式在其他情况下也会用到。其中的一个实例就是在Web浏览器中的使用。由于Web浏览器是通过网络来获取远程Web服务器的数据并将其显示出来的。因此,在显示较大的图片等文件时,会花费不少时间。于是,Web浏览器就可以把获取的数据暂时保存在磁盘中,然后在需要时再显示磁盘中的数据。也就是说,把低速的网络数据保存到相对高速的磁盘中。

3.虚拟内存把磁盘作为部分内存来使用

虚拟内存是指把磁盘的一部分作为假想的内存来使用。这与磁盘缓存是假想的磁盘(实际上是内存)相对,虚拟内存是假想的内存(实际上是磁盘)。

通过借助虚拟内存,在内存不足时也可以运行程序。例如,在只剩下5MB内存空间的情况下也能运行10MB大小的程序。不过,CPU只能执行加载到内存中的程序。虚拟内存虽说是把磁盘作为内存的一部分来使用,但实际上正在运行的程序部分,在这个时间点上是必须存在在内存中的。也就是说,为了实现虚拟内存,就必须把实际内存(也可称为物理内存)的内容,和磁盘上的虚拟内存的内容进行部分置换(swap),并同时运行程序。

Windows提供了虚拟内存机制作为操作系统。在当前的Windows中,虚拟内存依然发挥着很大的作用。虚拟内存的方法有分页式分段式$^1$两种。Windows采用的是分页式。该方式是指,在不考虑程序构造的情况下,把运行的程序按照一定大小的页(page)进行分割,并以页为单位在内存和磁盘间进行置换。在分页式中,我们把磁盘的内容读出到内存称为Page In,把内存的内容写入磁盘称为Page Out。一般情况下,Windows计算机的页的大小是4KB。也就是说,把大程序用4KB的页来进行切分,并以页为单位放入磁盘(虚拟内存)或内存中(图5-3)。

  1. 分段式虚拟内存是指,把要运行的程序分割成以处理集合及数据集合等为单位的段落,然后再以分割后的段落为单位在内存和磁盘之间进行数据置换。

为了实现虚拟内存功能,Windows在磁盘上提供了虚拟内存用的文件(page file,页文件)。该文件由Windows自动做成和管理。文件的大小也就是虚拟内存的大小,通常是实际内存的相同程度至两倍程度。通过Windows的控制面板,可以查看或变更当前虚拟内存的设定。

4.节约内存的编程方法

以图形用户界面(GUI,Graphical User Interface)$^1$为基础的Windows,可以说是一个巨大的操作系统。Windows的前身是MS-DOS操作系统,最初版本可以在128KB左右的内存上运行,而想要Windows流畅运行的话,至少需要512MB的内存。而且,由于Windows具有多任务功能,在巨大的Windows操作系统中可以同时运行多个应用,因此,即使是512MB的内存,有时也无法保证流畅运行。Windows操作系统经常为内存不足所困。

  1. 像Windows这样,窗口的菜单及图表等都可以进行可视化操作的方式称为图形用户界面。Windows的前身MS-DOS操作系统,是由键盘输入命令来进行操作的CLI(命令行界面)。

许多人可能会认为,通过借助磁盘虚拟内存就可以解决内存不足的问题。而虚拟内存也确实能避免因内存不足导致的应用无法启动。不过,由于使用虚拟内存时发生的Page In和Page Out往往伴随着低速的磁盘访问,因此在这个过程中应用的运行会变得迟钝起来。想必大家也都有过在操作应用的过程中硬盘访问灯一直亮着(这时正在进行Page In和Page Out),导致应用一时无法操作的不愉快经历吧。也就是说,虚拟内存无法彻底解决内存不足的问题。

为了从根本上解决内存不足的问题,需要增加内存的容量,或者尽量把运行的应用文件变小。接下来会向大家介绍两个把应用文件变小的编程方法。虽然增加内存容量更为便捷,但是花费也高。

4.1.通过DLL文件实现函数共有

DLL(Dynamic Link Library)文件,顾名思义,是在程序运行时可以动态加载Library(函数和数据的集合)的文件。此外,多个应用可以共有同一个DLL文件。而通过共有同一个DLL文件则可以达到节约内存的效果。

例如,假设我们编写了一个具有某些处理功能的函数MyFunc()。应用A和应用B都会使用这个函数。在各个应用的运行文件中内置函数MyFunc()(这个称为Static Link,静态链接)后同时运行这两个应用,内存中就存在了具有同一函数的两个程序。但这会导致内存的利用效率降低。所以,有两个同样的函数,还是有点浪费(图5-5)。

当程序运行时,计算机需要将程序的代码加载到内存中以便执行。较大的代码量意味着更多的指令和数据需要加载到内存中供CPU使用。

不仅仅是代码量,程序的复杂性和功能也会影响内存的使用情况。更多的功能通常需要更多的代码和数据,这可能导致程序使用更多的内存。

此外,程序运行时可能会生成临时数据或变量,这些也需要在内存中存储。如果程序需要处理大量数据或执行复杂的操作,它可能会动态地分配更多的内存空间来存储这些数据,进一步增加了内存的占用量。

那么,如果函数MyFunc()是独立的DLL文件而不是应用的执行文件(EXE文件),那结果会怎样呢?由于同一个DLL文件的内容在运行时可以被多个应用共有,因此内存中存在的函数MyFunc()的程序就只有1个。这样一来,内存的利用效率也就提高了。

Windows的操作系统本身也是多个DLL文件的集合体。有时在安装新应用时,DLL文件也会被追加。应用则会通过利用这些DLL文件的功能来运行。像这样,之所以要利用多个DLL文件,其中一个原因就是可以节约内存。而且DLL文件还有一个优点就是,在不变更EXE文件的情况下,只通过升级DLL文件就可以更新。

4.2.通过调用_stdcall来减小程序文件的大小

通过调用_stdcall$^1$来减小程序文件的方法,是用C语言编写应用时可以利用的高级技巧。

  1. _stdcall是standard call(标准调用)的略称。Windows提供的DLL文件内的函数,基本上都是_stdcall调用方式。这主要是为了节约内存。另一方面,用C语言编写的程序内的函数,默认设置都不是_stdcall。C语言特有的调用方式称为C调用。C语言之所以默认不使用_stdcall,是因为C语言所对应的函数的传入参数是可变的(可以设定任意参数),只有函数调用方才能知道到底有多少个参数,而这种情况下,栈的清理作业便无法进行。不过,在C语言中,如果函数的参数数量固定的话,指定_stdcall是没有任何问题的。

C语言中,在调用函数后,需要执行栈清理处理指令。栈清理处理是指,把不需要的数据从接收和传递函数的参数时使用的内存上的栈区域中清理出去。该命令不是程序记述的,而是在程序编译时由编译器自动附加到程序中的。编译器默认将该处理附加在函数调用方。

例如,在代码清单5-1中,从函数main()中调用了函数MyFunc()。按照默认设定,栈的清理处理会附加在函数main()这一方。在同一个程序中,同样的函数可能会被多次反复调用。而如果是同样的函数,栈清理处理的内容也是一样的。由于该处理是在调用函数一方,因此就会导致同一处理被反复进行。这就造成了内存的浪费。

虽然通过调查编译器生成的机器语言执行文件就可以得知栈清理的处理内容,不过鉴于原始的机器语言不太容易理解,所以这里我们用汇编语言的代码清单将其显示了出来。将代码清单5-1中调用函数MyFunc()的部分用汇编语言来表示,就如代码清单5-2所示。最后1行的处理就是清理处理。

C语言通过栈来传递函数的参数。push$^1$是往栈中存入数据的指令。32位CPU中,1次push指令可以存储4个字节的数据。代码清单5-2中,由于使用了两次push指令把两个参数(456和123)存入到了栈中,因此总的来说就是存储了8字节的数据。通过call指令调用函数MyFunc()后,栈中存储的数据就不再需要了。于是这时就通过add esp, 8这个指令,使存储着栈数据的esp寄存器$^2$前进8位(设定为指向高8位字节地址),来进行数据清理。由于栈是在各种情况下都可以再利用的内存领域,因此使用完毕后有必要将其恢复到原状态。上述这些操作就是栈的清理处理。另外,在C语言中,函数的返回值,是通过寄存器而非栈来返回的。

  1. CPU会提前准备好栈机制。往栈中存储数据的汇编语言指令是push。从栈中取出数据的汇编语言指令是pop。栈一般是用来实现函数调用机制的。如果想任意利用栈,程序员就需要自己用程序来实现所需要的栈机制。

  2. CPU中,栈中堆积的最高位的数据地址是保存在esp(esp是Pentium系列CPU的栈指针名)中的。连续运行两次pop指令,可以消除两个存储在栈中的4字节数据,而同样的功能也可以通过把esp的数值加8来实现。

栈清理处理,比起在函数调用方进行,在反复被调用的函数一方进行时,程序整体要小一些。这时所使用的就是_stdcall。在函数前加上_stdcall,就可以把栈清理处理变为在被调用函数一方进行。把代码清单5-1中的int MyFunc(int a, int b)部分转成int _stdcall MyFunc(int a, int b)进行再编译后,和代码清单5-2中add esp, 8同样的处理就会在函数MyFunc()一方执行。虽然该处理只能节约3个字节(add esp, 8是机器语的3个字节)的程序大小,不过在整个程序中还是有效果的(图5-7)。

配合函数的调用机制一起理解。

5.磁盘的物理结构

磁盘的物理结构是指磁盘存储数据的形式。

磁盘是通过把其物理表面划分成多个空间来使用的。划分的方式有扇区方式可变长方式两种,前者是指将磁盘划分为固定长度的空间,后者则是指把磁盘划分为长度可变的空间。一般的Windows计算机所使用的硬盘和软盘,采用的都是扇区方式。扇区方式中,把磁盘表面分成若干个同心圆的空间就是磁道,把磁道按照固定大小(能存储的数据长度相同)划分而成的空间就是扇区(图5-8)。

扇区是对磁盘进行物理读写的最小单位。Windows中使用的磁盘,一般1个扇区是512字节。不过,Windows在逻辑方面(软件方面)对磁盘进行读写的单位是扇区整数倍。根据磁盘容量的不同,1簇可以是512字节(1簇=1扇区)、1KB(1簇=2扇区)、2KB、4KB、8KB、16KB、32KB(1簇=64扇区)。磁盘的容量越大,簇的容量也越大。不过,在软盘中,1簇=512字节=1扇区,簇和扇区的大小是相等的。

不管是硬盘还是软盘,不同的文件是不能存储在同一个簇中的,否则就会导致只有一方的文件不能被删除。因此,不管是多么小的文件,都会占用1簇的空间。这样一来,所有的文件都会占用1簇的整数倍的磁盘空间。我们可以通过试验来确认这一点。

由于在硬盘上做试验比较麻烦,所以我们选择在软盘上进行。首先,把软盘按照“1.44MB,512字节/扇区”进行格式化。软盘中,1扇区=1簇。格式化完成后,我们可以看一下磁盘的属性,这时的已用空间应该是0字节,因为没有存储任何文件(图5-9)。

接下来,让我们用记事本等文本编辑工具做成一个只有1个半角文字的文件,并将其保存到软盘中,然后再来看一下磁盘的属性。这时我们就会发现,虽然文件的大小只有1字节,但使用空间却变成了512字节。

再次打开上述文件,并增加一些文字,然后覆盖保存。这时再查看一下磁盘的属性就会发现,当文件大小未达到512个半角文字(=512字节)时,已用空间一直是512字节。一旦达到513个文字,已用空间就会一下子变成1024字节(=2簇)。通过这个实验,想必大家都应该明白磁盘的数据保存是以簇为单位来进行了吧(图5-10)。

以簇为单位进行读写时,1簇中没有填满的区域会保持不被使用的状态。虽然这看起来是有点浪费,不过该机制就是如此规定的。另外,如果减少簇的容量,磁盘访问次数就会增加,就会导致读写文件的时间变长。由于在磁盘表面上,表示扇区区分的领域是必要的,因此,如果簇的容量过小,磁盘的整体容量也会减少。扇区和簇的大小,是由处理速度和存储容量的平衡来决定的。