第七章 对内存的思考
(1)虚拟内存:基本思路是用廉价但缓慢的磁盘来扩充快速却昂贵的内存。
在任一给定时刻,程序实际需要使用的虚拟内存区段的内容被载入物理内存中。当物理内存中的数据有一段时间未被使用,它们就可能被转移到硬盘中,节省下来的物理内存空间用于载入需要使用的其他数据。
虚拟内存通过“页”的形式组织。页就是操作系统在磁盘和内存之间移来移去或进行保护的单位,一般为几K字节。
进程只能操作位于物理内存中的页面。当进程引用一个不在物理内存中的页面时,MMU(内存管理单元),就会产生一个页错误,内核就对此事做出响应,并判断引用是否有效。如果无效,内核向进程发出一个"segmentation violation(段违规)"的信号。如果有效,内核从磁盘取回该页,换入到内存中。一旦页面进入内存,进程便被解锁,可以重新运行——进程本身并不知道它曾经因为页面换入事件等待了一会。
(2)数据段和堆
堆中的所有东西都是匿名的——不能按名字直接访问,只能通过指针间接访问。
从堆中获取内存的唯一办法就是通过调用malloc(以及同类的calloc、realloc等)库函数。calloc函数和malloc类似,但它在返回指针之前先把分配的内存的内容都清空为零。realloc函数改变一个指针所指向的内存块的大小,既可以将其扩大,也可以把它缩小,它经常把内存拷贝到别的地方,然后将指向新地址的指针返回。
用于管理内存的调用是:
malloc和free——从堆中获取内存以及把内存返回给堆。
brk和sbrk——调整数据段的大小至一个绝对值(通过某个增量)(参加brk和sbrk及内存分配函数相关)。
(3)内存泄露
堆经常会出现的两种类型的问题:
释放或改写仍在使用的内存(称为“内存损坏”)。
未释放不再使用的内存(称为“内存泄露”)。
一种简单的避免内存泄露的方法是在可能的时候使用alloca()来分配动态内存,当离开调用alloca的函数时,它所分配的内存会被自动释放。不提倡使用alloca,因为是它不是一种可移植的方法,如果处理器在硬件上不支持堆栈,alloca()就很难高效地实现。
如何检测内存泄露:
1. 使用swap命令观察还有多少可用的交换空间:
/usr/sbin/swap -a
total:17228k bytes allocated + 5396k reserved = 22624k used, 29548k avaliable
(共计:17228K已分配+5396K用于保留=22624K已用,29548可用)
还可以使用其他一些工具如netstat、vmstat等。如果发现不断有内存被分配且从不释放,一个可能的解释就是有个进程出现了内存泄露。
网络监测工具snoop,从网络中捕捉分组(packet),并在工作站上显示。(-a选项,可以使snoop让每个分组都在工作站的扬声器中输出一个滴答声,从而可以聆听网络的以太交通)。
2. 可疑的进程,看看它是不是该为内存泄露负责。(可以使用“ps -lu 用户名”命令来显示所有进程的大小,同样可以使用pagesize命令)
(4)bus error(core dumped) / segmentation fault(core dumped)
当硬件告诉操作系统一个有问题的内存引用时,就会出现以上两种错误。
信号时由于硬件中断而产生的。
总线错误几乎都是由于未对齐的读或写引起的。
段错误是由于内存管理单元(负责支持虚拟内存的硬件)的已查过所致,而该异常则通常由于解除引用一个未初始化或非法值的指针引起的。
通常导致段错误的几个直接原因:
1. 解除引用一个包含非法值的指针。
2. 解除引用一个空指针(常常由于从系统中返回空指针,并未检查就使用)。
3. 在未得到正确的权限时进行访问。例如,试图往一个只读的文本段存储值就会引起段错误。
4. 用完了堆栈或堆空间(虚拟内存虽然巨大但绝非无限)。
以发生频率为序,最终可能导致段错误的常见编程错误:
1. 坏指针值错误:在指针赋值之前就使用它来引用内存,或者向库函数传送一个坏指针(不要上当,如果调试器显示系统程序中出现了段错误,并不是因为系统程序引起了段错误,问题很可能还存在于自己的代码中)。第三种可能导致坏指针的原因是对指针进行释放之后再访问它的内容。可以修改free语句,在指针释放之后再将它置为空值。
free(p); p = NULL;
2. 改写(overwrite)错误:越过数组边界写入数据,在动态分配的内存两端之外写入数据,或改写一些堆管理数据结构(在动态分配的内存之前写入数据就很容易发生这种情况)。
3. 指针释放引起的错误:释放同一个内存块两次,或释放一块未曾使用malloc分配的内存,或释放仍在使用中的内存,或释放一个无效的指针。一个极为常见的与释放内存有关的错误就是在for(p=start; p; p=p->next)这样的循环中迭代一个链表,并在循环体内使用free(p)语句,这样,在下一次循环迭代时,程序就会对已经释放的指针进行解除引用操作,从而导致不可预料的结果。
如何在链表中释放元素:
在遍历链表时正确释放元素的方法是使用临时遍历存储下一个元素的地址。这样就可以安全地在任何时候释放当前元素,不必担心在去下一个元素的地址时还要引用它。代码如下:
struct node *p, *start, *temp;
for(p = start; p; p = temp)
{
temp = p->next;
free(p);
}