形象:地址空间
晚期零碎
从内存来看,晚期的机器并没有提供多少形象给用户。基本上,机器的物理内存看起来如图所示。
操作系统已经是一组函数(实际上是一个库),在内存中(在本例中,从物理地址 0 开始),而后有一个正在运行的程序(过程),目前在物理内存中(在本例中,从物理地址 64KB 开始),并应用残余的内存。
多道程序和时候共享
过了一段时间,因为机器低廉,人们开始更无效地共享机器。因而,多道程序零碎和分时系统别离开启了。
在下图中,有 3 个过程(A、B、C),每个过程领有从 512KB 物理内存中切出来给它们的一小部分内存。假设只有一个 CPU,操作系统抉择运行其中一个过程(比方 A),同时其余过程(B 和 C)则在队列中期待运行。
随着时候共享变得风行,人们对操作系统又有了新的要求。特地是多个程序同时驻留在内存中,使爱护(protection)成为重要问题。
地址空间
为了解决这些问题, 操作系统须要提供一个易用(easy to use)的物理内存形象。这个形象叫作地址空间(address space),是运行的程序看到的零碎中的内存 。
一个过程的地址空间蕴含运行的程序的所有内存状态。比方:程序的代码(code,指令)必须在内存中,因而它们在地址空间里。当程序在运行的时候,利用栈(stack)来保留以后的函数调用信息,调配空间给局部变量,传递参数和函数返回值。最初,堆(heap)用于治理动态分配的、用户治理的内存。当然,还有其余的货色(例如,动态初始化的变量),但当初假如只有这 3 个局部:代码、栈和堆。
在下图的例子中,咱们有一个很小的地址空间(只有 16KB)。程序代码位于地址空间的顶部(在本例中从 0 开始,并且装入到地址空间的前 1KB)。代码是动态的(因而很容易放在内存中),所以能够将它放在地址空间的顶部,咱们晓得程序运行时不再须要新的空间。
当咱们形容地址空间时,所形容的是操作系统提供给运行程序的形象。程序不在物理地址 0~16KB 的内存中,而是加载在任意的物理地址。然而运行的程序意识不到这点,它认为本人被加载到特定地址(例如 0)的内存中,并且具备十分大的地址空间。这就是虚拟内存零碎须要做的事件。
指标
虚拟内存(VM)零碎的一个次要指标是通明(transparency)。操作系统实现虚拟内存的形式,应该让运行的程序看不见。因而,程序不应该感知到内存被虚拟化的事实,相同,程序的行为就如同它领有本人的公有物理内存。
虚拟内存的另一个指标是效率(efficiency)。操作系统应该谋求虚拟化尽可能高效(efficient),包含工夫上(即不会使程序运行得更慢)和空间上(即不须要太多额定的内存来反对虚拟化)。在实现高效率虚拟化时,操作系统将不得不依附硬件反对,包含 TLB 这样的硬件性能。
最初,虚拟内存第三个指标是爱护(protection)。操作系统应确保过程受到爱护(protect),不会受其余过程影响,操作系统自身也不会受过程影响。当一个过程执行加载、存储或指令提取时,它不应该以任何形式拜访或影响任何其余过程或操作系统自身的内存内容(即在它的地址空间之外的任何内容)。
内存操作 API
在运行一个 C 程序的时候,会调配两种类型的内存。第一种称为栈内存,它的申请和开释操作是编译器来隐式治理的,所以有时也称为主动(automatic)内存。第二种类型的内存,即所谓的堆(heap)内存,其中所有的申请和开释操作都由程序员显式地实现。
malloc
malloc 函数非常简单:传入要申请的堆空间的大小,它胜利就返回一个指向新申请空间的指针,失败就返回 NULL。
#include <stdlib.h>
...
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(),操作系统也会在程序完结运行时,发出过程的所有内存。