第七章 数据管理
7.1 内存管理
这篇为linux的内存管理,代码在内存管理代码下载。在所有计算机系统中,内存都是一种稀缺资源。linux为应用程序提供了一个简洁的视图,它能反映一个巨大的可直接寻址的内存空间,此外,linux还提供了内存保护机制,它避免了不同的应用程序之间的互相干扰。如果机器被正确配置并且有足够的交换空间,linux还允许应用程序访问比实际物理内存更大的内存空间。
7.1.1 简单的内存分配
使用标准C语言函数库中的malloc调用来分配内存:
#include <stdlib.h>
void *malloc(size_t size);
编写程序memory1.c
这个程序要求malloc函数给它返回一个指向1MB内存空间的指针。首先检查并确保malloc函数被成功调用,然后通过使用其中的部分内存来表明分配的内存确实已经存在。
由于malloc函数返回的是一个void*指针,因此需要通过类型转换,将其转换为需要的char*类型指针。malloc函数可以保证其返回的内存是地址对齐的,所以它可以被转换成任何类型的指针。
7.1.2 分配大量的内存
编写程序memory2.c
应用程序所分配的内存是由linux内核管理的。每次程序请求内存或者尝试读写它已经分配的内存时,便会由linux内核接管病决定如何处理这些请求。
刚开始时,内核只是通过使用空闲的物理内存来满足应用程序的内存请求,但是当物理内存耗尽时,它便会开始使用所谓的交换空间(swap space)。在linux系统中,交换空间是一个安装系统时分配的独立的磁盘区域。如果熟悉windows操作系统的话,linux交换空间的作用有点像隐藏的windows交换文件。但是与windows不同,linux的交换空间没有局部堆、全局堆或可丢弃内存段等需要在代码中操心的内容——linux内核会为你完成所有的管理工作。
内核会在物理内存和交换空间之间移动数据和程序代码,使得每次读写内存时,数据看起来总像是已存在于物理内存中,而不管在你访问它们之前,它们究竟在哪里。
linux实现了一个“按需换页的虚拟内存系统”,用户程序看到的所有内存都是虚拟的,它并不真正存在于程序使用的物理地址上。linux讲所有的内存都以页为单位进行划分,通常每一页大小为4096字节。每当程序员试图访问内存时,就会发生虚拟内存到物理内存的转换,转换的具体实现和耗费的时间取决于所使用的特定硬件情况。当所访问的内存在物理上并不存在时,就会产生一个页面错误将控制权交给内核。
linux内核会对访问的内存地址进行检查,如果这个地址对于程序来说是合法可用的,内核就会确定需要向程序提供哪一个物理内存页面。然后,如果该页面之前从未被写入过,内核就直接分配它,如果它已经被保存在硬盘的交换空间上,内核就读取包含数据的页面到物理内存(可能需要把一个已有页面从内存中移出硬盘)。接着,在完成虚拟内存地址到物理地址的映射之后,内核允许用户程序继续运行。linux应用程序不需要操心这一过程,因为所有的具体实习那都已隐藏在内核中了。
最终,当应用程序耗尽所有的物理内存和交换空间,或者当最大栈长度被超过时,内核将拒绝此后的内存请求,并可能终止程序的运行。
7.1.3 滥用内存
编写memory4.c
memory4.c中先分配一些内存,然后尝试在分配的内存之后写一些数据,运行结果为:段错误(吐核),见段错误(吐核)详解。linux内存管理系统能保护系统的其他部分免受这种内存滥用的影响。为了确保一个行为恶劣的程序无法破坏任何其他程序,linux会终止其运行。
7.1. 4 空指针
linux对空指针指向地址的读写提供了很强的保护。
编写memory5a.c程序来演示访问空指针时发生的情况
7.1.5 释放内存
linux内存管理系统保证在程序结束时,把分配给它的内存返回给系统。但是,大多数程序需要的并不仅仅是分配一些内存,使用一小段时间,然后就退出。一种更常见的用法是根据需要动态地使用内存。
动态使用内存的程序应该总是通过free调用,来把不用的内存释放给malloc内存管理器。这样做可以将分散的内存块重新合并到一起,并由malloc函数库而不是应用程序来管理它。如果一个运行中的程序自己使用并释放内存,则这些自由内存实际上仍然处于被分配给该进程的状态。在幕后,linux将程序员使用的内存块作为一个物理页面集来管理,通常内存中的每个页面为4K字节。但是如果一个内存页面未被使用,linux内存管理器就可以将其从物理内存置换到交换空间中(换页),从而减轻它对资源使用的影响。如果程序师徒访问位于已置换到交换空间中的内存页中的数据,那么linxu会短暂的暂停程序,将内存页从交换空间再次置换到物理内存,然后允许程序继续运行,就像数据一直存在于内存中一样。
#include <stdlib.h
void free(void *ptr_to_memory);
调用free时使用的指针参数必须是指向由malloc、calloc或realloc调用所分配的内存。
编写程序memory6.c
这个程序调用free来释放内存,free函数带有一个指向先前分配内存的指针参数。
一旦调用free释放了一块内存,它就不再属于这个进程。它将由malloc函数库负责管理。在对一块内存调用free之后,就绝不能再对其进行读写操作了。
7.1.6 其他内存分配函数
另外两个内存分配函数并不像malloc和free使用的那样频繁,calloc和realloc原型为:
#include <stdlib.h>
void *calloc(size_t number_of_elements, size_t element_size);
void *realloc(void *existing_memory, size_t new_size);
calloc函数为一个结构数组分配内存,因此需要把元素个数和每个元素的大小作为其参数。它所分配的内存将全部初始化为0.如果calloc调用成功,将返回指向数组中第一个元素的指针。与malloc调用类似,后续的calloc调用无法保证能返回一个连续的内存空间,因此不能通过重复调用calloc期望返回的内存恰好与之前的内存连续。
realloc函数用来改变已经分配的内存块的长度。它需要传递一个先前通过calloc调用分配的内存的指针,然后根据new_size参数的值来增加或者减少其长度。为了完成这一任务,realloc函数可能不得不移动数据。因此,要确保一旦内存被重新分配之后,必须使用新的指针而不是使用realloc调用之前的那个指针去访问内存。
如果realloc无法调整内存块大小的话,它会返回一个null指针,这就意味着必须避免使用类似下面的代码:
my_ptr = malloc(BLOCK_SIZE);
...
my_ptr = realloc(my_ptr, BLOCK_SIZE * 10);
如果realloc调用失败,它将返回一个空指针,my_ptr将指向null,而先前用malloc分配的内存将无法再通过my_ptr进行访问。因此,在释放老内存块之前,最好的办法是先用malloc请求一块新内存,再通过memcpy调用把数据从老内存块复制到新的内存块。这样即使出现错误,应用程序还是可以继续访问存储在原来内存块中的数据,从而能够实现一个干净的程序终止。