【操作系统—虚拟化】内存空间和地址转换

抽象:地址空间

早期系统

从内存来看,早期的机器并没有提供多少抽象给用户。基本上,机器的物理内存看起来如图所示。

【操作系统—虚拟化】内存空间和地址转换_第1张图片

操作系统曾经是一组函数(实际上是一个库),在内存中(在本例中,从物理地址0开始),然后有一个正在运行的程序(进程),目前在物理内存中(在本例中,从物理地址64KB开始),并使用剩余的内存。

多道程序和时分共享

过了一段时间,由于机器昂贵,人们开始更有效地共享机器。因此,多道程序系统和分时系统分别开启了。

在下图中,有3个进程(A、B、C),每个进程拥有从512KB物理内存中切出来给它们的一小部分内存。假定只有一个CPU,操作系统选择运行其中一个进程(比如A),同时其他进程(B和C)则在队列中等待运行。

【操作系统—虚拟化】内存空间和地址转换_第2张图片

随着时分共享变得流行,人们对操作系统又有了新的要求。特别是多个程序同时驻留在内存中,使保护(protection)成为重要问题。

地址空间

为了解决这些问题,操作系统需要提供一个易用(easy to use)的物理内存抽象。这个抽象叫作地址空间(address space),是运行的程序看到的系统中的内存

一个进程的地址空间包含运行的程序的所有内存状态。比如:程序的代码(code,指令)必须在内存中,因此它们在地址空间里。当程序在运行的时候,利用栈(stack)来保存当前的函数调用信息,分配空间给局部变量,传递参数和函数返回值。最后,堆(heap)用于管理动态分配的、用户管理的内存。当然,还有其他的东西(例如,静态初始化的变量),但现在假设只有这3个部分:代码、栈和堆。

在下图的例子中,我们有一个很小的地址空间(只有16KB)。程序代码位于地址空间的顶部(在本例中从0开始,并且装入到地址空间的前1KB)。代码是静态的(因此很容易放在内存中),所以可以将它放在地址空间的顶部,我们知道程序运行时不再需要新的空间。

【操作系统—虚拟化】内存空间和地址转换_第3张图片

当我们描述地址空间时,所描述的是操作系统提供给运行程序的抽象。程序不在物理地址0~16KB的内存中,而是加载在任意的物理地址。但是运行的程序意识不到这点,它认为自己被加载到特定地址(例如0)的内存中,并且具有非常大的地址空间。这就是虚拟内存系统需要做的事情。

目标

虚拟内存(VM)系统的一个主要目标是透明(transparency)。操作系统实现虚拟内存的方式,应该让运行的程序看不见。因此,程序不应该感知到内存被虚拟化的事实,相反,程序的行为就好像它拥有自己的私有物理内存。

虚拟内存的另一个目标是效率(efficiency)。操作系统应该追求虚拟化尽可能高效(efficient),包括时间上(即不会使程序运行得更慢)和空间上(即不需要太多额外的内存来支持虚拟化)。在实现高效率虚拟化时,操作系统将不得不依靠硬件支持,包括TLB这样的硬件功能。

最后,虚拟内存第三个目标是保护(protection)。操作系统应确保进程受到保护(protect),不会受其他进程影响,操作系统本身也不会受进程影响。当一个进程执行加载、存储或指令提取时,它不应该以任何方式访问或影响任何其他进程或操作系统本身的内存内容(即在它的地址空间之外的任何内容)。

内存操作API

在运行一个C程序的时候,会分配两种类型的内存。第一种称为栈内存,它的申请和释放操作是编译器来隐式管理的,所以有时也称为自动(automatic)内存。第二种类型的内存,即所谓的堆(heap)内存,其中所有的申请和释放操作都由程序员显式地完成。

malloc

malloc函数非常简单:传入要申请的堆空间的大小,它成功就返回一个指向新申请空间的指针,失败就返回NULL。

#include 
...
void *malloc(size_t size);

free

要释放不再使用的堆内存,程序员只需调用free():

int *x = malloc(10 * sizeof(int));
...
free(x);

该函数接受一个参数,即一个由malloc()返回的指针。分配区域的大小不会被用户传入,必须由内存分配库本身记录追踪。

常见错误

在使用malloc()和free()时会出现一些常见的错误。

忘记分配内存

许多例程在调用之前,都希望你为它们分配内存。例如,例程strcpy(dst, src)将源字符串中的字符串复制到目标指针。但是,如果不小心,你可能会这样做:

char *src = "hello";
char *dst;        // oops! unallocated
strcpy(dst, src); // segfault and die
没有分配足够的内存

另一个相关的错误是没有分配足够的内存,有时称为缓冲区溢出(buffer overflow)。一个常见的错误是为目标缓冲区留出“几乎”足够的空间。

char *src = "hello";
char *dst = (char *) malloc(strlen(src)); // too small!
strcpy(dst, src); // work properly
忘记初始化分配的内存

在这个错误中,程序员正确地调用malloc(),但忘记在新分配的数据类型中填写一些值。这样的话程序最终会遇到未初始化的读取(uninitialized read),它从堆中读取了一些未知值的数据。

忘记释放内存

另一个常见错误称为内存泄露(memory leak),如果忘记释放内存,就会发生。在长时间运行的应用程序或系统(如操作系统本身)中,这是一个巨大的问题,因为缓慢泄露的内存会导致内存不足,此时需要重新启动。

在用完之前释放内存

有时候程序会在用完之前释放内存,这种错误称为悬挂指针(dangling pointer)。随后的使用可能会导致程序崩溃或覆盖有效的内存(例如,你调用了free(),但随后再次调用malloc()来分配其他内容,这重新利用了错误释放的内存)。

反复释放内存

程序有时还会不止一次地释放内存,这被称为重复释放(double free)。这样做的结果是未定义的。

注:系统中实际存在两级内存管理。第一级是由操作系统执行的内存管理,操作系统在进程运行时将内存交给进程,并在进程退出(或以其他方式结束)时将其回收。第二级管理在每个进程中,例如在调用malloc()和free()时,在堆内管理。即使你没有调用free(),操作系统也会在程序结束运行时,收回进程的所有内存。

你可能感兴趣的:(操作系统)